From 10f8beae8ecb64407df758d44601802cb768e35a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 31 Jul 2020 11:52:00 +0200 Subject: [PATCH 001/115] [Discover] Context unskip date nanos functional tests (#73781) * Use _source value of timestamp for search_after since ES allows this now * Unskip functional tests * Remove unused convertIsoToNanosAsStr --- .../angular/context/api/utils/date_conversion.ts | 9 --------- .../context/api/utils/get_es_query_search_after.ts | 10 ++-------- test/functional/apps/context/_date_nanos.js | 3 +-- .../apps/context/_date_nanos_custom_timestamp.js | 5 +---- 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts index 64544a335c911..4369234a3ce9a 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts @@ -31,15 +31,6 @@ export function extractNanos(timeFieldValue: string = ''): string { return fractionSeconds.length !== 9 ? fractionSeconds.padEnd(9, '0') : fractionSeconds; } -/** - * extract the nanoseconds as string of a given ISO formatted timestamp - */ -export function convertIsoToNanosAsStr(isoValue: string): string { - const nanos = extractNanos(isoValue); - const millis = convertIsoToMillis(isoValue); - return `${millis}${nanos.substr(3, 6)}`; -} - /** * convert an iso formatted string to number of milliseconds since * 1970-01-01T00:00:00.000Z diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index d4ee9e0e0f287..24ac19a7e3bc3 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { convertIsoToNanosAsStr } from './date_conversion'; import { SurrDocType, EsHitRecordList, EsHitRecord } from '../context'; export type EsQuerySearchAfter = [string | number, string | number]; @@ -38,15 +37,10 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - const afterTimeValue = nanoSeconds - ? convertIsoToNanosAsStr(afterTimeDoc.fields[timeFieldName][0]) - : afterTimeDoc.sort[0]; + const afterTimeValue = nanoSeconds ? afterTimeDoc._source[timeFieldName] : afterTimeDoc.sort[0]; return [afterTimeValue, afterTimeDoc.sort[1]]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string - return [ - nanoSeconds ? convertIsoToNanosAsStr(anchor.fields[timeFieldName][0]) : anchor.sort[0], - anchor.sort[1], - ]; + return [nanoSeconds ? anchor._source[timeFieldName] : anchor.sort[0], anchor.sort[1]]; } diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index 89769caaea253..cdf2d6c04be83 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -30,8 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // FLAKY/FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos', () => { + describe('context view for date_nanos', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 6329f6c431e6a..8fe08d13af0aa 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -30,10 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // skipped due to a recent change in ES that caused search_after queries with data containing - // custom timestamp formats like in the testdata to fail - // https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos with custom timestamp', () => { + describe('context view for date_nanos with custom timestamp', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); await esArchiver.loadIfNeeded('date_nanos_custom'); From dbb603f9793d6477fb73663d53216cbefa3b1d2e Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Fri, 31 Jul 2020 09:21:47 -0400 Subject: [PATCH 002/115] [Canvas][tech-debt] Ensure cursor is called until full results are received (#73347) * Ensure cursor is called until full results are receeived * Fix Typecheck * Convert dependencies to typescript * Fix typings Co-authored-by: Elastic Machine --- .../functions/server/esdocs.ts | 1 - .../functions/server/essql.ts | 1 - ...uild_bool_array.js => build_bool_array.ts} | 5 +- x-pack/plugins/canvas/server/lib/filters.js | 38 ------- x-pack/plugins/canvas/server/lib/filters.ts | 74 ++++++++++++ .../{get_es_filter.js => get_es_filter.ts} | 8 +- .../{normalize_type.js => normalize_type.ts} | 4 +- .../plugins/canvas/server/lib/query_es_sql.js | 59 ---------- .../canvas/server/lib/query_es_sql.test.ts | 106 ++++++++++++++++++ .../plugins/canvas/server/lib/query_es_sql.ts | 96 ++++++++++++++++ .../{sanitize_name.js => sanitize_name.ts} | 4 +- .../server/routes/es_fields/es_fields.ts | 1 - x-pack/plugins/canvas/types/filters.ts | 32 ++++++ x-pack/plugins/canvas/types/index.ts | 1 + 14 files changed, 320 insertions(+), 110 deletions(-) rename x-pack/plugins/canvas/server/lib/{build_bool_array.js => build_bool_array.ts} (66%) delete mode 100644 x-pack/plugins/canvas/server/lib/filters.js create mode 100644 x-pack/plugins/canvas/server/lib/filters.ts rename x-pack/plugins/canvas/server/lib/{get_es_filter.js => get_es_filter.ts} (75%) rename x-pack/plugins/canvas/server/lib/{normalize_type.js => normalize_type.ts} (89%) delete mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.js create mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.test.ts create mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.ts rename x-pack/plugins/canvas/server/lib/{sanitize_name.js => sanitize_name.ts} (85%) create mode 100644 x-pack/plugins/canvas/types/filters.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index a090f09a76ea2..23fbc912d739a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -7,7 +7,6 @@ import squel from 'squel'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index 5ac91bec849c2..2e053f9084296 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -6,7 +6,6 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/server/lib/build_bool_array.js b/x-pack/plugins/canvas/server/lib/build_bool_array.ts similarity index 66% rename from x-pack/plugins/canvas/server/lib/build_bool_array.js rename to x-pack/plugins/canvas/server/lib/build_bool_array.ts index f1cab93ceebbb..bd418394cf375 100644 --- a/x-pack/plugins/canvas/server/lib/build_bool_array.js +++ b/x-pack/plugins/canvas/server/lib/build_bool_array.ts @@ -5,10 +5,11 @@ */ import { getESFilter } from './get_es_filter'; +import { ExpressionValueFilter } from '../../types'; -const compact = (arr) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); +const compact = (arr: T[]) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); -export function buildBoolArray(canvasQueryFilterArray) { +export function buildBoolArray(canvasQueryFilterArray: ExpressionValueFilter[]) { return compact( canvasQueryFilterArray.map((clause) => { try { diff --git a/x-pack/plugins/canvas/server/lib/filters.js b/x-pack/plugins/canvas/server/lib/filters.js deleted file mode 100644 index afa58c7ee30c2..0000000000000 --- a/x-pack/plugins/canvas/server/lib/filters.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - TODO: This could be pluggable -*/ - -export function time(filter) { - if (!filter.column) { - throw new Error('column is required for Elasticsearch range filters'); - } - return { - range: { - [filter.column]: { gte: filter.from, lte: filter.to }, - }, - }; -} - -export function luceneQueryString(filter) { - return { - query_string: { - query: filter.query || '*', - }, - }; -} - -export function exactly(filter) { - return { - term: { - [filter.column]: { - value: filter.value, - }, - }, - }; -} diff --git a/x-pack/plugins/canvas/server/lib/filters.ts b/x-pack/plugins/canvas/server/lib/filters.ts new file mode 100644 index 0000000000000..9997640154e2c --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/filters.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FilterType, + ExpressionValueFilter, + CanvasTimeFilter, + CanvasLuceneFilter, + CanvasExactlyFilter, +} from '../../types'; + +/* + TODO: This could be pluggable +*/ + +const isTimeFilter = ( + maybeTimeFilter: ExpressionValueFilter +): maybeTimeFilter is CanvasTimeFilter => { + return maybeTimeFilter.filterType === FilterType.time; +}; +const isLuceneFilter = ( + maybeLuceneFilter: ExpressionValueFilter +): maybeLuceneFilter is CanvasLuceneFilter => { + return maybeLuceneFilter.filterType === FilterType.luceneQueryString; +}; +const isExactlyFilter = ( + maybeExactlyFilter: ExpressionValueFilter +): maybeExactlyFilter is CanvasExactlyFilter => { + return maybeExactlyFilter.filterType === FilterType.exactly; +}; + +export function time(filter: ExpressionValueFilter) { + if (!isTimeFilter(filter) || !filter.column) { + throw new Error('column is required for Elasticsearch range filters'); + } + return { + range: { + [filter.column]: { gte: filter.from, lte: filter.to }, + }, + }; +} + +export function luceneQueryString(filter: ExpressionValueFilter) { + if (!isLuceneFilter(filter)) { + throw new Error('Filter is not a lucene filter'); + } + return { + query_string: { + query: filter.query || '*', + }, + }; +} + +export function exactly(filter: ExpressionValueFilter) { + if (!isExactlyFilter(filter)) { + throw new Error('Filter is not an exactly filter'); + } + return { + term: { + [filter.column]: { + value: filter.value, + }, + }, + }; +} + +export const filters: Record = { + exactly, + time, + luceneQueryString, +}; diff --git a/x-pack/plugins/canvas/server/lib/get_es_filter.js b/x-pack/plugins/canvas/server/lib/get_es_filter.ts similarity index 75% rename from x-pack/plugins/canvas/server/lib/get_es_filter.js rename to x-pack/plugins/canvas/server/lib/get_es_filter.ts index 7c025ed8dee9b..acc222ecc376f 100644 --- a/x-pack/plugins/canvas/server/lib/get_es_filter.js +++ b/x-pack/plugins/canvas/server/lib/get_es_filter.ts @@ -10,11 +10,11 @@ filter is the abstracted canvas filter. */ -/*eslint import/namespace: ['error', { allowComputed: true }]*/ -import * as filters from './filters'; +import { filters } from './filters'; +import { ExpressionValueFilter } from '../../types'; -export function getESFilter(filter) { - if (!filters[filter.filterType]) { +export function getESFilter(filter: ExpressionValueFilter) { + if (!filter.filterType || !filters[filter.filterType]) { throw new Error(`Unknown filter type: ${filter.filterType}`); } diff --git a/x-pack/plugins/canvas/server/lib/normalize_type.js b/x-pack/plugins/canvas/server/lib/normalize_type.ts similarity index 89% rename from x-pack/plugins/canvas/server/lib/normalize_type.js rename to x-pack/plugins/canvas/server/lib/normalize_type.ts index fda2fbe631646..b684325aacba9 100644 --- a/x-pack/plugins/canvas/server/lib/normalize_type.js +++ b/x-pack/plugins/canvas/server/lib/normalize_type.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export function normalizeType(type) { - const normalTypes = { +export function normalizeType(type: string) { + const normalTypes: Record = { string: ['string', 'text', 'keyword', '_type', '_id', '_index', 'geo_point'], number: [ 'float', diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.js b/x-pack/plugins/canvas/server/lib/query_es_sql.js deleted file mode 100644 index 442703b00ea3a..0000000000000 --- a/x-pack/plugins/canvas/server/lib/query_es_sql.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { map, zipObject } from 'lodash'; -import { buildBoolArray } from './build_bool_array'; -import { sanitizeName } from './sanitize_name'; -import { normalizeType } from './normalize_type'; - -export const queryEsSQL = (elasticsearchClient, { count, query, filter, timezone }) => - elasticsearchClient('transport.request', { - path: '/_sql?format=json', - method: 'POST', - body: { - query, - time_zone: timezone, - fetch_size: count, - client_id: 'canvas', - filter: { - bool: { - must: [{ match_all: {} }, ...buildBoolArray(filter)], - }, - }, - }, - }) - .then((res) => { - const columns = res.columns.map(({ name, type }) => { - return { name: sanitizeName(name), type: normalizeType(type) }; - }); - const columnNames = map(columns, 'name'); - const rows = res.rows.map((row) => zipObject(columnNames, row)); - - if (!!res.cursor) { - elasticsearchClient('transport.request', { - path: '/_sql/close', - method: 'POST', - body: { - cursor: res.cursor, - }, - }).catch((e) => { - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); - } - - return { - type: 'datatable', - columns, - rows, - }; - }) - .catch((e) => { - if (e.message.indexOf('parsing_exception') > -1) { - throw new Error( - `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` - ); - } - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts new file mode 100644 index 0000000000000..c3c122d1e301a --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { zipObject } from 'lodash'; +import { queryEsSQL } from './query_es_sql'; +// @ts-expect-error +import { buildBoolArray } from './build_bool_array'; + +const response = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [ + ['foo', 'bar'], + ['buz', 'baz'], + ], + cursor: 'cursor-value', +}; + +const baseArgs = { + count: 1, + query: 'query', + filter: [], + timezone: 'timezone', +}; + +const getApi = (resp = response) => { + const api = jest.fn(); + api.mockResolvedValue(resp); + return api; +}; + +describe('query_es_sql', () => { + it('should call the api with the given args', async () => { + const api = getApi(); + + queryEsSQL(api, baseArgs); + + expect(api).toHaveBeenCalled(); + const givenArgs = api.mock.calls[0][1]; + + expect(givenArgs.body.fetch_size).toBe(baseArgs.count); + expect(givenArgs.body.query).toBe(baseArgs.query); + expect(givenArgs.body.time_zone).toBe(baseArgs.timezone); + }); + + it('formats the response', async () => { + const api = getApi(); + + const result = await queryEsSQL(api, baseArgs); + + const expectedColumns = response.columns.map((c) => ({ name: c.name, type: 'string' })); + const columnNames = expectedColumns.map((c) => c.name); + const expectedRows = response.rows.map((r) => zipObject(columnNames, r)); + + expect(result.type).toBe('datatable'); + expect(result.columns).toEqual(expectedColumns); + expect(result.rows).toEqual(expectedRows); + }); + + it('fetches pages until it has the requested count', async () => { + const pageOne = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [['foo', 'bar']], + cursor: 'cursor-value', + }; + + const pageTwo = { + rows: [['buz', 'baz']], + }; + + const api = getApi(pageOne); + api.mockReturnValueOnce(pageOne).mockReturnValueOnce(pageTwo); + + const result = await queryEsSQL(api, { ...baseArgs, count: 2 }); + expect(result.rows).toHaveLength(2); + }); + + it('closes any cursors that remain open', async () => { + const api = getApi(); + + await queryEsSQL(api, baseArgs); + expect(api.mock.calls[1][1].body.cursor).toBe(response.cursor); + }); + + it('throws on errors', async () => { + const api = getApi(); + api.mockRejectedValueOnce(new Error('parsing_exception')); + api.mockRejectedValueOnce(new Error('generic es error')); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: parsing_exception"` + ); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unexpected error from Elasticsearch: generic es error"` + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.ts new file mode 100644 index 0000000000000..8639cfa31dca8 --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { map, zipObject } from 'lodash'; +import { buildBoolArray } from './build_bool_array'; +import { sanitizeName } from './sanitize_name'; +import { normalizeType } from './normalize_type'; +import { LegacyAPICaller } from '../../../../../src/core/server'; +import { ExpressionValueFilter } from '../../types'; + +interface Args { + count: number; + query: string; + timezone?: string; + filter: ExpressionValueFilter[]; +} + +interface CursorResponse { + cursor?: string; + rows: string[][]; +} + +type QueryResponse = CursorResponse & { + columns: Array<{ + name: string; + type: string; + }>; + cursor?: string; + rows: string[][]; +}; + +export const queryEsSQL = async ( + elasticsearchClient: LegacyAPICaller, + { count, query, filter, timezone }: Args +) => { + try { + let response: QueryResponse = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + query, + time_zone: timezone, + fetch_size: count, + client_id: 'canvas', + filter: { + bool: { + must: [{ match_all: {} }, ...buildBoolArray(filter)], + }, + }, + }, + }); + + const columns = response.columns.map(({ name, type }) => { + return { name: sanitizeName(name), type: normalizeType(type) }; + }); + const columnNames = map(columns, 'name'); + let rows = response.rows.map((row) => zipObject(columnNames, row)); + + while (rows.length < count && response.cursor !== undefined) { + response = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + + rows = [...rows, ...response.rows.map((row) => zipObject(columnNames, row))]; + } + + if (response.cursor !== undefined) { + elasticsearchClient('transport.request', { + path: '/_sql/close', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + } + + return { + type: 'datatable', + columns, + rows, + }; + } catch (e) { + if (e.message.indexOf('parsing_exception') > -1) { + throw new Error( + `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` + ); + } + throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); + } +}; diff --git a/x-pack/plugins/canvas/server/lib/sanitize_name.js b/x-pack/plugins/canvas/server/lib/sanitize_name.ts similarity index 85% rename from x-pack/plugins/canvas/server/lib/sanitize_name.js rename to x-pack/plugins/canvas/server/lib/sanitize_name.ts index 4c787c816a331..781ab20509b36 100644 --- a/x-pack/plugins/canvas/server/lib/sanitize_name.js +++ b/x-pack/plugins/canvas/server/lib/sanitize_name.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function sanitizeName(name) { +export function sanitizeName(name: string) { // invalid characters const invalid = ['(', ')']; const pattern = invalid.map((v) => escapeRegExp(v)).join('|'); @@ -12,6 +12,6 @@ export function sanitizeName(name) { return name.replace(regex, '_'); } -function escapeRegExp(string) { +function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts index 7a9830124e305..000b7f6029952 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts @@ -8,7 +8,6 @@ import { mapValues, keys } from 'lodash'; import { schema } from '@kbn/config-schema'; import { API_ROUTE } from '../../../common/lib'; import { catchErrorHandler } from '../catch_error_handler'; -// @ts-expect-error unconverted lib import { normalizeType } from '../../lib/normalize_type'; import { RouteInitializerDeps } from '..'; diff --git a/x-pack/plugins/canvas/types/filters.ts b/x-pack/plugins/canvas/types/filters.ts new file mode 100644 index 0000000000000..356ebbbb76ac0 --- /dev/null +++ b/x-pack/plugins/canvas/types/filters.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExpressionValueFilter } from '.'; + +export enum FilterType { + luceneQueryString = 'luceneQueryString', + time = 'time', + exactly = 'exactly', +} + +export type CanvasTimeFilter = ExpressionValueFilter & { + filterType: typeof FilterType.time; + to: string; + from: string; +}; + +export type CanvasLuceneFilter = ExpressionValueFilter & { + filterType: typeof FilterType.luceneQueryString; + query: string; +}; + +export type CanvasExactlyFilter = ExpressionValueFilter & { + filterType: typeof FilterType.exactly; + value: string; + column: string; +}; + +export type CanvasFilter = CanvasTimeFilter | CanvasExactlyFilter | CanvasLuceneFilter; diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 0799627ce9b5a..f39c2d4367f9e 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -8,6 +8,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './filters'; export * from './functions'; export * from './renderers'; export * from './shortcuts'; From c66ea65ec1ca28172b93f4f5db8ae24e7023940e Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 31 Jul 2020 15:22:04 +0200 Subject: [PATCH 003/115] [APM] Use apmEventClient for querying APM event indices (#73449) Co-authored-by: Elastic Machine --- x-pack/plugins/apm/common/processor_event.ts | 14 + x-pack/plugins/apm/common/projections.ts | 16 ++ .../apm/common/projections/services.ts | 64 ----- .../app/ErrorGroupOverview/index.tsx | 4 +- .../components/app/RumDashboard/index.tsx | 4 +- .../components/app/ServiceMetrics/index.tsx | 4 +- .../app/ServiceNodeOverview/index.tsx | 4 +- .../components/app/ServiceOverview/index.tsx | 4 +- .../components/app/TraceOverview/index.tsx | 4 +- .../app/TransactionDetails/index.tsx | 4 +- .../app/TransactionOverview/index.tsx | 4 +- .../components/shared/KueryBar/index.tsx | 2 +- .../shared/LocalUIFilters/index.tsx | 4 +- .../shared/charts/MetricsChart/index.tsx | 2 +- .../context/UrlParamsContext/helpers.ts | 7 +- .../public/context/UrlParamsContext/types.ts | 4 +- .../public/hooks/useDynamicIndexPattern.ts | 4 +- .../apm/public/hooks/useLocalUIFilters.ts | 4 +- .../plugins/apm/public/utils/testHelpers.tsx | 5 +- .../get_all_environments.test.ts.snap | 42 +-- .../lib/environments/get_all_environments.ts | 25 +- .../errors/__snapshots__/queries.test.ts.snap | 33 ++- .../__snapshots__/queries.test.ts.snap | 22 +- .../__snapshots__/get_buckets.test.ts.snap | 11 +- .../__tests__/get_buckets.test.ts | 8 +- .../lib/errors/distribution/get_buckets.ts | 11 +- .../apm/server/lib/errors/get_error_group.ts | 12 +- .../apm/server/lib/errors/get_error_groups.ts | 25 +- .../call_client_with_debug.ts | 72 +++++ .../add_filter_to_exclude_legacy_data.ts | 31 +++ .../create_apm_event_client/index.ts | 91 +++++++ .../unpack_processor_events.ts | 61 +++++ .../create_internal_es_client/index.ts | 66 +++++ .../apm/server/lib/helpers/es_client.test.ts | 48 ---- .../apm/server/lib/helpers/es_client.ts | 228 ---------------- .../server/lib/helpers/setup_request.test.ts | 250 +++++++++--------- .../apm/server/lib/helpers/setup_request.ts | 33 +-- .../get_dynamic_index_pattern.ts | 19 +- .../__snapshots__/queries.test.ts.snap | 165 ++++++------ .../java/gc/fetch_and_transform_gc_metrics.ts | 8 +- .../metrics/fetch_and_transform_metrics.ts | 12 +- .../get_service_count.ts | 34 +-- .../get_transaction_coordinates.ts | 14 +- .../lib/observability_overview/has_data.ts | 32 +-- .../__snapshots__/queries.test.ts.snap | 44 ++- .../lib/rum_client/get_client_metrics.ts | 8 +- .../rum_client/get_page_load_distribution.ts | 12 +- .../lib/rum_client/get_page_view_trends.ts | 8 +- .../lib/rum_client/get_pl_dist_breakdown.ts | 18 +- .../server/lib/rum_client/get_rum_services.ts | 8 +- .../lib/rum_client/get_visitor_breakdown.ts | 8 +- .../fetch_service_paths_from_trace_ids.ts | 22 +- .../server/lib/service_map/get_service_map.ts | 8 +- .../get_service_map_service_node_info.test.ts | 4 +- .../get_service_map_service_node_info.ts | 42 +-- .../lib/service_map/get_trace_sample_ids.ts | 17 +- .../__snapshots__/queries.test.ts.snap | 33 ++- .../apm/server/lib/service_nodes/index.ts | 8 +- .../__snapshots__/queries.test.ts.snap | 153 ++++------- .../get_derived_service_annotations.ts | 25 +- .../lib/services/get_service_agent_name.ts | 21 +- .../lib/services/get_service_node_metadata.ts | 8 +- .../services/get_service_transaction_types.ts | 11 +- .../get_services/get_legacy_data_status.ts | 19 +- .../get_services/get_services_items.ts | 4 +- .../get_services/get_services_items_stats.ts | 132 +++------ .../get_services/has_historical_agent_data.ts | 39 +-- .../__snapshots__/queries.test.ts.snap | 27 +- .../create_or_update_configuration.ts | 2 +- .../get_agent_name_by_service.ts | 29 +- .../agent_configuration/get_service_names.ts | 31 +-- .../get_transaction.test.ts.snap | 25 +- .../create_or_update_custom_link.ts | 2 +- .../settings/custom_link/get_transaction.ts | 15 +- .../traces/__snapshots__/queries.test.ts.snap | 11 +- .../apm/server/lib/traces/get_trace_items.ts | 32 +-- .../__snapshots__/queries.test.ts.snap | 77 +++--- .../server/lib/transaction_groups/fetcher.ts | 9 +- .../lib/transaction_groups/get_error_rate.ts | 10 +- .../get_transaction_group_stats.ts | 11 +- .../__snapshots__/queries.test.ts.snap | 77 +++--- .../avg_duration_by_browser/fetcher.test.ts | 2 +- .../avg_duration_by_browser/fetcher.ts | 10 +- .../avg_duration_by_country/index.ts | 11 +- .../lib/transactions/breakdown/index.test.ts | 2 +- .../lib/transactions/breakdown/index.ts | 11 +- .../__snapshots__/fetcher.test.ts.snap | 11 +- .../get_timeseries_data/fetcher.test.ts | 14 +- .../charts/get_timeseries_data/fetcher.ts | 11 +- .../distribution/get_buckets/fetcher.ts | 12 +- .../distribution/get_distribution_max.ts | 11 +- .../lib/transactions/get_transaction/index.ts | 14 +- .../get_transaction_by_trace/index.ts | 16 +- .../__snapshots__/queries.test.ts.snap | 42 +-- .../server/lib/ui_filters/get_environments.ts | 23 +- .../__snapshots__/queries.test.ts.snap | 21 +- .../get_local_filter_query.ts | 4 +- .../lib/ui_filters/local_ui_filters/index.ts | 6 +- .../local_ui_filters/queries.test.ts | 2 +- .../{common => server}/projections/errors.ts | 15 +- .../{common => server}/projections/metrics.ts | 17 +- .../projections/rum_overview.ts | 13 +- .../projections/service_nodes.ts | 3 +- .../apm/server/projections/services.ts | 47 ++++ .../projections/transaction_groups.ts | 7 +- .../projections/transactions.ts | 15 +- .../{common => server}/projections/typings.ts | 16 +- .../util/merge_projection/index.test.ts | 21 +- .../util/merge_projection/index.ts | 8 +- .../plugins/apm/server/routes/ui_filters.ts | 16 +- 110 files changed, 1346 insertions(+), 1576 deletions(-) create mode 100644 x-pack/plugins/apm/common/projections.ts delete mode 100644 x-pack/plugins/apm/common/projections/services.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/helpers/es_client.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/helpers/es_client.ts rename x-pack/plugins/apm/{common => server}/projections/errors.ts (70%) rename x-pack/plugins/apm/{common => server}/projections/metrics.ts (72%) rename x-pack/plugins/apm/{common => server}/projections/rum_overview.ts (71%) rename x-pack/plugins/apm/{common => server}/projections/service_nodes.ts (88%) create mode 100644 x-pack/plugins/apm/server/projections/services.ts rename x-pack/plugins/apm/{common => server}/projections/transaction_groups.ts (86%) rename x-pack/plugins/apm/{common => server}/projections/transactions.ts (76%) rename x-pack/plugins/apm/{common => server}/projections/typings.ts (56%) rename x-pack/plugins/apm/{common => server}/projections/util/merge_projection/index.test.ts (73%) rename x-pack/plugins/apm/{common => server}/projections/util/merge_projection/index.ts (82%) diff --git a/x-pack/plugins/apm/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts index 3e8b0ba0e8b5e..cd8bcaa1de237 100644 --- a/x-pack/plugins/apm/common/processor_event.ts +++ b/x-pack/plugins/apm/common/processor_event.ts @@ -8,4 +8,18 @@ export enum ProcessorEvent { transaction = 'transaction', error = 'error', metric = 'metric', + span = 'span', + onboarding = 'onboarding', + sourcemap = 'sourcemap', } +/** + * Processor events that are searchable in the UI via the query bar. + * + * Some client-sideroutes will define 1 or more processor events that + * will be used to fetch the dynamic index pattern for the query bar. + */ + +export type UIProcessorEvent = + | ProcessorEvent.transaction + | ProcessorEvent.error + | ProcessorEvent.metric; diff --git a/x-pack/plugins/apm/common/projections.ts b/x-pack/plugins/apm/common/projections.ts new file mode 100644 index 0000000000000..a5fd9d3951cc9 --- /dev/null +++ b/x-pack/plugins/apm/common/projections.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum Projection { + services = 'services', + transactionGroups = 'transactionGroups', + traces = 'traces', + transactions = 'transactions', + metrics = 'metrics', + errorGroups = 'errorGroups', + serviceNodes = 'serviceNodes', + rumOverview = 'rumOverview', +} diff --git a/x-pack/plugins/apm/common/projections/services.ts b/x-pack/plugins/apm/common/projections/services.ts deleted file mode 100644 index 809caeeaf6088..0000000000000 --- a/x-pack/plugins/apm/common/projections/services.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - Setup, - SetupUIFilters, - SetupTimeRange, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../server/lib/helpers/setup_request'; -import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; - -export function getServicesProjection({ - setup, - noEvents, -}: { - setup: Setup & SetupTimeRange & SetupUIFilters; - noEvents?: boolean; -}) { - const { start, end, uiFiltersES, indices } = setup; - - return { - ...(noEvents - ? {} - : { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], - }), - body: { - size: 0, - query: { - bool: { - filter: [ - ...(noEvents - ? [] - : [ - { - terms: { - [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'], - }, - }, - ]), - { range: rangeFilter(start, end) }, - ...uiFiltersES, - ], - }, - }, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index fe2303d645ec9..92ea044720531 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -14,7 +14,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useFetcher } from '../../../hooks/useFetcher'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -79,7 +79,7 @@ function ErrorGroupOverview() { params: { serviceName, }, - projection: PROJECTION.ERROR_GROUPS, + projection: Projection.errorGroups, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 9b88202b2e5ef..8d1959ec14d15 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -13,7 +13,7 @@ import { } from '@elastic/eui'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { RumDashboard } from './RumDashboard'; import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -28,7 +28,7 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], - projection: PROJECTION.RUM_OVERVIEW, + projection: Projection.rumOverview, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 9af6a8d988c11..9b01f9ebb7e99 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -16,7 +16,7 @@ import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { MetricsChart } from '../../shared/charts/MetricsChart'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; interface ServiceMetricsProps { @@ -36,7 +36,7 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) { serviceName, serviceNodeName, }, - projection: PROJECTION.METRICS, + projection: Projection.metrics, showCount: false, }), [serviceName, serviceNodeName] diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 5537a73d228e8..3cde48aa483cb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; @@ -46,7 +46,7 @@ function ServiceNodeOverview() { params: { serviceName, }, - projection: PROJECTION.SERVICE_NODES, + projection: Projection.serviceNodes, }), [serviceName] ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 7d05ae90afb87..7146e471a7f82 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -15,7 +15,7 @@ import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; @@ -88,7 +88,7 @@ export function ServiceOverview() { const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['host', 'agentName'], - projection: PROJECTION.SERVICES, + projection: Projection.services, }), [] ); diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cdebb3aac129b..06b4459fb56eb 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -11,7 +11,7 @@ import { TraceList } from './TraceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TracesAPIResponse = APIReturnType<'/api/apm/traces'>; @@ -48,7 +48,7 @@ export function TraceOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'host', 'containerId', 'podName'], - projection: PROJECTION.TRACES, + projection: Projection.traces, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index c4d5be5874215..0dc2f607b1ef2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -27,7 +27,7 @@ import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; @@ -52,7 +52,7 @@ export function TransactionDetails() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'serviceVersion'], - projection: PROJECTION.TRANSACTIONS, + projection: Projection.transactions, params: { transactionName, transactionType, diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 98702fe3686ff..d9bd3e59d281f 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -35,7 +35,7 @@ import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; @@ -103,7 +103,7 @@ export function TransactionOverview() { serviceName, transactionType, }, - projection: PROJECTION.TRANSACTION_GROUPS, + projection: Projection.transactionGroups, }), [serviceName, transactionType] ); diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 502f5f0034b5f..6c605886e6e00 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -9,7 +9,7 @@ import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; -// @ts-ignore +// @ts-expect-error import { Typeahead } from './Typeahead'; import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index fedf96b4cc4ea..ba700e68b59bc 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -17,10 +17,10 @@ import styled from 'styled-components'; import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; interface Props { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; showCount?: boolean; diff --git a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 61632700b81d8..5b167e8160ffa 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -7,7 +7,7 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; -// @ts-ignore +// @ts-expect-error import CustomPlot from '../CustomPlot'; import { asDecimal, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts index d9781400f2272..65514ff71d02b 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -7,10 +7,13 @@ import { compact, pickBy } from 'lodash'; import datemath from '@elastic/datemath'; import { IUrlParams } from './types'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; interface PathParams { - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; serviceName?: string; errorGroupId?: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts index 78fe662b88d75..7b50a705afa33 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts @@ -6,7 +6,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { UIProcessorEvent } from '../../../common/processor_event'; export type IUrlParams = { detailTab?: string; @@ -32,6 +32,6 @@ export type IUrlParams = { pageSize?: number; serviceNodeName?: string; searchTerm?: string; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; traceIdLink?: string; } & Partial>; diff --git a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts index 64f333d72f0f5..0b4978acdfcb1 100644 --- a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts @@ -5,10 +5,10 @@ */ import { useFetcher } from './useFetcher'; -import { ProcessorEvent } from '../../common/processor_event'; +import { UIProcessorEvent } from '../../common/processor_event'; export function useDynamicIndexPattern( - processorEvent: ProcessorEvent | undefined + processorEvent: UIProcessorEvent | undefined ) { const { data, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 3354e676cf323..45ede7e7f2607 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -17,7 +17,7 @@ import { import { history } from '../utils/history'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { PROJECTION } from '../../common/projections/typings'; +import { Projection } from '../../common/projections'; import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; @@ -35,7 +35,7 @@ export function useLocalUIFilters({ filterNames, params, }: { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; }) { diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 418312743c324..e750102de2baa 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -99,10 +99,9 @@ export function expectTextsInDocument(output: any, texts: string[]) { } interface MockSetup { - dynamicIndexPattern: any; start: number; end: number; - client: any; + apmEventClient: any; internalClient: any; config: APMConfig; uiFiltersES: ESFilter[]; @@ -148,7 +147,7 @@ export async function inspectSearchParams( const mockSetup = { start: 1528113600000, end: 1528977600000, - client: { search: spy } as any, + apmEventClient: { search: spy } as any, internalClient: { search: spy } as any, config: new Proxy({}, { get: () => 'myIndex' }) as APMConfig, uiFiltersES: [{ term: { 'my.custom.ui.filter': 'foo-bar' } }], diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap index b943102b39de8..da2309afa07cf 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap @@ -2,6 +2,13 @@ exports[`getAllEnvironments fetches all environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -15,15 +22,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -34,16 +32,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`getAllEnvironments fetches all environments with includeMissing 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -57,15 +57,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -76,10 +67,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 9b17033a1f2a5..423b87cb78c3c 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; @@ -21,7 +21,7 @@ export async function getAllEnvironments({ setup: Setup; includeMissing?: boolean; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; // omit filter for service.name if "All" option is selected const serviceNameFilter = serviceName @@ -29,21 +29,18 @@ export async function getAllEnvironments({ : []; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ...serviceNameFilter, - ], + filter: [...serviceNameFilter], }, }, aggs: { @@ -58,7 +55,7 @@ export async function getAllEnvironments({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const environments = resp.aggregations?.environments.buckets.map( (bucket) => bucket.key as string diff --git a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index 982ad558dc91d..63b6c9cde4d0d 100644 --- a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error queries fetches a single error group 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "query": Object { "bool": Object { @@ -11,11 +16,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "error.grouping_key": "groupId", @@ -57,12 +57,16 @@ Object { }, ], }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -104,11 +108,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups when sortField = latestOccurrenceAt 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -180,11 +183,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -204,6 +202,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap index b71b2d697126a..ea142ca2acc00 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error distribution queries fetches an error distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -19,11 +24,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -48,12 +48,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error distribution queries fetches an error distribution with a group id 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -71,11 +75,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -105,6 +104,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap index d336d71424750..085bedf774c46 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should make the correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -21,11 +26,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -50,7 +50,6 @@ Array [ }, "size": 0, }, - "index": "apm-*", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 5f23a9329a583..e0df4d7744610 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { getBuckets } from '../get_buckets'; import { APMConfig } from '../../../..'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let clientSpy: jest.Mock; @@ -29,7 +29,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { + apmEventClient: { search: clientSpy, } as any, internalClient: { @@ -66,8 +66,6 @@ describe('timeseriesFetcher', () => { it('should limit query results to error documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([{ term: { [PROCESSOR_EVENT]: 'error' } }]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.error]); }); }); diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index db36ad1ede91c..de6df15354e79 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../../common/utils/range_filter'; @@ -28,9 +28,8 @@ export async function getBuckets({ bucketSize: number; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -41,7 +40,9 @@ export async function getBuckets({ } const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, query: { @@ -65,7 +66,7 @@ export async function getBuckets({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const buckets = (resp.aggregations?.distribution.buckets || []).map( (bucket) => ({ diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 3d20f84ccfbc2..b23c955b57183 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_SAMPLED, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, @@ -32,17 +31,18 @@ export async function getErrorGroup({ groupId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { size: 1, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [ERROR_GROUP_ID]: groupId } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -57,7 +57,7 @@ export async function getErrorGroup({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const error = resp.hits.hits[0]?._source; const transactionId = error?.transaction?.id; const traceId = error?.trace?.id; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index ad216de271f37..ab1c2149be343 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -13,14 +13,13 @@ import { ERROR_LOG_MESSAGE, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getErrorGroupsProjection } from '../../../common/projections/errors'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getErrorGroupsProjection } from '../../projections/errors'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SortOptions } from '../../../typings/elasticsearch/aggregations'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -38,7 +37,7 @@ export async function getErrorGroups({ sortDirection?: 'asc' | 'desc'; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; @@ -92,23 +91,7 @@ export async function getErrorGroups({ }, }); - interface SampleError { - '@timestamp': APMError['@timestamp']; - error: { - log?: { - message: string; - }; - exception?: Array<{ - handled?: boolean; - message?: string; - type?: string; - }>; - culprit: APMError['error']['culprit']; - grouping_key: APMError['error']['grouping_key']; - }; - } - - const resp = await client.search(params); + const resp = await apmEventClient.search(params); // aggregations can be undefined when no matching indices are found. // this is an exception rather than the rule so the ES type does not account for this. diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts new file mode 100644 index 0000000000000..c475640595227 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-console */ + +import chalk from 'chalk'; +import { + LegacyAPICaller, + KibanaRequest, +} from '../../../../../../../src/core/server'; + +function formatObj(obj: Record) { + return JSON.stringify(obj, null, 2); +} + +export async function callClientWithDebug({ + apiCaller, + operationName, + params, + debug, + request, +}: { + apiCaller: LegacyAPICaller; + operationName: string; + params: Record; + debug: boolean; + request: KibanaRequest; +}) { + const startTime = process.hrtime(); + + let res: any; + let esError = null; + try { + res = apiCaller(operationName, params); + } catch (e) { + // catch error and throw after outputting debug info + esError = e; + } + + if (debug) { + const highlightColor = esError ? 'bgRed' : 'inverse'; + const diff = process.hrtime(startTime); + const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const routeInfo = `${request.route.method.toUpperCase()} ${ + request.route.path + }`; + + console.log( + chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + ); + + if (operationName === 'search') { + console.log(`GET ${params.index}/_${operationName}`); + console.log(formatObj(params.body)); + } else { + console.log(chalk.bold('ES operation:'), operationName); + + console.log(chalk.bold('ES query:')); + console.log(formatObj(params)); + } + console.log(`\n`); + } + + if (esError) { + throw esError; + } + + return res; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts new file mode 100644 index 0000000000000..494cd6cbf0eec --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep } from 'lodash'; +import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fieldnames'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; + +/* + Adds a range query to the ES request to exclude legacy data +*/ + +export function addFilterToExcludeLegacyData( + params: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } +) { + const nextParams = cloneDeep(params); + + // add filter for omitting pre-7.x data + nextParams.body.query.bool.filter.push({ + range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, + }); + + return nextParams; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts new file mode 100644 index 0000000000000..2bfd3c94ed34c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ValuesType } from 'utility-types'; +import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESSearchResponse, +} from '../../../../../typings/elasticsearch'; +import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data'; +import { callClientWithDebug } from '../call_client_with_debug'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { unpackProcessorEvents } from './unpack_processor_events'; + +export type APMEventESSearchRequest = Omit & { + apm: { + events: ProcessorEvent[]; + }; +}; + +type TypeOfProcessorEvent = { + [ProcessorEvent.error]: APMError; + [ProcessorEvent.transaction]: Transaction; + [ProcessorEvent.span]: Span; + [ProcessorEvent.metric]: APMBaseDoc; + [ProcessorEvent.onboarding]: unknown; + [ProcessorEvent.sourcemap]: unknown; +}[T]; + +type ESSearchRequestOf = Omit< + TParams, + 'apm' +> & { index: string[] | string }; + +type TypedSearchResponse< + TParams extends APMEventESSearchRequest +> = ESSearchResponse< + TypeOfProcessorEvent>, + ESSearchRequestOf +>; + +export type APMEventClient = ReturnType; + +export function createApmEventClient({ + context, + request, + indices, + options: { includeFrozen } = { includeFrozen: false }, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; + indices: ApmIndicesConfig; + options: { + includeFrozen: boolean; + }; +}) { + const client = context.core.elasticsearch.legacy.client; + + return { + search( + params: TParams, + { includeLegacyData } = { includeLegacyData: false } + ): Promise> { + const withProcessorEventFilter = unpackProcessorEvents(params, indices); + + const withPossibleLegacyDataFilter = !includeLegacyData + ? addFilterToExcludeLegacyData(withProcessorEventFilter) + : withProcessorEventFilter; + + return callClientWithDebug({ + apiCaller: client.callAsCurrentUser, + operationName: 'search', + params: { + ...withPossibleLegacyDataFilter, + ignore_throttled: !includeFrozen, + }, + request, + debug: context.params.query._debug, + }); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts new file mode 100644 index 0000000000000..d35403ad35d94 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { uniq, defaultsDeep, cloneDeep } from 'lodash'; +import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; +import { APMEventESSearchRequest } from '.'; +import { + ApmIndicesConfig, + ApmIndicesName, +} from '../../../settings/apm_indices/get_apm_indices'; + +export const processorEventIndexMap: Record = { + [ProcessorEvent.transaction]: 'apm_oss.transactionIndices', + [ProcessorEvent.span]: 'apm_oss.spanIndices', + [ProcessorEvent.metric]: 'apm_oss.metricsIndices', + [ProcessorEvent.error]: 'apm_oss.errorIndices', + [ProcessorEvent.sourcemap]: 'apm_oss.sourcemapIndices', + [ProcessorEvent.onboarding]: 'apm_oss.onboardingIndices', +}; + +export function unpackProcessorEvents( + request: APMEventESSearchRequest, + indices: ApmIndicesConfig +) { + const { apm, ...params } = request; + + const index = uniq( + apm.events.map((event) => indices[processorEventIndexMap[event]]) + ); + + const withFilterForProcessorEvent: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } = defaultsDeep(cloneDeep(params), { + body: { + query: { + bool: { + filter: [], + }, + }, + }, + }); + + withFilterForProcessorEvent.body.query.bool.filter.push({ + terms: { + [PROCESSOR_EVENT]: apm.events, + }, + }); + + return { + index, + ...withFilterForProcessorEvent, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts new file mode 100644 index 0000000000000..072391606d574 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IndexDocumentParams, + IndicesCreateParams, + DeleteDocumentResponse, + DeleteDocumentParams, +} from 'elasticsearch'; +import { KibanaRequest } from 'src/core/server'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { + ESSearchResponse, + ESSearchRequest, +} from '../../../../../typings/elasticsearch'; +import { callClientWithDebug } from '../call_client_with_debug'; + +// `type` was deprecated in 7.0 +export type APMIndexDocumentParams = Omit, 'type'>; + +export type APMInternalClient = ReturnType; + +export function createInternalESClient({ + context, + request, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; +}) { + const { callAsInternalUser } = context.core.elasticsearch.legacy.client; + + const callEs = (operationName: string, params: Record) => { + return callClientWithDebug({ + apiCaller: callAsInternalUser, + operationName, + params, + request, + debug: context.params.query._debug, + }); + }; + + return { + search: async < + TDocument = unknown, + TSearchRequest extends ESSearchRequest = ESSearchRequest + >( + params: TSearchRequest + ): Promise> => { + return callEs('search', params); + }, + index: (params: APMIndexDocumentParams) => { + return callEs('index', params); + }, + delete: ( + params: Omit + ): Promise => { + return callEs('delete', params); + }, + indicesCreate: (params: IndicesCreateParams) => { + return callEs('indices.create', params); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts deleted file mode 100644 index 61c9d751bf533..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isApmIndex } from './es_client'; - -describe('isApmIndex', () => { - const apmIndices = [ - 'apm-*-metric-*', - 'apm-*-onboarding-*', - 'apm-*-span-*', - 'apm-*-transaction-*', - 'apm-*-error-*', - ]; - describe('when indexParam is a string', () => { - it('should return true if it matches any of the items in apmIndices', () => { - const indexParam = 'apm-*-transaction-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it('should return false if it does not match any of the items in `apmIndices`', () => { - const indexParam = '.ml-anomalies-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is an array', () => { - it('should return true if all values in `indexParam` matches values in `apmIndices`', () => { - const indexParam = ['apm-*-transaction-*', 'apm-*-span-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it("should return false if some of the values don't match with `apmIndices`", () => { - const indexParam = ['apm-*-transaction-*', '.ml-anomalies-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is neither a string or an array', () => { - it('should return false', () => { - [true, false, undefined].forEach((indexParam) => { - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts deleted file mode 100644 index 2d730933e2473..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-console */ -import { - IndexDocumentParams, - SearchParams, - IndicesCreateParams, - DeleteDocumentResponse, - DeleteDocumentParams, -} from 'elasticsearch'; -import { cloneDeep, isString, merge } from 'lodash'; -import { KibanaRequest } from 'src/core/server'; -import chalk from 'chalk'; -import { - ESSearchRequest, - ESSearchResponse, -} from '../../../typings/elasticsearch'; -import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { pickKeys } from '../../../common/utils/pick_keys'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -// `type` was deprecated in 7.0 -export type APMIndexDocumentParams = Omit, 'type'>; - -export interface IndexPrivileges { - has_all_requested: boolean; - index: Record; -} - -interface IndexPrivilegesParams { - index: Array<{ - names: string[] | string; - privileges: string[]; - }>; -} - -export function isApmIndex( - apmIndices: string[], - indexParam: SearchParams['index'] -) { - if (isString(indexParam)) { - return apmIndices.includes(indexParam); - } else if (Array.isArray(indexParam)) { - // return false if at least one of the indices is not an APM index - return indexParam.every((index) => apmIndices.includes(index)); - } - return false; -} - -function addFilterForLegacyData( - apmIndices: string[], - params: ESSearchRequest, - { includeLegacyData = false } = {} -): SearchParams { - // search across all data (including data) - if (includeLegacyData || !isApmIndex(apmIndices, params.index)) { - return params; - } - - const nextParams = merge( - { - body: { - query: { - bool: { - filter: [], - }, - }, - }, - }, - cloneDeep(params) - ); - - // add filter for omitting pre-7.x data - nextParams.body.query.bool.filter.push({ - range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, - }); - - return nextParams; -} - -// add additional params for search (aka: read) requests -function getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - includeLegacyData, -}: { - context: APMRequestHandlerContext; - params: ESSearchRequest; - indices: ApmIndicesConfig; - includeFrozen: boolean; - includeLegacyData?: boolean; -}) { - // Get indices for legacy data filter (only those which apply) - const apmIndices = Object.values( - pickKeys( - indices, - 'apm_oss.sourcemapIndices', - 'apm_oss.errorIndices', - 'apm_oss.onboardingIndices', - 'apm_oss.spanIndices', - 'apm_oss.transactionIndices', - 'apm_oss.metricsIndices' - ) - ); - return { - ...addFilterForLegacyData(apmIndices, params, { includeLegacyData }), // filter out pre-7.0 data - ignore_throttled: !includeFrozen, // whether to query frozen indices or not - }; -} - -interface APMOptions { - includeLegacyData: boolean; -} - -interface ClientCreateOptions { - clientAsInternalUser?: boolean; - indices: ApmIndicesConfig; - includeFrozen: boolean; -} - -export type ESClient = ReturnType; - -function formatObj(obj: Record) { - return JSON.stringify(obj, null, 2); -} - -export function getESClient( - context: APMRequestHandlerContext, - request: KibanaRequest, - { clientAsInternalUser = false, indices, includeFrozen }: ClientCreateOptions -) { - const { - callAsCurrentUser, - callAsInternalUser, - } = context.core.elasticsearch.legacy.client; - - async function callEs(operationName: string, params: Record) { - const startTime = process.hrtime(); - - let res: any; - let esError = null; - try { - res = clientAsInternalUser - ? await callAsInternalUser(operationName, params) - : await callAsCurrentUser(operationName, params); - } catch (e) { - // catch error and throw after outputting debug info - esError = e; - } - - if (context.params.query._debug) { - const highlightColor = esError ? 'bgRed' : 'inverse'; - const diff = process.hrtime(startTime); - const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; - const routeInfo = `${request.route.method.toUpperCase()} ${ - request.route.path - }`; - - console.log( - chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) - ); - - if (operationName === 'search') { - console.log(`GET ${params.index}/_${operationName}`); - console.log(formatObj(params.body)); - } else { - console.log(chalk.bold('ES operation:'), operationName); - - console.log(chalk.bold('ES query:')); - console.log(formatObj(params)); - } - console.log(`\n`); - } - - if (esError) { - throw esError; - } - - return res; - } - - return { - search: async < - TDocument = unknown, - TSearchRequest extends ESSearchRequest = {} - >( - params: TSearchRequest, - apmOptions?: APMOptions - ): Promise> => { - const nextParams = await getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - ...apmOptions, - }); - - return callEs('search', nextParams); - }, - index: (params: APMIndexDocumentParams) => { - return callEs('index', params); - }, - delete: ( - params: Omit - ): Promise => { - return callEs('delete', params); - }, - indicesCreate: (params: IndicesCreateParams) => { - return callEs('indices.create', params); - }, - hasPrivileges: ( - params: IndexPrivilegesParams - ): Promise => { - return callEs('transport.request', { - method: 'POST', - path: '/_security/user/_has_privileges', - body: params, - }); - }, - }; -} diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 5a4bc62b87486..d8dbd8273f476 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -7,6 +7,8 @@ import { setupRequest } from './setup_request'; import { APMConfig } from '../..'; import { APMRequestHandlerContext } from '../../routes/typings'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; jest.mock('../settings/apm_indices/get_apm_indices', () => ({ getApmIndices: async () => ({ @@ -93,163 +95,175 @@ function getMockRequest() { } describe('setupRequest', () => { - it('should call callWithRequest with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*', body: { foo: 'bar' } } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - it('should call callWithInternalUser with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { internalClient } = await setupRequest(mockContext, mockRequest); - await internalClient.search({ - index: 'apm-*', - body: { foo: 'bar' }, - } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsInternalUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - describe('observer.version_major filter', () => { - describe('if index is apm-*', () => { - it('should merge `observer.version_major` filter with existing boolean filters', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, - }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ + describe('with default args', () => { + it('calls callWithRequest', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { events: [ProcessorEvent.transaction] }, + body: { foo: 'bar' }, + }); + expect( + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', query: { bool: { filter: [ - { term: 'someTerm' }, + { terms: { 'processor.event': ['transaction'] } }, { range: { 'observer.version_major': { gte: 7 } } }, ], }, }, - }); + }, + ignore_throttled: true, }); + }); - it('should add `observer.version_major` filter if none exists', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*' }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }); + it('calls callWithInternalUser', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { internalClient } = await setupRequest(mockContext, mockRequest); + await internalClient.search({ + index: ['apm-*'], + body: { foo: 'bar' }, + } as any); + expect( + mockContext.core.elasticsearch.legacy.client.callAsInternalUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', + }, }); + }); + }); - it('should not add `observer.version_major` filter if `includeLegacyData=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search( - { - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + describe('with a bool filter', () => { + it('adds a range filter for `observer.version_major` to the existing filter', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { term: 'someTerm' }, + { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], }, - { - includeLegacyData: true, - } - ); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { bool: { filter: [{ term: 'someTerm' }] } }, - }); + }, }); }); - it('if index is not an APM index, it should not add `observer.version_major` filter', async () => { + it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => { const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: '.ml-*', - body: { - query: { bool: { filter: [{ term: 'someTerm' }] } }, + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search( + { + apm: { + events: [ProcessorEvent.error], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, }, - }); + { + includeLegacyData: true, + } + ); const params = mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock .calls[0][1]; expect(params.body).toEqual({ query: { bool: { - filter: [{ term: 'someTerm' }], + filter: [ + { term: 'someTerm' }, + { + terms: { + [PROCESSOR_EVENT]: ['error'], + }, + }, + ], }, }, }); }); }); +}); - describe('ignore_throttled', () => { - it('should set `ignore_throttled=true` if `includeFrozen=false`', async () => { - const { mockContext, mockRequest } = getMockRequest(); +describe('without a bool filter', () => { + it('adds a range filter for `observer.version_major`', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { terms: { [PROCESSOR_EVENT]: ['error'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], + }, + }, + }); + }); +}); - // mock includeFrozen to return false - mockContext.core.uiSettings.client.get.mockResolvedValue(false); +describe('with includeFrozen=false', () => { + it('sets `ignore_throttled=true`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return false + mockContext.core.uiSettings.client.get.mockResolvedValue(false); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(true); + await apmEventClient.search({ + apm: { + events: [], + }, }); - it('should set `ignore_throttled=false` if `includeFrozen=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(true); + }); +}); - // mock includeFrozen to return true - mockContext.core.uiSettings.client.get.mockResolvedValue(true); +describe('with includeFrozen=true', () => { + it('sets `ignore_throttled=false`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return true + mockContext.core.uiSettings.client.get.mockResolvedValue(true); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(false); + await apmEventClient.search({ + apm: { events: [] }, }); + + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(false); }); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 6f381d4945ab4..ddad2eb2d22dc 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -13,11 +13,17 @@ import { ApmIndicesConfig, } from '../settings/apm_indices/get_apm_indices'; import { ESFilter } from '../../../typings/elasticsearch'; -import { ESClient } from './es_client'; import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; import { APMRequestHandlerContext } from '../../routes/typings'; -import { getESClient } from './es_client'; import { ProcessorEvent } from '../../../common/processor_event'; +import { + APMEventClient, + createApmEventClient, +} from './create_es_client/create_apm_event_client'; +import { + APMInternalClient, + createInternalESClient, +} from './create_es_client/create_internal_es_client'; function decodeUiFilters(uiFiltersEncoded?: string) { if (!uiFiltersEncoded) { @@ -30,8 +36,8 @@ function decodeUiFilters(uiFiltersEncoded?: string) { // https://github.com/microsoft/TypeScript/issues/34933 export interface Setup { - client: ESClient; - internalClient: ESClient; + apmEventClient: APMEventClient; + internalClient: APMInternalClient; ml?: ReturnType; config: APMConfig; indices: ApmIndicesConfig; @@ -78,22 +84,19 @@ export async function setupRequest( context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); - const createClientOptions = { - indices, - includeFrozen, - }; - const uiFiltersES = decodeUiFilters(query.uiFilters); const coreSetupRequest = { indices, - client: getESClient(context, request, { - clientAsInternalUser: false, - ...createClientOptions, + apmEventClient: createApmEventClient({ + context, + request, + indices, + options: { includeFrozen }, }), - internalClient: getESClient(context, request, { - clientAsInternalUser: true, - ...createClientOptions, + internalClient: createInternalESClient({ + context, + request, }), ml: getMlSetup(context, request), config, diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index ee03e77de3580..cb30c6c064848 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -11,7 +11,10 @@ import { IIndexPattern, } from '../../../../../../src/plugins/data/server'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; import { APMRequestHandlerContext } from '../../routes/typings'; const cache = new LRU({ @@ -27,7 +30,7 @@ export const getDynamicIndexPattern = async ({ }: { context: APMRequestHandlerContext; indices: ApmIndicesConfig; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; }) => { const patternIndices = getPatternIndices(indices, processorEvent); const indexPatternTitle = patternIndices.join(','); @@ -75,17 +78,17 @@ export const getDynamicIndexPattern = async ({ function getPatternIndices( indices: ApmIndicesConfig, - processorEvent?: ProcessorEvent + processorEvent?: UIProcessorEvent ) { const indexNames = processorEvent ? [processorEvent] - : ['transaction' as const, 'metric' as const, 'error' as const]; + : [ProcessorEvent.transaction, ProcessorEvent.metric, ProcessorEvent.error]; const indicesMap = { - transaction: indices['apm_oss.transactionIndices'], - metric: indices['apm_oss.metricsIndices'], - error: indices['apm_oss.errorIndices'], + [ProcessorEvent.transaction]: indices['apm_oss.transactionIndices'], + [ProcessorEvent.metric]: indices['apm_oss.metricsIndices'], + [ProcessorEvent.error]: indices['apm_oss.errorIndices'], }; - return indexNames.map((name) => indicesMap[name]); + return indexNames.map((name) => indicesMap[name as UIProcessorEvent]); } diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index d8119ac96a536..b88c90a213c67 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`metrics queries with a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -66,11 +71,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -95,12 +95,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -155,11 +159,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -189,12 +188,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -251,11 +254,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -290,12 +288,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -350,11 +352,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -384,12 +381,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -434,11 +435,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -468,12 +464,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -538,11 +538,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -573,12 +568,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -633,11 +632,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -673,12 +667,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -735,11 +733,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -780,12 +773,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -840,11 +837,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -880,12 +872,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -930,11 +926,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -970,12 +961,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -1040,11 +1035,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1064,12 +1054,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -1124,11 +1118,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1153,12 +1142,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -1215,11 +1208,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1249,12 +1237,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -1309,11 +1301,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1338,12 +1325,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -1388,11 +1379,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1417,6 +1403,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 3ed6e4a944b51..e5c573ba1ec02 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -18,8 +18,8 @@ import { } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; import { ChartBase } from '../../../types'; -import { getMetricsProjection } from '../../../../../../common/projections/metrics'; -import { mergeProjection } from '../../../../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../../../../projections/metrics'; +import { mergeProjection } from '../../../../../projections/util/merge_projection'; import { AGENT_NAME, LABEL_NAME, @@ -42,7 +42,7 @@ export async function fetchAndTransformGcMetrics({ chartBase: ChartBase; fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const { bucketSize } = getBucketSize(start, end, 'auto'); @@ -105,7 +105,7 @@ export async function fetchAndTransformGcMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); const { aggregations } = response; diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 895920a9b6c7d..f6e201b395c37 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -5,7 +5,6 @@ */ import { Unionize, Overwrite } from 'utility-types'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange, @@ -14,9 +13,10 @@ import { import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { ChartBase } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; -import { getMetricsProjection } from '../../../common/projections/metrics'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../projections/metrics'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; type MetricsAggregationMap = Unionize<{ min: AggregationOptionsByType['min']; @@ -28,7 +28,7 @@ type MetricsAggregationMap = Unionize<{ type MetricAggs = Record; export type GenericMetricsRequest = Overwrite< - ESSearchRequest, + APMEventESSearchRequest, { body: { aggs: { @@ -65,7 +65,7 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const projection = getMetricsProjection({ setup, @@ -91,7 +91,7 @@ export async function fetchAndTransformMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); return transformDataToMetricsChart(response, chartBase); } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts index 4c4d058c7139d..8a1f3cb0e0149 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts @@ -6,10 +6,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { - SERVICE_NAME, - PROCESSOR_EVENT, -} from '../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getServiceCount({ @@ -17,36 +14,27 @@ export async function getServiceCount({ }: { setup: Setup & SetupTimeRange; }) { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { range: rangeFilter(start, end) }, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.transaction, - ProcessorEvent.metric, - ], - }, - }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); return aggregations?.serviceCount.value || 0; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts index 0d1a4274c16dc..116b37a395299 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts @@ -10,7 +10,6 @@ */ import { rangeFilter } from '../../../common/utils/range_filter'; import { Coordinates } from '../../../../observability/public'; -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ProcessorEvent } from '../../../common/processor_event'; @@ -21,18 +20,17 @@ export async function getTransactionCoordinates({ setup: Setup & SetupTimeRange; bucketSize: string; }): Promise { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; - const { aggregations } = await client.search({ - index: indices['apm_oss.transactionIndices'], + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { range: rangeFilter(start, end) }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index fc7445ab4a225..66d82b9f88355 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -3,41 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; export async function hasData({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; try { const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, terminateAfter: 1, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.metric, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response.hits.total.value > 0; } catch (e) { return false; diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 602eb88ba8940..c5264373ea495 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`rum client dashboard queries fetches client metrics 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "backEnd": Object { @@ -34,11 +39,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page load distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "durPercentiles": Object { @@ -101,11 +105,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -126,12 +125,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page view trends 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "pageViews": Object { @@ -154,11 +157,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -179,12 +177,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches rum services 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -206,11 +208,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -231,6 +228,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index 8b3f733fc402a..194c136e2b3d0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -45,9 +45,9 @@ export async function getClientMetrics({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { backEnd, domInteractive, pageViews } = response.aggregations!; // Divide by 1000 to convert ms into seconds diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index e847a87264759..2a0c709ea9235 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -57,12 +57,12 @@ export async function getPageLoadDistribution({ }, }); - const { client } = setup; + const { apmEventClient } = setup; const { aggregations, hits: { total }, - } = await client.search(params); + } = await apmEventClient.search(params); if (total.value === 0) { return null; @@ -130,9 +130,9 @@ const getPercentilesDistribution = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDist = aggregations?.loadDistribution.values ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 30b2677d3c217..23169ddaca534 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -56,9 +56,9 @@ export async function getPageViewTrends({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.pageViews.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index ea9d701e64c3d..ffb06e649b9be 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -16,6 +17,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution'; @@ -53,11 +55,11 @@ export const getPageLoadDistBreakdown = async ( }); const params = mergeProjection(projection, { + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { breakdowns: { terms: { @@ -67,7 +69,7 @@ export const getPageLoadDistBreakdown = async ( aggs: { page_dist: { percentile_ranks: { - field: 'transaction.duration.us', + field: TRANSACTION_DURATION, values: stepValues, keyed: false, hdr: { @@ -81,9 +83,9 @@ export const getPageLoadDistBreakdown = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDistBreakdowns = aggregations?.breakdowns.buckets; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts index 5957a25239307..9bfa109f00faf 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; export async function getRumServices({ setup, @@ -38,9 +38,9 @@ export async function getRumServices({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.services.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index a14affb6eeec5..3681923b484b0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -55,9 +55,9 @@ export async function getVisitorBreakdown({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { browsers, os, devices } = response.aggregations!; return { diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts index 08c8aba5f0207..14047f4bacea9 100644 --- a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - PROCESSOR_EVENT, - TRACE_ID, -} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { TRACE_ID } from '../../../common/elasticsearch_fieldnames'; import { ConnectionNode, ExternalConnectionNode, @@ -18,23 +16,17 @@ export async function fetchServicePathsFromTraceIds( setup: Setup, traceIds: string[] ) { - const { indices, client } = setup; + const { apmEventClient } = setup; const serviceMapParams = { - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ - { - terms: { - [PROCESSOR_EVENT]: ['span', 'transaction'], - }, - }, { terms: { [TRACE_ID]: traceIds, @@ -212,7 +204,7 @@ export async function fetchServicePathsFromTraceIds( }, }; - const serviceMapFromTraceIdsScriptResponse = await client.search( + const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search( serviceMapParams ); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index cd125f944f8a5..b162c3b61d928 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -10,8 +10,8 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getServicesProjection } from '../../../common/projections/services'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServicesProjection } from '../../projections/services'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; @@ -118,9 +118,9 @@ async function getServicesData(options: IEnvOptions) { }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); return ( response.aggregations?.services.buckets.map((bucket) => { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index 1e0d001340edf..d1c99d778c8f0 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -12,7 +12,7 @@ describe('getServiceMapServiceNodeInfo', () => { describe('with no results', () => { it('returns null data', async () => { const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 0 } }, @@ -49,7 +49,7 @@ describe('getServiceMapServiceNodeInfo', () => { }); const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 1 } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 0f7136d6d74a4..330d38739a063 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -6,13 +6,12 @@ import { UIFilters } from '../../../typings/ui_filters'; import { + SERVICE_NAME, + TRANSACTION_DURATION, TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -109,17 +108,18 @@ async function getTransactionStats({ avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }> { - const { indices, client } = setup; + const { apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ ...filter, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { terms: { [TRANSACTION_TYPE]: [ @@ -135,8 +135,9 @@ async function getTransactionStats({ aggs: { duration: { avg: { field: TRANSACTION_DURATION } } }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); const docCount = response.hits.total.value; + return { avgTransactionDuration: response.aggregations?.duration.value ?? null, avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, @@ -147,18 +148,17 @@ async function getCpuStats({ setup, filter, }: TaskParameters): Promise<{ avgCpuUsage: number | null }> { - const { indices, client } = setup; + const { apmEventClient } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, - { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }, - ]), + filter: [...filter, { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }], }, }, aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } }, @@ -172,17 +172,19 @@ async function getMemoryStats({ setup, filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { - const { client, indices } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const { apmEventClient } = setup; + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: 'metric' } }, + filter: [ + ...filter, { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ]), + ], }, }, aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 11c3a00f32980..d6d681f24ab85 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq, take, sortBy } from 'lodash'; +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, TRACE_ID, @@ -26,18 +26,13 @@ export async function getTraceSampleIds({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices, config } = setup; + const { start, end, apmEventClient, config } = setup; const rangeQuery = { range: rangeFilter(start, end) }; const query = { bool: { filter: [ - { - term: { - [PROCESSOR_EVENT]: 'span', - }, - }, { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE, @@ -67,7 +62,9 @@ export async function getTraceSampleIds({ const samplerShardSize = traceIdBucketSize * 10; const params = { - index: [indices['apm_oss.spanIndices']], + apm: { + events: [ProcessorEvent.span], + }, body: { size: 0, query, @@ -126,9 +123,7 @@ export async function getTraceSampleIds({ }, }; - const tracesSampleResponse = await client.search( - params - ); + const tracesSampleResponse = await apmEventClient.search(params); // make sure at least one trace per composite/connection bucket // is queried diff --git a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap index 3935ecda42db9..87aca0d056909 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`service node queries fetches metadata for a service node 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -30,11 +35,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches metadata for unidentified service nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -93,11 +97,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches services nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nodes": Object { @@ -174,11 +177,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -197,6 +195,5 @@ Object { }, }, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index de66c242815a4..a83aba192dba9 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -9,8 +9,8 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { METRIC_PROCESS_CPU_PERCENT, @@ -26,7 +26,7 @@ const getServiceNodes = async ({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) => { - const { client } = setup; + const { apmEventClient } = setup; const projection = getServiceNodesProjection({ setup, serviceName }); @@ -66,7 +66,7 @@ const getServiceNodes = async ({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); if (!response.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 0fc1f89a3723b..ca86c1d93fa6e 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -2,48 +2,32 @@ exports[`services queries fetches the agent status 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "metric", + "sourcemap", + "transaction", + ], + }, "body": Object { - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "error", - "metric", - "sourcemap", - "transaction", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; exports[`services queries fetches the legacy data status 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "observer.version_major": Object { @@ -56,13 +40,19 @@ Object { }, "size": 0, }, - "index": "myIndex", "terminateAfter": 1, } `; exports[`services queries fetches the service agent name 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "transaction", + "metric", + ], + }, "body": Object { "aggs": Object { "agents": Object { @@ -80,15 +70,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "error", - "transaction", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -103,11 +84,6 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; @@ -115,6 +91,11 @@ Object { exports[`services queries fetches the service items 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -148,20 +129,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", - "size": 0, }, Object { + "apm": Object { + "events": Array [ + "metric", + "error", + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -198,27 +179,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "metric", - "error", - "transaction", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -245,19 +217,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -284,19 +255,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "metric", + "transaction", + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -330,31 +302,22 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, ] `; exports[`services queries fetches the service transaction types 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "types": Object { @@ -372,13 +335,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -393,6 +349,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 6a8aaf8dca8a6..ad3f47d443b87 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { isNumber } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SetupTimeRange, Setup } from '../../helpers/setup_request'; import { ESFilter } from '../../../../typings/elasticsearch'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; @@ -24,23 +24,24 @@ export async function getDerivedServiceAnnotations({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, ...getEnvironmentUiFilterES(environment), ]; const versions = ( - await client.search({ - index: indices['apm_oss.transactionIndices'], + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ range: rangeFilter(start, end) }), + filter: [...filter, { range: rangeFilter(start, end) }], }, }, aggs: { @@ -59,17 +60,15 @@ export async function getDerivedServiceAnnotations({ } const annotations = await Promise.all( versions.map(async (version) => { - const response = await client.search({ - index: indices['apm_oss.transactionIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ - term: { - [SERVICE_VERSION]: version, - }, - }), + filter: [...filter, { term: { [SERVICE_VERSION]: version } }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index 8d75d746c7fca..a95c27df0e502 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, AGENT_NAME, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -15,24 +15,23 @@ export async function getServiceAgentName( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { - terms: { [PROCESSOR_EVENT]: ['error', 'transaction', 'metric'] }, - }, { range: rangeFilter(start, end) }, ], }, @@ -45,7 +44,7 @@ export async function getServiceAgentName( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agents.buckets[0]?.key as string | undefined; return { agentName }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index c2d9fa6c1df39..fca472b0ce8c2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -14,8 +14,8 @@ import { CONTAINER_ID, } from '../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; export async function getServiceNodeMetadata({ serviceName, @@ -26,7 +26,7 @@ export async function getServiceNodeMetadata({ serviceNodeName: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; const query = mergeProjection( getServiceNodesProjection({ @@ -55,7 +55,7 @@ export async function getServiceNodeMetadata({ } ); - const response = await client.search(query); + const response = await apmEventClient.search(query); return { host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index d88be4055dc21..6c6e03ab0b46f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; @@ -15,17 +15,18 @@ export async function getServiceTransactionTypes( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, { range: rangeFilter(start, end) }, ], }, @@ -38,7 +39,7 @@ export async function getServiceTransactionTypes( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const transactionTypes = aggregations?.types.buckets.map((bucket) => bucket.key as string) || []; return { transactionTypes }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts index dde726c51393f..1be95967cb47a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts @@ -4,33 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - OBSERVER_VERSION_MAJOR, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { OBSERVER_VERSION_MAJOR } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; // returns true if 6.x data is found export async function getLegacyDataStatus(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, - { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }, - ], + filter: [{ range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }], }, }, }, }; - const resp = await client.search(params, { includeLegacyData: true }); + const resp = await apmEventClient.search(params, { includeLegacyData: true }); const hasLegacyData = resp.hits.total.value > 0; return hasLegacyData; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 14772e77fe1c2..d888b43b63fac 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -10,7 +10,7 @@ import { SetupTimeRange, SetupUIFilters, } from '../../helpers/setup_request'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; import { getTransactionDurationAverages, getAgentNames, @@ -25,7 +25,7 @@ export type ServicesItemsProjection = ReturnType; export async function getServicesItems(setup: ServicesItemsSetup) { const params = { - projection: getServicesProjection({ setup, noEvents: true }), + projection: getServicesProjection({ setup }), setup, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index de699028f5675..ddce3b667a603 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -5,12 +5,11 @@ */ import { - PROCESSOR_EVENT, TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, } from '../../../../common/elasticsearch_fieldnames'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; +import { mergeProjection } from '../../../projections/util/merge_projection'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ServicesItemsSetup, @@ -31,22 +30,15 @@ export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; + const { apmEventClient } = setup; - const response = await client.search( + const response = await apmEventClient.search( mergeProjection(projection, { - size: 0, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { - query: { - bool: { - filter: projection.body.query.bool.filter.concat({ - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }), - }, - }, + size: 0, aggs: { services: { terms: { @@ -82,32 +74,18 @@ export const getAgentNames = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.error, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.metric, - ProcessorEvent.error, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -136,11 +114,7 @@ export const getAgentNames = async ({ return aggregations.services.buckets.map((bucket) => ({ serviceName: bucket.key as string, - agentName: (bucket.agent_name.hits.hits[0]?._source as { - agent: { - name: string; - }; - }).agent.name, + agentName: bucket.agent_name.hits.hits[0]?._source.agent.name, })); }; @@ -148,24 +122,14 @@ export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -199,24 +163,14 @@ export const getErrorRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.error, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -250,32 +204,18 @@ export const getEnvironments = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.transaction, + ProcessorEvent.error, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.transaction, - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts index 42f53fc93fa60..eed9f2588152d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts @@ -4,43 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; // Note: this logic is duplicated in tutorials/apm/envs/on_prem export async function hasHistoricalAgentData(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - indices['apm_oss.sourcemapIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.metric, + ProcessorEvent.sourcemap, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - 'error', - 'metric', - 'sourcemap', - 'transaction', - ], - }, - }, - ], - }, - }, }, }; - const resp = await client.search(params); - const hasHistorialAgentData = resp.hits.total.value > 0; - return hasHistorialAgentData; + const resp = await apmEventClient.search(params); + return resp.hits.total.value > 0; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 24a1840bc0ab8..2b465a0f87475 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -115,6 +115,13 @@ Object { exports[`agent configuration queries getServiceNames fetches service names 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -124,28 +131,8 @@ Object { }, }, }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 4d61e1e9ae284..86aeb95e165a0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -10,7 +10,7 @@ import { AgentConfiguration, AgentConfigurationIntake, } from '../../../../common/agent_configuration/configuration_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateConfiguration({ configurationId, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts index 39674ee57abf6..9f0e65d492a8f 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { AGENT_NAME } from '../../../../common/elasticsearch_fieldnames'; export async function getAgentNameByService({ @@ -18,25 +16,22 @@ export async function getAgentNameByService({ serviceName: string; setup: Setup; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - { term: { [SERVICE_NAME]: serviceName } }, - ], + filter: [{ term: { [SERVICE_NAME]: serviceName } }], }, }, aggs: { @@ -47,7 +42,7 @@ export async function getAgentNameByService({ }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agent_names.buckets[0]?.key; return agentName as string | undefined; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 068bb30ddcf79..8b6c1d82beab0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -4,37 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; export async function getServiceNames({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -46,7 +37,7 @@ export async function getServiceNames({ setup }: { setup: Setup }) { }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const serviceNames = resp.aggregations?.services.buckets .map((bucket) => bucket.key as string) diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap index a91641b592526..0649c8c38d29a 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap @@ -2,15 +2,15 @@ exports[`custom link get transaction fetches with all filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "terms": Object { "service.name": Array [ @@ -43,7 +43,6 @@ Object { }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } @@ -51,20 +50,18 @@ Object { exports[`custom link get transaction fetches without filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { - "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, - ], + "filter": Array [], }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts index 16a694c04c485..48b115619283c 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts @@ -8,9 +8,9 @@ import { CustomLink, CustomLinkES, } from '../../../../common/custom_link/custom_link_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; import { Setup } from '../../helpers/setup_request'; import { toESFormat } from './helper'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateCustomLink({ customLinkId, diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts index e3becc040580f..9bf489e768a4b 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import * as t from 'io-ts'; -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { filterOptionsRt } from './custom_link_types'; @@ -18,7 +16,7 @@ export async function getTransaction({ setup: Setup; filters?: t.TypeOf; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const esFilters = Object.entries(filters) // loops through the filters splitting the value by comma and removing white spaces @@ -32,19 +30,18 @@ export async function getTransaction({ const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, size: 1, body: { query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...esFilters, - ], + filter: esFilters, }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap index 0a9f9d38b2be7..3c521839b587e 100644 --- a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`trace queries fetches a trace 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "by_transaction_id": Object { @@ -20,11 +25,6 @@ Object { "trace.id": "foo", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -48,6 +48,5 @@ Object { }, "size": "myIndex", }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index f9374558dfeeb..17f9743ae9f00 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, TRANSACTION_DURATION, @@ -13,8 +13,6 @@ import { TRANSACTION_ID, ERROR_LOG_LEVEL, } from '../../../common/elasticsearch_fieldnames'; -import { Span } from '../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -28,19 +26,20 @@ export async function getTraceItems( traceId: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, config, indices } = setup; + const { start, end, apmEventClient, config } = setup; const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; const excludedLogLevels = ['debug', 'info', 'warning']; - const errorResponsePromise = client.search({ - index: indices['apm_oss.errorIndices'], + const errorResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ], must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, @@ -59,18 +58,16 @@ export async function getTraceItems( }, }); - const traceResponsePromise = client.search({ - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + const traceResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) }, ], should: { @@ -91,22 +88,17 @@ export async function getTraceItems( // explicit intermediary types to avoid TS "excessively deep" error PromiseValueType, PromiseValueType - // @ts-ignore ] = await Promise.all([errorResponsePromise, traceResponsePromise]); const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - const items = (traceResponse.hits.hits as Array<{ - _source: Transaction | Span; - }>).map((hit) => hit._source); + const items = traceResponse.hits.hits.map((hit) => hit._source); const errorFrequencies: { errorsPerTransaction: ErrorsPerTransaction; errorDocs: APMError[]; } = { - errorDocs: errorResponse.hits.hits.map( - ({ _source }) => _source as APMError - ), + errorDocs: errorResponse.hits.hits.map(({ _source }) => _source), errorsPerTransaction: errorResponse.aggregations?.by_transaction_id.buckets.reduce( (acc, current) => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index deca46f4ebd0c..0ea7bcf7ce8ab 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -3,6 +3,11 @@ exports[`transaction group queries fetches top traces 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -46,11 +51,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -84,10 +84,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -131,11 +135,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -152,10 +151,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -199,11 +202,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -220,7 +218,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] @@ -229,6 +226,11 @@ Array [ exports[`transaction group queries fetches top transactions 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -257,11 +259,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -298,10 +295,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -330,11 +331,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -354,10 +350,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -386,11 +386,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -410,10 +405,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -448,11 +447,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -472,7 +466,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 73bf1d01924e7..b06d1a8af3bc5 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -7,13 +7,12 @@ import { take, sortBy } from 'lodash'; import { Unionize } from 'utility-types'; import moment from 'moment'; import { joinByKey } from '../../../common/utils/join_by_key'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getTransactionGroupsProjection } from '../../projections/transaction_groups'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { Transaction } from '../../../typings/es_schemas/ui/transaction'; @@ -45,7 +44,9 @@ export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export type TransactionGroupRequestBase = ESSearchRequest & { +export type TransactionGroupRequestBase = ReturnType< + typeof getTransactionGroupsProjection +> & { body: { aggs: { transaction_groups: Unionize< diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 6a1ee8daad7c7..8fb2ceb30db85 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -5,7 +5,6 @@ */ import { mean } from 'lodash'; import { - PROCESSOR_EVENT, HTTP_RESPONSE_STATUS_CODE, TRANSACTION_NAME, TRANSACTION_TYPE, @@ -31,7 +30,7 @@ export async function getErrorRate({ transactionName?: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const transactionNamefilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -42,7 +41,6 @@ export async function getErrorRate({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: rangeFilter(start, end) }, { exists: { field: HTTP_RESPONSE_STATUS_CODE } }, ...transactionNamefilter, @@ -51,7 +49,9 @@ export async function getErrorRate({ ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -68,7 +68,7 @@ export async function getErrorRate({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const noHits = resp.hits.total.value === 0; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 59fb370113ec2..7d45f39e08a83 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -5,7 +5,6 @@ */ import { merge } from 'lodash'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { TRANSACTION_SAMPLED, TRANSACTION_DURATION, @@ -52,7 +51,7 @@ export async function getSamples({ request, setup }: MetricParams) { { '@timestamp': { order: 'desc' as const } }, ]; - const response = await setup.client.search({ + const response = await setup.apmEventClient.search({ ...params, body: { ...params.body, @@ -73,7 +72,7 @@ export async function getSamples({ request, setup }: MetricParams) { return { key: bucket.key as BucketKey, count: bucket.doc_count, - sample: bucket.sample.hits.hits[0]._source as Transaction, + sample: bucket.sample.hits.hits[0]._source, }; }); } @@ -87,7 +86,7 @@ export async function getAverages({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -108,7 +107,7 @@ export async function getSums({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -131,7 +130,7 @@ export async function getPercentiles({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index cc5900919f829..9bc4b1d69d9ac 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -2,15 +2,15 @@ exports[`transaction queries fetches a transaction 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.id": "foo", @@ -35,12 +35,16 @@ Object { }, "size": 1, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -146,11 +150,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -170,12 +169,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions for a transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -281,11 +284,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -310,12 +308,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -376,11 +378,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -405,12 +402,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -471,11 +472,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -505,12 +501,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type and transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -571,11 +571,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -610,12 +605,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "stats": Object { @@ -632,11 +631,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "baz", @@ -666,6 +660,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts index d8175a34ceb9f..278819ea20a83 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts @@ -15,7 +15,7 @@ describe('fetcher', () => { it('performs a search', async () => { const search = jest.fn(); const setup = ({ - client: { search }, + apmEventClient: { search }, indices: {}, uiFiltersES: [], } as unknown) as Setup & SetupTimeRange & SetupUIFilters; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index b4d98ec41fc2d..f68082dfaa1e1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -7,7 +7,6 @@ import { ESFilter } from '../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, USER_AGENT_NAME, @@ -23,7 +22,7 @@ import { ProcessorEvent } from '../../../../common/processor_event'; export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { - const { end, client, indices, start, uiFiltersES } = options.setup; + const { end, apmEventClient, start, uiFiltersES } = options.setup; const { serviceName, transactionName } = options; const { intervalString } = getBucketSize(start, end, 'auto'); @@ -32,7 +31,6 @@ export function fetcher(options: Options) { : []; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { range: rangeFilter(start, end) }, @@ -41,7 +39,9 @@ export function fetcher(options: Options) { ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -80,5 +80,5 @@ export function fetcher(options: Options) { }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index ea6213f64ee36..9bb42d2fa7aad 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { CLIENT_GEO_COUNTRY_ISO_CODE, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_TYPE, @@ -29,12 +29,14 @@ export async function getTransactionAvgDurationByCountry({ serviceName: string; transactionName?: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] : []; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { @@ -42,7 +44,6 @@ export async function getTransactionAvgDurationByCountry({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, ...transactionNameFilter, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } }, { range: rangeFilter(start, end) }, @@ -66,7 +67,7 @@ export async function getTransactionAvgDurationByCountry({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); if (!resp.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 85d4eab448c72..3c1618ed7715f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -26,7 +26,7 @@ function getMockSetup(esResponse: any) { return { start: 0, end: 500000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 3c48c14c2a471..7248399d1f93f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -5,6 +5,7 @@ */ import { flatten, orderBy, last } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, SPAN_SUBTYPE, @@ -13,7 +14,6 @@ import { TRANSACTION_TYPE, TRANSACTION_NAME, TRANSACTION_BREAKDOWN_COUNT, - PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { Setup, @@ -36,7 +36,7 @@ export async function getTransactionBreakdown({ transactionName?: string; transactionType: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const subAggs = { sum_all_self_times: { @@ -82,7 +82,6 @@ export async function getTransactionBreakdown({ const filters = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ]; @@ -92,7 +91,9 @@ export async function getTransactionBreakdown({ } const params = { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { @@ -110,7 +111,7 @@ export async function getTransactionBreakdown({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const formatBucket = ( aggs: diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap index 25ebb15fd73e8..7bc60a7fc7f1a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should call client with correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -64,11 +69,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -98,7 +98,6 @@ Array [ }, "size": 0, }, - "index": "myIndex", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index fb357040f5781..09e1287f032f5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { ESResponse, timeseriesFetcher } from './fetcher'; import { APMConfig } from '../../../../../server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let res: ESResponse; @@ -21,7 +21,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, @@ -54,15 +54,7 @@ describe('timeseriesFetcher', () => { it('should restrict results to only transaction documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([ - { - term: { - [PROCESSOR_EVENT]: 'transaction', - }, - } as any, - ]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.transaction]); }); it('should return correct response', () => { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 8e19af926ce02..1498c22e327d6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESFilter } from '../../../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -34,11 +34,10 @@ export function timeseriesFetcher({ transactionName: string | undefined; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const { intervalString } = getBucketSize(start, end, 'auto'); const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -54,7 +53,9 @@ export function timeseriesFetcher({ } const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter } }, @@ -95,5 +96,5 @@ export function timeseriesFetcher({ }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index 3f8bf635712be..bfe72bf7c00f9 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { ProcessorEvent } from '../../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRACE_ID, TRANSACTION_DURATION, @@ -32,17 +31,18 @@ export async function bucketFetcher( bucketSize: number, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { range: rangeFilter(start, end) }, @@ -85,7 +85,7 @@ export async function bucketFetcher( }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response; } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index 8289113fddae9..139dac3df1171 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -23,17 +23,18 @@ export async function getDistributionMax( transactionType: string, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { @@ -59,6 +60,6 @@ export async function getDistributionMax( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.aggregations ? resp.aggregations.stats.max : null; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index a7de93a3bf650..9aa1a8f4de87f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, @@ -27,16 +25,17 @@ export async function getTransaction({ traceId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const params = { - index: indices['apm_oss.transactionIndices'], + const resp = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 1, query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [TRANSACTION_ID]: transactionId } }, { term: { [TRACE_ID]: traceId } }, { range: rangeFilter(start, end) }, @@ -44,8 +43,7 @@ export async function getTransaction({ }, }, }, - }; + }); - const resp = await client.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts index ad4f58d85d188..8ba61c6c726a4 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; @@ -17,9 +15,12 @@ export async function getRootTransactionByTraceId( traceId: string, setup: Setup ) { - const { client, indices } = setup; + const { apmEventClient } = setup; + const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 1, query: { @@ -35,16 +36,13 @@ export async function getRootTransactionByTraceId( }, }, ], - filter: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ], + filter: [{ term: { [TRACE_ID]: traceId } }], }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return { transaction: resp.hits.hits[0]?._source, }; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap index 30e75f46ad5e7..d94b766aee6a8 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`ui filter queries fetches environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -14,15 +21,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -42,16 +40,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`ui filter queries fetches environments without a service name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -64,15 +64,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -87,10 +78,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index 3fca30634be6a..98f00bf8e6555 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -18,12 +18,9 @@ export async function getEnvironments( setup: Setup & SetupTimeRange, serviceName?: string ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const filter: ESFilter[] = [ - { terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } }, - { range: rangeFilter(start, end) }, - ]; + const filter: ESFilter[] = [{ range: rangeFilter(start, end) }]; if (serviceName) { filter.push({ @@ -32,11 +29,13 @@ export async function getEnvironments( } const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, body: { size: 0, query: { @@ -55,7 +54,7 @@ export async function getEnvironments( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const aggs = resp.aggregations; const environmentsBuckets = aggs?.environments.buckets || []; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap index e6b6a9a52adfe..5f38432719280 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "by_terms": Object { @@ -28,15 +35,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -56,10 +54,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts index e892284fd87cd..cfbd79d37c041 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -5,8 +5,8 @@ */ import { omit } from 'lodash'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; -import { Projection } from '../../../../common/projections/typings'; +import { mergeProjection } from '../../../projections/util/merge_projection'; +import { Projection } from '../../../projections/typings'; import { UIFilters } from '../../../../typings/ui_filters'; import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_es'; import { localUIFilters, LocalUIFilterName } from './config'; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index 3833b93c8d1f7..12c02679d0859 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -5,7 +5,7 @@ */ import { cloneDeep, orderBy } from 'lodash'; import { UIFilters } from '../../../../typings/ui_filters'; -import { Projection } from '../../../../common/projections/typings'; +import { Projection } from '../../../projections/typings'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { getLocalFilterQuery } from './get_local_filter_query'; import { Setup } from '../../helpers/setup_request'; @@ -26,7 +26,7 @@ export async function getLocalUIFilters({ uiFilters: UIFilters; localFilterNames: LocalUIFilterName[]; }) { - const { client } = setup; + const { apmEventClient } = setup; const projectionWithoutAggs = cloneDeep(projection); @@ -40,7 +40,7 @@ export async function getLocalUIFilters({ localUIFilterName: name, }); - const response = await client.search(query); + const response = await apmEventClient.search(query); const filter = localUIFilters[name]; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts index ac61910968850..92ee67de49314 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -9,7 +9,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../../../public/utils/testHelpers'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; describe('local ui filter queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/common/projections/errors.ts b/x-pack/plugins/apm/server/projections/errors.ts similarity index 70% rename from x-pack/plugins/apm/common/projections/errors.ts rename to x-pack/plugins/apm/server/projections/errors.ts index 390a8a0968102..49a0e9f479d26 100644 --- a/x-pack/plugins/apm/common/projections/errors.ts +++ b/x-pack/plugins/apm/server/projections/errors.ts @@ -8,15 +8,13 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, ERROR_GROUP_ID, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getErrorGroupsProjection({ setup, @@ -25,16 +23,17 @@ export function getErrorGroupsProjection({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; return { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ], diff --git a/x-pack/plugins/apm/common/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts similarity index 72% rename from x-pack/plugins/apm/common/projections/metrics.ts rename to x-pack/plugins/apm/server/projections/metrics.ts index 45998bfe82e96..eb80a6bc73248 100644 --- a/x-pack/plugins/apm/common/projections/metrics.ts +++ b/x-pack/plugins/apm/server/projections/metrics.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, - PROCESSOR_EVENT, SERVICE_NODE_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; -import { SERVICE_NODE_NAME_MISSING } from '../service_nodes'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { SERVICE_NODE_NAME_MISSING } from '../../common/service_nodes'; +import { ProcessorEvent } from '../../common/processor_event'; function getServiceNodeNameFilters(serviceNodeName?: string) { if (!serviceNodeName) { @@ -40,18 +38,19 @@ export function getMetricsProjection({ serviceName: string; serviceNodeName?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...getServiceNodeNameFilters(serviceNodeName), ...uiFiltersES, ]; return { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { diff --git a/x-pack/plugins/apm/common/projections/rum_overview.ts b/x-pack/plugins/apm/server/projections/rum_overview.ts similarity index 71% rename from x-pack/plugins/apm/common/projections/rum_overview.ts rename to x-pack/plugins/apm/server/projections/rum_overview.ts index b1218546d09ff..4588ec2a0451f 100644 --- a/x-pack/plugins/apm/common/projections/rum_overview.ts +++ b/x-pack/plugins/apm/server/projections/rum_overview.ts @@ -8,22 +8,21 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../elasticsearch_fieldnames'; -import { rangeFilter } from '../utils/range_filter'; +import { TRANSACTION_TYPE } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getRumOverviewProjection({ setup, }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: 'page-load' } }, { // Adding this filter to cater for some inconsistent rum data @@ -36,7 +35,9 @@ export function getRumOverviewProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/service_nodes.ts b/x-pack/plugins/apm/server/projections/service_nodes.ts similarity index 88% rename from x-pack/plugins/apm/common/projections/service_nodes.ts rename to x-pack/plugins/apm/server/projections/service_nodes.ts index 1bc68f51a26ed..87fe815a12d0d 100644 --- a/x-pack/plugins/apm/common/projections/service_nodes.ts +++ b/x-pack/plugins/apm/server/projections/service_nodes.ts @@ -8,9 +8,8 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { SERVICE_NODE_NAME } from '../elasticsearch_fieldnames'; +import { SERVICE_NODE_NAME } from '../../common/elasticsearch_fieldnames'; import { mergeProjection } from './util/merge_projection'; import { getMetricsProjection } from './metrics'; diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts new file mode 100644 index 0000000000000..18fa79f31d6f3 --- /dev/null +++ b/x-pack/plugins/apm/server/projections/services.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Setup, + SetupUIFilters, + SetupTimeRange, +} from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; + +export function getServicesProjection({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES } = setup; + + return { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [{ range: rangeFilter(start, end) }, ...uiFiltersES], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + }, + }, + }, + }, + }; +} diff --git a/x-pack/plugins/apm/common/projections/transaction_groups.ts b/x-pack/plugins/apm/server/projections/transaction_groups.ts similarity index 86% rename from x-pack/plugins/apm/common/projections/transaction_groups.ts rename to x-pack/plugins/apm/server/projections/transaction_groups.ts index 1708d89aad4ec..8aa085cccf82a 100644 --- a/x-pack/plugins/apm/common/projections/transaction_groups.ts +++ b/x-pack/plugins/apm/server/projections/transaction_groups.ts @@ -8,10 +8,11 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { TRANSACTION_NAME, PARENT_ID } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { + TRANSACTION_NAME, + PARENT_ID, +} from '../../common/elasticsearch_fieldnames'; import { Options } from '../../server/lib/transaction_groups/fetcher'; import { getTransactionsProjection } from './transactions'; import { mergeProjection } from './util/merge_projection'; diff --git a/x-pack/plugins/apm/common/projections/transactions.ts b/x-pack/plugins/apm/server/projections/transactions.ts similarity index 76% rename from x-pack/plugins/apm/common/projections/transactions.ts rename to x-pack/plugins/apm/server/projections/transactions.ts index b6cd73ca9aaad..f428a76a8b0cb 100644 --- a/x-pack/plugins/apm/common/projections/transactions.ts +++ b/x-pack/plugins/apm/server/projections/transactions.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, TRANSACTION_TYPE, - PROCESSOR_EVENT, TRANSACTION_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getTransactionsProjection({ setup, @@ -30,7 +28,7 @@ export function getTransactionsProjection({ transactionName?: string; transactionType?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -45,7 +43,6 @@ export function getTransactionsProjection({ const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, ...transactionNameFilter, ...transactionTypeFilter, ...serviceNameFilter, @@ -54,7 +51,9 @@ export function getTransactionsProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts similarity index 56% rename from x-pack/plugins/apm/common/projections/typings.ts rename to x-pack/plugins/apm/server/projections/typings.ts index 693795b09e1d0..77a5beaf54605 100644 --- a/x-pack/plugins/apm/common/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchRequest, ESSearchBody } from '../../typings/elasticsearch'; +import { ESSearchBody } from '../../typings/elasticsearch'; import { AggregationOptionsByType, AggregationInputMap, } from '../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; -export type Projection = Omit & { +export type Projection = Omit & { body: Omit & { aggs?: { [key: string]: { @@ -20,14 +21,3 @@ export type Projection = Omit & { }; }; }; - -export enum PROJECTION { - SERVICES = 'services', - TRANSACTION_GROUPS = 'transactionGroups', - TRACES = 'traces', - TRANSACTIONS = 'transactions', - METRICS = 'metrics', - ERROR_GROUPS = 'errorGroups', - SERVICE_NODES = 'serviceNodes', - RUM_OVERVIEW = 'rumOverview', -} diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts similarity index 73% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts index 33727fcb9c735..aa02c8898d218 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts @@ -10,10 +10,19 @@ describe('mergeProjection', () => { it('overrides arrays', () => { expect( mergeProjection( - { body: { query: { bool: { must: [{ terms: ['a'] }] } } } }, - { body: { query: { bool: { must: [{ term: 'b' }] } } } } + { + apm: { events: [] }, + body: { query: { bool: { must: [{ terms: ['a'] }] } } }, + }, + { + apm: { events: [] }, + body: { query: { bool: { must: [{ term: 'b' }] } } }, + } ) ).toEqual({ + apm: { + events: [], + }, body: { query: { bool: { @@ -32,8 +41,11 @@ describe('mergeProjection', () => { const termsAgg = { terms: { field: 'bar' } }; expect( mergeProjection( - { body: { query: {}, aggs: { foo: termsAgg } } }, + { apm: { events: [] }, body: { query: {}, aggs: { foo: termsAgg } } }, { + apm: { + events: [], + }, body: { aggs: { foo: { ...termsAgg, aggs: { bar: { terms: { field: 'baz' } } } }, @@ -42,6 +54,9 @@ describe('mergeProjection', () => { } ) ).toEqual({ + apm: { + events: [], + }, body: { query: {}, aggs: { diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts similarity index 82% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index 9dc1c815bf169..ea7267dd337c2 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -6,15 +6,13 @@ import { mergeWith, isPlainObject, cloneDeep } from 'lodash'; import { DeepPartial } from 'utility-types'; import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; -import { - ESSearchRequest, - ESSearchBody, -} from '../../../../typings/elasticsearch'; +import { ESSearchBody } from '../../../../typings/elasticsearch'; import { Projection } from '../../typings'; +import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; type PlainObject = Record; -type SourceProjection = Omit, 'body'> & { +type SourceProjection = Omit, 'body'> & { body: Omit, 'aggs'> & { aggs?: AggregationInputMap; }; diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index a47d72751dfc4..864f5033c9d62 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -13,23 +13,23 @@ import { SetupTimeRange, } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; -import { Projection } from '../../common/projections/typings'; +import { Projection } from '../projections/typings'; import { localUIFilterNames, LocalUIFilterName, } from '../lib/ui_filters/local_ui_filters/config'; import { getUiFiltersES } from '../lib/helpers/convert_ui_filters/get_ui_filters_es'; import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; -import { getServicesProjection } from '../../common/projections/services'; -import { getTransactionGroupsProjection } from '../../common/projections/transaction_groups'; -import { getMetricsProjection } from '../../common/projections/metrics'; -import { getErrorGroupsProjection } from '../../common/projections/errors'; -import { getTransactionsProjection } from '../../common/projections/transactions'; +import { getServicesProjection } from '../projections/services'; +import { getTransactionGroupsProjection } from '../projections/transaction_groups'; +import { getMetricsProjection } from '../projections/metrics'; +import { getErrorGroupsProjection } from '../projections/errors'; +import { getTransactionsProjection } from '../projections/transactions'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { jsonRt } from '../../common/runtime_types/json_rt'; -import { getServiceNodesProjection } from '../../common/projections/service_nodes'; -import { getRumOverviewProjection } from '../../common/projections/rum_overview'; +import { getServiceNodesProjection } from '../projections/service_nodes'; +import { getRumOverviewProjection } from '../projections/rum_overview'; export const uiFiltersEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/ui_filters/environments', From 3793ae538148a1cdd650db9ecca2a141404875ee Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 31 Jul 2020 09:57:07 -0400 Subject: [PATCH 004/115] Check for security first (#73821) Co-authored-by: Elastic Machine --- .../__test__/get_collection_status.test.js | 52 ++++++++++++++++--- .../setup/collection/get_collection_status.js | 7 +++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js b/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js index e56627369475b..083ebfb27fd51 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js @@ -10,7 +10,12 @@ import { getCollectionStatus } from '..'; import { getIndexPatterns } from '../../../cluster/get_index_patterns'; const liveClusterUuid = 'a12'; -const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = true) => { +const mockReq = ( + searchResult = {}, + securityEnabled = true, + userHasPermissions = true, + securityErrorMessage = null +) => { return { server: { newPlatform: { @@ -37,12 +42,14 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = }, }, plugins: { - xpack_main: { + monitoring: { info: { - isAvailable: () => true, - feature: () => ({ - isEnabled: () => securityEnabled, - }), + getSecurityFeature: () => { + return { + isAvailable: securityEnabled, + isEnabled: securityEnabled, + }; + }, }, }, elasticsearch: { @@ -61,6 +68,11 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = params && params.path === '/_security/user/_has_privileges' ) { + if (securityErrorMessage !== null) { + return Promise.reject({ + message: securityErrorMessage, + }); + } return Promise.resolve({ has_all_requested: userHasPermissions }); } if (type === 'transport.request' && params && params.path === '/_nodes') { @@ -245,6 +257,34 @@ describe('getCollectionStatus', () => { expect(result.kibana.detected.doesExist).to.be(true); }); + it('should work properly with an unknown security message', async () => { + const req = mockReq({ hits: { total: { value: 1 } } }, true, true, 'foobar'); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result._meta.hasPermissions).to.be(false); + }); + + it('should work properly with a known security message', async () => { + const req = mockReq( + { hits: { total: { value: 1 } } }, + true, + true, + 'no handler found for uri [/_security/user/_has_privileges] and method [POST]' + ); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + + it('should work properly with another known security message', async () => { + const req = mockReq( + { hits: { total: { value: 1 } } }, + true, + true, + 'Invalid index name [_security]' + ); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + it('should not work if the user does not have the necessary permissions', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, true, false); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 607503673276b..81cdfd6ecd172 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -233,6 +233,10 @@ function isBeatFromAPM(bucket) { } async function hasNecessaryPermissions(req) { + const securityFeature = req.server.plugins.monitoring.info.getSecurityFeature(); + if (!securityFeature.isAvailable || !securityFeature.isEnabled) { + return true; + } try { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); const response = await callWithRequest(req, 'transport.request', { @@ -250,6 +254,9 @@ async function hasNecessaryPermissions(req) { ) { return true; } + if (err.message.includes('Invalid index name [_security]')) { + return true; + } return false; } } From 9a1a6d35bc556a6debdd270f8cbf9ff720065b89 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 31 Jul 2020 10:04:58 -0400 Subject: [PATCH 005/115] Use defaultsDeep to match what monitoring is doing (#73325) Co-authored-by: Elastic Machine --- src/legacy/server/status/routes/api/register_stats.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 0221c7e0ea085..2cd780d21f681 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -19,6 +19,7 @@ import Joi from 'joi'; import boom from 'boom'; +import { defaultsDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { wrapAuthConfig } from '../../wrap_auth_config'; import { getKibanaInfoForStats } from '../../lib'; @@ -120,10 +121,9 @@ export function registerStatsApi(usageCollection, server, config, kbnServer) { }, }; } else { - accum = { - ...accum, - [usageKey]: usage[usageKey], - }; + // I don't think we need to it this for the above conditions, but do it for most as it will + // match the behavior done in monitoring/bulk_uploader + defaultsDeep(accum, { [usageKey]: usage[usageKey] }); } return accum; From 4454b30db2854296aa9903ac6152d453f29a82d8 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 31 Jul 2020 16:33:40 +0200 Subject: [PATCH 006/115] reset validation counter (#73459) --- .../vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index a9b542af68c9d..dd748ea2d3815 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -42,5 +42,7 @@ export const tsvbTelemetrySavedObjectType: SavedObjectsType = { migrations: { '7.7.0': flow(resetCount), '7.8.0': flow(resetCount), + '7.9.0': flow(resetCount), + '7.10.0': flow(resetCount), }, }; From d51e277c3e41ed3aabe6a114682f74a534207757 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 31 Jul 2020 08:34:27 -0600 Subject: [PATCH 007/115] [Security Solution][Detections] Fixes risk score mapping bug and updates copy on empty rules message (#73901) ## Summary Fixes issue where Rules with a `Risk Score Mapping` could not be created. Fixes copy for the Rules Table empty view that says all rules are disabled by default (no longer true for the `Elastic Endpoint Security Rule`)

### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) --- .../components/rules/pre_packaged_rules/translations.ts | 2 +- .../detections/components/rules/risk_score_mapping/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts index 49da7dbf6d514..9b0cec99b1b38 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts @@ -17,7 +17,7 @@ export const PRE_BUILT_MSG = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage', { defaultMessage: - 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', + 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules except the Elastic Endpoint Security rule are disabled. You can select additional rules you want to activate.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index 35816e82540d1..0f16cb99862a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -70,7 +70,7 @@ export const RiskScoreField = ({ { field: newField?.name ?? '', operator: 'equals', - value: undefined, + value: '', riskScore: undefined, }, ], From 747e9e47363581539d8a767ede9006c67f32479b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 31 Jul 2020 16:35:47 +0200 Subject: [PATCH 008/115] Stabilize graph test (#73918) --- x-pack/test/functional/apps/graph/graph.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index c2500dca78444..68e5045c1f36c 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -129,17 +129,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show venn when clicking a line', async function () { await buildGraph(); - const { edges } = await PageObjects.graph.getGraphObjects(); await PageObjects.graph.isolateEdge('test', '/test/wp-admin/'); await PageObjects.graph.stopLayout(); await PageObjects.common.sleep(1000); - const testTestWpAdminBlogEdge = edges.find( - ({ sourceNode, targetNode }) => - targetNode.label === '/test/wp-admin/' && sourceNode.label === 'test' - )!; - await testTestWpAdminBlogEdge.element.click(); + await browser.execute(() => { + const event = document.createEvent('SVGEvents'); + event.initEvent('click', true, true); + return document.getElementsByClassName('gphEdge')[0].dispatchEvent(event); + }); await PageObjects.common.sleep(1000); await PageObjects.graph.startLayout(); From df3e209262fbca24bdee9ed2353e077965a329b6 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 31 Jul 2020 10:39:01 -0400 Subject: [PATCH 009/115] [Uptime] Unskip alerting functional tests (#72963) * Unskip monitor status alert test. * Trying to resolve flakiness. * Remove commented code. * Simplify test expect. * Revert conditional block change. * Remove line in question. Co-authored-by: Elastic Machine --- .../functional/services/uptime/navigation.ts | 2 +- .../apps/uptime/alert_flyout.ts | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index ab511abf130a5..710923c886cbe 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -17,7 +17,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv if (await testSubjects.exists('uptimeSettingsToOverviewLink', { timeout: 0 })) { await testSubjects.click('uptimeSettingsToOverviewLink'); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); - } else if (!(await testSubjects.exists('uptimeOverviewPage', { timeout: 0 }))) { + } else { await PageObjects.common.navigateToApp('uptime'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 6cb74aff95be2..a6de87d6f7b1a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -8,8 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - // FLAKY: https://github.com/elastic/kibana/issues/65948 - describe.skip('uptime alerts', () => { + describe('uptime alerts', () => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); @@ -105,7 +104,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { alertTypeId, consumer, id, - params: { numTimes, timerange, locations, filters }, + params: { numTimes, timerangeUnit, timerangeCount, filters }, schedule: { interval }, tags, } = alert; @@ -119,14 +118,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(interval).to.eql('11m'); expect(tags).to.eql(['uptime', 'another']); expect(numTimes).to.be(3); - expect(timerange.from).to.be('now-1h'); - expect(timerange.to).to.be('now'); - expect(locations).to.eql(['mpls']); - expect(filters).to.eql( - '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + - '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' + expect(timerangeUnit).to.be('h'); + expect(timerangeCount).to.be(1); + expect(JSON.stringify(filters)).to.eql( + `{"url.port":["5678"],"observer.geo.name":["mpls"],"monitor.type":["http"],"tags":[]}` ); } finally { await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); From ff3877d61db17aa8343357f29f50a409e256e16a Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Fri, 31 Jul 2020 09:43:56 -0500 Subject: [PATCH 010/115] Hide Canvas toolbar close button when tray is closed (#73845) Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/public/components/toolbar/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/canvas/public/components/toolbar/index.js b/x-pack/plugins/canvas/public/components/toolbar/index.js index 16860063f8a45..a95371f5f032a 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/index.js +++ b/x-pack/plugins/canvas/public/components/toolbar/index.js @@ -44,6 +44,6 @@ export const Toolbar = compose( props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); }, }), - withState('tray', 'setTray', (props) => props.tray), + withState('tray', 'setTray', null), withState('showWorkpadManager', 'setShowWorkpadManager', false) )(Component); From 9c5bbe4e5f18df9e5f6212d24e7150b5f2cdcb84 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Fri, 31 Jul 2020 10:44:45 -0400 Subject: [PATCH 011/115] [ML] DF Analytics creation wizard: ensure user can switch back to form from JSON editor (#73752) * wip: add reducer action to switch to form * rename getFormStateFromJobConfig * wip: types fix * show destIndex input when switching back from editor * ensure validation up to date when switching to form * cannot switch back to form if advanced config * update types * localization fix --- .../details_step/details_step_form.tsx | 14 ++-- .../pages/analytics_creation/page.tsx | 71 +++++++++++-------- .../use_create_analytics_form/actions.ts | 5 +- .../use_create_analytics_form/reducer.ts | 57 +++++++++++++-- .../use_create_analytics_form/state.test.ts | 14 ++-- .../hooks/use_create_analytics_form/state.ts | 16 ++++- .../use_create_analytics_form.ts | 9 ++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 131 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 0ac237bb33e76..1d6a603caa817 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -44,7 +44,7 @@ export const DetailsStepForm: FC = ({ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setFormState } = actions; - const { form, cloneJob, isJobCreated } = state; + const { form, cloneJob, hasSwitchedToEditor, isJobCreated } = state; const { createIndexPattern, description, @@ -61,7 +61,9 @@ export const DetailsStepForm: FC = ({ resultsField, } = form; - const [destIndexSameAsId, setDestIndexSameAsId] = useState(cloneJob === undefined); + const [destIndexSameAsId, setDestIndexSameAsId] = useState( + cloneJob === undefined && hasSwitchedToEditor === false + ); const forceInput = useRef(null); @@ -90,7 +92,11 @@ export const DetailsStepForm: FC = ({ useEffect(() => { if (destinationIndexNameValid === true) { debouncedIndexCheck(); - } else if (destinationIndex.trim() === '' && destinationIndexNameExists === true) { + } else if ( + typeof destinationIndex === 'string' && + destinationIndex.trim() === '' && + destinationIndexNameExists === true + ) { setFormState({ destinationIndexNameExists: false }); } @@ -102,7 +108,7 @@ export const DetailsStepForm: FC = ({ useEffect(() => { if (destIndexSameAsId === true && !jobIdEmpty && jobIdValid) { setFormState({ destinationIndex: jobId }); - } else if (destIndexSameAsId === false) { + } else if (destIndexSameAsId === false && hasSwitchedToEditor === false) { setFormState({ destinationIndex: '' }); } }, [destIndexSameAsId, jobId]); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index 04dd25896d443..2f0e2ed3428c0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -6,7 +6,6 @@ import React, { FC, useEffect, useState } from 'react'; import { - EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -16,7 +15,7 @@ import { EuiSpacer, EuiSteps, EuiStepStatus, - EuiText, + EuiSwitch, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -48,9 +47,15 @@ export const Page: FC = ({ jobId }) => { const { currentIndexPattern } = mlContext; const createAnalyticsForm = useCreateAnalyticsForm(); - const { isAdvancedEditorEnabled } = createAnalyticsForm.state; - const { jobType } = createAnalyticsForm.state.form; - const { initiateWizard, setJobClone, switchToAdvancedEditor } = createAnalyticsForm.actions; + const { state } = createAnalyticsForm; + const { isAdvancedEditorEnabled, disableSwitchToForm } = state; + const { jobType } = state.form; + const { + initiateWizard, + setJobClone, + switchToAdvancedEditor, + switchToForm, + } = createAnalyticsForm.actions; useEffect(() => { initiateWizard(); @@ -170,34 +175,40 @@ export const Page: FC = ({ jobId }) => { - {isAdvancedEditorEnabled === false && ( - - + + - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch', - { - defaultMessage: 'Switch to json editor', - } - )} - - - - - )} + checked={isAdvancedEditorEnabled} + onChange={(e) => { + if (e.target.checked === true) { + switchToAdvancedEditor(); + } else { + switchToForm(); + } + }} + data-test-subj="mlAnalyticsCreateJobWizardAdvancedEditorSwitch" + /> + + {isAdvancedEditorEnabled === true && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 4bfee9f308313..5f3045696f170 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -25,6 +25,7 @@ export enum ACTION { SET_JOB_CONFIG, SET_JOB_IDS, SWITCH_TO_ADVANCED_EDITOR, + SWITCH_TO_FORM, SET_ESTIMATED_MODEL_MEMORY_LIMIT, SET_JOB_CLONE, } @@ -38,7 +39,8 @@ export type Action = | ACTION.OPEN_MODAL | ACTION.RESET_ADVANCED_EDITOR_MESSAGES | ACTION.RESET_FORM - | ACTION.SWITCH_TO_ADVANCED_EDITOR; + | ACTION.SWITCH_TO_ADVANCED_EDITOR + | ACTION.SWITCH_TO_FORM; } // Actions with custom payloads: | { type: ACTION.ADD_REQUEST_MESSAGE; requestMessage: FormMessage } @@ -71,6 +73,7 @@ export interface ActionDispatchers { setJobConfig: (payload: State['jobConfig']) => void; startAnalyticsJob: () => void; switchToAdvancedEditor: () => void; + switchToForm: () => void; setEstimatedModelMemoryLimit: (value: State['estimatedModelMemoryLimit']) => void; setJobClone: (cloneJob: DeepReadonly) => Promise; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index acdaf15cdf4b7..8d8421a116b91 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -8,13 +8,17 @@ import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; // @ts-ignore import numeral from '@elastic/numeral'; -import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; import { Action, ACTION } from './actions'; -import { getInitialState, getJobConfigFromFormState, State } from './state'; +import { + getInitialState, + getFormStateFromJobConfig, + getJobConfigFromFormState, + State, +} from './state'; import { isJobIdValid, validateModelMemoryLimitUnits, @@ -41,6 +45,7 @@ import { TRAINING_PERCENT_MAX, } from '../../../../common/analytics'; import { indexPatterns } from '../../../../../../../../../../src/plugins/data/public'; +import { isAdvancedConfig } from '../../components/action_clone/clone_button'; const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( ', ' @@ -458,13 +463,16 @@ export function reducer(state: State, action: Action): State { case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: let resultJobConfig; + let disableSwitchToForm = false; try { resultJobConfig = JSON.parse(collapseLiteralStrings(action.advancedEditorRawString)); + disableSwitchToForm = isAdvancedConfig(resultJobConfig); } catch (e) { return { ...state, advancedEditorRawString: action.advancedEditorRawString, isAdvancedEditorValidJson: false, + disableSwitchToForm: true, advancedEditorMessages: [], }; } @@ -473,6 +481,7 @@ export function reducer(state: State, action: Action): State { ...validateAdvancedEditor({ ...state, jobConfig: resultJobConfig }), advancedEditorRawString: action.advancedEditorRawString, isAdvancedEditorValidJson: true, + disableSwitchToForm, }; case ACTION.SET_FORM_STATE: @@ -538,17 +547,53 @@ export function reducer(state: State, action: Action): State { case ACTION.SWITCH_TO_ADVANCED_EDITOR: let { jobConfig } = state; - const isJobConfigEmpty = isEmpty(state.jobConfig); - if (isJobConfigEmpty) { - jobConfig = getJobConfigFromFormState(state.form); - } + jobConfig = getJobConfigFromFormState(state.form); + const shouldDisableSwitchToForm = isAdvancedConfig(jobConfig); + return validateAdvancedEditor({ ...state, advancedEditorRawString: JSON.stringify(jobConfig, null, 2), isAdvancedEditorEnabled: true, + disableSwitchToForm: shouldDisableSwitchToForm, + hasSwitchedToEditor: true, jobConfig, }); + case ACTION.SWITCH_TO_FORM: + const { jobConfig: config, jobIds } = state; + const { jobId } = state.form; + // @ts-ignore + const formState = getFormStateFromJobConfig(config, false); + + if (typeof jobId === 'string' && jobId.trim() !== '') { + formState.jobId = jobId; + } + + formState.jobIdExists = jobIds.some((id) => formState.jobId === id); + formState.jobIdEmpty = jobId === ''; + formState.jobIdValid = isJobIdValid(jobId); + formState.jobIdInvalidMaxLength = !!maxLengthValidator(JOB_ID_MAX_LENGTH)(jobId); + + formState.destinationIndexNameEmpty = formState.destinationIndex === ''; + formState.destinationIndexNameValid = isValidIndexName(formState.destinationIndex || ''); + formState.destinationIndexPatternTitleExists = + state.indexPatternsMap[formState.destinationIndex || ''] !== undefined; + + if (formState.numTopFeatureImportanceValues !== undefined) { + formState.numTopFeatureImportanceValuesValid = validateNumTopFeatureImportanceValues( + formState.numTopFeatureImportanceValues + ); + } + + return validateForm({ + ...state, + // @ts-ignore + form: formState, + isAdvancedEditorEnabled: false, + advancedEditorRawString: JSON.stringify(config, null, 2), + jobConfig: config, + }); + case ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT: return { ...state, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts index d397dfc315da4..499318ebddc19 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getCloneFormStateFromJobConfig, - getInitialState, - getJobConfigFromFormState, -} from './state'; +import { getFormStateFromJobConfig, getInitialState, getJobConfigFromFormState } from './state'; const regJobConfig = { id: 'reg-test-01', @@ -96,8 +92,8 @@ describe('useCreateAnalyticsForm', () => { ]); }); - test('state: getCloneFormStateFromJobConfig() regression', () => { - const clonedState = getCloneFormStateFromJobConfig(regJobConfig); + test('state: getFormStateFromJobConfig() regression', () => { + const clonedState = getFormStateFromJobConfig(regJobConfig); expect(clonedState?.sourceIndex).toBe('reg-test-index'); expect(clonedState?.includes).toStrictEqual([]); @@ -112,8 +108,8 @@ describe('useCreateAnalyticsForm', () => { expect(clonedState?.jobId).toBe(undefined); }); - test('state: getCloneFormStateFromJobConfig() outlier detection', () => { - const clonedState = getCloneFormStateFromJobConfig(outlierJobConfig); + test('state: getFormStateFromJobConfig() outlier detection', () => { + const clonedState = getFormStateFromJobConfig(outlierJobConfig); expect(clonedState?.sourceIndex).toBe('outlier-test-index'); expect(clonedState?.includes).toStrictEqual(['field', 'other_field']); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 725fc8751408e..69599f43ef297 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -12,6 +12,7 @@ import { DataFrameAnalyticsId, DataFrameAnalyticsConfig, ANALYSIS_CONFIG_TYPE, + defaultSearchQuery, } from '../../../../common/analytics'; import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; @@ -44,6 +45,7 @@ export interface FormMessage { export interface State { advancedEditorMessages: FormMessage[]; advancedEditorRawString: string; + disableSwitchToForm: boolean; form: { computeFeatureInfluence: string; createIndexPattern: boolean; @@ -97,6 +99,7 @@ export interface State { indexPatternsMap: SourceIndexMap; isAdvancedEditorEnabled: boolean; isAdvancedEditorValidJson: boolean; + hasSwitchedToEditor: boolean; isJobCreated: boolean; isJobStarted: boolean; isValid: boolean; @@ -110,6 +113,7 @@ export interface State { export const getInitialState = (): State => ({ advancedEditorMessages: [], advancedEditorRawString: '', + disableSwitchToForm: false, form: { computeFeatureInfluence: 'true', createIndexPattern: true, @@ -131,7 +135,7 @@ export const getInitialState = (): State => ({ jobIdInvalidMaxLength: false, jobIdValid: false, jobType: undefined, - jobConfigQuery: { match_all: {} }, + jobConfigQuery: defaultSearchQuery, jobConfigQueryString: undefined, lambda: undefined, loadingFieldOptions: false, @@ -167,6 +171,7 @@ export const getInitialState = (): State => ({ indexPatternsMap: {}, isAdvancedEditorEnabled: false, isAdvancedEditorValidJson: true, + hasSwitchedToEditor: false, isJobCreated: false, isJobStarted: false, isValid: false, @@ -283,8 +288,9 @@ function toCamelCase(property: string): string { * Extracts form state for a job clone from the analytics job configuration. * For cloning we keep job id and destination index empty. */ -export function getCloneFormStateFromJobConfig( - analyticsJobConfig: Readonly +export function getFormStateFromJobConfig( + analyticsJobConfig: Readonly, + isClone: boolean = true ): Partial { const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE; @@ -300,6 +306,10 @@ export function getCloneFormStateFromJobConfig( includes: analyticsJobConfig.analyzed_fields.includes, }; + if (isClone === false) { + resultState.destinationIndex = analyticsJobConfig?.dest.index ?? ''; + } + const analysisConfig = analyticsJobConfig.analysis[jobType]; for (const key in analysisConfig) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 035610684d556..9612b9213d120 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -28,7 +28,7 @@ import { FormMessage, State, SourceIndexMap, - getCloneFormStateFromJobConfig, + getFormStateFromJobConfig, } from './state'; import { ANALYTICS_STEPS } from '../../../analytics_creation/page'; @@ -283,6 +283,10 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SWITCH_TO_ADVANCED_EDITOR }); }; + const switchToForm = () => { + dispatch({ type: ACTION.SWITCH_TO_FORM }); + }; + const setEstimatedModelMemoryLimit = (value: State['estimatedModelMemoryLimit']) => { dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value }); }; @@ -294,7 +298,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { setJobConfig(config); switchToAdvancedEditor(); } else { - setFormState(getCloneFormStateFromJobConfig(config)); + setFormState(getFormStateFromJobConfig(config)); setEstimatedModelMemoryLimit(config.model_memory_limit); } @@ -311,6 +315,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { setJobConfig, startAnalyticsJob, switchToAdvancedEditor, + switchToForm, setEstimatedModelMemoryLimit, setJobClone, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c81aade2b063e..25d37334d0b82 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11084,7 +11084,6 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "編集", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "Kibanaインデックスパターンの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。", - "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "JSONエディターからこのフォームには戻れません。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "データフレーム分析ジョブの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "既存のデータフレーム分析ジョブIDの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index aba5adf72c2f8..b3886ffb1ecb1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11086,7 +11086,6 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "编辑", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误:", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。", - "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "您不能从 json 编辑器切回到此表单。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "创建数据帧分析作业时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "获取现有数据帧分析作业 ID 时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", From 147d4c7ad0afe83143faa22136aa56a821ef57e6 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 31 Jul 2020 08:52:27 -0600 Subject: [PATCH 012/115] [SIEM] Fixes a bug where invalid regular expressions within the index patterns can cause UI toaster errors (#73754) ## Summary https://github.com/elastic/kibana/issues/49753 When you have no data you get a toaster error when we don't want a toaster error. Before with the toaster error: ![error](https://user-images.githubusercontent.com/1151048/88860918-0e2a5900-d1ba-11ea-95e7-5ed7324fc831.png) After: You don't get an error toaster because I catch any regular expression errors and do not report them up to the UI. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../server/utils/beat_schema/index.test.ts | 7 +++++++ .../server/utils/beat_schema/index.ts | 14 ++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts index 56ceca2b70e9c..5f002aa7fad7b 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts @@ -401,10 +401,17 @@ describe('Schema Beat', () => { const result = getIndexAlias([leadingWildcardIndex], leadingWildcardIndex); expect(result).toBe(leadingWildcardIndex); }); + test('getIndexAlias no match returns "unknown" string', () => { const index = 'auditbeat-*'; const result = getIndexAlias([index], 'hello'); expect(result).toBe('unknown'); }); + + test('empty index should not cause an error to return although it will cause an invalid regular expression to occur', () => { + const index = ''; + const result = getIndexAlias([index], 'hello'); + expect(result).toBe('unknown'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts index ff7331cf39bc7..6ec15d328714d 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts @@ -77,10 +77,16 @@ const convertFieldsToAssociativeArray = ( : {}; export const getIndexAlias = (defaultIndex: string[], indexName: string): string => { - const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); - if (found != null) { - return found; - } else { + try { + const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); + if (found != null) { + return found; + } else { + return 'unknown'; + } + } catch (error) { + // if we encounter an error because the index contains invalid regular expressions then we should return an unknown + // rather than blow up with a toaster error upstream return 'unknown'; } }; From 61194b65aaa2f04bc0600171eeecc2bc56985640 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 31 Jul 2020 11:04:41 -0400 Subject: [PATCH 013/115] [Uptime] Unskip certs func tests (#73086) * Temporarily unload all other functional tests. * Unskip certificates test. * Uncomment skipped test files. * Add explicit fields to check generator call to prevent grouping issues that can lead to test flakiness. * added wait for loading * update missing func Co-authored-by: Elastic Machine Co-authored-by: Shahzad --- x-pack/test/functional/apps/uptime/certificates.ts | 12 ++++++++++-- x-pack/test/functional/apps/uptime/index.ts | 1 + .../test/functional/services/uptime/certificates.ts | 5 ++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index ccf35a1e63e37..7e9a2cd85935e 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -14,8 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - // Failing: See https://github.com/elastic/kibana/issues/70493 - describe.skip('certificates', function () { + describe('certificates', function () { before(async () => { await makeCheck({ es, tls: true }); await uptime.goToRoot(true); @@ -58,6 +57,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const certId = getSha256(); const { monitorId } = await makeCheck({ es, + monitorId: 'cert-test-check-id', + fields: { + monitor: { + name: 'Cert Test Check', + }, + url: { + full: 'https://site-to-check.com/', + }, + }, tls: { sha256: certId, }, diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 028ab3ff8803a..6b2b61cba2b64 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -55,6 +55,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./certificates')); }); + describe('with real-world data', () => { before(async () => { await esArchiver.unload(ARCHIVE); diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts index 2ceab1ca89e54..06de9be5af7e9 100644 --- a/x-pack/test/functional/services/uptime/certificates.ts +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -7,10 +7,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export function UptimeCertProvider({ getService }: FtrProviderContext) { +export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'timePicker', 'header']); + const changeSearchField = async (text: string) => { const input = await testSubjects.find('uptimeCertSearch'); await input.clearValueWithKeyboard(); @@ -61,6 +63,7 @@ export function UptimeCertProvider({ getService }: FtrProviderContext) { const self = this; return retry.tryForTime(60 * 1000, async () => { await changeSearchField(monId); + await PageObjects.header.waitUntilLoadingHasFinished(); await self.hasCertificates(1); }); }, From a1872b1a7029d871bd72cf29e5edac7cadc3e25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 31 Jul 2020 17:11:01 +0200 Subject: [PATCH 014/115] [ML] Removes link from helper text on ML overview page (#73819) --- .../overview/components/sidebar.tsx | 20 +------------------ .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx index 119346ec8035a..903a3c467a38b 100644 --- a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx @@ -9,29 +9,12 @@ import { EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui import { FormattedMessage } from '@kbn/i18n/react'; import { useMlKibana } from '../../contexts/kibana'; -const createJobLink = '#/jobs/new_job/step/index_or_search'; const feedbackLink = 'https://www.elastic.co/community/'; interface Props { createAnomalyDetectionJobDisabled: boolean; } -function getCreateJobLink(createAnomalyDetectionJobDisabled: boolean) { - return createAnomalyDetectionJobDisabled === true ? ( - - ) : ( - - - - ); -} - export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }) => { const { services: { @@ -59,7 +42,7 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

@@ -69,7 +52,6 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled } /> ), - createJob: getCreateJobLink(createAnomalyDetectionJobDisabled), transforms: ( Date: Fri, 31 Jul 2020 11:14:00 -0400 Subject: [PATCH 015/115] moved config option for allowing or disallowing by value embeddables to dashboard plugin (#73870) --- src/plugins/dashboard/config.ts | 26 +++++++++++++++++++++++++ src/plugins/dashboard/public/plugin.tsx | 6 ++++++ src/plugins/dashboard/server/index.ts | 10 +++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/plugins/dashboard/config.ts diff --git a/src/plugins/dashboard/config.ts b/src/plugins/dashboard/config.ts new file mode 100644 index 0000000000000..ff968a51679e0 --- /dev/null +++ b/src/plugins/dashboard/config.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + allowByValueEmbeddables: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index f0b57fec169fd..f1319665d258b 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -94,6 +94,10 @@ declare module '../../share/public' { export type DashboardUrlGenerator = UrlGeneratorContract; +interface DashboardFeatureFlagConfig { + allowByValueEmbeddables: boolean; +} + interface SetupDependencies { data: DataPublicPluginSetup; embeddable: EmbeddableSetup; @@ -125,6 +129,7 @@ export interface DashboardStart { embeddableType: string; }) => void | undefined; dashboardUrlGenerator?: DashboardUrlGenerator; + dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; } @@ -411,6 +416,7 @@ export class DashboardPlugin getSavedDashboardLoader: () => savedDashboardLoader, addEmbeddableToDashboard: this.addEmbeddableToDashboard.bind(this, core), dashboardUrlGenerator: this.dashboardUrlGenerator, + dashboardFeatureFlagConfig: this.initializerContext.config.get(), DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), diff --git a/src/plugins/dashboard/server/index.ts b/src/plugins/dashboard/server/index.ts index 9719586001c59..3ef7abba5776b 100644 --- a/src/plugins/dashboard/server/index.ts +++ b/src/plugins/dashboard/server/index.ts @@ -17,8 +17,16 @@ * under the License. */ -import { PluginInitializerContext } from '../../../core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../core/server'; import { DashboardPlugin } from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + allowByValueEmbeddables: true, + }, + schema: configSchema, +}; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. From 10cc600d5cdc9239ce174683b9fee6c3b22c8f4c Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 31 Jul 2020 09:19:43 -0600 Subject: [PATCH 016/115] Fix aborted$ event and add completed$ event to KibanaRequest (#73898) --- ...e-server.kibanarequestevents.completed_.md | 18 ++++ ...-plugin-core-server.kibanarequestevents.md | 1 + .../http/integration_tests/request.test.ts | 91 +++++++++++++++++++ src/core/server/http/router/request.ts | 19 +++- src/core/server/server.api.md | 1 + 5 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md new file mode 100644 index 0000000000000..c9f8ab11f6b12 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) > [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) + +## KibanaRequestEvents.completed$ property + +Observable that emits once if and when the request has been completely handled. + +Signature: + +```typescript +completed$: Observable; +``` + +## Remarks + +The request may be considered completed if: - A response has been sent to the client; or - The request was aborted. + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md index 21826c8b29383..dfd7efd27cb5a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md @@ -17,4 +17,5 @@ export interface KibanaRequestEvents | Property | Type | Description | | --- | --- | --- | | [aborted$](./kibana-plugin-core-server.kibanarequestevents.aborted_.md) | Observable<void> | Observable that emits once if and when the request has been aborted. | +| [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) | Observable<void> | Observable that emits once if and when the request has been completely handled. | diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 2d018f7f464b5..3a7335583296e 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -23,6 +23,7 @@ import { HttpService } from '../http_service'; import { contextServiceMock } from '../../context/context_service.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createHttpServer } from '../test_utils'; +import { schema } from '@kbn/config-schema'; let server: HttpService; @@ -195,6 +196,96 @@ describe('KibanaRequest', () => { expect(nextSpy).toHaveBeenCalledTimes(0); expect(completeSpy).toHaveBeenCalledTimes(1); }); + + it('does not complete before response has been sent', async () => { + const { server: innerServer, createRouter, registerOnPreAuth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + const completeSpy = jest.fn(); + + registerOnPreAuth((req, res, toolkit) => { + req.events.aborted$.subscribe({ + next: nextSpy, + complete: completeSpy, + }); + return toolkit.next(); + }); + + router.post( + { path: '/', validate: { body: schema.any() } }, + async (context, request, res) => { + expect(completeSpy).not.toHaveBeenCalled(); + return res.ok({ body: 'ok' }); + } + ); + + await server.start(); + + await supertest(innerServer.listener).post('/').send({ data: 'test' }).expect(200); + + expect(nextSpy).toHaveBeenCalledTimes(0); + expect(completeSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('completed$', () => { + it('emits once and completes when response is sent', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + const completeSpy = jest.fn(); + + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: completeSpy, + }); + + expect(nextSpy).not.toHaveBeenCalled(); + expect(completeSpy).not.toHaveBeenCalled(); + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + await supertest(innerServer.listener).get('/').expect(200); + expect(nextSpy).toHaveBeenCalledTimes(1); + expect(completeSpy).toHaveBeenCalledTimes(1); + }); + + it('emits once and completes when response is aborted', async (done) => { + expect.assertions(2); + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: () => { + expect(nextSpy).toHaveBeenCalledTimes(1); + done(); + }, + }); + + expect(nextSpy).not.toHaveBeenCalled(); + await delay(30000); + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + const incomingRequest = supertest(innerServer.listener) + .get('/') + // end required to send request + .end(); + setTimeout(() => incomingRequest.abort(), 50); + }); }); }); }); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 0e73431fe7c6d..93ffb5aa48259 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -64,6 +64,16 @@ export interface KibanaRequestEvents { * Observable that emits once if and when the request has been aborted. */ aborted$: Observable; + + /** + * Observable that emits once if and when the request has been completely handled. + * + * @remarks + * The request may be considered completed if: + * - A response has been sent to the client; or + * - The request was aborted. + */ + completed$: Observable; } /** @@ -186,11 +196,16 @@ export class KibanaRequest< private getEvents(request: Request): KibanaRequestEvents { const finish$ = merge( - fromEvent(request.raw.req, 'end'), // all data consumed + fromEvent(request.raw.res, 'finish'), // Response has been sent fromEvent(request.raw.req, 'close') // connection was closed ).pipe(shareReplay(1), first()); + + const aborted$ = fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)); + const completed$ = merge(finish$, aborted$).pipe(shareReplay(1), first()); + return { - aborted$: fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)), + aborted$, + completed$, } as const; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c1054c27d084e..21ef66230f698 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1071,6 +1071,7 @@ export class KibanaRequest; + completed$: Observable; } // @public From 708a30abb329355a9df7fab2f99938a2fd0c6654 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Fri, 31 Jul 2020 11:39:12 -0400 Subject: [PATCH 017/115] [Canvas][tech-debt] Update Redux components to reflect new structure (#73844) Co-authored-by: Elastic Machine --- .../embeddable_flyout/flyout.component.tsx | 78 ++++++ .../components/embeddable_flyout/flyout.tsx | 160 +++++++----- .../components/embeddable_flyout/index.ts | 11 + .../components/embeddable_flyout/index.tsx | 113 -------- .../public/components/page_manager/index.ts | 27 +- ...manager.tsx => page_manager.component.tsx} | 0 .../components/page_manager/page_manager.ts | 31 +++ .../public/components/page_preview/index.ts | 20 +- ...preview.tsx => page_preview.component.tsx} | 0 .../components/page_preview/page_preview.ts | 24 ++ .../saved_elements_modal.stories.tsx | 2 +- .../components/saved_elements_modal/index.ts | 132 +--------- ...tsx => saved_elements_modal.component.tsx} | 0 .../saved_elements_modal.ts | 136 ++++++++++ .../element_settings.component.tsx | 55 ++++ .../element_settings/element_settings.tsx | 63 ++--- .../sidebar/element_settings/index.ts | 8 + .../sidebar/element_settings/index.tsx | 34 --- .../components/workpad_color_picker/index.ts | 19 +- ...tsx => workpad_color_picker.component.tsx} | 0 .../workpad_color_picker.ts | 23 ++ .../public/components/workpad_config/index.ts | 39 +-- ...onfig.tsx => workpad_config.component.tsx} | 0 .../workpad_config/workpad_config.ts | 43 ++++ .../__examples__/edit_menu.stories.tsx | 2 +- ...{edit_menu.tsx => edit_menu.component.tsx} | 0 .../workpad_header/edit_menu/edit_menu.ts | 133 ++++++++++ .../workpad_header/edit_menu/index.ts | 129 +--------- .../__examples__/element_menu.stories.tsx | 2 +- .../element_menu/element_menu.component.tsx | 214 ++++++++++++++++ .../element_menu/element_menu.tsx | 241 +++--------------- .../workpad_header/element_menu/index.ts | 8 + .../workpad_header/element_menu/index.tsx | 47 ---- .../public/components/workpad_header/index.ts | 8 + .../components/workpad_header/index.tsx | 46 ---- .../workpad_header/refresh_control/index.ts | 18 +- ...trol.tsx => refresh_control.component.tsx} | 0 .../refresh_control/refresh_control.ts | 22 ++ .../__examples__/share_menu.stories.tsx | 2 +- ..._flyout.stories.tsx => flyout.stories.tsx} | 2 +- ...ebsite_flyout.tsx => flyout.component.tsx} | 2 +- .../share_menu/flyout/flyout.ts | 101 ++++++++ .../workpad_header/share_menu/flyout/index.ts | 94 +------ .../share_menu/flyout/runtime_step.tsx | 2 +- .../share_menu/flyout/snippets_step.tsx | 2 +- .../share_menu/flyout/workpad_step.tsx | 2 +- .../workpad_header/share_menu/index.ts | 94 +------ ...hare_menu.tsx => share_menu.component.tsx} | 0 .../workpad_header/share_menu/share_menu.ts | 98 +++++++ .../__examples__/view_menu.stories.tsx | 2 +- .../workpad_header/view_menu/index.ts | 96 +------ ...{view_menu.tsx => view_menu.component.tsx} | 0 .../workpad_header/view_menu/view_menu.ts | 100 ++++++++ .../workpad_header.component.tsx | 150 +++++++++++ .../workpad_header/workpad_header.tsx | 174 +++---------- .../canvas/storybook/storyshots.test.js | 2 +- 56 files changed, 1466 insertions(+), 1345 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx rename x-pack/plugins/canvas/public/components/page_manager/{page_manager.tsx => page_manager.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/page_manager/page_manager.ts rename x-pack/plugins/canvas/public/components/page_preview/{page_preview.tsx => page_preview.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/page_preview/page_preview.ts rename x-pack/plugins/canvas/public/components/saved_elements_modal/{saved_elements_modal.tsx => saved_elements_modal.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts create mode 100644 x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx rename x-pack/plugins/canvas/public/components/workpad_color_picker/{workpad_color_picker.tsx => workpad_color_picker.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts rename x-pack/plugins/canvas/public/components/workpad_config/{workpad_config.tsx => workpad_config.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts rename x-pack/plugins/canvas/public/components/workpad_header/edit_menu/{edit_menu.tsx => edit_menu.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/workpad_header/index.tsx rename x-pack/plugins/canvas/public/components/workpad_header/refresh_control/{refresh_control.tsx => refresh_control.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/{share_website_flyout.stories.tsx => flyout.stories.tsx} (94%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/{share_website_flyout.tsx => flyout.component.tsx} (98%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/{share_menu.tsx => share_menu.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts rename x-pack/plugins/canvas/public/components/workpad_header/view_menu/{view_menu.tsx => view_menu.component.tsx} (100%) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx new file mode 100644 index 0000000000000..0b5bd8adf8cb9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; +import { + SavedObjectFinderUi, + SavedObjectMetaData, +} from '../../../../../../src/plugins/saved_objects/public/'; +import { ComponentStrings } from '../../../i18n'; +import { useServices } from '../../services'; + +const { AddEmbeddableFlyout: strings } = ComponentStrings; + +export interface Props { + onClose: () => void; + onSelect: (id: string, embeddableType: string) => void; + availableEmbeddables: string[]; +} + +export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { + const services = useServices(); + const { embeddables, platform } = services; + const { getEmbeddableFactories } = embeddables; + const { getSavedObjects, getUISettings } = platform; + + const onAddPanel = (id: string, savedObjectType: string, name: string) => { + const embeddableFactories = getEmbeddableFactories(); + + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories).find((embeddableFactory) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); + + const foundEmbeddableType = found ? found.type : 'unknown'; + + onSelect(id, foundEmbeddableType); + }; + + const embeddableFactories = getEmbeddableFactories(); + + const availableSavedObjects = Array.from(embeddableFactories) + .filter((factory) => { + return availableEmbeddables.includes(factory.type); + }) + .map((factory) => factory.savedObjectMetaData) + .filter>(function ( + maybeSavedObjectMetaData + ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { + return maybeSavedObjectMetaData !== undefined; + }); + + return ( + + + +

{strings.getTitleText()}

+ + + + + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 0b5bd8adf8cb9..8c84e3d7a85d8 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -4,75 +4,107 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; -import { - SavedObjectFinderUi, - SavedObjectMetaData, -} from '../../../../../../src/plugins/saved_objects/public/'; -import { ComponentStrings } from '../../../i18n'; -import { useServices } from '../../services'; - -const { AddEmbeddableFlyout: strings } = ComponentStrings; - -export interface Props { - onClose: () => void; - onSelect: (id: string, embeddableType: string) => void; - availableEmbeddables: string[]; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { compose } from 'recompose'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component'; +// @ts-expect-error untyped local +import { addElement } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; + +const allowedEmbeddables = { + [EmbeddableTypes.map]: (id: string) => { + return `savedMap id="${id}" | render`; + }, + [EmbeddableTypes.lens]: (id: string) => { + return `savedLens id="${id}" | render`; + }, + [EmbeddableTypes.visualization]: (id: string) => { + return `savedVisualization id="${id}" | render`; + }, + /* + [EmbeddableTypes.search]: (id: string) => { + return `filters | savedSearch id="${id}" | render`; + },*/ +}; + +interface StateProps { + pageId: string; +} + +interface DispatchProps { + addEmbeddable: (pageId: string, partialElement: { expression: string }) => void; } -export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { - const services = useServices(); - const { embeddables, platform } = services; - const { getEmbeddableFactories } = embeddables; - const { getSavedObjects, getUISettings } = platform; +// FIX: Missing state type +const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); - const onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = getEmbeddableFactories(); +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => + dispatch(addElement(pageId, partialElement)), +}); - // Find the embeddable type from the saved object type - const found = Array.from(embeddableFactories).find((embeddableFactory) => { - return Boolean( - embeddableFactory.savedObjectMetaData && - embeddableFactory.savedObjectMetaData.type === savedObjectType - ); - }); +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { pageId, ...remainingStateProps } = stateProps; + const { addEmbeddable } = dispatchProps; - const foundEmbeddableType = found ? found.type : 'unknown'; + return { + ...remainingStateProps, + ...ownProps, + onSelect: (id: string, type: string): void => { + const partialElement = { + expression: `markdown "Could not find embeddable for type ${type}" | render`, + }; + if (allowedEmbeddables[type]) { + partialElement.expression = allowedEmbeddables[type](id); + } - onSelect(id, foundEmbeddableType); + addEmbeddable(pageId, partialElement); + ownProps.onClose(); + }, }; - - const embeddableFactories = getEmbeddableFactories(); - - const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return availableEmbeddables.includes(factory.type); - }) - .map((factory) => factory.savedObjectMetaData) - .filter>(function ( - maybeSavedObjectMetaData - ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { - return maybeSavedObjectMetaData !== undefined; - }); - - return ( - - - -

{strings.getTitleText()}

-
-
- - - -
- ); }; + +export class EmbeddableFlyoutPortal extends React.Component { + el?: HTMLElement; + + constructor(props: ComponentProps) { + super(props); + + this.el = document.createElement('div'); + } + componentDidMount() { + const body = document.querySelector('body'); + if (body && this.el) { + body.appendChild(this.el); + } + } + + componentWillUnmount() { + const body = document.querySelector('body'); + + if (body && this.el) { + body.removeChild(this.el); + } + } + + render() { + if (this.el) { + return ReactDOM.createPortal( + , + this.el + ); + } + } +} + +export const AddEmbeddablePanel = compose void }>( + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts new file mode 100644 index 0000000000000..a7fac10b0c02d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EmbeddableFlyoutPortal, AddEmbeddablePanel } from './flyout'; +export { + AddEmbeddableFlyout as AddEmbeddableFlyoutComponent, + Props as AddEmbeddableFlyoutComponentProps, +} from './flyout.component'; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx deleted file mode 100644 index 62a073daf4c59..0000000000000 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { compose } from 'recompose'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { AddEmbeddableFlyout, Props } from './flyout'; -// @ts-expect-error untyped local -import { addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; - -const allowedEmbeddables = { - [EmbeddableTypes.map]: (id: string) => { - return `savedMap id="${id}" | render`; - }, - [EmbeddableTypes.lens]: (id: string) => { - return `savedLens id="${id}" | render`; - }, - [EmbeddableTypes.visualization]: (id: string) => { - return `savedVisualization id="${id}" | render`; - }, - /* - [EmbeddableTypes.search]: (id: string) => { - return `filters | savedSearch id="${id}" | render`; - },*/ -}; - -interface StateProps { - pageId: string; -} - -interface DispatchProps { - addEmbeddable: (pageId: string, partialElement: { expression: string }) => void; -} - -// FIX: Missing state type -const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => - dispatch(addElement(pageId, partialElement)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: Props -): Props => { - const { pageId, ...remainingStateProps } = stateProps; - const { addEmbeddable } = dispatchProps; - - return { - ...remainingStateProps, - ...ownProps, - onSelect: (id: string, type: string): void => { - const partialElement = { - expression: `markdown "Could not find embeddable for type ${type}" | render`, - }; - if (allowedEmbeddables[type]) { - partialElement.expression = allowedEmbeddables[type](id); - } - - addEmbeddable(pageId, partialElement); - ownProps.onClose(); - }, - }; -}; - -export class EmbeddableFlyoutPortal extends React.Component { - el?: HTMLElement; - - constructor(props: Props) { - super(props); - - this.el = document.createElement('div'); - } - componentDidMount() { - const body = document.querySelector('body'); - if (body && this.el) { - body.appendChild(this.el); - } - } - - componentWillUnmount() { - const body = document.querySelector('body'); - - if (body && this.el) { - body.removeChild(this.el); - } - } - - render() { - if (this.el) { - return ReactDOM.createPortal( - , - this.el - ); - } - } -} - -export const AddEmbeddablePanel = compose void }>( - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/page_manager/index.ts b/x-pack/plugins/canvas/public/components/page_manager/index.ts index d19540cd6a687..abe7a4a3a5bb1 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/index.ts +++ b/x-pack/plugins/canvas/public/components/page_manager/index.ts @@ -4,28 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { PageManager as Component } from './page_manager'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - pages: getPages(state), - selectedPage: getSelectedPage(state), - workpadId: getWorkpad(state).id, - workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onAddPage: () => dispatch(pageActions.addPage()), - onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), - onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), -}); - -export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); +export { PageManager } from './page_manager'; +export { PageManager as PageManagerComponent } from './page_manager.component'; diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx rename to x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts new file mode 100644 index 0000000000000..a92f7c6b4c352 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { PageManager as Component } from './page_manager.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + pages: getPages(state), + selectedPage: getSelectedPage(state), + workpadId: getWorkpad(state).id, + workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onAddPage: () => dispatch(pageActions.addPage()), + onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), + onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), +}); + +export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_preview/index.ts b/x-pack/plugins/canvas/public/components/page_preview/index.ts index 25d3254595d2e..22e3861eb9652 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/index.ts +++ b/x-pack/plugins/canvas/public/components/page_preview/index.ts @@ -4,21 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { isWriteable } from '../../state/selectors/workpad'; -import { PagePreview as Component } from './page_preview'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), -}); - -export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); +export { PagePreview } from './page_preview'; +export { PagePreview as PagePreviewComponent } from './page_preview.component'; diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx rename to x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts b/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts new file mode 100644 index 0000000000000..8768a2fc169ef --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_preview/page_preview.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { isWriteable } from '../../state/selectors/workpad'; +import { PagePreview as Component } from './page_preview.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), +}); + +export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx index 4941d8cb2efa7..a811a296f2e7b 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { SavedElementsModal } from '../saved_elements_modal'; +import { SavedElementsModal } from '../saved_elements_modal.component'; import { testCustomElements } from './fixtures/test_elements'; import { CustomElement } from '../../../../types'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts index da2955c146193..46faf8d14f9b5 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -4,130 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { compose, withState } from 'recompose'; -import { camelCase } from 'lodash'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import * as customElementService from '../../lib/custom_element_service'; -import { withServices, WithServicesProps } from '../../services'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../state/actions/transient'; -// @ts-expect-error untyped local -import { insertNodes } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { SavedElementsModal as Component, Props as ComponentProps } from './saved_elements_modal'; -import { State, PositionedElement, CustomElement } from '../../../types'; - -const customElementAdded = 'elements-custom-added'; - -interface OwnProps { - onClose: () => void; -} - -interface OwnPropsWithState extends OwnProps { - customElements: CustomElement[]; - setCustomElements: (customElements: CustomElement[]) => void; - search: string; - setSearch: (search: string) => void; -} - -interface DispatchProps { - selectToplevelNodes: (nodes: PositionedElement[]) => void; - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; -} - -interface StateProps { - pageId: string; -} - -const mapStateToProps = (state: State): StateProps => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes - .filter((e: PositionedElement): boolean => !e.position.parent) - .map((e: PositionedElement): string => e.id) - ) - ), - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: OwnPropsWithState & WithServicesProps -): ComponentProps => { - const { pageId } = stateProps; - const { onClose, search, setCustomElements } = ownProps; - - const findCustomElements = async () => { - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - }; - - return { - ...ownProps, - // add custom element to the page - addCustomElement: (customElement: CustomElement) => { - const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) - dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - onClose(); - trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); - }, - // custom element search - findCustomElements: async (text?: string) => { - try { - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't find custom elements`, - }); - } - }, - // remove custom element - removeCustomElement: async (id: string) => { - try { - await customElementService.remove(id); - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't delete custom elements`, - }); - } - }, - // update custom element - updateCustomElement: async (id: string, name: string, description: string, image: string) => { - try { - await customElementService.update(id, { - name: camelCase(name), - displayName: name, - image, - help: description, - }); - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't update custom elements`, - }); - } - }, - }; -}; - -export const SavedElementsModal = compose( - withServices, - withState('search', 'setSearch', ''), - withState('customElements', 'setCustomElements', []), - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(Component); +export { SavedElementsModal } from './saved_elements_modal'; +export { + SavedElementsModal as SavedElementsModalComponent, + Props as SavedElementsModalComponentProps, +} from './saved_elements_modal.component'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts new file mode 100644 index 0000000000000..a5c5a2e0adce9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { compose, withState } from 'recompose'; +import { camelCase } from 'lodash'; +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import * as customElementService from '../../lib/custom_element_service'; +import { withServices, WithServicesProps } from '../../services'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../state/actions/transient'; +// @ts-expect-error untyped local +import { insertNodes } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { + SavedElementsModal as Component, + Props as ComponentProps, +} from './saved_elements_modal.component'; +import { State, PositionedElement, CustomElement } from '../../../types'; + +const customElementAdded = 'elements-custom-added'; + +interface OwnProps { + onClose: () => void; +} + +interface OwnPropsWithState extends OwnProps { + customElements: CustomElement[]; + setCustomElements: (customElements: CustomElement[]) => void; + search: string; + setSearch: (search: string) => void; +} + +interface DispatchProps { + selectToplevelNodes: (nodes: PositionedElement[]) => void; + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; +} + +interface StateProps { + pageId: string; +} + +const mapStateToProps = (state: State): StateProps => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes + .filter((e: PositionedElement): boolean => !e.position.parent) + .map((e: PositionedElement): string => e.id) + ) + ), + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnPropsWithState & WithServicesProps +): ComponentProps => { + const { pageId } = stateProps; + const { onClose, search, setCustomElements } = ownProps; + + const findCustomElements = async () => { + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + }; + + return { + ...ownProps, + // add custom element to the page + addCustomElement: (customElement: CustomElement) => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + onClose(); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); + }, + // custom element search + findCustomElements: async (text?: string) => { + try { + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't find custom elements`, + }); + } + }, + // remove custom element + removeCustomElement: async (id: string) => { + try { + await customElementService.remove(id); + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't delete custom elements`, + }); + } + }, + // update custom element + updateCustomElement: async (id: string, name: string, description: string, image: string) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't update custom elements`, + }); + } + }, + }; +}; + +export const SavedElementsModal = compose( + withServices, + withState('search', 'setSearch', ''), + withState('customElements', 'setCustomElements', []), + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx new file mode 100644 index 0000000000000..e3f4e00f4de01 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiTabbedContent } from '@elastic/eui'; +// @ts-expect-error unconverted component +import { Datasource } from '../../datasource'; +// @ts-expect-error unconverted component +import { FunctionFormList } from '../../function_form_list'; +import { PositionedElement } from '../../../../types'; +import { ComponentStrings } from '../../../../i18n'; + +interface Props { + /** + * a Canvas element used to populate config forms + */ + element: PositionedElement; +} + +const { ElementSettings: strings } = ComponentStrings; + +export const ElementSettings: FunctionComponent = ({ element }) => { + const tabs = [ + { + id: 'edit', + name: strings.getDisplayTabLabel(), + content: ( +
+
+ +
+
+ ), + }, + { + id: 'data', + name: strings.getDataTabLabel(), + content: ( +
+ +
+ ), + }, + ]; + + return ; +}; + +ElementSettings.propTypes = { + element: PropTypes.object, +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index e3f4e00f4de01..ba7e31a25daba 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -3,53 +3,32 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import React, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -import { EuiTabbedContent } from '@elastic/eui'; -// @ts-expect-error unconverted component -import { Datasource } from '../../datasource'; -// @ts-expect-error unconverted component -import { FunctionFormList } from '../../function_form_list'; -import { PositionedElement } from '../../../../types'; -import { ComponentStrings } from '../../../../i18n'; +import React from 'react'; +import { connect } from 'react-redux'; +import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; +import { ElementSettings as Component } from './element_settings.component'; +import { State, PositionedElement } from '../../../../types'; interface Props { - /** - * a Canvas element used to populate config forms - */ - element: PositionedElement; + selectedElementId: string; } -const { ElementSettings: strings } = ComponentStrings; +const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ + element: getElementById(state, selectedElementId, getSelectedPage(state)), +}); + +interface StateProps { + element: PositionedElement | undefined; +} -export const ElementSettings: FunctionComponent = ({ element }) => { - const tabs = [ - { - id: 'edit', - name: strings.getDisplayTabLabel(), - content: ( -
-
- -
-
- ), - }, - { - id: 'data', - name: strings.getDataTabLabel(), - content: ( -
- -
- ), - }, - ]; +const renderIfElement: React.FunctionComponent = (props) => { + if (props.element) { + return ; + } - return ; + return null; }; -ElementSettings.propTypes = { - element: PropTypes.object, -}; +export const ElementSettings = connect(mapStateToProps)( + renderIfElement +); diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts new file mode 100644 index 0000000000000..68b90f232fb8b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElementSettings } from './element_settings'; +export { ElementSettings as ElementSettingsComponent } from './element_settings.component'; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx deleted file mode 100644 index b8d5882234899..0000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { connect } from 'react-redux'; -import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; -import { ElementSettings as Component } from './element_settings'; -import { State, PositionedElement } from '../../../../types'; - -interface Props { - selectedElementId: string; -} - -const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ - element: getElementById(state, selectedElementId, getSelectedPage(state)), -}); - -interface StateProps { - element: PositionedElement | undefined; -} - -const renderIfElement: React.FunctionComponent = (props) => { - if (props.element) { - return ; - } - - return null; -}; - -export const ElementSettings = connect(mapStateToProps)( - renderIfElement -); diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts index abd40731078ec..34e3d3ff4b057 100644 --- a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts @@ -4,20 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { addColor, removeColor } from '../../state/actions/workpad'; -import { getWorkpadColors } from '../../state/selectors/workpad'; - -import { WorkpadColorPicker as Component } from '../workpad_color_picker/workpad_color_picker'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - colors: getWorkpadColors(state), -}); - -const mapDispatchToProps = { - onAddColor: addColor, - onRemoveColor: removeColor, -}; - -export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); +export { WorkpadColorPicker } from './workpad_color_picker'; +export { WorkpadColorPicker as WorkpadColorPickerComponent } from './workpad_color_picker.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx rename to x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts new file mode 100644 index 0000000000000..2f4b0fe7b4ec1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { addColor, removeColor } from '../../state/actions/workpad'; +import { getWorkpadColors } from '../../state/selectors/workpad'; + +import { WorkpadColorPicker as Component } from '../workpad_color_picker/workpad_color_picker.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + colors: getWorkpadColors(state), +}); + +const mapDispatchToProps = { + onAddColor: addColor, + onRemoveColor: removeColor, +}; + +export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_config/index.ts b/x-pack/plugins/canvas/public/components/workpad_config/index.ts index bba08d7647e9e..63db96ca5aef9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_config/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_config/index.ts @@ -4,40 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; - -import { get } from 'lodash'; -import { - sizeWorkpad as setSize, - setName, - setWorkpadCSS, - updateWorkpadVariables, -} from '../../state/actions/workpad'; - -import { getWorkpad } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { WorkpadConfig as Component } from './workpad_config'; -import { State, CanvasVariable } from '../../../types'; - -const mapStateToProps = (state: State) => { - const workpad = getWorkpad(state); - - return { - name: get(workpad, 'name'), - size: { - width: get(workpad, 'width'), - height: get(workpad, 'height'), - }, - css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), - variables: get(workpad, 'variables', []), - }; -}; - -const mapDispatchToProps = { - setSize, - setName, - setWorkpadCSS, - setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars), -}; - -export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); +export { WorkpadConfig } from './workpad_config'; +export { WorkpadConfig as WorkpadConfigComponent } from './workpad_config.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx rename to x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts new file mode 100644 index 0000000000000..e4ddf31141972 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; + +import { get } from 'lodash'; +import { + sizeWorkpad as setSize, + setName, + setWorkpadCSS, + updateWorkpadVariables, +} from '../../state/actions/workpad'; + +import { getWorkpad } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { WorkpadConfig as Component } from './workpad_config.component'; +import { State, CanvasVariable } from '../../../types'; + +const mapStateToProps = (state: State) => { + const workpad = getWorkpad(state); + + return { + name: get(workpad, 'name'), + size: { + width: get(workpad, 'width'), + height: get(workpad, 'height'), + }, + css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), + variables: get(workpad, 'variables', []), + }; +}; + +const mapDispatchToProps = { + setSize, + setName, + setWorkpadCSS, + setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars), +}; + +export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx index 8bbc3e09af4bf..be6247b0bbcab 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { EditMenu } from '../edit_menu'; +import { EditMenu } from '../edit_menu.component'; import { PositionedElement } from '../../../../../types'; const handlers = { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts new file mode 100644 index 0000000000000..3a2264c05eb4b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, PositionedElement } from '../../../../types'; +import { getClipboardData } from '../../../lib/clipboard'; +// @ts-expect-error untyped local +import { flatten } from '../../../lib/aeroelastic/functional'; +// @ts-expect-error untyped local +import { globalStateUpdater } from '../../workpad_page/integration_utils'; +// @ts-expect-error untyped local +import { crawlTree } from '../../workpad_page/integration_utils'; +// @ts-expect-error untyped local +import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; +// @ts-expect-error untyped local +import { undoHistory, redoHistory } from '../../../state/actions/history'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../../state/actions/transient'; +import { + getSelectedPage, + getNodes, + getSelectedToplevelNodes, +} from '../../../state/selectors/workpad'; +import { + layerHandlerCreators, + clipboardHandlerCreators, + basicHandlerCreators, + groupHandlerCreators, + alignmentDistributionHandlerCreators, +} from '../../../lib/element_handler_creators'; +import { EditMenu as Component, Props as ComponentProps } from './edit_menu.component'; + +type LayoutState = any; + +type CommitFn = (type: string, payload: any) => LayoutState; + +interface OwnProps { + commit: CommitFn; +} + +const withGlobalState = ( + commit: CommitFn, + updateGlobalState: (layoutState: LayoutState) => void +) => (type: string, payload: any) => { + const newLayoutState = commit(type, payload); + if (newLayoutState.currentScene.gestureEnd) { + updateGlobalState(newLayoutState); + } +}; + +/* + * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts + */ +const mapStateToProps = (state: State) => { + const pageId = getSelectedPage(state); + const nodes = getNodes(state, pageId) as PositionedElement[]; + const selectedToplevelNodes = getSelectedToplevelNodes(state); + + const selectedPrimaryShapeObjects = selectedToplevelNodes + .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) + .filter((shape?: PositionedElement) => shape) as PositionedElement[]; + + const selectedPersistentPrimaryNodes = flatten( + selectedPrimaryShapeObjects.map((shape: PositionedElement) => + nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? + ? [shape.id] + : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) + ) + ); + + const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + const selectedNodes = selectedNodeIds + .map((id: string) => nodes.find((s) => s.id === id)) + .filter((node: PositionedElement | undefined): node is PositionedElement => { + return !!node; + }); + + return { + pageId, + selectedToplevelNodes, + selectedNodes, + state, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), + removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes.filter((e: PositionedElement) => !e.position.parent).map((e) => e.id) + ) + ), + elementLayer: (pageId: string, elementId: string, movement: number) => { + dispatch(elementLayer({ pageId, elementId, movement })); + }, + undoHistory: () => dispatch(undoHistory()), + redoHistory: () => dispatch(redoHistory()), + dispatch, +}); + +const mergeProps = ( + { state, selectedToplevelNodes, ...restStateProps }: ReturnType, + { dispatch, ...restDispatchProps }: ReturnType, + { commit }: OwnProps +) => { + const updateGlobalState = globalStateUpdater(dispatch, state); + + return { + ...restDispatchProps, + ...restStateProps, + commit: withGlobalState(commit, updateGlobalState), + groupIsSelected: + selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), + }; +}; + +export const EditMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), + withHandlers(basicHandlerCreators), + withHandlers(clipboardHandlerCreators), + withHandlers(layerHandlerCreators), + withHandlers(groupHandlerCreators), + withHandlers(alignmentDistributionHandlerCreators) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts index 8f013f70aefcd..0db425f01cccd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts @@ -4,130 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withHandlers, withProps } from 'recompose'; -import { Dispatch } from 'redux'; -import { State, PositionedElement } from '../../../../types'; -import { getClipboardData } from '../../../lib/clipboard'; -// @ts-expect-error untyped local -import { flatten } from '../../../lib/aeroelastic/functional'; -// @ts-expect-error untyped local -import { globalStateUpdater } from '../../workpad_page/integration_utils'; -// @ts-expect-error untyped local -import { crawlTree } from '../../workpad_page/integration_utils'; -// @ts-expect-error untyped local -import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; -// @ts-expect-error untyped local -import { undoHistory, redoHistory } from '../../../state/actions/history'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../../state/actions/transient'; -import { - getSelectedPage, - getNodes, - getSelectedToplevelNodes, -} from '../../../state/selectors/workpad'; -import { - layerHandlerCreators, - clipboardHandlerCreators, - basicHandlerCreators, - groupHandlerCreators, - alignmentDistributionHandlerCreators, -} from '../../../lib/element_handler_creators'; -import { EditMenu as Component, Props as ComponentProps } from './edit_menu'; - -type LayoutState = any; - -type CommitFn = (type: string, payload: any) => LayoutState; - -interface OwnProps { - commit: CommitFn; -} - -const withGlobalState = ( - commit: CommitFn, - updateGlobalState: (layoutState: LayoutState) => void -) => (type: string, payload: any) => { - const newLayoutState = commit(type, payload); - if (newLayoutState.currentScene.gestureEnd) { - updateGlobalState(newLayoutState); - } -}; - -/* - * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts - */ -const mapStateToProps = (state: State) => { - const pageId = getSelectedPage(state); - const nodes = getNodes(state, pageId) as PositionedElement[]; - const selectedToplevelNodes = getSelectedToplevelNodes(state); - - const selectedPrimaryShapeObjects = selectedToplevelNodes - .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) - .filter((shape?: PositionedElement) => shape) as PositionedElement[]; - - const selectedPersistentPrimaryNodes = flatten( - selectedPrimaryShapeObjects.map((shape: PositionedElement) => - nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? - ? [shape.id] - : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) - ) - ); - - const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); - const selectedNodes = selectedNodeIds - .map((id: string) => nodes.find((s) => s.id === id)) - .filter((node: PositionedElement | undefined): node is PositionedElement => { - return !!node; - }); - - return { - pageId, - selectedToplevelNodes, - selectedNodes, - state, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), - removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes.filter((e: PositionedElement) => !e.position.parent).map((e) => e.id) - ) - ), - elementLayer: (pageId: string, elementId: string, movement: number) => { - dispatch(elementLayer({ pageId, elementId, movement })); - }, - undoHistory: () => dispatch(undoHistory()), - redoHistory: () => dispatch(redoHistory()), - dispatch, -}); - -const mergeProps = ( - { state, selectedToplevelNodes, ...restStateProps }: ReturnType, - { dispatch, ...restDispatchProps }: ReturnType, - { commit }: OwnProps -) => { - const updateGlobalState = globalStateUpdater(dispatch, state); - - return { - ...restDispatchProps, - ...restStateProps, - commit: withGlobalState(commit, updateGlobalState), - groupIsSelected: - selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), - }; -}; - -export const EditMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), - withHandlers(basicHandlerCreators), - withHandlers(clipboardHandlerCreators), - withHandlers(layerHandlerCreators), - withHandlers(groupHandlerCreators), - withHandlers(alignmentDistributionHandlerCreators) -)(Component); +export { EditMenu } from './edit_menu'; +export { EditMenu as EditMenuComponent } from './edit_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx index 9aca5ce33ba02..cf9b334ffe8ea 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx @@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ElementSpec } from '../../../../../types'; -import { ElementMenu } from '../element_menu'; +import { ElementMenu } from '../element_menu.component'; const testElements: { [key: string]: ElementSpec } = { areaChart: { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx new file mode 100644 index 0000000000000..6d9233aaba22b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sortBy } from 'lodash'; +import React, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ElementSpec } from '../../../../types'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { getId } from '../../../lib/get_id'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { AssetManager } from '../../asset_manager'; +import { SavedElementsModal } from '../../saved_elements_modal'; + +interface CategorizedElementLists { + [key: string]: ElementSpec[]; +} + +interface ElementTypeMeta { + [key: string]: { name: string; icon: string }; +} + +export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; + +// label and icon for the context menu item for each element type +const elementTypeMeta: ElementTypeMeta = { + chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, + filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, + image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, + other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, + progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, + shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, + text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, +}; + +const getElementType = (element: ElementSpec): string => + element && element.type && Object.keys(elementTypeMeta).includes(element.type) + ? element.type + : 'other'; + +const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { + elements = sortBy(elements, 'displayName'); + + const categories: CategorizedElementLists = { other: [] }; + + elements.forEach((element: ElementSpec) => { + const type = getElementType(element); + + if (categories[type]) { + categories[type].push(element); + } else { + categories[type] = [element]; + } + }); + + return categories; +}; + +export interface Props { + /** + * Dictionary of elements from elements registry + */ + elements: { [key: string]: ElementSpec }; + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: ElementSpec) => void; + /** + * Renders embeddable flyout + */ + renderEmbedPanel: (onClose: () => void) => JSX.Element; +} + +export const ElementMenu: FunctionComponent = ({ + elements, + addElement, + renderEmbedPanel, +}) => { + const [isAssetModalVisible, setAssetModalVisible] = useState(false); + const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); + const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); + + const hideAssetModal = () => setAssetModalVisible(false); + const showAssetModal = () => setAssetModalVisible(true); + const hideEmbedPanel = () => setEmbedPanelVisible(false); + const showEmbedPanel = () => setEmbedPanelVisible(true); + const hideSavedElementsModal = () => setSavedElementsModalVisible(false); + const showSavedElementsModal = () => setSavedElementsModalVisible(true); + + const { + chart: chartElements, + filter: filterElements, + image: imageElements, + other: otherElements, + progress: progressElements, + shape: shapeElements, + text: textElements, + } = categorizeElementsByType(Object.values(elements)); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ + name: element.displayName || element.name, + icon: element.icon, + onClick: () => { + addElement(element); + closePopover(); + }, + }); + + const elementListToMenuItems = (elementList: ElementSpec[]) => { + const type = getElementType(elementList[0]); + const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; + + if (elementList.length > 1) { + return { + name, + icon: , + panel: { + id: getId('element-type'), + title: name, + items: elementList.map(elementToMenuItem), + }, + }; + } + + return elementToMenuItem(elementList[0]); + }; + + return { + id: 0, + items: [ + elementListToMenuItems(textElements), + elementListToMenuItems(shapeElements), + elementListToMenuItems(chartElements), + elementListToMenuItems(imageElements), + elementListToMenuItems(filterElements), + elementListToMenuItems(progressElements), + elementListToMenuItems(otherElements), + { + name: strings.getMyElementsMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'saved-elements-menu-option', + icon: , + onClick: () => { + showSavedElementsModal(); + closePopover(); + }, + }, + { + name: strings.getAssetsMenuItemLabel(), + icon: , + onClick: () => { + showAssetModal(); + closePopover(); + }, + }, + { + name: strings.getEmbedObjectMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: , + onClick: () => { + showEmbedPanel(); + closePopover(); + }, + }, + ], + }; + }; + + const exportControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getElementMenuButtonLabel()} + + ); + + return ( + + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {isAssetModalVisible ? : null} + {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} + {isSavedElementsModalVisible ? : null} + + ); +}; + +ElementMenu.propTypes = { + elements: PropTypes.object, + addElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index 6d9233aaba22b..2cbe4ae5a6575 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -4,211 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; -import React, { Fragment, FunctionComponent, useState } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiButton, - EuiContextMenu, - EuiIcon, - EuiContextMenuPanelItemDescriptor, -} from '@elastic/eui'; -import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; -import { ComponentStrings } from '../../../../i18n/components'; -import { ElementSpec } from '../../../../types'; -import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; -import { getId } from '../../../lib/get_id'; -import { Popover, ClosePopoverFn } from '../../popover'; -import { AssetManager } from '../../asset_manager'; -import { SavedElementsModal } from '../../saved_elements_modal'; - -interface CategorizedElementLists { - [key: string]: ElementSpec[]; -} - -interface ElementTypeMeta { - [key: string]: { name: string; icon: string }; +import React from 'react'; +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, ElementSpec } from '../../../../types'; +// @ts-expect-error untyped local +import { elementsRegistry } from '../../../lib/elements_registry'; +import { ElementMenu as Component, Props as ComponentProps } from './element_menu.component'; +// @ts-expect-error untyped local +import { addElement } from '../../../state/actions/elements'; +import { getSelectedPage } from '../../../state/selectors/workpad'; +import { AddEmbeddablePanel } from '../../embeddable_flyout'; + +interface StateProps { + pageId: string; } -export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; - -// label and icon for the context menu item for each element type -const elementTypeMeta: ElementTypeMeta = { - chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, - filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, - image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, - other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, - progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, - shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, - text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, -}; - -const getElementType = (element: ElementSpec): string => - element && element.type && Object.keys(elementTypeMeta).includes(element.type) - ? element.type - : 'other'; - -const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { - elements = sortBy(elements, 'displayName'); - - const categories: CategorizedElementLists = { other: [] }; - - elements.forEach((element: ElementSpec) => { - const type = getElementType(element); - - if (categories[type]) { - categories[type].push(element); - } else { - categories[type] = [element]; - } - }); - - return categories; -}; - -export interface Props { - /** - * Dictionary of elements from elements registry - */ - elements: { [key: string]: ElementSpec }; - /** - * Handler for adding a selected element to the workpad - */ - addElement: (element: ElementSpec) => void; - /** - * Renders embeddable flyout - */ - renderEmbedPanel: (onClose: () => void) => JSX.Element; +interface DispatchProps { + addElement: (pageId: string) => (partialElement: ElementSpec) => void; } -export const ElementMenu: FunctionComponent = ({ - elements, - addElement, - renderEmbedPanel, -}) => { - const [isAssetModalVisible, setAssetModalVisible] = useState(false); - const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); - const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); - - const hideAssetModal = () => setAssetModalVisible(false); - const showAssetModal = () => setAssetModalVisible(true); - const hideEmbedPanel = () => setEmbedPanelVisible(false); - const showEmbedPanel = () => setEmbedPanelVisible(true); - const hideSavedElementsModal = () => setSavedElementsModalVisible(false); - const showSavedElementsModal = () => setSavedElementsModalVisible(true); - - const { - chart: chartElements, - filter: filterElements, - image: imageElements, - other: otherElements, - progress: progressElements, - shape: shapeElements, - text: textElements, - } = categorizeElementsByType(Object.values(elements)); - - const getPanelTree = (closePopover: ClosePopoverFn) => { - const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ - name: element.displayName || element.name, - icon: element.icon, - onClick: () => { - addElement(element); - closePopover(); - }, - }); - - const elementListToMenuItems = (elementList: ElementSpec[]) => { - const type = getElementType(elementList[0]); - const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; - - if (elementList.length > 1) { - return { - name, - icon: , - panel: { - id: getId('element-type'), - title: name, - items: elementList.map(elementToMenuItem), - }, - }; - } - - return elementToMenuItem(elementList[0]); - }; - - return { - id: 0, - items: [ - elementListToMenuItems(textElements), - elementListToMenuItems(shapeElements), - elementListToMenuItems(chartElements), - elementListToMenuItems(imageElements), - elementListToMenuItems(filterElements), - elementListToMenuItems(progressElements), - elementListToMenuItems(otherElements), - { - name: strings.getMyElementsMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - 'data-test-subj': 'saved-elements-menu-option', - icon: , - onClick: () => { - showSavedElementsModal(); - closePopover(); - }, - }, - { - name: strings.getAssetsMenuItemLabel(), - icon: , - onClick: () => { - showAssetModal(); - closePopover(); - }, - }, - { - name: strings.getEmbedObjectMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - icon: , - onClick: () => { - showEmbedPanel(); - closePopover(); - }, - }, - ], - }; - }; - - const exportControl = (togglePopover: React.MouseEventHandler) => ( - - {strings.getElementMenuButtonLabel()} - - ); - - return ( - - - {({ closePopover }: { closePopover: ClosePopoverFn }) => ( - - )} - - {isAssetModalVisible ? : null} - {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} - {isSavedElementsModalVisible ? : null} - - ); -}; - -ElementMenu.propTypes = { - elements: PropTypes.object, - addElement: PropTypes.func.isRequired, -}; +const mapStateToProps = (state: State) => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), +}); + +const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ + ...stateProps, + ...dispatchProps, + addElement: dispatchProps.addElement(stateProps.pageId), + // Moved this section out of the main component to enable stories + renderEmbedPanel: (onClose: () => void) => , +}); + +export const ElementMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ elements: elementsRegistry.toJS() })) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts new file mode 100644 index 0000000000000..26f81e125f6e2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElementMenu } from './element_menu'; +export { ElementMenu as ElementMenuComponent } from './element_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx deleted file mode 100644 index 264873fc994dd..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { Dispatch } from 'redux'; -import { State, ElementSpec } from '../../../../types'; -// @ts-expect-error untyped local -import { elementsRegistry } from '../../../lib/elements_registry'; -import { ElementMenu as Component, Props as ComponentProps } from './element_menu'; -// @ts-expect-error untyped local -import { addElement } from '../../../state/actions/elements'; -import { getSelectedPage } from '../../../state/selectors/workpad'; -import { AddEmbeddablePanel } from '../../embeddable_flyout'; - -interface StateProps { - pageId: string; -} - -interface DispatchProps { - addElement: (pageId: string) => (partialElement: ElementSpec) => void; -} - -const mapStateToProps = (state: State) => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), -}); - -const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ - ...stateProps, - ...dispatchProps, - addElement: dispatchProps.addElement(stateProps.pageId), - // Moved this section out of the main component to enable stories - renderEmbedPanel: (onClose: () => void) => , -}); - -export const ElementMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ elements: elementsRegistry.toJS() })) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/index.ts new file mode 100644 index 0000000000000..0b6f8cc06d198 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WorkpadHeader } from './workpad_header'; +export { WorkpadHeader as WorkpadHeaderComponent } from './workpad_header.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/index.tsx deleted file mode 100644 index 407b4ff932811..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; -import { setWriteable } from '../../state/actions/workpad'; -import { State } from '../../../types'; -import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header'; - -interface StateProps { - isWriteable: boolean; - canUserWrite: boolean; - selectedPage: string; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; -} - -const mapStateToProps = (state: State): StateProps => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - canUserWrite: canUserWrite(state), - selectedPage: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), -}); - -export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts index 87b926d93ccb9..8db62f5ac2d87 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts @@ -4,19 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../../../state/actions/elements'; -import { getInFlight } from '../../../state/selectors/resolved_args'; -import { State } from '../../../../types'; -import { RefreshControl as Component } from './refresh_control'; - -const mapStateToProps = (state: State) => ({ - inFlight: getInFlight(state), -}); - -const mapDispatchToProps = { - doRefresh: fetchAllRenderables, -}; - -export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); +export { RefreshControl } from './refresh_control'; +export { RefreshControl as RefreshControlComponent } from './refresh_control.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts new file mode 100644 index 0000000000000..a7f01e46927ce --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +import { getInFlight } from '../../../state/selectors/resolved_args'; +import { State } from '../../../../types'; +import { RefreshControl as Component } from './refresh_control.component'; + +const mapStateToProps = (state: State) => ({ + inFlight: getInFlight(state), +}); + +const mapDispatchToProps = { + doRefresh: fetchAllRenderables, +}; + +export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx index ab9137b1676c9..e0a1f0e381fd3 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { ShareMenu } from '../share_menu'; +import { ShareMenu } from '../share_menu.component'; storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => ( { + const renderers: string[] = []; + const expressions = getRenderedWorkpadExpressions(state); + expressions.forEach((expression) => { + if (!renderFunctionNames.includes(expression)) { + renderers.push(expression); + } + }); + + return renderers; +}; + +const mapStateToProps = (state: State) => ({ + renderedWorkpad: getRenderedWorkpad(state), + unsupportedRenderers: getUnsupportedRenderers(state), + workpad: getWorkpad(state), +}); + +interface Props { + onClose: OnCloseFn; + renderedWorkpad: CanvasRenderedWorkpad; + unsupportedRenderers: string[]; + workpad: CanvasWorkpad; +} + +export const ShareWebsiteFlyout = compose>( + connect(mapStateToProps), + withKibana, + withProps( + ({ + unsupportedRenderers, + renderedWorkpad, + onClose, + workpad, + kibana, + }: Props & WithKibanaProps): ComponentProps => ({ + unsupportedRenderers, + onClose, + onCopy: () => { + kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); + }, + onDownload: (type) => { + switch (type) { + case 'share': + downloadRenderedWorkpad(renderedWorkpad); + return; + case 'shareRuntime': + downloadRuntime(kibana.services.http.basePath.get()); + return; + case 'shareZip': + const basePath = kibana.services.http.basePath.get(); + arrayBufferFetch + .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) + .then((blob) => downloadZippedRuntime(blob.data)) + .catch((err: Error) => { + kibana.services.canvas.notify.error(err, { + title: strings.getShareableZipErrorTitle(workpad.name), + }); + }); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts index 1e1eac2a1dcf3..335c5dff6ed74 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts @@ -4,95 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { - getWorkpad, - getRenderedWorkpad, - getRenderedWorkpadExpressions, -} from '../../../../state/selectors/workpad'; -import { - downloadRenderedWorkpad, - downloadRuntime, - downloadZippedRuntime, -} from '../../../../lib/download_workpad'; -import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; -import { State, CanvasWorkpad } from '../../../../../types'; -import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; -import { arrayBufferFetch } from '../../../../../common/lib/fetch'; -import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; -import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; - -import { ComponentStrings } from '../../../../../i18n/components'; -import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; -import { OnCloseFn } from '../share_menu'; -import { WithKibanaProps } from '../../../../index'; -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -const getUnsupportedRenderers = (state: State) => { - const renderers: string[] = []; - const expressions = getRenderedWorkpadExpressions(state); - expressions.forEach((expression) => { - if (!renderFunctionNames.includes(expression)) { - renderers.push(expression); - } - }); - - return renderers; -}; - -const mapStateToProps = (state: State) => ({ - renderedWorkpad: getRenderedWorkpad(state), - unsupportedRenderers: getUnsupportedRenderers(state), - workpad: getWorkpad(state), -}); - -interface Props { - onClose: OnCloseFn; - renderedWorkpad: CanvasRenderedWorkpad; - unsupportedRenderers: string[]; - workpad: CanvasWorkpad; -} - -export const ShareWebsiteFlyout = compose>( - connect(mapStateToProps), - withKibana, - withProps( - ({ - unsupportedRenderers, - renderedWorkpad, - onClose, - workpad, - kibana, - }: Props & WithKibanaProps): ComponentProps => ({ - unsupportedRenderers, - onClose, - onCopy: () => { - kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); - }, - onDownload: (type) => { - switch (type) { - case 'share': - downloadRenderedWorkpad(renderedWorkpad); - return; - case 'shareRuntime': - downloadRuntime(kibana.services.http.basePath.get()); - return; - case 'shareZip': - const basePath = kibana.services.http.basePath.get(); - arrayBufferFetch - .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) - .then((blob) => downloadZippedRuntime(blob.data)) - .catch((err: Error) => { - kibana.services.canvas.notify.error(err, { - title: strings.getShareableZipErrorTitle(workpad.name), - }); - }); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); +export { ShareWebsiteFlyout } from './flyout'; +export { ShareWebsiteFlyout as ShareWebsiteFlyoutComponent } from './flyout.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx index ea8aba688b2a6..b38226bb12a23 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx @@ -9,7 +9,7 @@ import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; import { ComponentStrings } from '../../../../../i18n/components'; -import { OnDownloadFn } from './share_website_flyout'; +import { OnDownloadFn } from './flyout'; const { ShareWebsiteRuntimeStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx index 81f559651eb25..42497fcd316fe 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx @@ -19,7 +19,7 @@ import { import { ComponentStrings } from '../../../../../i18n/components'; import { Clipboard } from '../../../clipboard'; -import { OnCopyFn } from './share_website_flyout'; +import { OnCopyFn } from './flyout'; const { ShareWebsiteSnippetsStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx index 1a5884d89d066..ac4dfe6872d3c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx @@ -9,7 +9,7 @@ import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; import { ComponentStrings } from '../../../../../i18n/components'; -import { OnDownloadFn } from './share_website_flyout'; +import { OnDownloadFn } from './flyout'; const { ShareWebsiteWorkpadStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts index 01bcfebc0dba9..19dc9b668e61a 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts @@ -4,95 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; -import { getWorkpad, getPages } from '../../../state/selectors/workpad'; -import { getWindow } from '../../../lib/get_window'; -import { downloadWorkpad } from '../../../lib/download_workpad'; -import { ShareMenu as Component, Props as ComponentProps } from './share_menu'; -import { getPdfUrl, createPdf } from './utils'; -import { State, CanvasWorkpad } from '../../../../types'; -import { withServices, WithServicesProps } from '../../../services'; - -import { ComponentStrings } from '../../../../i18n'; - -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -const mapStateToProps = (state: State) => ({ - workpad: getWorkpad(state), - pageCount: getPages(state).length, -}); - -const getAbsoluteUrl = (path: string) => { - const { location } = getWindow(); - - if (!location) { - return path; - } // fallback for mocked window object - - const { protocol, hostname, port } = location; - return `${protocol}//${hostname}:${port}${path}`; -}; - -interface Props { - workpad: CanvasWorkpad; - pageCount: number; -} - -export const ShareMenu = compose( - connect(mapStateToProps), - withServices, - withProps( - ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ - getExportUrl: (type) => { - if (type === 'pdf') { - const pdfUrl = getPdfUrl( - workpad, - { pageCount }, - services.platform.getBasePathInterface() - ); - return getAbsoluteUrl(pdfUrl); - } - - throw new Error(strings.getUnknownExportErrorMessage(type)); - }, - onCopy: (type) => { - switch (type) { - case 'pdf': - services.notify.info(strings.getCopyPDFMessage()); - break; - case 'reportingConfig': - services.notify.info(strings.getCopyReportingConfigMessage()); - break; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - onExport: (type) => { - switch (type) { - case 'pdf': - return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) - .then(({ data }: { data: { job: { id: string } } }) => { - services.notify.info(strings.getExportPDFMessage(), { - title: strings.getExportPDFTitle(workpad.name), - }); - - // register the job so a completion notification shows up when it's ready - jobCompletionNotifications.add(data.job.id); - }) - .catch((err: Error) => { - services.notify.error(err, { - title: strings.getExportPDFErrorTitle(workpad.name), - }); - }); - case 'json': - downloadWorkpad(workpad.id); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); +export { ShareMenu } from './share_menu'; +export { ShareMenu as ShareMenuComponent } from './share_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts new file mode 100644 index 0000000000000..85c4b14a28c13 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; +import { getWorkpad, getPages } from '../../../state/selectors/workpad'; +import { getWindow } from '../../../lib/get_window'; +import { downloadWorkpad } from '../../../lib/download_workpad'; +import { ShareMenu as Component, Props as ComponentProps } from './share_menu.component'; +import { getPdfUrl, createPdf } from './utils'; +import { State, CanvasWorkpad } from '../../../../types'; +import { withServices, WithServicesProps } from '../../../services'; + +import { ComponentStrings } from '../../../../i18n'; + +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +const mapStateToProps = (state: State) => ({ + workpad: getWorkpad(state), + pageCount: getPages(state).length, +}); + +const getAbsoluteUrl = (path: string) => { + const { location } = getWindow(); + + if (!location) { + return path; + } // fallback for mocked window object + + const { protocol, hostname, port } = location; + return `${protocol}//${hostname}:${port}${path}`; +}; + +interface Props { + workpad: CanvasWorkpad; + pageCount: number; +} + +export const ShareMenu = compose( + connect(mapStateToProps), + withServices, + withProps( + ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ + getExportUrl: (type) => { + if (type === 'pdf') { + const pdfUrl = getPdfUrl( + workpad, + { pageCount }, + services.platform.getBasePathInterface() + ); + return getAbsoluteUrl(pdfUrl); + } + + throw new Error(strings.getUnknownExportErrorMessage(type)); + }, + onCopy: (type) => { + switch (type) { + case 'pdf': + services.notify.info(strings.getCopyPDFMessage()); + break; + case 'reportingConfig': + services.notify.info(strings.getCopyReportingConfigMessage()); + break; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + onExport: (type) => { + switch (type) { + case 'pdf': + return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) + .then(({ data }: { data: { job: { id: string } } }) => { + services.notify.info(strings.getExportPDFMessage(), { + title: strings.getExportPDFTitle(workpad.name), + }); + + // register the job so a completion notification shows up when it's ready + jobCompletionNotifications.add(data.job.id); + }) + .catch((err: Error) => { + services.notify.error(err, { + title: strings.getExportPDFErrorTitle(workpad.name), + }); + }); + case 'json': + downloadWorkpad(workpad.id); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx index 5b4de05da3a3d..6b033feb26021 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { ViewMenu } from '../view_menu'; +import { ViewMenu } from '../view_menu.component'; const handlers = { setZoomScale: action('setZoomScale'), diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts index e2a05d13b017e..167b3822fd13d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -4,97 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withHandlers } from 'recompose'; -import { Dispatch } from 'redux'; -import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; -import { State, CanvasWorkpadBoundingBox } from '../../../../types'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../../../state/actions/elements'; -// @ts-expect-error untyped local -import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; -import { - setWriteable, - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, -} from '../../../state/actions/workpad'; -import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; -import { - getWorkpadBoundingBox, - getWorkpadWidth, - getWorkpadHeight, - isWriteable, - getRefreshInterval, - getAutoplay, -} from '../../../state/selectors/workpad'; -import { ViewMenu as Component, Props as ComponentProps } from './view_menu'; -import { getFitZoomScale } from './lib/get_fit_zoom_scale'; - -interface StateProps { - zoomScale: number; - boundingBox: CanvasWorkpadBoundingBox; - workpadWidth: number; - workpadHeight: number; - isWriteable: boolean; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; - setZoomScale: (scale: number) => void; - setFullscreen: (showFullscreen: boolean) => void; -} - -const mapStateToProps = (state: State) => { - const { enabled, interval } = getAutoplay(state); - - return { - zoomScale: getZoomScale(state), - boundingBox: getWorkpadBoundingBox(state), - workpadWidth: getWorkpadWidth(state), - workpadHeight: getWorkpadHeight(state), - isWriteable: isWriteable(state) && canUserWrite(state), - refreshInterval: getRefreshInterval(state), - autoplayEnabled: enabled, - autoplayInterval: interval, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), - setFullscreen: (value: boolean) => { - dispatch(setFullscreen(value)); - - if (value) { - dispatch(selectToplevelNodes([])); - } - }, - doRefresh: () => dispatch(fetchAllRenderables()), - setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), - enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), - setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => { - const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; - - return { - ...remainingStateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), - enterFullscreen: () => dispatchProps.setFullscreen(true), - fitToWindow: () => - dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), - }; -}; - -export const ViewMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withHandlers(zoomHandlerCreators) -)(Component); +export { ViewMenu } from './view_menu'; +export { ViewMenu as ViewMenuComponent } from './view_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts new file mode 100644 index 0000000000000..c9650a35ea2a6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers } from 'recompose'; +import { Dispatch } from 'redux'; +import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; +import { State, CanvasWorkpadBoundingBox } from '../../../../types'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +// @ts-expect-error untyped local +import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; +import { + setWriteable, + setRefreshInterval, + enableAutoplay, + setAutoplayInterval, +} from '../../../state/actions/workpad'; +import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; +import { + getWorkpadBoundingBox, + getWorkpadWidth, + getWorkpadHeight, + isWriteable, + getRefreshInterval, + getAutoplay, +} from '../../../state/selectors/workpad'; +import { ViewMenu as Component, Props as ComponentProps } from './view_menu.component'; +import { getFitZoomScale } from './lib/get_fit_zoom_scale'; + +interface StateProps { + zoomScale: number; + boundingBox: CanvasWorkpadBoundingBox; + workpadWidth: number; + workpadHeight: number; + isWriteable: boolean; +} + +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; + setZoomScale: (scale: number) => void; + setFullscreen: (showFullscreen: boolean) => void; +} + +const mapStateToProps = (state: State) => { + const { enabled, interval } = getAutoplay(state); + + return { + zoomScale: getZoomScale(state), + boundingBox: getWorkpadBoundingBox(state), + workpadWidth: getWorkpadWidth(state), + workpadHeight: getWorkpadHeight(state), + isWriteable: isWriteable(state) && canUserWrite(state), + refreshInterval: getRefreshInterval(state), + autoplayEnabled: enabled, + autoplayInterval: interval, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), + setFullscreen: (value: boolean) => { + dispatch(setFullscreen(value)); + + if (value) { + dispatch(selectToplevelNodes([])); + } + }, + doRefresh: () => dispatch(fetchAllRenderables()), + setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), + enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), + setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; + + return { + ...remainingStateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), + enterFullscreen: () => dispatchProps.setFullscreen(true), + fitToWindow: () => + dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), + }; +}; + +export const ViewMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withHandlers(zoomHandlerCreators) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx new file mode 100644 index 0000000000000..eb4b451896b46 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +// @ts-expect-error no @types definition +import { Shortcuts } from 'react-shortcuts'; +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { ComponentStrings } from '../../../i18n'; +import { ToolTipShortcut } from '../tool_tip_shortcut/'; +import { RefreshControl } from './refresh_control'; +// @ts-expect-error untyped local +import { FullscreenControl } from './fullscreen_control'; +import { EditMenu } from './edit_menu'; +import { ElementMenu } from './element_menu'; +import { ShareMenu } from './share_menu'; +import { ViewMenu } from './view_menu'; + +const { WorkpadHeader: strings } = ComponentStrings; + +export interface Props { + isWriteable: boolean; + toggleWriteable: () => void; + canUserWrite: boolean; + commit: (type: string, payload: any) => any; +} + +export const WorkpadHeader: FunctionComponent = ({ + isWriteable, + canUserWrite, + toggleWriteable, + commit, +}) => { + const keyHandler = (action: string) => { + if (action === 'EDITING') { + toggleWriteable(); + } + }; + + const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( + + {strings.getFullScreenTooltip()}{' '} + + + } + > + + + ); + + const getEditToggleToolTipText = () => { + if (!canUserWrite) { + return strings.getNoWritePermissionTooltipText(); + } + + const content = isWriteable + ? strings.getHideEditControlTooltip() + : strings.getShowEditControlTooltip(); + + return content; + }; + + const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { + const content = getEditToggleToolTipText(); + + if (textOnly) { + return content; + } + + return ( + + {content} + + ); + }; + + return ( + + + + {isWriteable && ( + + + + )} + + + + + + + + + + + + + + + {canUserWrite && ( + + )} + + + + + + + + + {fullscreenButton} + + + + + ); +}; + +WorkpadHeader.propTypes = { + isWriteable: PropTypes.bool, + toggleWriteable: PropTypes.func, + canUserWrite: PropTypes.bool, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx index eb4b451896b46..1f630040b0c36 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -4,147 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -// @ts-expect-error no @types definition -import { Shortcuts } from 'react-shortcuts'; -import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n'; -import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { RefreshControl } from './refresh_control'; -// @ts-expect-error untyped local -import { FullscreenControl } from './fullscreen_control'; -import { EditMenu } from './edit_menu'; -import { ElementMenu } from './element_menu'; -import { ShareMenu } from './share_menu'; -import { ViewMenu } from './view_menu'; - -const { WorkpadHeader: strings } = ComponentStrings; - -export interface Props { +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; +import { setWriteable } from '../../state/actions/workpad'; +import { State } from '../../../types'; +import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header.component'; + +interface StateProps { isWriteable: boolean; - toggleWriteable: () => void; canUserWrite: boolean; - commit: (type: string, payload: any) => any; + selectedPage: string; } -export const WorkpadHeader: FunctionComponent = ({ - isWriteable, - canUserWrite, - toggleWriteable, - commit, -}) => { - const keyHandler = (action: string) => { - if (action === 'EDITING') { - toggleWriteable(); - } - }; - - const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( - - {strings.getFullScreenTooltip()}{' '} - - - } - > - - - ); - - const getEditToggleToolTipText = () => { - if (!canUserWrite) { - return strings.getNoWritePermissionTooltipText(); - } - - const content = isWriteable - ? strings.getHideEditControlTooltip() - : strings.getShowEditControlTooltip(); - - return content; - }; - - const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { - const content = getEditToggleToolTipText(); - - if (textOnly) { - return content; - } - - return ( - - {content} - - ); - }; - - return ( - - - - {isWriteable && ( - - - - )} - - - - - - - - - - - - - - - {canUserWrite && ( - - )} - - - - - - - - - {fullscreenButton} - - - - - ); -}; +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; +} -WorkpadHeader.propTypes = { - isWriteable: PropTypes.bool, - toggleWriteable: PropTypes.func, - canUserWrite: PropTypes.bool, -}; +const mapStateToProps = (state: State): StateProps => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + canUserWrite: canUserWrite(state), + selectedPage: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => ({ + ...stateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), +}); + +export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.js b/x-pack/plugins/canvas/storybook/storyshots.test.js index e3a9654bb49fa..dbcbbff6398b5 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/storybook/storyshots.test.js @@ -73,7 +73,7 @@ jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( - '../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories', + '../public/components/workpad_header/share_menu/flyout/__examples__/flyout.stories', () => { return 'Disabled Panel'; } From b0d51c8e0a80d34b5356bd4a5c746834a1f5c055 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Fri, 31 Jul 2020 12:01:11 -0400 Subject: [PATCH 018/115] Resolver/test panel presence (#73889) * Test for panel presence --- .../resolver/test_utilities/simulator/index.tsx | 14 ++++++++++++++ .../public/resolver/view/clickthrough.test.tsx | 10 ++++++++++ .../public/resolver/view/panel.tsx | 2 +- .../view/panels/panel_content_process_list.tsx | 7 ++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 7a61427c56a3b..2a2354921a3d4 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -251,6 +251,20 @@ export class Simulator { return this.findInDOM('[data-test-subj="resolver:graph"]'); } + /** + * The outer panel container. + */ + public panelElement(): ReactWrapper { + return this.findInDOM('[data-test-subj="resolver:panel"]'); + } + + /** + * The panel content element (which may include tables, lists, other data depending on the view). + */ + public panelContentElement(): ReactWrapper { + return this.findInDOM('[data-test-subj^="resolver:panel:"]'); + } + /** * Like `this.wrapper.find` but only returns DOM nodes. */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 9cb900736677e..f339d128944cc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -63,6 +63,16 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( expect(simulator.processNodeElements().length).toBe(3); }); + it(`should have the default "process list" panel present`, async () => { + expect(simulator.panelElement().length).toBe(1); + expect(simulator.panelContentElement().length).toBe(1); + const testSubjectName = simulator + .panelContentElement() + .getDOMNode() + .getAttribute('data-test-subj'); + expect(testSubjectName).toMatch(/process-list/g); + }); + describe("when the second child node's first button has been clicked", () => { beforeEach(() => { // Click the first button under the second child element. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 83d3930065da6..f378ab36bac94 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -220,7 +220,7 @@ PanelContent.displayName = 'PanelContent'; export const Panel = memo(function Event({ className }: { className?: string }) { return ( - + ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx index efb96cde431e5..8ca002ace26fe 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx @@ -187,7 +187,12 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ {showWarning && } - items={processTableView} columns={columns} sorting /> + + data-test-subj="resolver:panel:process-list" + items={processTableView} + columns={columns} + sorting + /> ); }); From 69844e45eb8247344ea913d006680deced5d3b34 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 31 Jul 2020 11:22:14 -0500 Subject: [PATCH 019/115] [ML] Add API integration testing for AD annotations (#73068) Co-authored-by: Elastic Machine --- .../apis/ml/annotations/common_jobs.ts | 58 ++++++ .../apis/ml/annotations/create_annotations.ts | 89 +++++++++ .../apis/ml/annotations/delete_annotations.ts | 91 +++++++++ .../apis/ml/annotations/get_annotations.ts | 130 +++++++++++++ .../apis/ml/annotations/index.ts | 16 ++ .../apis/ml/annotations/update_annotations.ts | 175 ++++++++++++++++++ x-pack/test/api_integration/apis/ml/index.ts | 1 + x-pack/test/functional/services/ml/api.ts | 88 +++++++++ 8 files changed, 648 insertions(+) create mode 100644 x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts diff --git a/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts new file mode 100644 index 0000000000000..873cdc5d71baa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; + +export const commonJobConfig = { + description: 'test_job_annotation', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'min', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, +}; + +export const createJobConfig = (jobId: string) => { + return { ...commonJobConfig, job_id: jobId }; +}; + +export const testSetupJobConfigs = [1, 2, 3, 4].map((num) => ({ + ...commonJobConfig, + job_id: `job_annotation_${num}_${Date.now()}`, + description: `Test annotation ${num}`, +})); +export const jobIds = testSetupJobConfigs.map((j) => j.job_id); + +export const createAnnotationRequestBody = (jobId: string): Partial => { + return { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; +}; + +export const testSetupAnnotations = testSetupJobConfigs.map((job) => + createAnnotationRequestBody(job.job_id) +); diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts new file mode 100644 index 0000000000000..14ecf1bfe524e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { createJobConfig, createAnnotationRequestBody } from './common_jobs'; +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `job_annotation_${Date.now()}`; + const testJobConfig = createJobConfig(jobId); + const annotationRequestBody = createAnnotationRequestBody(jobId); + + describe('create_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createAnomalyDetectionJob(testJobConfig); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should successfully create annotations for anomaly job', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + const annotationId = body._id; + + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + + expect(fetchedAnnotation).to.not.be(undefined); + + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_POWERUSER); + }); + + it('should successfully create annotation for user with ML read permissions', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + + const annotationId = body._id; + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + expect(fetchedAnnotation).to.not.be(undefined); + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_VIEWER); + }); + + it('should not allow to create annotation for unauthorized user', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts new file mode 100644 index 0000000000000..4fbb26e9b5a3e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('delete_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should delete annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should delete annotation by id for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should not delete annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + await ml.api.waitForAnnotationToExist(annotationIdToDelete); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts new file mode 100644 index 0000000000000..710473eed6901 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { omit } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('get_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should fetch all annotations for jobId', async () => { + const requestBody = { + jobIds: [jobIds[0]], + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + [jobIds[0]].forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for multiple jobs', async () => { + const requestBody = { + jobIds, + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for user with ML read permissions', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should not allow to fetch annotation for unauthorized user', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/index.ts b/x-pack/test/api_integration/apis/ml/annotations/index.ts new file mode 100644 index 0000000000000..7d73ee43d4d99 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('annotations', function () { + loadTestFile(require.resolve('./create_annotations')); + loadTestFile(require.resolve('./get_annotations')); + loadTestFile(require.resolve('./delete_annotations')); + loadTestFile(require.resolve('./update_annotations')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts new file mode 100644 index 0000000000000..ba73617151120 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const commonAnnotationUpdateRequestBody: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + partition_field_name: 'airline', + partition_field_value: 'ANA', + }; + + describe('update_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should correctly update annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } + }); + + it('should correctly update annotation for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } + }); + + it('should not update annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + expect(updatedAnnotation).to.eql(originalAnnotation._source); + }); + + it('should override fields correctly', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[3]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBodyWithMissingFields: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + job_id: originalAnnotation._source.job_id, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + _id: originalAnnotation._id, + }; + await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBodyWithMissingFields) + .expect(200); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(annotationUpdateRequestBodyWithMissingFields).forEach((key) => { + if (key !== '_id') { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql( + annotationUpdateRequestBodyWithMissingFields[field] + ); + } + }); + } + // validate missing fields in the annotationUpdateRequestBody + expect(updatedAnnotation?.partition_field_name).to.be(undefined); + expect(updatedAnnotation?.partition_field_value).to.be(undefined); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index b29bc47b50394..969f291b0d8b3 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -60,5 +60,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./data_frame_analytics')); loadTestFile(require.resolve('./filters')); loadTestFile(require.resolve('./calendars')); + loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 9dfec3a17dec0..401a96c5c11bd 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,13 +5,29 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { IndexDocumentParams } from 'elasticsearch'; import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index'; +import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; +import { + ML_ANNOTATIONS_INDEX_ALIAS_READ, + ML_ANNOTATIONS_INDEX_ALIAS_WRITE, +} from '../../../../plugins/ml/common/constants/index_patterns'; + +interface EsIndexResult { + _index: string; + _id: string; + _version: number; + result: string; + _shards: any; + _seq_no: number; + _primary_term: number; +} export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); @@ -634,5 +650,77 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } }); }, + + async getAnnotations(jobId: string) { + log.debug(`Fetching annotations for job '${jobId}'...`); + + const results = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + query: { + match: { + job_id: jobId, + }, + }, + }, + }); + expect(results).to.not.be(undefined); + expect(results).to.have.property('hits'); + return results.hits.hits; + }, + + async getAnnotationById(annotationId: string): Promise { + log.debug(`Fetching annotation '${annotationId}'...`); + + const result = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + size: 1, + query: { + match: { + _id: annotationId, + }, + }, + }, + }); + // @ts-ignore due to outdated type for hits.total + if (result.hits.total.value === 1) { + return result?.hits?.hits[0]?._source as Annotation; + } + return undefined; + }, + + async indexAnnotation(annotationRequestBody: Partial) { + log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`); + // @ts-ignore due to outdated type for IndexDocumentParams.type + const params: IndexDocumentParams> = { + index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, + body: annotationRequestBody, + refresh: 'wait_for', + }; + const results: EsIndexResult = await es.index(params); + await this.waitForAnnotationToExist(results._id); + return results; + }, + + async waitForAnnotationToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) !== undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should exist`); + } + }); + }, + + async waitForAnnotationNotToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) === undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should not exist`); + } + }); + }, }; } From 1c0b77c494c7d0a85a922f84597a5c3b2ecd4da8 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 31 Jul 2020 18:59:51 +0200 Subject: [PATCH 020/115] fix: pinned filters not applied (#73825) --- .../lens/public/app_plugin/app.test.tsx | 21 +++++++++++++++++++ x-pack/plugins/lens/public/app_plugin/app.tsx | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index a72f4f429a1be..b30a586487009 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -249,6 +249,27 @@ describe('Lens App', () => { expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); }); + it('passes global filters to frame', async () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); + args.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { + return [pinnedFilter]; + }); + const component = mount(); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + dateRange: { fromDate: 'now-7d', toDate: 'now' }, + query: { query: '', language: 'kuery' }, + filters: [pinnedFilter], + }) + ); + }); + it('sets breadcrumbs when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); instance = mount(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 2a7eaff32fa08..ab4c4820315ac 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -94,7 +94,7 @@ export function App({ toDate: currentRange.to, }, originatingApp, - filters: [], + filters: data.query.filterManager.getFilters(), indicateNoData: false, }; }); From 002e4598a5de5e2e65d99b1f52d069a01c3ffbd9 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 31 Jul 2020 14:04:31 -0400 Subject: [PATCH 021/115] [Ingest Manager] Revert fleet config concurrency rollout to rate limit (#73940) --- .../ingest_manager/common/types/index.ts | 3 +- x-pack/plugins/ingest_manager/server/index.ts | 3 +- .../agents/checkin/rxjs_utils.test.ts | 45 ----------------- .../services/agents/checkin/rxjs_utils.ts | 50 +++++++++++++------ .../agents/checkin/state_new_actions.ts | 9 ++-- 5 files changed, 43 insertions(+), 67 deletions(-) delete mode 100644 x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 7acef263f973a..69bcc498c18be 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -22,7 +22,8 @@ export interface IngestManagerConfigType { host?: string; ca_sha256?: string; }; - agentConfigRolloutConcurrency: number; + agentConfigRolloutRateLimitIntervalMs: number; + agentConfigRolloutRateLimitRequestPerInterval: number; }; } diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 6f8c4948559d3..e2f659f54d625 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -35,7 +35,8 @@ export const config = { host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), }), - agentConfigRolloutConcurrency: schema.number({ defaultValue: 10 }), + agentConfigRolloutRateLimitIntervalMs: schema.number({ defaultValue: 5000 }), + agentConfigRolloutRateLimitRequestPerInterval: schema.number({ defaultValue: 5 }), }), }), }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts deleted file mode 100644 index 70207dcf325c4..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Rx from 'rxjs'; -import { share } from 'rxjs/operators'; -import { createSubscriberConcurrencyLimiter } from './rxjs_utils'; - -function createSpyObserver(o: Rx.Observable): [Rx.Subscription, jest.Mock] { - const spy = jest.fn(); - const observer = o.subscribe(spy); - return [observer, spy]; -} - -describe('createSubscriberConcurrencyLimiter', () => { - it('should not publish to more than n concurrent subscriber', async () => { - const subject = new Rx.Subject(); - const sharedObservable = subject.pipe(share()); - - const limiter = createSubscriberConcurrencyLimiter(2); - - const [observer1, spy1] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer2, spy2] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer3, spy3] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer4, spy4] = createSpyObserver(sharedObservable.pipe(limiter())); - subject.next('test1'); - - expect(spy1).toBeCalled(); - expect(spy2).toBeCalled(); - expect(spy3).not.toBeCalled(); - expect(spy4).not.toBeCalled(); - - observer1.unsubscribe(); - expect(spy3).toBeCalled(); - expect(spy4).not.toBeCalled(); - - observer2.unsubscribe(); - expect(spy4).toBeCalled(); - - observer3.unsubscribe(); - observer4.unsubscribe(); - }); -}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts index dc0ed35207e46..dddade6841460 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts @@ -43,23 +43,37 @@ export const toPromiseAbortable = ( } }); -export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { - let observers: Array<[Rx.Subscriber, any]> = []; - let activeObservers: Array> = []; +export function createRateLimiter( + ratelimitIntervalMs: number, + ratelimitRequestPerInterval: number +) { + function createCurrentInterval() { + return { + startedAt: Rx.asyncScheduler.now(), + numRequests: 0, + }; + } - function processNext() { - if (activeObservers.length >= maxConcurrency) { - return; - } - const observerValuePair = observers.shift(); + let currentInterval: { startedAt: number; numRequests: number } = createCurrentInterval(); + let observers: Array<[Rx.Subscriber, any]> = []; + let timerSubscription: Rx.Subscription | undefined; - if (!observerValuePair) { + function createTimeout() { + if (timerSubscription) { return; } - - const [observer, value] = observerValuePair; - activeObservers.push(observer); - observer.next(value); + timerSubscription = Rx.asyncScheduler.schedule(() => { + timerSubscription = undefined; + currentInterval = createCurrentInterval(); + for (const [waitingObserver, value] of observers) { + if (currentInterval.numRequests >= ratelimitRequestPerInterval) { + createTimeout(); + continue; + } + currentInterval.numRequests++; + waitingObserver.next(value); + } + }, ratelimitIntervalMs); } return function limit(): Rx.MonoTypeOperatorFunction { @@ -67,8 +81,14 @@ export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { new Rx.Observable((observer) => { const subscription = observable.subscribe({ next(value) { + if (currentInterval.numRequests < ratelimitRequestPerInterval) { + currentInterval.numRequests++; + observer.next(value); + return; + } + observers = [...observers, [observer, value]]; - processNext(); + createTimeout(); }, error(err) { observer.error(err); @@ -79,10 +99,8 @@ export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { }); return () => { - activeObservers = activeObservers.filter((o) => o !== observer); observers = observers.filter((o) => o[0] !== observer); subscription.unsubscribe(); - processNext(); }; }); }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index 53270afe453c4..1547b6b5ea053 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -28,7 +28,7 @@ import * as APIKeysService from '../../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_UPDATE_ACTIONS_INTERVAL_MS } from '../../../constants'; import { createAgentAction, getNewActionsSince } from '../actions'; import { appContextService } from '../../app_context'; -import { toPromiseAbortable, AbortError, createSubscriberConcurrencyLimiter } from './rxjs_utils'; +import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -134,8 +134,9 @@ export function agentCheckinStateNewActionsFactory() { const agentConfigs$ = new Map>(); const newActions$ = createNewActionsSharedObservable(); // Rx operators - const concurrencyLimiter = createSubscriberConcurrencyLimiter( - appContextService.getConfig()?.fleet.agentConfigRolloutConcurrency ?? 10 + const rateLimiter = createRateLimiter( + appContextService.getConfig()?.fleet.agentConfigRolloutRateLimitIntervalMs ?? 5000, + appContextService.getConfig()?.fleet.agentConfigRolloutRateLimitRequestPerInterval ?? 50 ); async function subscribeToNewActions( @@ -158,7 +159,7 @@ export function agentCheckinStateNewActionsFactory() { const stream$ = agentConfig$.pipe( timeout(appContextService.getConfig()?.fleet.pollingRequestTimeout || 0), filter((config) => shouldCreateAgentConfigAction(agent, config)), - concurrencyLimiter(), + rateLimiter(), mergeMap((config) => createAgentActionFromConfig(soClient, agent, config)), merge(newActions$), mergeMap(async (data) => { From 1f2c01531945890a1d944e7d18c16af2d1a29af7 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 31 Jul 2020 14:05:43 -0400 Subject: [PATCH 022/115] Fix a typo. (#73948) --- x-pack/plugins/ingest_manager/dev_docs/definitions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/dev_docs/definitions.md b/x-pack/plugins/ingest_manager/dev_docs/definitions.md index a33d95f3afa38..bd20780611055 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/definitions.md +++ b/x-pack/plugins/ingest_manager/dev_docs/definitions.md @@ -10,7 +10,7 @@ This section is to define terms used across ingest management. A package config is a definition on how to collect data from a service, for example `nginx`. A package config contains definitions for one or multiple inputs and each input can contain one or multiple streams. -With the example of the nginx Package Config, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected +With the example of the nginx Package Config, it contains two inputs: `logs` and `nginx/metrics`. Logs and metrics are collected differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. ## Data Stream From d9efd265c94283c9a021fa91830f2ef21cedb194 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 31 Jul 2020 13:22:40 -0500 Subject: [PATCH 023/115] [build/sysv] fix missing env variable rename (#73977) --- .../tasks/os_packages/service_templates/sysv/etc/init.d/kibana | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index 8facbb709cc5c..449fc4e75fce8 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -22,7 +22,7 @@ pidfile="/var/run/kibana/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name [ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name -export KIBANA_PATH_CONF +export KBN_PATH_CONF export NODE_OPTIONS [ -z "$nice" ] && nice=0 From 357139d67c476d7dbe28413870c21fb8ff6f1190 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 31 Jul 2020 14:55:29 -0400 Subject: [PATCH 024/115] [Ingest Manager] Fix limited concurrency helper (#73976) --- .../ingest_manager/server/routes/limited_concurrency.test.ts | 2 +- .../ingest_manager/server/routes/limited_concurrency.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts index f84f417ce402d..e5b5a83743287 100644 --- a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts @@ -39,7 +39,7 @@ describe('registerLimitedConcurrencyRoutes', () => { }); // assertions for calls to .decrease are commented out because it's called on the -// "req.events.aborted$ observable (which) will never emit from a mocked request in a jest unit test environment" +// "req.events.completed$ observable (which) will never emit from a mocked request in a jest unit test environment" // https://github.com/elastic/kibana/pull/72338#issuecomment-661908791 describe('preAuthHandler', () => { test(`ignores routes when !isMatch`, async () => { diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts index 11fdc944e031d..7ba8e151b726c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts @@ -66,9 +66,7 @@ export function createLimitedPreAuthHandler({ maxCounter.increase(); - // requests.events.aborted$ has a bug (but has test which explicitly verifies) where it's fired even when the request completes - // https://github.com/elastic/kibana/pull/70495#issuecomment-656288766 - request.events.aborted$.toPromise().then(() => { + request.events.completed$.toPromise().then(() => { maxCounter.decrease(); }); From 17e8d18a40a032802fc1947d18ae4d799ea1925f Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Fri, 31 Jul 2020 12:03:09 -0700 Subject: [PATCH 025/115] [APM] docs: Update machine learning integration (#73597) --- docs/apm/images/apm-anomaly-alert.png | Bin 0 -> 62561 bytes docs/apm/machine-learning.asciidoc | 49 +++++++++++++++++++------- 2 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 docs/apm/images/apm-anomaly-alert.png diff --git a/docs/apm/images/apm-anomaly-alert.png b/docs/apm/images/apm-anomaly-alert.png new file mode 100644 index 0000000000000000000000000000000000000000..35ce9a2296c9c965cc383740ff4a5cb9fb10cdbe GIT binary patch literal 62561 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 aXE?dYgZea;GI0d>+`FT5JNK6L^Zx@%rUa+} literal 0 HcmV?d00001 diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc index b203b8668072f..db2a1ef6e2da0 100644 --- a/docs/apm/machine-learning.asciidoc +++ b/docs/apm/machine-learning.asciidoc @@ -1,36 +1,61 @@ [role="xpack"] [[machine-learning-integration]] -=== integration +=== Machine learning integration ++++ Integrate with machine learning ++++ -The Machine Learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. -Jobs can be created per transaction type, and are based on the service's average response time. +The Machine learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. +With this integration, you can quickly pinpoint anomalous transactions and see the health of +any upstream and downstream services. -After a machine learning job is created, results are shown in two places: +Machine learning jobs are created per environment, and are based on a service's average response time. +Because jobs are created at the environment level, +you can add new services to your existing environments without the need for additional machine learning jobs. -The transaction duration graph will show the expected bounds and add an annotation when the anomaly score is 75 or above. +After a machine learning job is created, results are shown in two places: +* The transaction duration chart will show the expected bounds and add an annotation when the anomaly score is 75 or above. ++ [role="screenshot"] image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in the APM app] -Service maps will display a color-coded anomaly indicator based on the detected anomaly score. - +* Service maps will display a color-coded anomaly indicator based on the detected anomaly score. ++ [role="screenshot"] image::apm/images/apm-service-map-anomaly.png[Example view of anomaly scores on service maps in the APM app] [float] [[create-ml-integration]] -=== Create a new machine learning job +=== Enable anomaly detection + +To enable machine learning anomaly detection: + +. From the Services overview, Traces overview, or Service Map tab, +select **Anomaly detection**. + +. Click **Create ML Job**. -To enable machine learning anomaly detection, first choose a service to monitor. -Then, select **Integrations** > **Enable ML anomaly detection** and click **Create job**. +. Machine learning jobs are created at the environment level. +Select all of the service environments that you want to enable anomaly detection in. +Anomalies will surface for all services and transaction types within the selected environments. + +. Click **Create Jobs**. That's it! After a few minutes, the job will begin calculating results; -it might take additional time for results to appear on your graph. -Jobs can be managed in *Machine Learning jobs management*. +it might take additional time for results to appear on your service maps. +Existing jobs can be managed in *Machine Learning jobs management*. APM specific anomaly detection wizards are also available for certain Agents. See the machine learning {ml-docs}/ootb-ml-jobs-apm.html[APM anomaly detection configurations] for more information. + +[float] +[[warning-ml-integration]] +=== Anomaly detection warning + +To make machine learning as easy as possible to set up, +the APM app will warn you when filtered to an environment without a machine learning job. + +[role="screenshot"] +image::apm/images/apm-anomaly-alert.png[Example view of anomaly alert in the APM app] \ No newline at end of file From 2e37239e50a5632798e17a455699e828831d37fd Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Fri, 31 Jul 2020 13:20:11 -0700 Subject: [PATCH 026/115] [Metrics UI] Fix Metrics Explorer TSVB link to use workaround pattern (#73986) * [Metrics UI] Fix Metrics Explorer TSVB link to use workaround pattern * Adding link to TSVB bug to comment --- .../helpers/create_tsvb_link.test.ts | 19 ++++++++++++++++ .../components/helpers/create_tsvb_link.ts | 22 +++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts index 04aeba41fa00d..ca4fc0abc37a4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts @@ -157,6 +157,25 @@ describe('createTSVBLink()', () => { }); }); + it('should use the workaround index pattern when there are multiple listed in the source', () => { + const customSource = { + ...source, + metricAlias: 'my-beats-*,metrics-*', + fields: { ...source.fields, timestamp: 'time' }, + }; + const link = createTSVBLink(customSource, options, series, timeRange, chartOptions); + expect(link).toStrictEqual({ + app: 'visualize', + hash: '/create', + search: { + _a: + "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metric*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metric*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', + type: 'metrics', + }, + }); + }); + test('createFilterFromOptions()', () => { const customOptions = { ...options, groupBy: 'host.name' }; const customSeries = { ...series, id: 'test"foo' }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 3afc0d050e736..afddaf6621f10 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -23,6 +23,14 @@ import { SourceQuery } from '../../../../../graphql/types'; import { createMetricLabel } from './create_metric_label'; import { LinkDescriptor } from '../../../../../hooks/use_link_props'; +/* + We've recently changed the default index pattern in Metrics UI from `metricbeat-*` to + `metrics-*,metricbeat-*`. There is a bug in TSVB when there is an empty index in the pattern + the field dropdowns are not populated correctly. This index pattern is a temporary fix. + See: https://github.com/elastic/kibana/issues/73987 +*/ +const TSVB_WORKAROUND_INDEX_PATTERN = 'metric*'; + export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptionsMetric) => { if (metric.aggregation === 'rate') { const metricId = uuid.v1(); @@ -128,6 +136,13 @@ export const createFilterFromOptions = ( return { language: 'kuery', query: filters.join(' and ') }; }; +const createTSVBIndexPattern = (alias: string) => { + if (alias.split(',').length > 1) { + return TSVB_WORKAROUND_INDEX_PATTERN; + } + return alias; +}; + export const createTSVBLink = ( source: SourceQuery.Query['source']['configuration'] | undefined, options: MetricsExplorerOptions, @@ -135,6 +150,9 @@ export const createTSVBLink = ( timeRange: MetricsExplorerTimeOptions, chartOptions: MetricsExplorerChartOptions ): LinkDescriptor => { + const tsvbIndexPattern = createTSVBIndexPattern( + (source && source.metricAlias) || TSVB_WORKAROUND_INDEX_PATTERN + ); const appState = { filters: [], linked: false, @@ -147,8 +165,8 @@ export const createTSVBLink = ( axis_position: 'left', axis_scale: 'normal', id: uuid.v1(), - default_index_pattern: (source && source.metricAlias) || 'metricbeat-*', - index_pattern: (source && source.metricAlias) || 'metricbeat-*', + default_index_pattern: tsvbIndexPattern, + index_pattern: tsvbIndexPattern, interval: 'auto', series: options.metrics.map(mapMetricToSeries(chartOptions)), show_grid: 1, From 2c71a3fba9508c61995870b643c2cdb1da5d14a2 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 31 Jul 2020 22:17:24 +0100 Subject: [PATCH 027/115] [Security Solution] Fix unexpected redirect (#73969) * fix unexpected redirect * fix types Co-authored-by: Patryk Kopycinski --- .../components/open_timeline/index.test.tsx | 61 ++++++++++++++----- .../open_timeline/use_timeline_types.tsx | 26 +++++--- .../public/timelines/pages/timelines_page.tsx | 2 +- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 6c1c88f511edb..75b6413bf08f9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -17,11 +17,25 @@ import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; -import { TimelineTabsStyle } from './types'; import { StatefulOpenTimeline } from '.'; + import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; + +import { useParams } from 'react-router-dom'; +import { TimelineType } from '../../../../common/types/timeline'; + jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/link_to'); + +jest.mock('./helpers', () => { + const originalModule = jest.requireActual('./helpers'); + return { + ...originalModule, + queryTimelineById: jest.fn(), + }; +}); + jest.mock('../../containers/all', () => { const originalModule = jest.requireActual('../../containers/all'); return { @@ -30,19 +44,21 @@ jest.mock('../../containers/all', () => { getAllTimeline: originalModule.getAllTimeline, }; }); -jest.mock('./use_timeline_types', () => { + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { - useTimelineTypes: jest.fn().mockReturnValue({ - timelineType: 'default', - timelineTabs:
, - timelineFilters:
, - }), + ...originalModule, + useParams: jest.fn(), + useHistory: jest.fn().mockReturnValue([]), }; }); describe('StatefulOpenTimeline', () => { const title = 'All Timelines / Open Timelines'; beforeEach(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), timelines: getAllTimeline( @@ -433,10 +449,7 @@ describe('StatefulOpenTimeline', () => { }); }); - /** - * enable this test when createtTemplateTimeline is ready - */ - test.skip('it renders the tabs', async () => { + test('it has the expected initial state for openTimeline - templateTimelineFilter', () => { const wrapper = mount( @@ -451,11 +464,27 @@ describe('StatefulOpenTimeline', () => { ); - await waitFor(() => { - expect( - wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists() - ).toEqual(true); - }); + expect(wrapper.find('[data-test-subj="open-timeline-subtabs"]').exists()).toEqual(true); + }); + + test('it has the expected initial state for openTimelineModalBody - templateTimelineFilter', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="open-timeline-modal-body-filters"]').exists()).toEqual( + true + ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 7d54bb2209850..55afe845cdfb3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -7,26 +7,31 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/link_to'; import * as i18n from './translations'; import { TimelineTabsStyle, TimelineTab } from './types'; -export const useTimelineTypes = ({ - defaultTimelineCount, - templateTimelineCount, -}: { +export interface UseTimelineTypesArgs { defaultTimelineCount?: number | null; templateTimelineCount?: number | null; -}): { +} + +export interface UseTimelineTypesResult { timelineType: TimelineTypeLiteralWithNull; timelineTabs: JSX.Element; timelineFilters: JSX.Element[]; -} => { +} + +export const useTimelineTypes = ({ + defaultTimelineCount, + templateTimelineCount, +}: UseTimelineTypesArgs): UseTimelineTypesResult => { const history = useHistory(); const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); - const { tabName } = useParams<{ pageName: string; tabName: string }>(); + const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [timelineType, setTimelineTypes] = useState( tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null ); @@ -61,7 +66,7 @@ export const useTimelineTypes = ({ timelineTabsStyle === TimelineTabsStyle.filter ? defaultTimelineCount ?? undefined : undefined, - onClick: goToTimeline, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop, }, { id: TimelineType.template, @@ -76,7 +81,7 @@ export const useTimelineTypes = ({ timelineTabsStyle === TimelineTabsStyle.filter ? templateTimelineCount ?? undefined : undefined, - onClick: goToTemplateTimeline, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop, }, ], [ @@ -106,7 +111,7 @@ export const useTimelineTypes = ({ const timelineTabs = useMemo(() => { return ( <> - + {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( { return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( { - const { tabName } = useParams(); + const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [importDataModalToggle, setImportDataModalToggle] = useState(false); const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); From 5aca964d689f2fad5ab18c06250d6d2e8432d494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Fri, 31 Jul 2020 23:20:47 +0200 Subject: [PATCH 028/115] [Security Solution] Fix timeline pin event callback (#73981) * [Security Solution] Fix timeline pin event callback * - added tests * - restored the original disabled button behavior Co-authored-by: Andrew Goldstein --- .../components/timeline/body/helpers.test.ts | 69 +++++++++++++++++++ .../components/timeline/body/helpers.ts | 14 ++-- .../components/timeline/pin/index.test.tsx | 69 ++++++++++++++++++- .../components/timeline/pin/index.tsx | 2 +- 4 files changed, 148 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts index c8adaa891610a..f4dc691f3d059 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts @@ -9,6 +9,7 @@ import { Ecs } from '../../../../graphql/types'; import { eventHasNotes, eventIsPinned, + getPinOnClick, getPinTooltip, stringifyEvent, isInvestigateInResolverActionEnabled, @@ -298,4 +299,72 @@ describe('helpers', () => { expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); }); }); + + describe('getPinOnClick', () => { + const eventId = 'abcd'; + + test('it invokes `onPinEvent` with the expected eventId when the event is NOT pinned, and allowUnpinning is true', () => { + const isEventPinned = false; // the event is NOT pinned + const allowUnpinning = true; + const onPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent: jest.fn(), + isEventPinned, + }); + + expect(onPinEvent).toBeCalledWith(eventId); + }); + + test('it does NOT invoke `onPinEvent` when the event is NOT pinned, and allowUnpinning is false', () => { + const isEventPinned = false; // the event is NOT pinned + const allowUnpinning = false; + const onPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent: jest.fn(), + isEventPinned, + }); + + expect(onPinEvent).not.toBeCalled(); + }); + + test('it invokes `onUnPinEvent` with the expected eventId when the event is pinned, and allowUnpinning is true', () => { + const isEventPinned = true; // the event is pinned + const allowUnpinning = true; + const onUnPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent: jest.fn(), + onUnPinEvent, + isEventPinned, + }); + + expect(onUnPinEvent).toBeCalledWith(eventId); + }); + + test('it does NOT invoke `onUnPinEvent` when the event is pinned, and allowUnpinning is false', () => { + const isEventPinned = true; // the event is pinned + const allowUnpinning = false; + const onUnPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent: jest.fn(), + onUnPinEvent, + isEventPinned, + }); + + expect(onUnPinEvent).not.toBeCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts index 6a5e25632c29b..73b5a58ef7b65 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty, noop } from 'lodash/fp'; + +import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; @@ -65,11 +66,16 @@ export const getPinOnClick = ({ onPinEvent, onUnPinEvent, isEventPinned, -}: GetPinOnClickParams): (() => void) => { +}: GetPinOnClickParams) => { if (!allowUnpinning) { - return noop; + return; + } + + if (isEventPinned) { + onUnPinEvent(eventId); + } else { + onPinEvent(eventId); } - return isEventPinned ? () => onUnPinEvent(eventId) : () => onPinEvent(eventId); }; /** diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx index 657976e2f4787..2ca27ded86c9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getPinIcon } from './'; +import { mount } from 'enzyme'; +import React from 'react'; + +import { TimelineType } from '../../../../../common/types/timeline'; + +import { getPinIcon, Pin } from './'; + +interface ButtonIcon { + isDisabled: boolean; +} describe('pin', () => { describe('getPinRotation', () => { @@ -16,4 +25,62 @@ describe('pin', () => { expect(getPinIcon(false)).toEqual('pin'); }); }); + + describe('disabled button behavior', () => { + test('the button is enabled when allowUnpinning is true, and timelineType is NOT `template` (the default)', () => { + const allowUnpinning = true; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(false); + }); + + test('the button is disabled when allowUnpinning is false, and timelineType is NOT `template` (the default)', () => { + const allowUnpinning = false; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + + test('the button is disabled when allowUnpinning is true, and timelineType is `template`', () => { + const allowUnpinning = true; + const timelineType = TimelineType.template; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + + test('the button is disabled when allowUnpinning is false, and timelineType is `template`', () => { + const allowUnpinning = false; + const timelineType = TimelineType.template; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx index 30fe8ae0ca1f6..27780c7754d00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx @@ -34,7 +34,7 @@ export const Pin = React.memo( iconSize={iconSize} iconType={getPinIcon(pinned)} onClick={onClick} - isDisabled={isTemplate} + isDisabled={isTemplate || !allowUnpinning} /> ); } From 75eda6690ef786f831bf7c46fc07260ffc0b37ff Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 31 Jul 2020 15:36:23 -0600 Subject: [PATCH 029/115] [SIEM] Fixes toaster errors when siemDefault index is an empty or empty spaces (#73991) ## Summary Fixes fully this issue: https://github.com/elastic/kibana/issues/49753 If you go to advanced settings and configure siemDefaultIndex to be an empty string or have empty spaces: Screen Shot 2020-07-31 at 12 52 00 PM You shouldn't get any toaster errors when going to any of the pages such as overview, detections, etc... This fixes that and adds both unit and integration tests around those areas. The fix is to add a filter which will filter all the patterns out that are either empty strings or have the _all within them rather than just looking for a single value to exist. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../graphql/source_status/resolvers.test.ts | 49 ++++++++ .../server/graphql/source_status/resolvers.ts | 31 +++-- .../lib/index_fields/elasticsearch_adapter.ts | 23 ++-- .../apis/security_solution/sources.ts | 107 +++++++++++++++--- 4 files changed, 168 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts new file mode 100644 index 0000000000000..1735c6473bb3a --- /dev/null +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { filterIndexes } from './resolvers'; + +describe('resolvers', () => { + test('it should filter single index that has an empty string', () => { + const emptyArray = filterIndexes(['']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter single index that has blanks within it', () => { + const emptyArray = filterIndexes([' ']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter indexes that has an empty string and a valid index', () => { + const emptyArray = filterIndexes(['', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter indexes that have blanks within them and a valid index', () => { + const emptyArray = filterIndexes([' ', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter single index that has _all within it', () => { + const emptyArray = filterIndexes(['_all']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter single index that has _all within it surrounded by spaces', () => { + const emptyArray = filterIndexes([' _all ']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter indexes that _all within them and a valid index', () => { + const emptyArray = filterIndexes(['_all', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter indexes that _all surrounded with spaces within them and a valid index', () => { + const emptyArray = filterIndexes([' _all ', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); +}); diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts index 8d55e645d6791..84320b1699531 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts @@ -32,27 +32,34 @@ export const createSourceStatusResolvers = (libs: { }; } => ({ SourceStatus: { - async indicesExist(source, args, { req }) { - if ( - args.defaultIndex.length === 1 && - (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') - ) { + async indicesExist(_, args, { req }) { + const indexes = filterIndexes(args.defaultIndex); + if (indexes.length !== 0) { + return libs.sourceStatus.hasIndices(req, indexes); + } else { return false; } - return libs.sourceStatus.hasIndices(req, args.defaultIndex); }, - async indexFields(source, args, { req }) { - if ( - args.defaultIndex.length === 1 && - (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') - ) { + async indexFields(_, args, { req }) { + const indexes = filterIndexes(args.defaultIndex); + if (indexes.length !== 0) { + return libs.fields.getFields(req, indexes); + } else { return []; } - return libs.fields.getFields(req, args.defaultIndex); }, }, }); +/** + * Given a set of indexes this will remove anything that is: + * - blank or empty strings are removed as not valid indexes + * - _all is removed as that is not a valid index + * @param indexes Indexes with invalid values removed + */ +export const filterIndexes = (indexes: string[]): string[] => + indexes.filter((index) => index.trim() !== '' && index.trim() !== '_all'); + export const toIFieldSubTypeNonNullableScalar = new GraphQLScalarType({ name: 'IFieldSubType', description: 'Represents value in index pattern field item', diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts index 944fc588afc8a..bb0a4b9e2ba9b 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts @@ -17,26 +17,21 @@ import { import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { FieldsAdapter, IndexFieldDescriptor } from './types'; -type IndexesAliasIndices = Record; - export class ElasticsearchIndexFieldAdapter implements FieldsAdapter { constructor(private readonly framework: FrameworkAdapter) {} public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise { const indexPatternsService = this.framework.getIndexPatternsService(request); - const indexesAliasIndices: IndexesAliasIndices = indices.reduce( - (accumulator: IndexesAliasIndices, indice: string) => { - const key = getIndexAlias(indices, indice); + const indexesAliasIndices = indices.reduce>((accumulator, indice) => { + const key = getIndexAlias(indices, indice); - if (get(key, accumulator)) { - accumulator[key] = [...accumulator[key], indice]; - } else { - accumulator[key] = [indice]; - } - return accumulator; - }, - {} as IndexesAliasIndices - ); + if (get(key, accumulator)) { + accumulator[key] = [...accumulator[key], indice]; + } else { + accumulator[key] = [indice]; + } + return accumulator; + }, {}); const responsesIndexFields: IndexFieldDescriptor[][] = await Promise.all( Object.values(indexesAliasIndices).map((indicesByGroup) => indexPatternsService.getFieldsForWildcard({ diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts index a9bbf09a9e6f9..f99dd4c65fc83 100644 --- a/x-pack/test/api_integration/apis/security_solution/sources.ts +++ b/x-pack/test/api_integration/apis/security_solution/sources.ts @@ -18,22 +18,97 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/default')); after(() => esArchiver.unload('auditbeat/default')); - it('Make sure that we get source information when auditbeat indices is there', () => { - return client - .query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - docValueFields: [], - }, - }) - .then((resp) => { - const sourceStatus = resp.data.source.status; - // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz - expect(sourceStatus.indexFields.length).to.be(397); - expect(sourceStatus.indicesExist).to.be(true); - }); + it('Make sure that we get source information when auditbeat indices is there', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz + expect(sourceStatus.indexFields.length).to.be(397); + expect(sourceStatus.indicesExist).to.be(true); + }); + + it('should find indexes as being available when they exist', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(true); + }); + + it('should not find indexes as existing when there is an empty array of them', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there is a _all within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['_all'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there are empty strings within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [''], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there are blank spaces within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [' '], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should find indexes when one is an empty index but the others are valid', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['', 'auditbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(true); }); }); } From 6eca0b47fa1d8c3dbd540f0ded2b3ff5c4d76193 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Fri, 31 Jul 2020 14:38:39 -0700 Subject: [PATCH 030/115] Closes #73998 by using `canAccessML` in the ML capabilities API to (#73999) enable anomaly detection settings in APM. --- x-pack/plugins/apm/public/components/app/Home/index.tsx | 4 ++-- x-pack/plugins/apm/public/components/app/Settings/index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index c6c0861c26a34..b2f15dbb11341 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -84,7 +84,7 @@ interface Props { export function Home({ tab }: Props) { const { config, core } = useApmPluginContext(); - const isMLEnabled = !!core.application.capabilities.ml; + const canAccessML = !!core.application.capabilities.ml?.canAccessML; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab @@ -106,7 +106,7 @@ export function Home({ tab }: Props) { - {isMLEnabled && ( + {canAccessML && ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index 1471bc345d850..cb4726244e50c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -20,7 +20,7 @@ import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function Settings(props: { children: ReactNode }) { const plugin = useApmPluginContext(); - const isMLEnabled = !!plugin.core.application.capabilities.ml; + const canAccessML = !!plugin.core.application.capabilities.ml?.canAccessML; const { search, pathname } = useLocation(); return ( <> @@ -51,7 +51,7 @@ export function Settings(props: { children: ReactNode }) { '/settings/agent-configuration' ), }, - ...(isMLEnabled + ...(canAccessML ? [ { name: i18n.translate( From 53b1875093a5355ba693634093b68f055dfb6f65 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 31 Jul 2020 17:50:38 -0400 Subject: [PATCH 031/115] Tweak injected metadata (#73990) Removes unnecessary fields from injected metadata for clients. --- .../rendering_service.test.ts.snap | 270 +++--------------- .../rendering/rendering_service.test.ts | 11 +- .../server/rendering/rendering_service.tsx | 5 +- src/core/server/rendering/types.ts | 7 +- 4 files changed, 55 insertions(+), 238 deletions(-) diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 95230b52c5c03..eab29731ea524 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -10,37 +10,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -83,37 +64,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -156,37 +118,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -233,37 +176,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/translations/en.json", @@ -306,37 +230,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -379,37 +284,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -452,37 +338,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/translations/en.json", @@ -525,37 +392,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -600,37 +448,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -673,37 +502,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index d1c527aca4dba..7caf4af850c10 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -30,17 +30,18 @@ const INJECTED_METADATA = { branch: expect.any(String), buildNumber: expect.any(Number), env: { - binDir: expect.any(String), - configDir: expect.any(String), - homeDir: expect.any(String), - logDir: expect.any(String), + mode: { + name: expect.any(String), + dev: expect.any(Boolean), + prod: expect.any(Boolean), + }, packageInfo: { branch: expect.any(String), buildNum: expect.any(Number), buildSha: expect.any(String), + dist: expect.any(Boolean), version: expect.any(String), }, - pluginSearchPaths: expect.any(Array), }, legacyMetadata: { branch: expect.any(String), diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 8f87d62496891..f49952ec713fb 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -55,7 +55,10 @@ export class RenderingService implements CoreService Date: Fri, 31 Jul 2020 18:39:59 -0400 Subject: [PATCH 032/115] [SECURITY_SOLUTION][ENDPOINT] Fix host list Configuration Status cell link loosing list page/size state (#73989) --- .../security_solution/public/management/common/routing.ts | 3 ++- .../public/management/pages/endpoint_hosts/view/index.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 3636358ebe842..eeb1533f57a67 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -54,7 +54,8 @@ export const getHostListPath = ( }; export const getHostDetailsPath = ( - props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostDetailsUrlProps, + props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostIndexUIQueryParams & + HostDetailsUrlProps, search?: string ) => { const { name, ...queryParams } = props; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 58442ab417b60..f91bba3e3125a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -263,6 +263,7 @@ export const HostList = () => { render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { const toRoutePath = getHostDetailsPath({ name: 'hostPolicyResponse', + ...queryParams, selected_host: item.metadata.host.id, }); const toRouteUrl = formatUrl(toRoutePath); From be47dc42612a4e027b3f408d382343098eef1f7b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 31 Jul 2020 18:20:08 -0600 Subject: [PATCH 033/115] [SIEM][Detection Engine] Fixes tags to accept characters such as AND, OR, (, ), ", * (#74003) ## Summary If you create a rule with tags that have an AND, OR, (, ), etc... then you would blow up with an error when you try to filter based off of that like the screen shot below: Screen Shot 2020-07-31 at 1 55 31 PM Now you don't blow up: Screen Shot 2020-07-31 at 2 37 11 PM This fixes it by adding double quotes around the filters and also red/green/TDD unit tests where I first exercised the error conditions then fixed them. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../detection_engine/rules/api.test.ts | 74 ++++++++++++++++++- .../containers/detection_engine/rules/api.ts | 2 +- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 46829b9cb8f7b..f58c95ed71e29 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -20,6 +20,7 @@ import { getPrePackagedRulesStatus, } from './api'; import { ruleMock, rulesMock } from './mock'; +import { buildEsQuery } from 'src/plugins/data/common'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -165,7 +166,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { method: 'GET', query: { - filter: 'alert.attributes.tags: hello AND alert.attributes.tags: world', + filter: 'alert.attributes.tags: "hello" AND alert.attributes.tags: "world"', page: 1, per_page: 20, sort_field: 'enabled', @@ -175,6 +176,75 @@ describe('Detections Rules API', () => { }); }); + test('query with tags KQL parses without errors when tags contain characters such as left parenthesis (', async () => { + await fetchRules({ + filterOptions: { + filter: 'ruleName', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['('], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + + test('query KQL parses without errors when filter contains characters such as double quotes', async () => { + await fetchRules({ + filterOptions: { + filter: '"test"', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: [], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + + test('query KQL parses without errors when tags contains characters such as double quotes', async () => { + await fetchRules({ + filterOptions: { + filter: '"test"', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['"test"'], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + test('check parameter url, query with all options', async () => { await fetchRules({ filterOptions: { @@ -191,7 +261,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: hello AND alert.attributes.tags: world', + 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: "hello" AND alert.attributes.tags: "world"', page: 1, per_page: 20, sort_field: 'enabled', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 08d564230b85f..3538d8ec8c9b9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -97,7 +97,7 @@ export const fetchRules = async ({ ...(filterOptions.showElasticRules ? [`alert.attributes.tags: "__internal_immutable:true"`] : []), - ...(filterOptions.tags?.map((t) => `alert.attributes.tags: ${t}`) ?? []), + ...(filterOptions.tags?.map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) ?? []), ]; const query = { From 329a98c2ac09fc21064735d3af0208994b6296d6 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 31 Jul 2020 20:56:39 -0400 Subject: [PATCH 034/115] [CI] In-progress Slack notifications (#74012) --- .../src/test/slackNotifications.groovy | 64 ++++++++++++- Jenkinsfile | 95 ++++++++++--------- vars/githubPr.groovy | 15 +-- vars/kibanaPipeline.groovy | 27 +++++- vars/slackNotifications.groovy | 58 +++++++++-- vars/workers.groovy | 2 +- 6 files changed, 183 insertions(+), 78 deletions(-) diff --git a/.ci/pipeline-library/src/test/slackNotifications.groovy b/.ci/pipeline-library/src/test/slackNotifications.groovy index f7e39f5fad903..33b3afed80bde 100644 --- a/.ci/pipeline-library/src/test/slackNotifications.groovy +++ b/.ci/pipeline-library/src/test/slackNotifications.groovy @@ -9,6 +9,7 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { super.setUp() helper.registerAllowedMethod('slackSend', [Map.class], null) + prop('buildState', loadScript("vars/buildState.groovy")) slackNotifications = loadScript('vars/slackNotifications.groovy') } @@ -25,13 +26,49 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { } @Test - void 'sendFailedBuild() should call slackSend() with message'() { + void 'sendFailedBuild() should call slackSend() with an in-progress message'() { mockFailureBuild() slackNotifications.sendFailedBuild() def args = fnMock('slackSend').args[0] + def expected = [ + channel: '#kibana-operations-alerts', + username: 'Kibana Operations', + iconEmoji: ':jenkins:', + color: 'danger', + message: ':hourglass_flowing_sand: elastic / kibana # master #1', + ] + + expected.each { + assertEquals(it.value.toString(), args[it.key].toString()) + } + + assertEquals( + ":hourglass_flowing_sand: **", + args.blocks[0].text.text.toString() + ) + + assertEquals( + "*Failed Steps*\n• ", + args.blocks[1].text.text.toString() + ) + + assertEquals( + "*Test Failures*\n• ", + args.blocks[2].text.text.toString() + ) + } + + @Test + void 'sendFailedBuild() should call slackSend() with message'() { + mockFailureBuild() + + slackNotifications.sendFailedBuild(isFinal: true) + + def args = fnMock('slackSend').args[0] + def expected = [ channel: '#kibana-operations-alerts', username: 'Kibana Operations', @@ -65,7 +102,7 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { mockFailureBuild() def counter = 0 helper.registerAllowedMethod('slackSend', [Map.class], { ++counter > 1 }) - slackNotifications.sendFailedBuild() + slackNotifications.sendFailedBuild(isFinal: true) def args = fnMocks('slackSend')[1].args[0] @@ -88,6 +125,29 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { ) } + @Test + void 'sendFailedBuild() should call slackSend() with a channel id and timestamp on second call'() { + mockFailureBuild() + helper.registerAllowedMethod('slackSend', [Map.class], { [ channelId: 'CHANNEL_ID', ts: 'TIMESTAMP' ] }) + slackNotifications.sendFailedBuild(isFinal: false) + slackNotifications.sendFailedBuild(isFinal: true) + + def args = fnMocks('slackSend')[1].args[0] + + def expected = [ + channel: 'CHANNEL_ID', + timestamp: 'TIMESTAMP', + username: 'Kibana Operations', + iconEmoji: ':jenkins:', + color: 'danger', + message: ':broken_heart: elastic / kibana # master #1', + ] + + expected.each { + assertEquals(it.value.toString(), args[it.key].toString()) + } + } + @Test void 'getTestFailures() should truncate list of failures to 10'() { prop('testUtils', [ diff --git a/Jenkinsfile b/Jenkinsfile index 818ba748ee165..ad1d244c78874 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,59 +4,60 @@ library 'kibana-pipeline-library' kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) { - githubPr.withDefaultPrComments { - ciStats.trackBuild { - catchError { - retryable.enable() - parallel([ - 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), - 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), - 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), - 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), - 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), - 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), - 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), - 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), - 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), - 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), - 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), - 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), - 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), - 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), - // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), - ]), - 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), - 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), - 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), - 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), - 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), - 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), - 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), - 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), - 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), - 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), - 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), - 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), - 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), - 'xpack-securitySolutionCypress': { processNumber -> - whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/', 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx']) { - kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) - } - }, + slackNotifications.onFailure(disabled: !params.NOTIFY_ON_FAILURE) { + githubPr.withDefaultPrComments { + ciStats.trackBuild { + catchError { + retryable.enable() + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), + // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), + 'xpack-securitySolutionCypress': { processNumber -> + whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/', 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx']) { + kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) + } + }, - // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), - ]), - ]) + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) + } } } } if (params.NOTIFY_ON_FAILURE) { - slackNotifications.onFailure() kibanaPipeline.sendMail() } } diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index da5348749f668..ec3dbd919fed6 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -15,7 +15,7 @@ */ def withDefaultPrComments(closure) { catchErrors { - // sendCommentOnError() needs to know if comments are enabled, so lets track it with a global + // kibanaPipeline.notifyOnError() needs to know if comments are enabled, so lets track it with a global // isPr() just ensures this functionality is skipped for non-PR builds buildState.set('PR_COMMENTS_ENABLED', isPr()) catchErrors { @@ -59,19 +59,6 @@ def sendComment(isFinal = false) { } } -def sendCommentOnError(Closure closure) { - try { - closure() - } catch (ex) { - // If this is the first failed step, it's likely that the error hasn't propagated up far enough to mark the build as a failure - currentBuild.result = 'FAILURE' - catchErrors { - sendComment(false) - } - throw ex - } -} - // Checks whether or not this currently executing build was triggered via a PR in the elastic/kibana repo def isPr() { return !!(env.ghprbPullId && env.ghprbPullLink && env.ghprbPullLink =~ /\/elastic\/kibana\//) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 410578886a01d..94bfe983b4653 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -16,6 +16,25 @@ def withPostBuildReporting(Closure closure) { } } +def notifyOnError(Closure closure) { + try { + closure() + } catch (ex) { + // If this is the first failed step, it's likely that the error hasn't propagated up far enough to mark the build as a failure + currentBuild.result = 'FAILURE' + catchErrors { + githubPr.sendComment(false) + } + catchErrors { + // an empty map is a valid config, but is falsey, so let's use .has() + if (buildState.has('SLACK_NOTIFICATION_CONFIG')) { + slackNotifications.sendFailedBuild(buildState.get('SLACK_NOTIFICATION_CONFIG')) + } + } + throw ex + } +} + def functionalTestProcess(String name, Closure closure) { return { processNumber -> def kibanaPort = "61${processNumber}1" @@ -35,7 +54,7 @@ def functionalTestProcess(String name, Closure closure) { "JOB=${name}", "KBN_NP_PLUGINS_BUILT=true", ]) { - githubPr.sendCommentOnError { + notifyOnError { closure() } } @@ -182,7 +201,7 @@ def bash(script, label) { } def doSetup() { - githubPr.sendCommentOnError { + notifyOnError { retryWithDelay(2, 15) { try { runbld("./test/scripts/jenkins_setup.sh", "Setup Build Environment and Dependencies") @@ -199,13 +218,13 @@ def doSetup() { } def buildOss() { - githubPr.sendCommentOnError { + notifyOnError { runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") } } def buildXpack() { - githubPr.sendCommentOnError { + notifyOnError { runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") } } diff --git a/vars/slackNotifications.groovy b/vars/slackNotifications.groovy index 30f86e6d6f0ad..02aad14d8ba3f 100644 --- a/vars/slackNotifications.groovy +++ b/vars/slackNotifications.groovy @@ -105,16 +105,26 @@ def getDefaultDisplayName() { return "${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}" } -def getDefaultContext() { - def duration = currentBuild.durationString.replace(' and counting', '') +def getDefaultContext(config = [:]) { + def progressMessage = "" + if (config && !config.isFinal) { + progressMessage = "In-progress" + } else { + def duration = currentBuild.durationString.replace(' and counting', '') + progressMessage = "${buildUtils.getBuildStatus().toLowerCase().capitalize()} after ${duration}" + } return contextBlock([ - "${buildUtils.getBuildStatus().toLowerCase().capitalize()} after ${duration}", + progressMessage, "", ].join(' · ')) } -def getStatusIcon() { +def getStatusIcon(config = [:]) { + if (config && !config.isFinal) { + return ':hourglass_flowing_sand:' + } + def status = buildUtils.getBuildStatus() if (status == 'UNSTABLE') { return ':yellow_heart:' @@ -124,7 +134,7 @@ def getStatusIcon() { } def getBackupMessage(config) { - return "${getStatusIcon()} ${config.title}\n\nFirst attempt at sending this notification failed. Please check the build." + return "${getStatusIcon(config)} ${config.title}\n\nFirst attempt at sending this notification failed. Please check the build." } def sendFailedBuild(Map params = [:]) { @@ -135,19 +145,32 @@ def sendFailedBuild(Map params = [:]) { color: 'danger', icon: ':jenkins:', username: 'Kibana Operations', - context: getDefaultContext(), + isFinal: false, ] + params - def title = "${getStatusIcon()} ${config.title}" - def message = "${getStatusIcon()} ${config.message}" + config.context = config.context ?: getDefaultContext(config) + + def title = "${getStatusIcon(config)} ${config.title}" + def message = "${getStatusIcon(config)} ${config.message}" def blocks = [markdownBlock(title)] getFailedBuildBlocks().each { blocks << it } blocks << dividerBlock() blocks << config.context + def channel = config.channel + def timestamp = null + + def previousResp = buildState.get('SLACK_NOTIFICATION_RESPONSE') + if (previousResp) { + // When using `timestamp` to update a previous message, you have to use the channel ID from the previous response + channel = previousResp.channelId + timestamp = previousResp.ts + } + def resp = slackSend( - channel: config.channel, + channel: channel, + timestamp: timestamp, username: config.username, iconEmoji: config.icon, color: config.color, @@ -156,7 +179,7 @@ def sendFailedBuild(Map params = [:]) { ) if (!resp) { - slackSend( + resp = slackSend( channel: config.channel, username: config.username, iconEmoji: config.icon, @@ -165,6 +188,10 @@ def sendFailedBuild(Map params = [:]) { blocks: [markdownBlock(getBackupMessage(config))] ) } + + if (resp) { + buildState.set('SLACK_NOTIFICATION_RESPONSE', resp) + } } def onFailure(Map options = [:]) { @@ -172,6 +199,7 @@ def onFailure(Map options = [:]) { def status = buildUtils.getBuildStatus() if (status != "SUCCESS") { catchErrors { + options.isFinal = true sendFailedBuild(options) } } @@ -179,6 +207,16 @@ def onFailure(Map options = [:]) { } def onFailure(Map options = [:], Closure closure) { + if (options.disabled) { + catchError { + closure() + } + + return + } + + buildState.set('SLACK_NOTIFICATION_CONFIG', options) + // try/finally will NOT work here, because the build status will not have been changed to ERROR when the finally{} block executes catchError { closure() diff --git a/vars/workers.groovy b/vars/workers.groovy index 74ce86516e863..f5a28c97c6812 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -126,7 +126,7 @@ def intake(jobName, String script) { return { ci(name: jobName, size: 's-highmem', ramDisk: true) { withEnv(["JOB=${jobName}"]) { - githubPr.sendCommentOnError { + kibanaPipeline.notifyOnError { runbld(script, "Execute ${jobName}") } } From e8de940d127aa9db03e579c71e6b35fb96456727 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Fri, 31 Jul 2020 21:04:46 -0400 Subject: [PATCH 035/115] [Canvas][tech-debt] Refactor Toolbar (completes Kill Recompose.pure) (#73309) Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/i18n/components.ts | 7 + .../canvas/public/components/navbar/navbar.js | 16 -- .../public/components/navbar/navbar.scss | 7 - .../__snapshots__/toolbar.stories.storyshot | 229 ++++++++++++++++++ .../toolbar/__examples__/toolbar.stories.tsx | 40 ++- .../canvas/public/components/toolbar/index.js | 49 ---- .../{navbar/index.js => toolbar/index.ts} | 6 +- .../{toolbar.tsx => toolbar.component.tsx} | 121 +++++---- .../public/components/toolbar/toolbar.scss | 8 + .../public/components/toolbar/toolbar.ts | 28 +++ .../public/components/toolbar/tray/index.ts | 5 +- .../public/components/toolbar/tray/tray.tsx | 11 +- x-pack/plugins/canvas/public/style/index.scss | 1 - .../storybook/decorators/router_decorator.tsx | 24 +- 14 files changed, 372 insertions(+), 180 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/navbar/navbar.js delete mode 100644 x-pack/plugins/canvas/public/components/navbar/navbar.scss create mode 100644 x-pack/plugins/canvas/public/components/toolbar/__examples__/__snapshots__/toolbar.stories.storyshot delete mode 100644 x-pack/plugins/canvas/public/components/toolbar/index.js rename x-pack/plugins/canvas/public/components/{navbar/index.js => toolbar/index.ts} (66%) rename x-pack/plugins/canvas/public/components/toolbar/{toolbar.tsx => toolbar.component.tsx} (64%) create mode 100644 x-pack/plugins/canvas/public/components/toolbar/toolbar.ts diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 9b1d60f38eb5e..03d6ade7bea69 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -913,6 +913,13 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.toolbar.workpadManagerCloseButtonLabel', { defaultMessage: 'Close', }), + getErrorMessage: (message: string) => + i18n.translate('xpack.canvas.toolbar.errorMessage', { + defaultMessage: 'TOOLBAR ERROR: {message}', + values: { + message, + }, + }), }, ToolbarTray: { getCloseTrayAriaLabel: () => diff --git a/x-pack/plugins/canvas/public/components/navbar/navbar.js b/x-pack/plugins/canvas/public/components/navbar/navbar.js deleted file mode 100644 index dcf6389acd4a3..0000000000000 --- a/x-pack/plugins/canvas/public/components/navbar/navbar.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -export const Navbar = ({ children }) => { - return
{children}
; -}; - -Navbar.propTypes = { - children: PropTypes.node, -}; diff --git a/x-pack/plugins/canvas/public/components/navbar/navbar.scss b/x-pack/plugins/canvas/public/components/navbar/navbar.scss deleted file mode 100644 index 7b490822763d2..0000000000000 --- a/x-pack/plugins/canvas/public/components/navbar/navbar.scss +++ /dev/null @@ -1,7 +0,0 @@ -.canvasNavbar { - width: 100%; - height: $euiSizeXL * 2; - background-color: darken($euiColorLightestShade, 5%); - position: relative; - z-index: 200; -} diff --git a/x-pack/plugins/canvas/public/components/toolbar/__examples__/__snapshots__/toolbar.stories.storyshot b/x-pack/plugins/canvas/public/components/toolbar/__examples__/__snapshots__/toolbar.stories.storyshot new file mode 100644 index 0000000000000..eec0de3c784f1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/toolbar/__examples__/__snapshots__/toolbar.stories.storyshot @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/Toolbar element selected 1`] = ` +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+`; + +exports[`Storyshots components/Toolbar no element selected 1`] = ` +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx index 5907c932ddabb..bd6ad7c8dc499 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx @@ -4,36 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -/* - TODO: uncomment and fix this test to address storybook errors as a result of nested component dependencies - https://github.com/elastic/kibana/issues/58289 - */ - -/* -import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -import { Toolbar } from '../toolbar'; +import { Toolbar } from '../toolbar.component'; + +// @ts-expect-error untyped local +import { getDefaultElement } from '../../../state/defaults'; storiesOf('components/Toolbar', module) - .addDecorator(story => ( -
- {story()} -
- )) - .add('with null metric', () => ( + .add('no element selected', () => ( + )) + .add('element selected', () => ( + )); -*/ diff --git a/x-pack/plugins/canvas/public/components/toolbar/index.js b/x-pack/plugins/canvas/public/components/toolbar/index.js deleted file mode 100644 index a95371f5f032a..0000000000000 --- a/x-pack/plugins/canvas/public/components/toolbar/index.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { pure, compose, withState, getContext, withHandlers } from 'recompose'; -import { canUserWrite } from '../../state/selectors/app'; - -import { - getWorkpad, - getWorkpadName, - getSelectedPageIndex, - getSelectedElement, - isWriteable, -} from '../../state/selectors/workpad'; - -import { Toolbar as Component } from './toolbar'; - -const mapStateToProps = (state) => ({ - workpadName: getWorkpadName(state), - workpadId: getWorkpad(state).id, - totalPages: getWorkpad(state).pages.length, - selectedPageNumber: getSelectedPageIndex(state) + 1, - selectedElement: getSelectedElement(state), - isWriteable: isWriteable(state) && canUserWrite(state), -}); - -export const Toolbar = compose( - pure, - connect(mapStateToProps), - getContext({ - router: PropTypes.object, - }), - withHandlers({ - nextPage: (props) => () => { - const pageNumber = Math.min(props.selectedPageNumber + 1, props.totalPages); - props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); - }, - previousPage: (props) => () => { - const pageNumber = Math.max(1, props.selectedPageNumber - 1); - props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); - }, - }), - withState('tray', 'setTray', null), - withState('showWorkpadManager', 'setShowWorkpadManager', false) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/navbar/index.js b/x-pack/plugins/canvas/public/components/toolbar/index.ts similarity index 66% rename from x-pack/plugins/canvas/public/components/navbar/index.js rename to x-pack/plugins/canvas/public/components/toolbar/index.ts index 6948ada93155d..dfa730307dafb 100644 --- a/x-pack/plugins/canvas/public/components/navbar/index.js +++ b/x-pack/plugins/canvas/public/components/toolbar/index.ts @@ -4,7 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Navbar as Component } from './navbar'; - -export const Navbar = pure(Component); +export { Toolbar } from './toolbar'; +export { Toolbar as ToolbarComponent } from './toolbar.component'; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx similarity index 64% rename from x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx rename to x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index c5475b2559444..6905b3ed23d3f 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC, useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, @@ -16,72 +16,77 @@ import { EuiModalFooter, EuiButton, } from '@elastic/eui'; -import { CanvasElement } from '../../../types'; - -import { ComponentStrings } from '../../../i18n'; -// @ts-expect-error untyped local -import { Navbar } from '../navbar'; // @ts-expect-error untyped local import { WorkpadManager } from '../workpad_manager'; +import { RouterContext } from '../router'; import { PageManager } from '../page_manager'; // @ts-expect-error untyped local import { Expression } from '../expression'; import { Tray } from './tray'; +import { CanvasElement } from '../../../types'; +import { ComponentStrings } from '../../../i18n'; + const { Toolbar: strings } = ComponentStrings; -enum TrayType { - pageManager = 'pageManager', - expression = 'expression', -} +type TrayType = 'pageManager' | 'expression'; interface Props { - workpadName: string; isWriteable: boolean; - canUserWrite: boolean; - tray: TrayType | null; - setTray: (tray: TrayType | null) => void; - - previousPage: () => void; - nextPage: () => void; + selectedElement?: CanvasElement; selectedPageNumber: number; totalPages: number; - - selectedElement: CanvasElement; - - showWorkpadManager: boolean; - setShowWorkpadManager: (show: boolean) => void; + workpadId: string; + workpadName: string; } -export const Toolbar = (props: Props) => { - const { - selectedElement, - tray, - setTray, - previousPage, - nextPage, - selectedPageNumber, - workpadName, - totalPages, - showWorkpadManager, - setShowWorkpadManager, - isWriteable, - } = props; +export const Toolbar: FC = ({ + isWriteable, + selectedElement, + selectedPageNumber, + totalPages, + workpadId, + workpadName, +}) => { + const [activeTray, setActiveTray] = useState(null); + const [showWorkpadManager, setShowWorkpadManager] = useState(false); + const router = useContext(RouterContext); + + // While the tray doesn't get activated if the workpad isn't writeable, + // this effect will ensure that if the tray is open and the workpad + // changes its writeable state, the tray will close. + useEffect(() => { + if (!isWriteable && activeTray === 'expression') { + setActiveTray(null); + } + }, [isWriteable, activeTray]); - const elementIsSelected = Boolean(selectedElement); + if (!router) { + return
{strings.getErrorMessage('Router Undefined')}
; + } - const done = () => setTray(null); + const nextPage = () => { + const page = Math.min(selectedPageNumber + 1, totalPages); + router.navigateTo('loadWorkpad', { id: workpadId, page }); + }; - if (!isWriteable && tray === TrayType.expression) { - done(); - } + const previousPage = () => { + const page = Math.max(1, selectedPageNumber - 1); + router.navigateTo('loadWorkpad', { id: workpadId, page }); + }; - const showHideTray = (exp: TrayType) => { - if (tray && tray === exp) { - return done(); + const elementIsSelected = Boolean(selectedElement); + + const toggleTray = (tray: TrayType) => { + if (activeTray === tray) { + setActiveTray(null); + } else { + if (!isWriteable && tray === 'expression') { + return; + } + setActiveTray(tray); } - setTray(exp); }; const closeWorkpadManager = () => setShowWorkpadManager(false); @@ -102,13 +107,13 @@ export const Toolbar = (props: Props) => { const trays = { pageManager: , - expression: !elementIsSelected ? null : , + expression: !elementIsSelected ? null : setActiveTray(null)} />, }; return (
- {tray !== null && {trays[tray]}} - + {activeTray !== null && setActiveTray(null)}>{trays[activeTray]}} +
openWorkpadManager()}> @@ -126,7 +131,7 @@ export const Toolbar = (props: Props) => { /> - showHideTray(TrayType.pageManager)}> + toggleTray('pageManager')}> {strings.getPageButtonLabel(selectedPageNumber, totalPages)} @@ -145,7 +150,7 @@ export const Toolbar = (props: Props) => { showHideTray(TrayType.expression)} + onClick={() => toggleTray('expression')} data-test-subj="canvasExpressionEditorButton" > {strings.getEditorButtonLabel()} @@ -153,23 +158,17 @@ export const Toolbar = (props: Props) => { )} - - +
{showWorkpadManager && workpadManager}
); }; Toolbar.propTypes = { - workpadName: PropTypes.string, - tray: PropTypes.string, - setTray: PropTypes.func.isRequired, - nextPage: PropTypes.func.isRequired, - previousPage: PropTypes.func.isRequired, + isWriteable: PropTypes.bool.isRequired, + selectedElement: PropTypes.object, selectedPageNumber: PropTypes.number.isRequired, totalPages: PropTypes.number.isRequired, - selectedElement: PropTypes.object, - showWorkpadManager: PropTypes.bool.isRequired, - setShowWorkpadManager: PropTypes.func.isRequired, - isWriteable: PropTypes.bool.isRequired, + workpadId: PropTypes.string.isRequired, + workpadName: PropTypes.string.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss index 7303f43dd269f..41bc718dcfec1 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss @@ -24,3 +24,11 @@ padding: $euiSizeM; height: 100%; } + +.canvasToolbar__container { + width: 100%; + height: $euiSizeXL * 2; + background-color: darken($euiColorLightestShade, 5%); + position: relative; + z-index: 200; +} diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts b/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts new file mode 100644 index 0000000000000..f93b42cb442b8 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { canUserWrite } from '../../state/selectors/app'; + +import { + getWorkpad, + getWorkpadName, + getSelectedPageIndex, + getSelectedElement, + isWriteable, +} from '../../state/selectors/workpad'; + +import { Toolbar as ToolbarComponent } from './toolbar.component'; +import { State } from '../../../types'; + +export const Toolbar = connect((state: State) => ({ + workpadName: getWorkpadName(state), + workpadId: getWorkpad(state).id, + totalPages: getWorkpad(state).pages.length, + selectedPageNumber: getSelectedPageIndex(state) + 1, + selectedElement: getSelectedElement(state), + isWriteable: isWriteable(state) && canUserWrite(state), +}))(ToolbarComponent); diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts b/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts index 1343bc8d01e9a..18c45190cbd48 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Tray as Component } from './tray'; - -export const Tray = pure(Component); +export { Tray } from './tray'; diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx index 2c0b4e69c240b..0699d30833ecd 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode, Fragment, MouseEventHandler } from 'react'; +import React, { ReactNode, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; @@ -18,7 +18,7 @@ interface Props { export const Tray = ({ children, done }: Props) => { return ( - + <> { /> -
{children}
-
+ ); }; Tray.propTypes = { - children: PropTypes.node, - done: PropTypes.func, + children: PropTypes.node.isRequired, + done: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 3937d7fc05544..41d12db3a1853 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -31,7 +31,6 @@ @import '../components/function_form/function_form'; @import '../components/layout_annotations/layout_annotations'; @import '../components/loading/loading'; -@import '../components/navbar/navbar'; @import '../components/page_manager/page_manager'; @import '../components/positionable/positionable'; @import '../components/shape_preview/shape_preview'; diff --git a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx index 43b0da6473f23..db775b697d248 100644 --- a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx @@ -6,25 +6,31 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { RouterContext } from '../../public/components/router'; -class RouterContext extends React.Component { +const context = { + router: { + getFullPath: () => 'path', + create: () => '', + }, + navigateTo: () => {}, +}; + +class RouterProvider extends React.Component { static childContextTypes = { router: PropTypes.object.isRequired, + navigateTo: PropTypes.func, }; getChildContext() { - return { - router: { - getFullPath: () => 'path', - create: () => '', - }, - }; + return context; } + render() { - return <>{this.props.children}; + return {this.props.children}; } } export function routerContextDecorator(story: Function) { - return {story()}; + return {story()}; } From 330212e955792298e73e4f1893ed3fcd3b916d46 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 31 Jul 2020 20:39:07 -0600 Subject: [PATCH 036/115] [maps] fix fit to bounds for ES document layers with joins (#73985) --- .../layers/vector_layer/vector_layer.js | 2 +- .../apps/maps/auto_fit_to_bounds.js | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 23889bdca2dd7..f5f5071bab158 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -158,7 +158,7 @@ export class VectorLayer extends AbstractLayer { async getBounds({ startLoading, stopLoading, registerCancelCallback, dataFilters }) { const isStaticLayer = !this.getSource().isBoundsAware(); - if (isStaticLayer) { + if (isStaticLayer || this.hasJoins()) { return getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()); } diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index 64c07273c9ccf..c8e8db84df96f 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -25,10 +25,30 @@ export default function ({ getPageObjects }) { await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "ios"'); await PageObjects.maps.waitForMapPanAndZoom(origView); - const { lat, lon, zoom } = await PageObjects.maps.getView(); + const { lat, lon } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(43); expect(Math.round(lon)).to.equal(-102); - expect(Math.round(zoom)).to.equal(5); + }); + }); + + describe('with joins', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('join example'); + await PageObjects.maps.enableAutoFitToBounds(); + }); + + it('should automatically fit to bounds when query is applied', async () => { + // Set view to other side of world so no matching results + await PageObjects.maps.setView(0, 0, 6); + + // Setting query should trigger fit to bounds and move map + const origView = await PageObjects.maps.getView(); + await PageObjects.maps.setAndSubmitQuery('prop1 >= 11'); + await PageObjects.maps.waitForMapPanAndZoom(origView); + + const { lat, lon } = await PageObjects.maps.getView(); + expect(Math.round(lat)).to.equal(0); + expect(Math.round(lon)).to.equal(60); }); }); }); From 01e6a4f1c0713adc8c50f3f2f8d2d1eb1cc08ff2 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 31 Jul 2020 20:44:21 -0600 Subject: [PATCH 037/115] [maps] convert top nav config to TS (#73851) * [maps] convert top nav config to TS * tslint * one more tslint change Co-authored-by: Elastic Machine --- .../page_elements/top_nav_menu/index.js | 50 -------- .../public/routing/routes/maps_app/index.js | 15 +++ .../routing/routes/maps_app/maps_app_view.js | 102 +++++++++++------ .../maps_app/top_nav_config.tsx} | 108 ++++-------------- 4 files changed, 109 insertions(+), 166 deletions(-) delete mode 100644 x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js rename x-pack/plugins/maps/public/routing/{page_elements/top_nav_menu/top_nav_menu.js => routes/maps_app/top_nav_config.tsx} (72%) diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js deleted file mode 100644 index 4692bb1db3477..0000000000000 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { MapsTopNavMenu } from './top_nav_menu'; -import { - enableFullScreen, - openMapSettings, - removePreviewLayers, - setSelectedLayer, - updateFlyout, -} from '../../../actions'; -import { FLYOUT_STATE } from '../../../reducers/ui'; -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; -import { getFlyoutDisplay } from '../../../selectors/ui_selectors'; -import { - getQuery, - getRefreshConfig, - getTimeFilters, - hasDirtyState, -} from '../../../selectors/map_selectors'; - -function mapStateToProps(state = {}) { - return { - isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, - inspectorAdapters: getInspectorAdapters(state), - isSaveDisabled: hasDirtyState(state), - query: getQuery(state), - refreshConfig: getRefreshConfig(state), - timeFilters: getTimeFilters(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - closeFlyout: () => { - dispatch(setSelectedLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(removePreviewLayers()); - }, - enableFullScreen: () => dispatch(enableFullScreen()), - openMapSettings: () => dispatch(openMapSettings()), - }; -} - -const connectedMapsTopNavMenu = connect(mapStateToProps, mapDispatchToProps)(MapsTopNavMenu); -export { connectedMapsTopNavMenu as MapsTopNavMenu }; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js index d7c754c91b89a..c5f959c54fb66 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js @@ -13,6 +13,7 @@ import { getQueryableUniqueIndexPatternIds, getRefreshConfig, getTimeFilters, + hasDirtyState, hasUnsavedChanges, } from '../../../selectors/map_selectors'; import { @@ -26,13 +27,20 @@ import { setRefreshConfig, setSelectedLayer, updateFlyout, + enableFullScreen, + openMapSettings, + removePreviewLayers, } from '../../../actions'; import { FLYOUT_STATE } from '../../../reducers/ui'; import { getMapsCapabilities } from '../../../kibana_services'; +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; function mapStateToProps(state = {}) { return { isFullScreen: getIsFullScreen(state), + isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + isSaveDisabled: hasDirtyState(state), + inspectorAdapters: getInspectorAdapters(state), nextIndexPatternIds: getQueryableUniqueIndexPatternIds(state), flyoutDisplay: getFlyoutDisplay(state), refreshConfig: getRefreshConfig(state), @@ -68,6 +76,13 @@ function mapDispatchToProps(dispatch) { dispatch(updateFlyout(FLYOUT_STATE.NONE)); dispatch(setReadOnly(!getMapsCapabilities().save)); }, + closeFlyout: () => { + dispatch(setSelectedLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removePreviewLayers()); + }, + enableFullScreen: () => dispatch(enableFullScreen()), + openMapSettings: () => dispatch(openMapSettings()), }; } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index d945aa9623b21..97a08f11a6757 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -9,13 +9,17 @@ import { i18n } from '@kbn/i18n'; import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; -import { getData, getCoreChrome } from '../../../kibana_services'; +import { + getData, + getCoreChrome, + getMapsCapabilities, + getNavigation, +} from '../../../kibana_services'; import { copyPersistentState } from '../../../reducers/util'; import { getInitialLayers, getInitialLayersFromUrlParam } from '../../bootstrap/get_initial_layers'; import { getInitialTimeFilters } from '../../bootstrap/get_initial_time_filters'; import { getInitialRefreshConfig } from '../../bootstrap/get_initial_refresh_config'; import { getInitialQuery } from '../../bootstrap/get_initial_query'; -import { MapsTopNavMenu } from '../../page_elements/top_nav_menu'; import { getGlobalState, updateGlobalState, @@ -27,6 +31,7 @@ import { esFilters } from '../../../../../../../src/plugins/data/public'; import { MapContainer } from '../../../connected_components/map_container'; import { goToSpecifiedPath } from '../../maps_router'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; +import { getTopNavConfig } from './top_nav_config'; const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', @@ -58,7 +63,10 @@ export class MapsAppView extends React.Component { this._updateFromGlobalState ); - this._updateStateFromSavedQuery(this._appStateManager.getAppState().savedQuery); + const initialSavedQuery = this._appStateManager.getAppState().savedQuery; + if (initialSavedQuery) { + this._updateStateFromSavedQuery(initialSavedQuery); + } this._initMap(); @@ -237,18 +245,10 @@ export class MapsAppView extends React.Component { ); } - _onTopNavRefreshConfig = ({ isPaused, refreshInterval }) => { - this._onRefreshConfigChange({ - isPaused, - interval: refreshInterval, - }); - }; + _updateStateFromSavedQuery = (savedQuery) => { + this.setState({ savedQuery: { ...savedQuery } }); + this._appStateManager.setQueryAndFilters({ savedQuery }); - _updateStateFromSavedQuery(savedQuery) { - if (!savedQuery) { - this.setState({ savedQuery: '' }); - return; - } const { filterManager } = getData().query; const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); @@ -266,7 +266,7 @@ export class MapsAppView extends React.Component { query: savedQuery.attributes.query, time: savedQuery.attributes.timefilter, }); - } + }; _initMap() { this._initMapAndLayerSettings(); @@ -295,27 +295,65 @@ export class MapsAppView extends React.Component { } _renderTopNav() { - return !this.props.isFullScreen ? ( - { - this.setState({ savedQuery: query }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + filters={this.props.filters} + query={this.props.query} + onQuerySubmit={({ dateRange, query }) => { + this._onQueryChange({ + query, + time: dateRange, + refresh: true, + }); }} - onSavedQueryUpdated={(query) => { - this.setState({ savedQuery: { ...query } }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + onFiltersUpdated={this._onFiltersChange} + dateRangeFrom={this.props.timeFilters.from} + dateRangeTo={this.props.timeFilters.to} + isRefreshPaused={this.props.refreshConfig.isPaused} + refreshInterval={this.props.refreshConfig.interval} + onRefreshChange={({ isPaused, refreshInterval }) => { + this._onRefreshConfigChange({ + isPaused, + interval: refreshInterval, + }); + }} + showSearchBar={true} + showFilterBar={true} + showDatePicker={true} + showSaveQuery={getMapsCapabilities().saveQuery} + savedQuery={this.state.savedQuery} + onSaved={this._updateStateFromSavedQuery} + onSavedQueryUpdated={this._updateStateFromSavedQuery} + onClearSavedQuery={() => { + const { filterManager, queryString } = getData().query; + this.setState({ savedQuery: '' }); + this._appStateManager.setQueryAndFilters({ savedQuery: '' }); + this._onQueryChange({ + filters: filterManager.getGlobalFilters(), + query: queryString.getDefaultQuery(), + }); }} - setBreadcrumbs={this._setBreadcrumbs} /> - ) : null; + ); } render() { diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx similarity index 72% rename from x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js rename to x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx index be474b43da81a..46d662b28a82f 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx @@ -6,109 +6,44 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { Adapters } from 'src/plugins/inspector/public'; +import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { - getNavigation, getCoreChrome, getMapsCapabilities, getInspector, getToasts, getCoreI18n, - getData, } from '../../../kibana_services'; import { SavedObjectSaveModal, + OnSaveProps, showSaveModal, } from '../../../../../../../src/plugins/saved_objects/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +// @ts-expect-error import { goToSpecifiedPath } from '../../maps_router'; +import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; -export function MapsTopNavMenu({ +export function getTopNavConfig({ savedMap, - query, - onQueryChange, - onQuerySaved, - onSavedQueryUpdated, - savedQuery, - timeFilters, - refreshConfig, - onRefreshConfigChange, - indexPatterns, - onFiltersChange, + isOpenSettingsDisabled, isSaveDisabled, closeFlyout, enableFullScreen, openMapSettings, inspectorAdapters, setBreadcrumbs, - isOpenSettingsDisabled, +}: { + savedMap: ISavedGisMap; + isOpenSettingsDisabled: boolean; + isSaveDisabled: boolean; + closeFlyout: () => void; + enableFullScreen: () => void; + openMapSettings: () => void; + inspectorAdapters: Adapters; + setBreadcrumbs: () => void; }) { - const { TopNavMenu } = getNavigation().ui; - const { filterManager, queryString } = getData().query; - const showSaveQuery = getMapsCapabilities().saveQuery; - const onClearSavedQuery = () => { - onQuerySaved(undefined); - onQueryChange({ - filters: filterManager.getGlobalFilters(), - query: queryString.getDefaultQuery(), - }); - }; - - // Nav settings - const config = getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs - ); - - const submitQuery = function ({ dateRange, query }) { - onQueryChange({ - query, - time: dateRange, - refresh: true, - }); - }; - - return ( - - ); -} - -function getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs -) { return [ { id: 'full-screen', @@ -180,11 +115,11 @@ function getTopNavConfig( newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate, - }) => { + }: OnSaveProps) => { const currentTitle = savedMap.title; savedMap.title = newTitle; savedMap.copyOnSave = newCopyOnSave; - const saveOptions = { + const saveOptions: SavedObjectSaveOpts = { confirmOverwrite: false, isTitleDuplicateConfirmed, onTitleDuplicate, @@ -218,7 +153,12 @@ function getTopNavConfig( ]; } -async function doSave(savedMap, saveOptions, closeFlyout, setBreadcrumbs) { +async function doSave( + savedMap: ISavedGisMap, + saveOptions: SavedObjectSaveOpts, + closeFlyout: () => void, + setBreadcrumbs: () => void +) { closeFlyout(); savedMap.syncWithStore(); let id; From 56e51bde7ade6497a15bde73e7b75168f2b8614c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Sat, 1 Aug 2020 11:01:01 +0200 Subject: [PATCH 038/115] Use "Apply_filter_trigger" in "explore underlying data" action (#71445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use apply filter trigger for “expore underlying data” * disable for maps for now Co-authored-by: Elastic Machine --- src/plugins/embeddable/kibana.json | 1 - src/plugins/embeddable/public/mocks.tsx | 2 - src/plugins/embeddable/public/plugin.tsx | 60 +---------- .../public/triggers/apply_filter_trigger.ts | 2 +- .../abstract_explore_data_action.ts | 2 - .../explore_data_chart_action.test.ts | 100 +++++++++--------- .../explore_data/explore_data_chart_action.ts | 25 +++-- .../explore_data_context_menu_action.test.ts | 8 -- .../discover_enhanced/public/plugin.ts | 6 +- 9 files changed, 72 insertions(+), 134 deletions(-) diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 3163c4bde4704..c9694ad7b9423 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -4,7 +4,6 @@ "server": false, "ui": true, "requiredPlugins": [ - "data", "inspector", "uiActions" ], diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 48e5483124704..6b451e71522c5 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -102,8 +102,6 @@ const createStartContract = (): Start => { getAttributeService: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), - filtersAndTimeRangeFromContext: jest.fn(), - filtersFromContext: jest.fn(), }; return startContract; }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 508c82c4247ed..319cbf8ec44b4 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -17,13 +17,7 @@ * under the License. */ import React from 'react'; -import { - DataPublicPluginSetup, - DataPublicPluginStart, - Filter, - TimeRange, - esFilters, -} from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { getSavedObjectFinder } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -44,9 +38,6 @@ import { IEmbeddable, EmbeddablePanel, SavedObjectEmbeddableInput, - ChartActionContext, - isRangeSelectTriggerContext, - isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { AttributeService } from './lib/embeddables/attribute_service'; @@ -92,18 +83,6 @@ export interface EmbeddableStart { type: string ) => AttributeService; - /** - * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. - */ - filtersFromContext: (context: ChartActionContext) => Promise; - - /** - * Returns possible time range and filters that can be constructed from {@link ChartActionContext} object. - */ - filtersAndTimeRangeFromContext: ( - context: ChartActionContext - ) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>; - EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -155,41 +134,6 @@ export class EmbeddablePublicPlugin implements Plugin { - try { - if (isRangeSelectTriggerContext(context)) - return await data.actions.createFiltersFromRangeSelectAction(context.data); - if (isValueClickTriggerContext(context)) - return await data.actions.createFiltersFromValueClickAction(context.data); - // eslint-disable-next-line no-console - console.warn("Can't extract filters from action.", context); - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Error extracting filters from action. Returning empty filter list.', error); - } - return []; - }; - - const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async ( - context - ) => { - const filters = await filtersFromContext(context); - - if (!context.data.timeFieldName) return { filters }; - - const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - context.data.timeFieldName, - filters - ); - - return { - filters: restOfFilters, - timeRange: timeRangeFilter - ? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter) - : undefined, - }; - }; - const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({ embeddable, hideHeader, @@ -216,8 +160,6 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), - filtersFromContext, - filtersAndTimeRangeFromContext, getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts index 7a95709ac28ba..fa9ace1a36c69 100644 --- a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts @@ -22,6 +22,6 @@ import { Trigger } from '.'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = { id: APPLY_FILTER_TRIGGER, - title: 'Filter click', + title: 'Apply filter', description: 'Triggered when user applies filter to an embeddable.', }; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 434d38c76d428..4ddcb3386f314 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; -import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public'; @@ -18,7 +17,6 @@ export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; export interface PluginDeps { discover: Pick; - embeddable: Pick; kibanaLegacy?: { dashboardConfig: { getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls']; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index 14cd48ae1f509..b6bdafc26b445 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -8,19 +8,14 @@ import { ExploreDataChartAction } from './explore_data_chart_action'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; -import { - EmbeddableStart, - RangeSelectContext, - ValueClickContext, - ChartActionContext, -} from '../../../../../../src/plugins/embeddable/public'; +import { ExploreDataChartActionContext } from './explore_data_chart_action'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, VISUALIZE_EMBEDDABLE_TYPE, } from '../../../../../../src/plugins/visualizations/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -import { Filter, TimeRange } from '../../../../../../src/plugins/data/public'; +import { Filter, RangeFilter } from '../../../../../../src/plugins/data/public'; const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; @@ -34,10 +29,19 @@ afterEach(() => { i18nTranslateSpy.mockClear(); }); -const setup = ({ - useRangeEvent = false, - dashboardOnlyMode = false, -}: { useRangeEvent?: boolean; dashboardOnlyMode?: boolean } = {}) => { +const setup = ( + { + useRangeEvent = false, + timeFieldName, + filters = [], + dashboardOnlyMode = false, + }: { + useRangeEvent?: boolean; + filters?: Filter[]; + timeFieldName?: string; + dashboardOnlyMode?: boolean; + } = { filters: [] } +) => { type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; const core = coreMock.createStart(); @@ -46,17 +50,10 @@ const setup = ({ createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; - const filtersAndTimeRangeFromContext = jest.fn((async () => ({ - filters: [], - })) as EmbeddableStart['filtersAndTimeRangeFromContext']); - const plugins: PluginDeps = { discover: { urlGenerator, }, - embeddable: { - filtersAndTimeRangeFromContext, - }, kibanaLegacy: { dashboardConfig: { getHideWriteControls: () => dashboardOnlyMode, @@ -91,19 +88,13 @@ const setup = ({ getOutput: () => output, } as unknown) as VisualizeEmbeddableContract; - const data: ChartActionContext['data'] = { - ...(useRangeEvent - ? ({ range: {} } as RangeSelectContext['data']) - : ({ data: [] } as ValueClickContext['data'])), - timeFieldName: 'order_date', - }; - const context = { + filters, + timeFieldName, embeddable, - data, - } as ChartActionContext; + } as ExploreDataChartActionContext; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context }; + return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -236,32 +227,41 @@ describe('"Explore underlying data" panel action', () => { }); test('applies chart event filters', async () => { - const { action, context, urlGenerator, plugins } = setup(); - - ((plugins.embeddable - .filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => { - const filters: Filter[] = [ - { - meta: { - alias: 'alias', - disabled: false, - negate: false, + const timeFieldName = 'timeField'; + const from = '2020-07-13T13:40:43.583Z'; + const to = '2020-07-13T13:44:43.583Z'; + const filters: Array = [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + field: timeFieldName, + params: { + gte: from, + lte: to, }, }, - ]; - const timeRange: TimeRange = { - from: 'from', - to: 'to', - }; - return { filters, timeRange }; - }); + range: { + [timeFieldName]: { + gte: from, + lte: to, + }, + }, + }, + ]; - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0); + const { action, context, urlGenerator } = setup({ filters, timeFieldName }); await action.getHref(context); - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1); - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context); expect(urlGenerator.createUrl).toHaveBeenCalledWith({ filters: [ { @@ -274,8 +274,8 @@ describe('"Explore underlying data" panel action', () => { ], indexPatternId: 'index-ptr-foo', timeRange: { - from: 'from', - to: 'to', + from, + to, }, }); }); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 658a6bcb3cf4d..a89fe3cd12a19 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -5,17 +5,19 @@ */ import { Action } from '../../../../../../src/plugins/ui_actions/public'; -import { - ValueClickContext, - RangeSelectContext, -} from '../../../../../../src/plugins/embeddable/public'; import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; -import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { + isTimeRange, + isQuery, + isFilters, + ApplyGlobalFilterActionContext, + esFilters, +} from '../../../../../../src/plugins/data/public'; import { KibanaURL } from './kibana_url'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; -export type ExploreDataChartActionContext = ValueClickContext | RangeSelectContext; +export type ExploreDataChartActionContext = ApplyGlobalFilterActionContext; export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; @@ -31,6 +33,11 @@ export class ExploreDataChartAction extends AbstractExploreDataAction { + if (context.embeddable?.type === 'map') return false; // TODO: https://github.com/elastic/kibana/issues/73043 + return super.isCompatible(context); + } + protected readonly getUrl = async ( context: ExploreDataChartActionContext ): Promise => { @@ -42,7 +49,11 @@ export class ExploreDataChartAction extends AbstractExploreDataAction Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; - const filtersAndTimeRangeFromContext = jest.fn((async () => ({ - filters: [], - })) as EmbeddableStart['filtersAndTimeRangeFromContext']); - const plugins: PluginDeps = { discover: { urlGenerator, }, - embeddable: { - filtersAndTimeRangeFromContext, - }, kibanaLegacy: { dashboardConfig: { getHideWriteControls: () => dashboardOnlyMode, diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index 4b018354aa092..9e66925132a7d 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -9,8 +9,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { UiActionsSetup, UiActionsStart, - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, + APPLY_FILTER_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; @@ -77,8 +76,7 @@ export class DiscoverEnhancedPlugin if (this.config.actions.exploreDataInChart.enabled) { const exploreDataChartAction = new ExploreDataChartAction(params); - uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction); - uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction); + uiActions.addTriggerAction(APPLY_FILTER_TRIGGER, exploreDataChartAction); } } } From 4f18f1dd86d52a8b2c00af972b96f60b95f0a8c0 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Sat, 1 Aug 2020 07:28:00 -0400 Subject: [PATCH 039/115] [Canvas] Storybook Redux Addon (#73227) Co-authored-by: Elastic Machine --- x-pack/package.json | 5 +- .../__examples__/asset.stories.tsx | 26 ++ .../__examples__/asset_manager.stories.tsx | 38 +- .../asset_manager/__examples__/assets.ts | 25 ++ .../asset_manager/__examples__/provider.tsx | 110 ----- .../asset_manager/asset.component.tsx | 7 +- x-pack/plugins/canvas/public/index.ts | 6 +- .../canvas/public/services/context.tsx | 23 +- x-pack/plugins/canvas/scripts/storybook.js | 22 +- .../canvas/storybook/addon/babel.config.js | 10 + .../canvas/storybook/addon/scripts/build.js | 65 +++ .../addon/src/components/action_list.tsx | 77 ++++ .../addon/src/components/action_tree.tsx | 89 ++++ .../src/components/index.ts} | 5 +- .../addon/src/components/state_change.tsx | 37 ++ .../canvas/storybook/addon/src/constants.ts | 15 + .../canvas/storybook/addon/src/panel.css | 171 ++++++++ .../canvas/storybook/addon/src/panel.tsx | 36 ++ .../canvas/storybook/addon/src/register.tsx | 35 ++ .../canvas/storybook/addon/src/state.ts | 52 +++ .../canvas/storybook/addon/src/types.ts | 19 + .../canvas/storybook/addon/tsconfig.json | 13 + x-pack/plugins/canvas/storybook/config.js | 73 ---- .../canvas/storybook/decorators/index.ts | 32 +- .../storybook/decorators/redux_decorator.tsx | 61 +++ .../storybook/decorators/router_decorator.tsx | 31 +- .../decorators/services_decorator.tsx | 13 + x-pack/plugins/canvas/storybook/index.ts | 11 + x-pack/plugins/canvas/storybook/main.ts | 14 + x-pack/plugins/canvas/storybook/manager.ts | 22 + .../{middleware.js => middleware.ts} | 7 +- x-pack/plugins/canvas/storybook/preview.ts | 36 ++ ...storyshots.test.js => storyshots.test.tsx} | 17 +- .../canvas/storybook/webpack.config.js | 394 ++++++++---------- .../canvas/storybook/webpack.dll.config.js | 4 +- yarn.lock | 17 +- 36 files changed, 1127 insertions(+), 491 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/asset_manager/__examples__/assets.ts delete mode 100644 x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx create mode 100644 x-pack/plugins/canvas/storybook/addon/babel.config.js create mode 100644 x-pack/plugins/canvas/storybook/addon/scripts/build.js create mode 100644 x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx create mode 100644 x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx rename x-pack/plugins/canvas/storybook/{addons.js => addon/src/components/index.ts} (66%) create mode 100644 x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx create mode 100644 x-pack/plugins/canvas/storybook/addon/src/constants.ts create mode 100644 x-pack/plugins/canvas/storybook/addon/src/panel.css create mode 100644 x-pack/plugins/canvas/storybook/addon/src/panel.tsx create mode 100644 x-pack/plugins/canvas/storybook/addon/src/register.tsx create mode 100644 x-pack/plugins/canvas/storybook/addon/src/state.ts create mode 100644 x-pack/plugins/canvas/storybook/addon/src/types.ts create mode 100644 x-pack/plugins/canvas/storybook/addon/tsconfig.json delete mode 100644 x-pack/plugins/canvas/storybook/config.js create mode 100644 x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx create mode 100644 x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx create mode 100644 x-pack/plugins/canvas/storybook/index.ts create mode 100644 x-pack/plugins/canvas/storybook/main.ts create mode 100644 x-pack/plugins/canvas/storybook/manager.ts rename x-pack/plugins/canvas/storybook/{middleware.js => middleware.ts} (74%) create mode 100644 x-pack/plugins/canvas/storybook/preview.ts rename x-pack/plugins/canvas/storybook/{storyshots.test.js => storyshots.test.tsx} (85%) diff --git a/x-pack/package.json b/x-pack/package.json index e3104aabbb02b..dcba01a771fd5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -75,6 +75,7 @@ "@types/hoist-non-react-statics": "^3.3.1", "@types/history": "^4.7.3", "@types/jest": "^25.2.3", + "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", "@types/js-search": "^1.4.0", "@types/js-yaml": "^3.11.1", @@ -119,6 +120,7 @@ "@types/xml2js": "^0.4.5", "@types/stats-lite": "^2.2.0", "@types/pretty-ms": "^5.0.0", + "@types/webpack-env": "^1.15.2", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "autoprefixer": "^9.7.4", @@ -158,6 +160,7 @@ "jest-cli": "^25.5.4", "jest-styled-components": "^7.0.2", "jsdom": "13.1.0", + "jsondiffpatch": "0.4.1", "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", @@ -394,4 +397,4 @@ "cypress-multi-reporters" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.stories.tsx new file mode 100644 index 0000000000000..0b99bbce50288 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.stories.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { reduxDecorator, getAddonPanelParameters } from '../../../../storybook'; +import { Asset, AssetComponent } from '../'; +import { AIRPLANE, MARKER, assets } from './assets'; + +storiesOf('components/Assets/Asset', module) + .addDecorator((story) =>
{story()}
) + .addDecorator(reduxDecorator({ assets })) + .addParameters(getAddonPanelParameters()) + .add('redux: Asset', () => { + return ; + }) + .add('airplane', () => ( + + )) + .add('marker', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx index 1434ef60cf0d8..673c66734b39a 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx @@ -4,35 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; -import React from 'react'; +import { reduxDecorator, getAddonPanelParameters } from '../../../../storybook'; import { AssetManager, AssetManagerComponent } from '../'; - -import { Provider, AIRPLANE, MARKER } from './provider'; +import { assets } from './assets'; storiesOf('components/Assets/AssetManager', module) - .add('redux: AssetManager', () => ( - - - - )) + .addDecorator(reduxDecorator({ assets })) + .addParameters(getAddonPanelParameters()) + .add('redux: AssetManager', () => ) .add('no assets', () => ( - - - + )) .add('two assets', () => ( - - - + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/assets.ts b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/assets.ts new file mode 100644 index 0000000000000..3b5576667ed26 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/assets.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AssetType } from '../../../../types'; + +export const AIRPLANE: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'airplane', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const MARKER: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'marker', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const assets = [AIRPLANE, MARKER]; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx deleted file mode 100644 index 1cd7562b59c47..0000000000000 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-console */ - -/* - This Provider is temporary. See https://github.com/elastic/kibana/pull/69357 -*/ - -import React, { FC } from 'react'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; -import thunkMiddleware from 'redux-thunk'; -import { Provider as ReduxProvider } from 'react-redux'; - -// @ts-expect-error untyped local -import { appReady } from '../../../../public/state/middleware/app_ready'; -// @ts-expect-error untyped local -import { resolvedArgs } from '../../../../public/state/middleware/resolved_args'; - -// @ts-expect-error untyped local -import { getRootReducer } from '../../../../public/state/reducers'; - -// @ts-expect-error Untyped local -import { getDefaultWorkpad } from '../../../../public/state/defaults'; -import { State, AssetType } from '../../../../types'; - -export const AIRPLANE: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'airplane', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -export const MARKER: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'marker', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -export const state: State = { - app: { - basePath: '/', - ready: true, - serverFunctions: [], - }, - assets: { - AIRPLANE, - MARKER, - }, - transient: { - canUserWrite: true, - zoomScale: 1, - elementStats: { - total: 0, - ready: 0, - pending: 0, - error: 0, - }, - inFlight: false, - fullScreen: false, - selectedTopLevelNodes: [], - resolvedArgs: {}, - refresh: { - interval: 0, - }, - autoplay: { - enabled: false, - interval: 10000, - }, - }, - persistent: { - schemaVersion: 2, - workpad: getDefaultWorkpad(), - }, -}; - -// @ts-expect-error untyped local -import { elementsRegistry } from '../../../lib/elements_registry'; -import { image } from '../../../../canvas_plugin_src/elements/image'; -elementsRegistry.register(image); - -export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( - action -) => { - const previousState = store.getState(); - const returnValue = dispatch(action); - const newState = store.getState(); - - console.group(action.type || '(thunk)'); - console.log('Previous State', previousState); - console.log('New State', newState); - console.groupEnd(); - - return returnValue; -}; - -export const Provider: FC = ({ children }) => { - const middleware = applyMiddleware(thunkMiddleware); - const reducer = getRootReducer(state); - const store = createStore(reducer, state, middleware); - store.dispatch = patchDispatch(store, store.dispatch); - - return {children}; -}; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx index a04d37cf7f9fc..ed000741bc542 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx @@ -17,7 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useNotifyService } from '../../services'; import { ConfirmModal } from '../confirm_modal'; import { Clipboard } from '../clipboard'; @@ -38,11 +38,10 @@ interface Props { } export const Asset: FC = ({ asset, onCreate, onDelete }) => { - const { services } = useKibana(); + const { success } = useNotifyService(); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); - const onCopy = (result: boolean) => - result && services.canvas.notify.success(`Copied '${asset.id}' to clipboard`); + const onCopy = (result: boolean) => result && success(`Copied '${asset.id}' to clipboard`); const confirmModal = ( new CanvasPlugin(); +export const plugin = (_initializerContext: PluginInitializerContext) => new CanvasPlugin(); diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 9bd86ef98f1e3..9f79e81369b6b 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -12,7 +12,7 @@ import React, { FC, ReactElement, } from 'react'; -import { CanvasServices, CanvasServiceProviders } from '.'; +import { CanvasServices, CanvasServiceProviders, services } from '.'; export interface WithServicesProps { services: CanvasServices; @@ -36,23 +36,22 @@ export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; export const withServices = (type: ComponentType) => { - const EnhancedType: FC = (props) => { - const services = useServices(); - return createElement(type, { ...props, services }); - }; + const EnhancedType: FC = (props) => + createElement(type, { ...props, services: useServices() }); return EnhancedType; }; export const ServicesProvider: FC<{ - providers: CanvasServiceProviders; + providers?: Partial; children: ReactElement; -}> = ({ providers, children }) => { +}> = ({ providers = {}, children }) => { + const specifiedProviders: CanvasServiceProviders = { ...services, ...providers }; const value = { - embeddables: providers.embeddables.getService(), - expressions: providers.expressions.getService(), - notify: providers.notify.getService(), - platform: providers.platform.getService(), - navLink: providers.navLink.getService(), + embeddables: specifiedProviders.embeddables.getService(), + expressions: specifiedProviders.expressions.getService(), + notify: specifiedProviders.notify.getService(), + platform: specifiedProviders.platform.getService(), + navLink: specifiedProviders.navLink.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index beea1814b54d2..671de53d74407 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -24,7 +24,7 @@ const storybookOptions = { run( ({ log, flags }) => { - const { dll, clean, stats, site } = flags; + const { addon, dll, clean, stats, site } = flags; // Delete the existing DLL if we're cleaning or building. if (clean || dll) { @@ -81,13 +81,20 @@ run( return; } + // Build the addon + execa.sync('node', ['scripts/build'], { + cwd: path.resolve(__dirname, '../storybook/addon'), + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }); + // Build site and exit if (site) { log.success('storybook: Generating Storybook site'); storybook({ ...storybookOptions, mode: 'static', - outputDir: path.resolve(__dirname, './../storybook'), + outputDir: path.resolve(__dirname, './../storybook/build'), }); return; } @@ -100,6 +107,14 @@ run( ...options, }); + if (addon) { + execa('node', ['scripts/build', '--watch'], { + cwd: path.resolve(__dirname, '../storybook/addon'), + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }); + } + storybook({ ...storybookOptions, port: 9001, @@ -110,8 +125,9 @@ run( Storybook runner for Canvas. `, flags: { - boolean: ['dll', 'clean', 'stats', 'site'], + boolean: ['addon', 'dll', 'clean', 'stats', 'site'], help: ` + --addon Watch the addon source code for changes. --clean Forces a clean of the Storybook DLL and exits. --dll Cleans and builds the Storybook dependency DLL and exits. --stats Produces a Webpack stats file. diff --git a/x-pack/plugins/canvas/storybook/addon/babel.config.js b/x-pack/plugins/canvas/storybook/addon/babel.config.js new file mode 100644 index 0000000000000..5081cf455906f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/babel.config.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + presets: ['@kbn/babel-preset/webpack_preset'], + plugins: ['@babel/plugin-proposal-class-properties'], +}; diff --git a/x-pack/plugins/canvas/storybook/addon/scripts/build.js b/x-pack/plugins/canvas/storybook/addon/scripts/build.js new file mode 100644 index 0000000000000..b3525244fad25 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/scripts/build.js @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const { resolve } = require('path'); + +const del = require('del'); +const supportsColor = require('supports-color'); +const { run, withProcRunner } = require('@kbn/dev-utils'); + +const ROOT_DIR = resolve(__dirname, '..'); +const BUILD_DIR = resolve(ROOT_DIR, 'target'); + +const padRight = (width, str) => + str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; + +run( + async ({ log, flags }) => { + await withProcRunner(log, async (proc) => { + if (!flags.watch) { + log.info('Deleting old output'); + await del(BUILD_DIR); + } + + const cwd = ROOT_DIR; + const env = { process }; + + if (supportsColor.stdout) { + env.FORCE_COLOR = 'true'; + } + + log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); + await proc.run(padRight(10, `babel`), { + cmd: 'babel', + args: [ + 'src', + '--config-file', + require.resolve('../babel.config.js'), + '--out-dir', + BUILD_DIR, + '--extensions', + '.ts,.js,.tsx', + '--copy-files', + ...(flags.watch ? ['--watch'] : ['--quiet']), + ], + wait: true, + env, + cwd, + }); + + log.success('Complete'); + }); + }, + { + description: 'Simple build tool for Canvas Storybook addon', + flags: { + boolean: ['watch'], + help: ` + --watch Run in watch mode + `, + }, + } +); diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx new file mode 100644 index 0000000000000..9c29a44a67318 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import { EuiSelectable, EuiSelectableOption } from '@elastic/eui'; +import addons from '@storybook/addons'; +import uuid from 'uuid/v4'; + +import { EVENTS } from '../constants'; +import { RecordedAction, RecordedPayload } from '../types'; + +export const ActionList: FC<{ + onSelect: (action: RecordedAction | null) => void; +}> = ({ onSelect }) => { + const [recordedActions, setRecordedActions] = useState>({}); + const [selectedAction, setSelectedAction] = useState(null); + + useEffect(() => { + onSelect(selectedAction); + }, [onSelect, selectedAction]); + + useEffect(() => { + const actionListener = (newAction: RecordedPayload) => { + const id = uuid(); + setRecordedActions({ ...recordedActions, [id]: { ...newAction, id } }); + }; + + const resetListener = () => { + setSelectedAction(null); + setRecordedActions({}); + }; + + const channel = addons.getChannel(); + channel.addListener(EVENTS.ACTION, actionListener); + channel.addListener(EVENTS.RESET, resetListener); + + return () => { + channel.removeListener(EVENTS.ACTION, actionListener); + channel.removeListener(EVENTS.RESET, resetListener); + }; + }); + + useEffect(() => { + const values = Object.values(recordedActions); + if (values.length > 0) { + setSelectedAction(values[values.length - 1]); + } + }, [recordedActions]); + + const options: EuiSelectableOption[] = Object.values(recordedActions).map((recordedAction) => ({ + id: recordedAction.id, + key: recordedAction.id, + label: recordedAction.action.type, + checked: recordedAction.id === selectedAction?.id ? 'on' : undefined, + })); + + const onChange: (selectedOptions: EuiSelectableOption[]) => void = (selectedOptions) => { + selectedOptions.forEach((option) => { + if (option && option.checked && option.id) { + const selected = recordedActions[option.id]; + + if (selected) { + setSelectedAction(selected); + } + } + }); + }; + + return ( + + {(list) => list} + + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx new file mode 100644 index 0000000000000..351b94edb351f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { isObject, isDate } from 'lodash'; +import uuid from 'uuid/v4'; +import { EuiTreeView } from '@elastic/eui'; + +import { Node } from '@elastic/eui/src/components/tree_view/tree_view'; + +import { RecordedAction } from '../types'; + +const actionToTree = (recordedAction: RecordedAction) => { + const { action, newState, previousState } = recordedAction; + + return [ + { + label: 'Action', + id: uuid(), + children: jsonToTree(action), + }, + { + label: 'Previous State', + id: uuid(), + children: jsonToTree(previousState), + }, + { + label: 'Current State', + id: uuid(), + children: jsonToTree(newState), + }, + ]; +}; + +const jsonToTree: (obj: Record) => Node[] = (obj) => { + const keys = Object.keys(obj); + + const values = keys.map((label) => { + const value = obj[label]; + + if (!value) { + return null; + } + + const id = uuid(); + + if (isDate(value)) { + return { label: `${label}: ${(value as Date).toDateString()}` }; + } + + if (isObject(value)) { + const children = jsonToTree(value); + + if (children !== null && Object.keys(children).length > 0) { + return { label, id, children }; + } else { + return { label, id }; + } + } + + return { label: `${label}: ${value.toString().slice(0, 100)}`, id }; + }); + + return values.filter((value) => value !== null) as Node[]; +}; + +export const ActionTree: FC<{ action: RecordedAction | null }> = ({ action }) => { + const items = action ? actionToTree(action) : null; + let tree = <>; + + if (action && items) { + tree = ( + + ); + } else if (action) { + tree =
No change
; + } + + return tree; +}; diff --git a/x-pack/plugins/canvas/storybook/addons.js b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts similarity index 66% rename from x-pack/plugins/canvas/storybook/addons.js rename to x-pack/plugins/canvas/storybook/addon/src/components/index.ts index 75bbe620c9e7b..5acb1acf3b459 100644 --- a/x-pack/plugins/canvas/storybook/addons.js +++ b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import '@storybook/addon-actions/register'; -import '@storybook/addon-knobs/register'; -import '@storybook/addon-console'; +export { ActionList } from './action_list'; +export { ActionTree } from './action_tree'; diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx new file mode 100644 index 0000000000000..4db3c23c93843 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiAccordion } from '@elastic/eui'; +import { formatters } from 'jsondiffpatch'; + +import { RecordedAction } from '../types'; + +interface Props { + action: RecordedAction | null; +} + +export const StateChange: FC = ({ action }) => { + if (!action) { + return null; + } + + const { change, previousState } = action; + const html = formatters.html.format(change, previousState); + formatters.html.hideUnchanged(); + + return ( + + {/* eslint-disable-next-line react/no-danger */} +
+ + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/constants.ts b/x-pack/plugins/canvas/storybook/addon/src/constants.ts new file mode 100644 index 0000000000000..fb2646ef3ba8f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ADDON_ID = 'kbn-canvas/redux-actions'; +export const ACTIONS_PANEL_ID = `${ADDON_ID}/panel`; + +const RESULT = `${ADDON_ID}/result`; +const REQUEST = `${ADDON_ID}/request`; +const ACTION = `${ADDON_ID}/action`; +const RESET = `${ADDON_ID}/reset`; + +export const EVENTS = { ACTION, RESULT, REQUEST, RESET }; diff --git a/x-pack/plugins/canvas/storybook/addon/src/panel.css b/x-pack/plugins/canvas/storybook/addon/src/panel.css new file mode 100644 index 0000000000000..b2b6591343b5f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/panel.css @@ -0,0 +1,171 @@ +.panel__tree { + font-family: monospace; + font-size: 85%; +} + +.panel__tree .euiTreeView { + padding-left: 12px; + font-size: 85%; +} + +.panel__resizeableContainer { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; +} + +.panel__stateChange .euiAccordion__button { + font-size: 12px; + font-family: monospace; +} + +.panel__stateChange .euiAccordion__iconWrapper { + transform: scale(.80); + transform-origin: top left; + margin: 8px 0px 4px 7px; +} + +.jsondiffpatch-delta { + font-family: monospace; + font-size: 12px; + line-height: 20px; + margin: 0; + padding: 0 0 0 12px; + display: inline-block; +} +.jsondiffpatch-delta pre { + font-size: 12px; + margin: 0; + padding: 0; + display: inline-block; +} +ul.jsondiffpatch-delta { + list-style-type: none; + padding: 0 0 0 20px; + margin: 0; +} +.jsondiffpatch-delta ul { + list-style-type: none; + padding: 0 0 0 20px; + margin: 0; +} + +.jsondiffpatch-added .jsondiffpatch-property-name, +.jsondiffpatch-added .jsondiffpatch-value pre, +.jsondiffpatch-modified .jsondiffpatch-right-value pre, +.jsondiffpatch-textdiff-added { + background: #bbffbb; +} + +.jsondiffpatch-deleted .jsondiffpatch-property-name, +.jsondiffpatch-deleted pre, +.jsondiffpatch-modified .jsondiffpatch-left-value pre, +.jsondiffpatch-textdiff-deleted { + background: #ffbbbb; + text-decoration: line-through; +} + +.jsondiffpatch-unchanged { display: none; } + +.jsondiffpatch-value { + display: inline-block; +} + +.jsondiffpatch-property-name { + display: inline-block; + padding-right: 5px; + vertical-align: top; +} + +.jsondiffpatch-property-name:after { + content: ': '; +} + +.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after { + content: ': ['; +} + +.jsondiffpatch-child-node-type-array:after { + content: '],'; +} + +div.jsondiffpatch-child-node-type-array:before { + content: '['; +} + +div.jsondiffpatch-child-node-type-array:after { + content: ']'; +} + +.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after { + content: ': {'; +} + +.jsondiffpatch-child-node-type-object:after { + content: '},'; +} + +div.jsondiffpatch-child-node-type-object:before { + content: '{'; +} + +div.jsondiffpatch-child-node-type-object:after { + content: '}'; +} + +.jsondiffpatch-value pre:after { + content: ','; +} + +li:last-child > .jsondiffpatch-value pre:after, +.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after { + content: ''; +} + +.jsondiffpatch-modified .jsondiffpatch-value { + display: inline-block; +} + +.jsondiffpatch-modified .jsondiffpatch-right-value { + margin-left: 5px; +} + +.jsondiffpatch-moved .jsondiffpatch-value { + display: none; +} + +.jsondiffpatch-moved .jsondiffpatch-moved-destination { + display: inline-block; + background: #ffffbb; + color: #888; +} + +.jsondiffpatch-moved .jsondiffpatch-moved-destination:before { + content: ' => '; +} + +ul.jsondiffpatch-textdiff { + padding: 0; +} + +.jsondiffpatch-textdiff-location { + color: #bbb; + display: inline-block; + min-width: 60px; +} + +.jsondiffpatch-textdiff-line { + display: inline-block; +} + +.jsondiffpatch-textdiff-line-number:after { + content: ','; +} + +.jsondiffpatch-error { + background: red; + color: white; + font-weight: bold; +} diff --git a/x-pack/plugins/canvas/storybook/addon/src/panel.tsx b/x-pack/plugins/canvas/storybook/addon/src/panel.tsx new file mode 100644 index 0000000000000..adf6e8555c00a --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/panel.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiResizableContainer } from '@elastic/eui'; +import { StateChange } from './components/state_change'; + +import '@elastic/eui/dist/eui_theme_light.css'; +import './panel.css'; + +import { RecordedAction } from './types'; +import { ActionList, ActionTree } from './components'; + +export const Panel = () => { + const [selectedAction, setSelectedAction] = useState(null); + + return ( + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/register.tsx b/x-pack/plugins/canvas/storybook/addon/src/register.tsx new file mode 100644 index 0000000000000..3a5c4a6818ac1 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/register.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import React from 'react'; +import { addons, types } from '@storybook/addons'; +import { AddonPanel } from '@storybook/components'; +import { STORY_CHANGED } from '@storybook/core-events'; + +import { ADDON_ID, EVENTS, ACTIONS_PANEL_ID } from './constants'; +import { Panel } from './panel'; + +addons.register(ADDON_ID, (api) => { + const channel = addons.getChannel(); + + api.on(STORY_CHANGED, (storyId) => { + channel.emit(EVENTS.RESET, storyId); + }); + + addons.add(ACTIONS_PANEL_ID, { + title: 'Redux Actions', + type: types.PANEL, + render: ({ active, key }) => { + return ( + + + + ); + }, + }); +}); diff --git a/x-pack/plugins/canvas/storybook/addon/src/state.ts b/x-pack/plugins/canvas/storybook/addon/src/state.ts new file mode 100644 index 0000000000000..6d601fff7184a --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/state.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* es-lint-disable import/no-extraneous-dependencies */ +import { applyMiddleware, Dispatch, Store } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import addons from '@storybook/addons'; +import { diff } from 'jsondiffpatch'; +import { isFunction } from 'lodash'; + +import { EVENTS } from './constants'; + +// @ts-expect-error untyped local +import { appReady } from '../../../public/state/middleware/app_ready'; +// @ts-expect-error untyped local +import { resolvedArgs } from '../../../public/state/middleware/resolved_args'; + +// @ts-expect-error untyped local +import { getRootReducer } from '../../../public/state/reducers'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../../public/state/defaults'; +// @ts-expect-error Untyped local +import { getInitialState as getState } from '../../../public/state/initial_state'; +import { State } from '../../../types'; + +export const getInitialState: () => State = () => getState(); +export const getMiddleware = () => applyMiddleware(thunkMiddleware); +export const getReducer = () => getRootReducer(getInitialState()); + +export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( + action +) => { + const channel = addons.getChannel(); + + const previousState = store.getState(); + const returnValue = dispatch(action); + const newState = store.getState(); + const change = diff(previousState, newState) || {}; + + channel.emit(EVENTS.ACTION, { + previousState, + newState, + change, + action: isFunction(action) ? { type: '(thunk)' } : action, + }); + + return returnValue; +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/types.ts b/x-pack/plugins/canvas/storybook/addon/src/types.ts new file mode 100644 index 0000000000000..e8a2cb70c89ff --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { State } from '../../../types'; + +export interface RecordedPayload { + previousState: State; + newState: State; + change: Partial; + action: Action; +} + +export interface RecordedAction extends RecordedPayload { + id: string; +} diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json new file mode 100644 index 0000000000000..9cab0af235f2e --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.json", + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "exclude": [ + "target" + ], + "compilerOptions": { + "declaration": false, + } +} diff --git a/x-pack/plugins/canvas/storybook/config.js b/x-pack/plugins/canvas/storybook/config.js deleted file mode 100644 index dc16d6c46084d..0000000000000 --- a/x-pack/plugins/canvas/storybook/config.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { configure, addDecorator, addParameters } from '@storybook/react'; -import { withInfo } from '@storybook/addon-info'; -import { create } from '@storybook/theming'; - -import { startServices } from '../public/services/stubs'; -import { addDecorators } from './decorators'; - -// If we're running Storyshots, be sure to register the require context hook. -// Otherwise, add the other decorators. -if (process.env.NODE_ENV === 'test') { - require('babel-plugin-require-context-hook/register')(); -} else { - // Customize the info for each story. - addDecorator( - withInfo({ - inline: true, - styles: { - infoBody: { - margin: 20, - }, - infoStory: { - margin: '40px 60px', - }, - }, - }) - ); -} - -addDecorators(); -startServices(); - -function loadStories() { - require('./dll_contexts'); - - // Only gather and require CSS files related to Canvas. The other CSS files - // are built into the DLL. - const css = require.context( - '../../../../built_assets/css', - true, - /plugins\/(?=canvas).*light\.css/ - ); - css.keys().forEach((filename) => css(filename)); - - // Find all files ending in *.stories.tsx - const req = require.context('./..', true, /.(stories).tsx$/); - req.keys().forEach((filename) => req(filename)); - - // Import Canvas CSS - require('../public/style/index.scss'); -} - -// Set up the Storybook environment with custom settings. -addParameters({ - options: { - theme: create({ - base: 'light', - brandTitle: 'Canvas Storybook', - brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', - }), - showPanel: true, - isFullscreen: false, - panelPosition: 'bottom', - isToolshown: true, - }, -}); - -configure(loadStories, module); diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts index aa1e958a410f5..8cd716cf7e3f1 100644 --- a/x-pack/plugins/canvas/storybook/decorators/index.ts +++ b/x-pack/plugins/canvas/storybook/decorators/index.ts @@ -5,15 +5,43 @@ */ import { addDecorator } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; // @ts-expect-error import { withInfo } from '@storybook/addon-info'; +import { Provider as ReduxProvider } from 'react-redux'; + +import { ServicesProvider } from '../../public/services'; +import { RouterContext } from '../../public/components/router'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { routerContextDecorator } from './router_decorator'; import { kibanaContextDecorator } from './kibana_decorator'; +import { servicesContextDecorator } from './services_decorator'; + +export { reduxDecorator } from './redux_decorator'; export const addDecorators = () => { - addDecorator(withKnobs); + if (process.env.NODE_ENV === 'test') { + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('babel-plugin-require-context-hook/register')(); + } else { + // Customize the info for each story. + addDecorator( + withInfo({ + inline: true, + styles: { + infoBody: { + margin: 20, + }, + infoStory: { + margin: '40px 60px', + }, + }, + propTablesExclude: [ReduxProvider, ServicesProvider, RouterContext, KibanaContextProvider], + }) + ); + } + addDecorator(kibanaContextDecorator); addDecorator(routerContextDecorator); + addDecorator(servicesContextDecorator); }; diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx new file mode 100644 index 0000000000000..e35b065a61764 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* es-lint-disable import/no-extraneous-dependencies */ + +import React from 'react'; +import { createStore } from 'redux'; +import { Provider as ReduxProvider } from 'react-redux'; +import { cloneDeep } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../public/state/defaults'; +import { CanvasWorkpad, CanvasElement, CanvasAsset } from '../../types'; + +// @ts-expect-error untyped local +import { elementsRegistry } from '../../public/lib/elements_registry'; +import { image } from '../../canvas_plugin_src/elements/image'; +elementsRegistry.register(image); + +import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state'; +export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants'; + +interface Params { + workpad?: CanvasWorkpad; + elements?: CanvasElement[]; + assets?: CanvasAsset[]; +} + +export const reduxDecorator = (params: Params = {}) => { + const state = cloneDeep(getInitialState()); + const { workpad, elements, assets } = params; + + if (workpad) { + set(state, 'persistent.workpad', workpad); + } + + if (elements) { + set(state, 'persistent.workpad.pages.0.elements', elements); + } + + if (assets) { + set( + state, + 'assets', + assets.reduce((obj: Record, item) => { + obj[item.id] = item; + return obj; + }, {}) + ); + } + + return (story: Function) => { + const store = createStore(getReducer(), state, getMiddleware()); + store.dispatch = patchDispatch(store, store.dispatch); + return {story()}; + }; +}; diff --git a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx index db775b697d248..464577b1f7c1e 100644 --- a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx @@ -5,32 +5,9 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -import { RouterContext } from '../../public/components/router'; - -const context = { - router: { - getFullPath: () => 'path', - create: () => '', - }, - navigateTo: () => {}, -}; - -class RouterProvider extends React.Component { - static childContextTypes = { - router: PropTypes.object.isRequired, - navigateTo: PropTypes.func, - }; - getChildContext() { - return context; - } - - render() { - return {this.props.children}; - } -} +import { RouterContext } from '../../public/components/router'; -export function routerContextDecorator(story: Function) { - return {story()}; -} +export const routerContextDecorator = (story: Function) => ( + {} }}>{story()} +); diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx new file mode 100644 index 0000000000000..918eaffb47d77 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { ServicesProvider } from '../../public/services'; + +export const servicesContextDecorator = (story: Function) => ( + {story()} +); diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts new file mode 100644 index 0000000000000..5cad89eb614e5 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ACTIONS_PANEL_ID } from './addon/src/constants'; + +export * from './decorators'; +export { ACTIONS_PANEL_ID } from './addon/src/constants'; +export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } }); diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts new file mode 100644 index 0000000000000..ad6d10f9bc75f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/main.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + stories: ['../**/*.stories.tsx'], + addons: [ + '@storybook/addon-actions', + '@storybook/addon-knobs', + './storybook/addon/target/register', + ], +}; diff --git a/x-pack/plugins/canvas/storybook/manager.ts b/x-pack/plugins/canvas/storybook/manager.ts new file mode 100644 index 0000000000000..6727040c9b27f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/manager.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addons } from '@storybook/addons'; +import { create } from '@storybook/theming'; +import { PANEL_ID } from '@storybook/addon-actions'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Canvas Storybook', + brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', + }), + showPanel: true, + isFullscreen: false, + panelPosition: 'bottom', + isToolshown: true, + selectedPanel: PANEL_ID, +}); diff --git a/x-pack/plugins/canvas/storybook/middleware.js b/x-pack/plugins/canvas/storybook/middleware.ts similarity index 74% rename from x-pack/plugins/canvas/storybook/middleware.js rename to x-pack/plugins/canvas/storybook/middleware.ts index baa524aefa709..d319a6918a02a 100644 --- a/x-pack/plugins/canvas/storybook/middleware.js +++ b/x-pack/plugins/canvas/storybook/middleware.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -const path = require('path'); -const serve = require('serve-static'); +import path from 'path'; +// @ts-expect-error +import serve from 'serve-static'; // Extend the Storybook Middleware to include a route to access Legacy UI assets -module.exports = function (router) { +module.exports = function (router: { get: (...args: any[]) => void }) { router.get( '/ui', serve(path.resolve(__dirname, '../../../../../src/core/server/core_app/assets')) diff --git a/x-pack/plugins/canvas/storybook/preview.ts b/x-pack/plugins/canvas/storybook/preview.ts new file mode 100644 index 0000000000000..fc194664c84b8 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/preview.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; + +import { startServices } from '../public/services/stubs'; +import { addDecorators } from './decorators'; + +// Import the modules from the DLL. +import './dll_contexts'; + +// Import Canvas CSS +import '../public/style/index.scss'; + +startServices({ + notify: { + success: (message) => action(`success: ${message}`)(), + error: (message) => action(`error: ${message}`)(), + info: (message) => action(`info: ${message}`)(), + warning: (message) => action(`warning: ${message}`)(), + }, +}); + +addDecorators(); + +// Only gather and require CSS files related to Canvas. The other CSS files +// are built into the DLL. +const css = require.context( + '../../../../built_assets/css', + true, + /plugins\/(?=canvas).*light\.css/ +); +css.keys().forEach((filename) => css(filename)); diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.js b/x-pack/plugins/canvas/storybook/storyshots.test.tsx similarity index 85% rename from x-pack/plugins/canvas/storybook/storyshots.test.js rename to x-pack/plugins/canvas/storybook/storyshots.test.tsx index dbcbbff6398b5..c66be4a011f8c 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReactChildren } from 'react'; import path from 'path'; import moment from 'moment'; import 'moment-timezone'; import ReactDOM from 'react-dom'; import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; +// @ts-expect-error untyped library import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; import { addSerializer } from 'jest-specific-snapshot'; // Several of the renderers, used by the runtime, use jQuery. import jquery from 'jquery'; +// @ts-expect-error jQuery global global.$ = jquery; +// @ts-expect-error jQuery global global.jQuery = jquery; // Set our default timezone to UTC for tests so we can generate predictable snapshots @@ -23,7 +27,7 @@ moment.tz.setDefault('UTC'); // Freeze time for the tests for predictable snapshots const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 -Date.now = jest.fn(() => testTime); +Date.now = jest.fn(() => testTime.getTime()); // Mock telemetry service jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => {} })); @@ -53,10 +57,10 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => { }); // Mock React Portal for components that use modals, tooltips, etc -ReactDOM.createPortal = jest.fn((element) => { - return element; -}); +// @ts-expect-error Portal mocks are notoriously difficult to type +ReactDOM.createPortal = jest.fn((element) => element); +// Mock the EUI HTML ID Generator so elements have a predictable ID in snapshots jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { htmlIdGenerator: () => () => `generated-id`, @@ -67,7 +71,7 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { // https://github.com/elastic/eui/issues/3712 jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { return { - EuiOverlayMask: ({ children }) => children, + EuiOverlayMask: ({ children }: { children: ReactChildren }) => children, }; }); @@ -79,6 +83,7 @@ jest.mock( } ); +// @ts-expect-error untyped library import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; jest.mock('@elastic/eui/test-env/components/observer/observer'); EuiObserver.mockImplementation(() => 'EuiObserver'); @@ -86,6 +91,7 @@ EuiObserver.mockImplementation(() => 'EuiObserver'); // This element uses a `ref` and cannot be rendered by Jest snapshots. import { RenderedElement } from '../shareable_runtime/components/rendered_element'; jest.mock('../shareable_runtime/components/rendered_element'); +// @ts-expect-error RenderedElement.mockImplementation(() => 'RenderedElement'); addSerializer(styleSheetSerializer); @@ -94,5 +100,6 @@ addSerializer(styleSheetSerializer); initStoryshots({ configPath: path.resolve(__dirname, './../storybook'), test: multiSnapshotWithOptions({}), + // Don't snapshot tests that start with 'redux' storyNameRegex: /^((?!.*?redux).)*$/, }); diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index 982185a731b14..1321ade30bbde 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -6,236 +6,198 @@ const path = require('path'); const webpack = require('webpack'); +const webpackMerge = require('webpack-merge'); const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); // Extend the Storybook Webpack config with some customizations -module.exports = async ({ config }) => { - // Find and alter the CSS rule to replace the Kibana public path string with a path - // to the route we've added in middleware.js - const cssRule = config.module.rules.find((rule) => rule.test.source.includes('.css$')); - cssRule.use.push({ - loader: 'string-replace-loader', - options: { - search: '__REPLACE_WITH_PUBLIC_PATH__', - replace: '/', - flags: 'g', - }, - }); - - // Include the React preset from Kibana for Storybook JS files. - config.module.rules.push({ - test: /\.js$/, - exclude: /node_modules/, - loaders: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }); - - // Handle Typescript files - config.module.rules.push({ - test: /\.tsx?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }, - ], - }); - - config.module.rules.push({ - test: /\.mjs$/, - include: /node_modules/, - type: 'javascript/auto', - }); - - // Parse props data for .tsx files - // This is notoriously slow, and is making Storybook unusable. Disabling for now. - // See: https://github.com/storybookjs/storybook/issues/7998 - // - // config.module.rules.push({ - // test: /\.tsx$/, - // // Exclude example files, as we don't display props info for them - // exclude: /\.examples.tsx$/, - // use: [ - // // Parse TS comments to create Props tables in the UI - // require.resolve('react-docgen-typescript-loader'), - // ], - // }); - - // Enable SASS, but exclude CSS Modules in Storybook - config.module.rules.push({ - test: /\.scss$/, - exclude: /\.module.(s(a|c)ss)$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader', options: { importLoaders: 2 } }, - { - loader: 'postcss-loader', - options: { - config: { - path: require.resolve('@kbn/optimizer/postcss.config.js'), - }, - }, - }, - { - loader: 'sass-loader', - options: { - prependData(loaderContext) { - return `@import ${stringifyRequest( - loaderContext, - path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') - )};\n`; - }, - sassOptions: { - includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], +module.exports = async ({ config: storybookConfig }) => { + const config = { + module: { + rules: [ + // Include the React preset from Kibana for JS(X) and TS(X) + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + loaders: 'babel-loader', + options: { + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], }, }, - }, - ], - }); - - // Enable CSS Modules in Storybook - config.module.rules.push({ - test: /\.module\.s(a|c)ss$/, - loader: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - modules: { - localIdentName: '[name]__[local]___[hash:base64:5]', - }, + // Parse props data for .tsx files + // This is notoriously slow, and is making Storybook unusable. Disabling for now. + // See: https://github.com/storybookjs/storybook/issues/7998 + // + // { + // test: /\.tsx$/, + // // Exclude example files, as we don't display props info for them + // exclude: /\.examples.tsx$/, + // use: [ + // // Parse TS comments to create Props tables in the UI + // require.resolve('react-docgen-typescript-loader'), + // ], + // }, + // Enable SASS, but exclude CSS Modules in Storybook + { + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') + )};\n`; + }, + sassOptions: { + includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], + }, + }, + }, + ], }, - }, - { - loader: 'postcss-loader', - options: { - config: { - path: require.resolve('@kbn/optimizer/postcss.config.js'), - }, + // Enable CSS Modules in Storybook (Shareable Runtime) + { + test: /\.module\.s(a|c)ss$/, + loader: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2, + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + }, + ], }, - }, - { - loader: 'sass-loader', - }, - ], - }); - - // Exclude large-dependency modules that need not be included in Storybook. - config.module.rules.push({ - test: [ - path.resolve(__dirname, '../public/components/embeddable_flyout'), - path.resolve(__dirname, '../../reporting/public'), - ], - use: 'null-loader', - }); - - // Ensure jQuery is global for Storybook, specifically for the runtime. - config.plugins.push( - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery', - }) - ); - - // Reference the built DLL file of static(ish) dependencies, which are removed - // during kbn:bootstrap and rebuilt if missing. - config.plugins.push( - new webpack.DllReferencePlugin({ - manifest: path.resolve(DLL_OUTPUT, 'manifest.json'), - context: KIBANA_ROOT, - }) - ); - - // Copy the DLL files to the Webpack build for use in the Storybook UI - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ { - from: path.resolve(DLL_OUTPUT, 'dll.js'), - to: 'dll.js', + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', }, + // Exclude large-dependency, troublesome or irrelevant modules. { - from: path.resolve(DLL_OUTPUT, 'dll.css'), - to: 'dll.css', + test: [ + path.resolve(__dirname, '../public/components/embeddable_flyout'), + path.resolve(__dirname, '../../reporting/public'), + path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/angular'), + path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/paginate'), + ], + use: 'null-loader', }, ], - }) - ); - - config.plugins.push( - // replace imports for `uiExports/*` modules with a synthetic module - // created by create_ui_exports_module.js - new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { - // uiExports used by Canvas - const extensions = { - hacks: [], - chromeNavControls: [], - }; - - // everything following the first / in the request is - // treated as a type of appExtension - const type = resource.request.slice(resource.request.indexOf('/') + 1); - - resource.request = [ - // the "val-loader" is used to execute create_ui_exports_module - // and use its return value as the source for the module in the - // bundle. This allows us to bypass writing to the file system - require.resolve('val-loader'), - '!', - require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), - '?', - // this JSON is parsed by create_ui_exports_module and determines - // what require() calls it will execute within the bundle - JSON.stringify({ type, modules: extensions[type] || [] }), - ].join(''); - }), - - // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/notify/, - path.resolve(__dirname, '../tasks/mocks/uiNotify') - ), - new webpack.NormalModuleReplacementPlugin( - /lib\/download_workpad/, - path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/custom_element_service/, - path.resolve(__dirname, '../tasks/mocks/customElementService') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/ui_metric/, - path.resolve(__dirname, '../tasks/mocks/uiMetric') - ) - ); - - // Tell Webpack about relevant extensions - config.resolve.extensions.push('.ts', '.tsx', '.scss'); - - // Alias imports to either a mock or the proper module or directory. - // NOTE: order is important here - `ui/notify` will override `ui/notify/foo` if it - // is added first. - config.resolve.alias['ui/notify/lib/format_msg'] = path.resolve( - __dirname, - '../tasks/mocks/uiNotifyFormatMsg' - ); - config.resolve.alias['ui/notify'] = path.resolve(__dirname, '../tasks/mocks/uiNotify'); - config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve( - __dirname, - '../tasks/mocks/uiAbsoluteToParsedUrl' - ); - config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome'); - config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'); - config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'); + }, + plugins: [ + // Reference the built DLL file of static(ish) dependencies, which are removed + // during kbn:bootstrap and rebuilt if missing. + new webpack.DllReferencePlugin({ + manifest: path.resolve(DLL_OUTPUT, 'manifest.json'), + context: KIBANA_ROOT, + }), + // Ensure jQuery is global for Storybook, specifically for the runtime. + new webpack.ProvidePlugin({ + $: 'jquery', + jQuery: 'jquery', + }), + // Copy the DLL files to the Webpack build for use in the Storybook UI + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(DLL_OUTPUT, 'dll.js'), + to: 'dll.js', + }, + { + from: path.resolve(DLL_OUTPUT, 'dll.css'), + to: 'dll.css', + }, + ], + }), + // replace imports for `uiExports/*` modules with a synthetic module + // created by create_ui_exports_module.js + new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { + // uiExports used by Canvas + const extensions = { + hacks: [], + chromeNavControls: [], + }; + + // everything following the first / in the request is + // treated as a type of appExtension + const type = resource.request.slice(resource.request.indexOf('/') + 1); + + resource.request = [ + // the "val-loader" is used to execute create_ui_exports_module + // and use its return value as the source for the module in the + // bundle. This allows us to bypass writing to the file system + require.resolve('val-loader'), + '!', + require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), + '?', + // this JSON is parsed by create_ui_exports_module and determines + // what require() calls it will execute within the bundle + JSON.stringify({ type, modules: extensions[type] || [] }), + ].join(''); + }), + + new webpack.NormalModuleReplacementPlugin( + /lib\/download_workpad/, + path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/custom_element_service/, + path.resolve(__dirname, '../tasks/mocks/customElementService') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/ui_metric/, + path.resolve(__dirname, '../tasks/mocks/uiMetric') + ), + ], + resolve: { + extensions: ['.ts', '.tsx', '.scss', '.mjs', '.html'], + alias: { + 'ui/url/absolute_to_parsed_url': path.resolve( + __dirname, + '../tasks/mocks/uiAbsoluteToParsedUrl' + ), + ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'), + ng_mock$: path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'), + }, + }, + }; - config.resolve.extensions.push('.mjs'); + // Find and alter the CSS rule to replace the Kibana public path string with a path + // to the route we've added in middleware.js + const cssRule = storybookConfig.module.rules.find((rule) => rule.test.source.includes('.css$')); + cssRule.use.push({ + loader: 'string-replace-loader', + options: { + search: '__REPLACE_WITH_PUBLIC_PATH__', + replace: '/', + flags: 'g', + }, + }); - return config; + return webpackMerge(storybookConfig, config); }; diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 81d19c035075f..4e54750f08eea 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -25,9 +25,6 @@ module.exports = { '@elastic/eui/dist/eui_theme_light.css', '@kbn/ui-framework/dist/kui_light.css', '@storybook/addon-actions/register', - '@storybook/addon-knobs', - '@storybook/addon-knobs/react', - '@storybook/addon-knobs/register', '@storybook/core', '@storybook/core/dist/server/common/polyfills.js', '@storybook/react', @@ -38,6 +35,7 @@ module.exports = { 'chroma-js', 'highlight.js', 'html-entities', + 'jsondiffpatch', 'jquery', 'lodash', 'markdown-it', diff --git a/yarn.lock b/yarn.lock index 4e9f732f1e0a0..a70f04e030447 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4808,7 +4808,7 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest-specific-snapshot@^0.5.3": +"@types/jest-specific-snapshot@^0.5.3", "@types/jest-specific-snapshot@^0.5.4": version "0.5.4" resolved "https://registry.yarnpkg.com/@types/jest-specific-snapshot/-/jest-specific-snapshot-0.5.4.tgz#997364c39a59ddeff0ee790a19415e79dd061d1e" integrity sha512-1qISn4fH8wkOOPFEx+uWRRjw6m/pP/It3OHLm8Ee1KQpO7Z9ZGYDtWPU5AgK05UXsNTAgOK+dPQvJKGdy9E/1g== @@ -5726,7 +5726,7 @@ "@types/node" "*" chokidar "^2.1.2" -"@types/webpack-env@^1.15.0": +"@types/webpack-env@^1.15.0", "@types/webpack-env@^1.15.2": version "1.15.2" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== @@ -11933,6 +11933,11 @@ diagnostics@^1.1.1: enabled "1.0.x" kuler "1.0.x" +diff-match-patch@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + diff-match-patch@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" @@ -19267,6 +19272,14 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsondiffpatch@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/jsondiffpatch/-/jsondiffpatch-0.4.1.tgz#9fb085036767f03534ebd46dcd841df6070c5773" + integrity sha512-t0etAxTUk1w5MYdNOkZBZ8rvYYN5iL+2dHCCx/DpkFm/bW28M6y5nUS83D4XdZiHy35Fpaw6LBb+F88fHZnVCw== + dependencies: + chalk "^2.3.0" + diff-match-patch "^1.0.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" From 2d2e959f5d765fb6a0bbd7cfd1602b7bfb0f12e9 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Sat, 1 Aug 2020 12:04:49 -0400 Subject: [PATCH 040/115] [Canvas][tech-debt] Rename __examples__ to __stories__ (#73853) Co-authored-by: Elastic Machine --- .../__snapshots__/advanced_filter.stories.storyshot | 0 .../{__examples__ => __stories__}/advanced_filter.stories.tsx | 0 .../__snapshots__/dropdown_filter.stories.storyshot | 0 .../{__examples__ => __stories__}/dropdown_filter.stories.tsx | 0 .../__snapshots__/time_filter.stories.storyshot | 0 .../{__examples__ => __stories__}/time_filter.stories.tsx | 0 .../__snapshots__/metric.stories.storyshot | 0 .../component/{__examples__ => __stories__}/metric.stories.tsx | 0 .../__snapshots__/palette.stories.storyshot | 0 .../arguments/{__examples__ => __stories__}/palette.stories.tsx | 0 .../__snapshots__/extended_template.stories.storyshot | 0 .../__snapshots__/simple_template.stories.storyshot | 0 .../{__examples__ => __stories__}/extended_template.stories.tsx | 0 .../{__examples__ => __stories__}/simple_template.stories.tsx | 0 .../__snapshots__/date_format.stories.storyshot | 0 .../{__examples__ => __stories__}/date_format.stories.tsx | 0 .../__snapshots__/number_format.stories.storyshot | 0 .../{__examples__ => __stories__}/number_format.stories.tsx | 0 .../__snapshots__/asset.stories.storyshot | 0 .../__snapshots__/asset_manager.stories.storyshot | 0 .../{__examples__ => __stories__}/asset.stories.tsx | 0 .../{__examples__ => __stories__}/asset_manager.stories.tsx | 0 .../asset_manager/{__examples__ => __stories__}/assets.ts | 0 .../__snapshots__/color_dot.stories.storyshot | 0 .../{__examples__ => __stories__}/color_dot.stories.tsx | 0 .../__snapshots__/color_manager.stories.storyshot | 0 .../{__examples__ => __stories__}/color_manager.stories.tsx | 0 .../__snapshots__/color_palette.stories.storyshot | 0 .../{__examples__ => __stories__}/color_palette.stories.tsx | 0 .../__snapshots__/color_picker.stories.storyshot | 0 .../{__examples__ => __stories__}/color_picker.stories.tsx | 0 .../__snapshots__/color_picker_popover.stories.storyshot | 0 .../color_picker_popover.stories.tsx | 0 .../__snapshots__/custom_element_modal.stories.storyshot | 0 .../custom_element_modal.stories.tsx | 0 .../__snapshots__/debug.stories.storyshot | 0 .../debug/{__examples__ => __stories__}/debug.stories.tsx | 0 .../components/debug/{__examples__ => __stories__}/helpers.tsx | 0 .../__snapshots__/element_card.stories.storyshot | 0 .../{__examples__ => __stories__}/element_card.stories.tsx | 0 .../__snapshots__/expression_input.stories.storyshot | 0 .../{__examples__ => __stories__}/expression_input.stories.tsx | 0 .../__snapshots__/item_grid.stories.storyshot | 0 .../{__examples__ => __stories__}/item_grid.stories.tsx | 0 .../__snapshots__/keyboard_shortcuts_doc.stories.storyshot | 0 .../keyboard_shortcuts_doc.stories.tsx | 0 .../__snapshots__/palette_picker.stories.storyshot | 0 .../{__examples__ => __stories__}/palette_picker.stories.tsx | 0 .../__snapshots__/element_controls.stories.storyshot | 0 .../__snapshots__/element_grid.stories.storyshot | 0 .../__snapshots__/saved_elements_modal.stories.storyshot | 0 .../{__examples__ => __stories__}/element_controls.stories.tsx | 0 .../{__examples__ => __stories__}/element_grid.stories.tsx | 0 .../{__examples__ => __stories__}/fixtures/test_elements.tsx | 0 .../saved_elements_modal.stories.tsx | 0 .../__snapshots__/shape_picker.stories.storyshot | 0 .../{__examples__ => __stories__}/shape_picker.stories.tsx | 0 .../__snapshots__/shape_picker_popover.stories.storyshot | 0 .../shape_picker_popover.stories.tsx | 0 .../__snapshots__/shape_preview.stories.storyshot | 0 .../{__examples__ => __stories__}/shape_preview.stories.tsx | 0 .../__snapshots__/group_settings.stories.storyshot | 0 .../__snapshots__/multi_element_settings.stories.storyshot | 0 .../{__examples__ => __stories__}/group_settings.stories.tsx | 0 .../multi_element_settings.stories.tsx | 0 .../__snapshots__/sidebar_header.stories.storyshot | 0 .../{__examples__ => __stories__}/sidebar_header.stories.tsx | 0 .../__snapshots__/tag.stories.storyshot | 0 .../tag/{__examples__ => __stories__}/tag.stories.tsx | 0 .../__snapshots__/tag_list.stories.storyshot | 0 .../tag_list/{__examples__ => __stories__}/tag_list.stories.tsx | 0 .../__snapshots__/tool_tip_shortcut.stories.storyshot | 0 .../{__examples__ => __stories__}/tool_tip_shortcut.stories.tsx | 0 .../__snapshots__/toolbar.stories.storyshot | 0 .../toolbar/{__examples__ => __stories__}/toolbar.stories.tsx | 0 .../__snapshots__/delete_var.stories.storyshot | 0 .../__snapshots__/edit_var.stories.storyshot | 0 .../__snapshots__/var_config.stories.storyshot | 0 .../{__examples__ => __stories__}/delete_var.stories.tsx | 0 .../{__examples__ => __stories__}/edit_var.stories.tsx | 0 .../{__examples__ => __stories__}/var_config.stories.tsx | 0 .../__snapshots__/edit_menu.stories.storyshot | 0 .../{__examples__ => __stories__}/edit_menu.stories.tsx | 0 .../__snapshots__/element_menu.stories.storyshot | 0 .../{__examples__ => __stories__}/element_menu.stories.tsx | 0 .../__snapshots__/pdf_panel.stories.storyshot | 0 .../__snapshots__/share_menu.stories.storyshot | 0 .../{__examples__ => __stories__}/pdf_panel.stories.tsx | 0 .../{__examples__ => __stories__}/share_menu.stories.tsx | 0 .../flyout/{__examples__ => __stories__}/flyout.stories.tsx | 0 .../__snapshots__/view_menu.stories.storyshot | 0 .../{__examples__ => __stories__}/view_menu.stories.tsx | 0 .../__snapshots__/extended_template.stories.storyshot | 0 .../__snapshots__/simple_template.stories.storyshot | 0 .../{__examples__ => __stories__}/extended_template.stories.tsx | 0 .../{__examples__ => __stories__}/simple_template.stories.tsx | 0 .../__snapshots__/extended_template.stories.storyshot | 0 .../__snapshots__/simple_template.stories.storyshot | 0 .../{__examples__ => __stories__}/extended_template.stories.tsx | 0 .../{__examples__ => __stories__}/simple_template.stories.tsx | 0 .../__snapshots__/canvas.stories.storyshot | 0 .../__snapshots__/page.stories.storyshot | 0 .../__snapshots__/rendered_element.stories.storyshot | 0 .../components/{__examples__ => __stories__}/canvas.stories.tsx | 0 .../components/{__examples__ => __stories__}/page.stories.tsx | 0 .../{__examples__ => __stories__}/rendered_element.stories.tsx | 0 .../__snapshots__/footer.stories.storyshot | 0 .../__snapshots__/page_controls.stories.storyshot | 0 .../__snapshots__/scrubber.stories.storyshot | 0 .../__snapshots__/title.stories.storyshot | 0 .../footer/{__examples__ => __stories__}/footer.stories.tsx | 0 .../{__examples__ => __stories__}/page_controls.stories.tsx | 0 .../footer/{__examples__ => __stories__}/scrubber.stories.tsx | 0 .../footer/{__examples__ => __stories__}/title.stories.tsx | 0 .../__snapshots__/autoplay_settings.stories.storyshot | 0 .../__snapshots__/settings.stories.storyshot | 0 .../__snapshots__/toolbar_settings.stories.storyshot | 0 .../{__examples__ => __stories__}/autoplay_settings.stories.tsx | 0 .../settings/{__examples__ => __stories__}/settings.stories.tsx | 0 .../{__examples__ => __stories__}/toolbar_settings.stories.tsx | 0 x-pack/plugins/canvas/storybook/storyshots.test.tsx | 2 +- 121 files changed, 1 insertion(+), 1 deletion(-) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/{__examples__ => __stories__}/__snapshots__/advanced_filter.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/{__examples__ => __stories__}/advanced_filter.stories.tsx (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/{__examples__ => __stories__}/__snapshots__/dropdown_filter.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/{__examples__ => __stories__}/dropdown_filter.stories.tsx (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/{__examples__ => __stories__}/__snapshots__/time_filter.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/{__examples__ => __stories__}/time_filter.stories.tsx (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/{__examples__ => __stories__}/__snapshots__/metric.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/{__examples__ => __stories__}/metric.stories.tsx (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/{__examples__ => __stories__}/__snapshots__/palette.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/{__examples__ => __stories__}/palette.stories.tsx (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/{__examples__ => __stories__}/__snapshots__/extended_template.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/{__examples__ => __stories__}/__snapshots__/simple_template.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/{__examples__ => __stories__}/extended_template.stories.tsx (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/{__examples__ => __stories__}/simple_template.stories.tsx (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/{__examples__ => __stories__}/__snapshots__/date_format.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/{__examples__ => __stories__}/date_format.stories.tsx (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/{__examples__ => __stories__}/__snapshots__/number_format.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/{__examples__ => __stories__}/number_format.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/asset_manager/{__examples__ => __stories__}/__snapshots__/asset.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/asset_manager/{__examples__ => __stories__}/__snapshots__/asset_manager.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/asset_manager/{__examples__ => __stories__}/asset.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/asset_manager/{__examples__ => __stories__}/asset_manager.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/asset_manager/{__examples__ => __stories__}/assets.ts (100%) rename x-pack/plugins/canvas/public/components/color_dot/{__examples__ => __stories__}/__snapshots__/color_dot.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/color_dot/{__examples__ => __stories__}/color_dot.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/color_manager/{__examples__ => __stories__}/__snapshots__/color_manager.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/color_manager/{__examples__ => __stories__}/color_manager.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/color_palette/{__examples__ => __stories__}/__snapshots__/color_palette.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/color_palette/{__examples__ => __stories__}/color_palette.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/color_picker/{__examples__ => __stories__}/__snapshots__/color_picker.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/color_picker/{__examples__ => __stories__}/color_picker.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/color_picker_popover/{__examples__ => __stories__}/__snapshots__/color_picker_popover.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/color_picker_popover/{__examples__ => __stories__}/color_picker_popover.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/custom_element_modal/{__examples__ => __stories__}/__snapshots__/custom_element_modal.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/custom_element_modal/{__examples__ => __stories__}/custom_element_modal.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/debug/{__examples__ => __stories__}/__snapshots__/debug.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/debug/{__examples__ => __stories__}/debug.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/debug/{__examples__ => __stories__}/helpers.tsx (100%) rename x-pack/plugins/canvas/public/components/element_card/{__examples__ => __stories__}/__snapshots__/element_card.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/element_card/{__examples__ => __stories__}/element_card.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/expression_input/{__examples__ => __stories__}/__snapshots__/expression_input.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/expression_input/{__examples__ => __stories__}/expression_input.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/item_grid/{__examples__ => __stories__}/__snapshots__/item_grid.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/item_grid/{__examples__ => __stories__}/item_grid.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/{__examples__ => __stories__}/__snapshots__/keyboard_shortcuts_doc.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/{__examples__ => __stories__}/keyboard_shortcuts_doc.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/palette_picker/{__examples__ => __stories__}/__snapshots__/palette_picker.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/palette_picker/{__examples__ => __stories__}/palette_picker.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/saved_elements_modal/{__examples__ => __stories__}/__snapshots__/element_controls.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/saved_elements_modal/{__examples__ => __stories__}/__snapshots__/element_grid.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/saved_elements_modal/{__examples__ => __stories__}/__snapshots__/saved_elements_modal.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/saved_elements_modal/{__examples__ => __stories__}/element_controls.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/saved_elements_modal/{__examples__ => __stories__}/element_grid.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/saved_elements_modal/{__examples__ => __stories__}/fixtures/test_elements.tsx (100%) rename x-pack/plugins/canvas/public/components/saved_elements_modal/{__examples__ => __stories__}/saved_elements_modal.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/shape_picker/{__examples__ => __stories__}/__snapshots__/shape_picker.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/shape_picker/{__examples__ => __stories__}/shape_picker.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/shape_picker_popover/{__examples__ => __stories__}/__snapshots__/shape_picker_popover.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/shape_picker_popover/{__examples__ => __stories__}/shape_picker_popover.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/shape_preview/{__examples__ => __stories__}/__snapshots__/shape_preview.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/shape_preview/{__examples__ => __stories__}/shape_preview.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/sidebar/{__examples__ => __stories__}/__snapshots__/group_settings.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/sidebar/{__examples__ => __stories__}/__snapshots__/multi_element_settings.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/sidebar/{__examples__ => __stories__}/group_settings.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/sidebar/{__examples__ => __stories__}/multi_element_settings.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/sidebar_header/{__examples__ => __stories__}/__snapshots__/sidebar_header.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/sidebar_header/{__examples__ => __stories__}/sidebar_header.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/tag/{__examples__ => __stories__}/__snapshots__/tag.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/tag/{__examples__ => __stories__}/tag.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/tag_list/{__examples__ => __stories__}/__snapshots__/tag_list.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/tag_list/{__examples__ => __stories__}/tag_list.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/tool_tip_shortcut/{__examples__ => __stories__}/__snapshots__/tool_tip_shortcut.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/tool_tip_shortcut/{__examples__ => __stories__}/tool_tip_shortcut.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/toolbar/{__examples__ => __stories__}/__snapshots__/toolbar.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/toolbar/{__examples__ => __stories__}/toolbar.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/var_config/{__examples__ => __stories__}/__snapshots__/delete_var.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/var_config/{__examples__ => __stories__}/__snapshots__/edit_var.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/var_config/{__examples__ => __stories__}/__snapshots__/var_config.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/var_config/{__examples__ => __stories__}/delete_var.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/var_config/{__examples__ => __stories__}/edit_var.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/var_config/{__examples__ => __stories__}/var_config.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/workpad_header/edit_menu/{__examples__ => __stories__}/__snapshots__/edit_menu.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/workpad_header/edit_menu/{__examples__ => __stories__}/edit_menu.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/workpad_header/element_menu/{__examples__ => __stories__}/__snapshots__/element_menu.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/workpad_header/element_menu/{__examples__ => __stories__}/element_menu.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/{__examples__ => __stories__}/__snapshots__/pdf_panel.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/{__examples__ => __stories__}/__snapshots__/share_menu.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/{__examples__ => __stories__}/pdf_panel.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/{__examples__ => __stories__}/share_menu.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/{__examples__ => __stories__}/flyout.stories.tsx (100%) rename x-pack/plugins/canvas/public/components/workpad_header/view_menu/{__examples__ => __stories__}/__snapshots__/view_menu.stories.storyshot (100%) rename x-pack/plugins/canvas/public/components/workpad_header/view_menu/{__examples__ => __stories__}/view_menu.stories.tsx (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/container_style/{__examples__ => __stories__}/__snapshots__/extended_template.stories.storyshot (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/container_style/{__examples__ => __stories__}/__snapshots__/simple_template.stories.storyshot (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/container_style/{__examples__ => __stories__}/extended_template.stories.tsx (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/container_style/{__examples__ => __stories__}/simple_template.stories.tsx (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/series_style/{__examples__ => __stories__}/__snapshots__/extended_template.stories.storyshot (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/series_style/{__examples__ => __stories__}/__snapshots__/simple_template.stories.storyshot (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/series_style/{__examples__ => __stories__}/extended_template.stories.tsx (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/series_style/{__examples__ => __stories__}/simple_template.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/{__examples__ => __stories__}/__snapshots__/canvas.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/{__examples__ => __stories__}/__snapshots__/page.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/{__examples__ => __stories__}/__snapshots__/rendered_element.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/{__examples__ => __stories__}/canvas.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/{__examples__ => __stories__}/page.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/{__examples__ => __stories__}/rendered_element.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/{__examples__ => __stories__}/__snapshots__/footer.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/{__examples__ => __stories__}/__snapshots__/page_controls.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/{__examples__ => __stories__}/__snapshots__/scrubber.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/{__examples__ => __stories__}/__snapshots__/title.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/{__examples__ => __stories__}/footer.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/{__examples__ => __stories__}/page_controls.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/{__examples__ => __stories__}/scrubber.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/{__examples__ => __stories__}/title.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/{__examples__ => __stories__}/__snapshots__/autoplay_settings.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/{__examples__ => __stories__}/__snapshots__/settings.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/{__examples__ => __stories__}/__snapshots__/toolbar_settings.stories.storyshot (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/{__examples__ => __stories__}/autoplay_settings.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/{__examples__ => __stories__}/settings.stories.tsx (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/{__examples__ => __stories__}/toolbar_settings.stories.tsx (100%) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/__snapshots__/advanced_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/__snapshots__/advanced_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/advanced_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/advanced_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/advanced_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/advanced_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/dropdown_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/dropdown_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/time_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/time_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/time_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/time_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/__snapshots__/metric.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/__snapshots__/metric.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/metric.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/metric.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/date_format.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/date_format.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/date_format.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/date_format.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/number_format.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/number_format.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.stories.tsx rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/assets.ts b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/assets.ts rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts diff --git a/x-pack/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.stories.storyshot b/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.stories.storyshot rename to x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/color_dot/__examples__/color_dot.stories.tsx b/x-pack/plugins/canvas/public/components/color_dot/__stories__/color_dot.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/color_dot/__examples__/color_dot.stories.tsx rename to x-pack/plugins/canvas/public/components/color_dot/__stories__/color_dot.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.stories.storyshot rename to x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/color_manager/__examples__/color_manager.stories.tsx b/x-pack/plugins/canvas/public/components/color_manager/__stories__/color_manager.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/color_manager/__examples__/color_manager.stories.tsx rename to x-pack/plugins/canvas/public/components/color_manager/__stories__/color_manager.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.stories.storyshot b/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.stories.storyshot rename to x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/color_palette/__examples__/color_palette.stories.tsx b/x-pack/plugins/canvas/public/components/color_palette/__stories__/color_palette.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/color_palette/__examples__/color_palette.stories.tsx rename to x-pack/plugins/canvas/public/components/color_palette/__stories__/color_palette.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.stories.storyshot rename to x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.stories.tsx b/x-pack/plugins/canvas/public/components/color_picker/__stories__/color_picker.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.stories.tsx rename to x-pack/plugins/canvas/public/components/color_picker/__stories__/color_picker.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/color_picker_popover/__examples__/__snapshots__/color_picker_popover.stories.storyshot b/x-pack/plugins/canvas/public/components/color_picker_popover/__stories__/__snapshots__/color_picker_popover.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/color_picker_popover/__examples__/__snapshots__/color_picker_popover.stories.storyshot rename to x-pack/plugins/canvas/public/components/color_picker_popover/__stories__/__snapshots__/color_picker_popover.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/color_picker_popover/__examples__/color_picker_popover.stories.tsx b/x-pack/plugins/canvas/public/components/color_picker_popover/__stories__/color_picker_popover.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/color_picker_popover/__examples__/color_picker_popover.stories.tsx rename to x-pack/plugins/canvas/public/components/color_picker_popover/__stories__/color_picker_popover.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.stories.storyshot rename to x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/custom_element_modal.stories.tsx b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/custom_element_modal.stories.tsx rename to x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/debug/__examples__/__snapshots__/debug.stories.storyshot b/x-pack/plugins/canvas/public/components/debug/__stories__/__snapshots__/debug.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/debug/__examples__/__snapshots__/debug.stories.storyshot rename to x-pack/plugins/canvas/public/components/debug/__stories__/__snapshots__/debug.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/debug/__examples__/debug.stories.tsx b/x-pack/plugins/canvas/public/components/debug/__stories__/debug.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/debug/__examples__/debug.stories.tsx rename to x-pack/plugins/canvas/public/components/debug/__stories__/debug.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/debug/__examples__/helpers.tsx b/x-pack/plugins/canvas/public/components/debug/__stories__/helpers.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/debug/__examples__/helpers.tsx rename to x-pack/plugins/canvas/public/components/debug/__stories__/helpers.tsx diff --git a/x-pack/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.stories.storyshot b/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.stories.storyshot rename to x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/element_card/__examples__/element_card.stories.tsx b/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/element_card/__examples__/element_card.stories.tsx rename to x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.stories.storyshot b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.stories.storyshot rename to x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.stories.tsx b/x-pack/plugins/canvas/public/components/expression_input/__stories__/expression_input.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.stories.tsx rename to x-pack/plugins/canvas/public/components/expression_input/__stories__/expression_input.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.stories.storyshot rename to x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.stories.tsx b/x-pack/plugins/canvas/public/components/item_grid/__stories__/item_grid.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.stories.tsx rename to x-pack/plugins/canvas/public/components/item_grid/__stories__/item_grid.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot rename to x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/keyboard_shortcuts_doc.stories.tsx b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/keyboard_shortcuts_doc.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/keyboard_shortcuts_doc.stories.tsx rename to x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/keyboard_shortcuts_doc.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot rename to x-pack/plugins/canvas/public/components/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx b/x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx rename to x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_controls.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_controls.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/shape_picker/__examples__/__snapshots__/shape_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_picker/__stories__/__snapshots__/shape_picker.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker/__examples__/__snapshots__/shape_picker.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_picker/__stories__/__snapshots__/shape_picker.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_picker/__examples__/shape_picker.stories.tsx b/x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker/__examples__/shape_picker.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/shape_picker_popover.stories.tsx b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/shape_picker_popover.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/shape_preview/__examples__/__snapshots__/shape_preview.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_preview/__examples__/__snapshots__/shape_preview.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_preview/__examples__/shape_preview.stories.tsx b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_preview/__examples__/shape_preview.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/group_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/group_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/multi_element_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/multi_element_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/group_settings.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar/__stories__/group_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/group_settings.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/group_settings.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/multi_element_settings.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar/__stories__/multi_element_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/multi_element_settings.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/multi_element_settings.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.stories.storyshot b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.stories.storyshot rename to x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tag/__examples__/tag.stories.tsx b/x-pack/plugins/canvas/public/components/tag/__stories__/tag.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tag/__examples__/tag.stories.tsx rename to x-pack/plugins/canvas/public/components/tag/__stories__/tag.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.stories.storyshot b/x-pack/plugins/canvas/public/components/tag_list/__stories__/__snapshots__/tag_list.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.stories.storyshot rename to x-pack/plugins/canvas/public/components/tag_list/__stories__/__snapshots__/tag_list.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tag_list/__examples__/tag_list.stories.tsx b/x-pack/plugins/canvas/public/components/tag_list/__stories__/tag_list.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tag_list/__examples__/tag_list.stories.tsx rename to x-pack/plugins/canvas/public/components/tag_list/__stories__/tag_list.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/__snapshots__/tool_tip_shortcut.stories.storyshot b/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/__snapshots__/tool_tip_shortcut.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/__snapshots__/tool_tip_shortcut.stories.storyshot rename to x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/__snapshots__/tool_tip_shortcut.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/tool_tip_shortcut.stories.tsx b/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/tool_tip_shortcut.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/tool_tip_shortcut.stories.tsx rename to x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/tool_tip_shortcut.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/toolbar/__examples__/__snapshots__/toolbar.stories.storyshot b/x-pack/plugins/canvas/public/components/toolbar/__stories__/__snapshots__/toolbar.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/toolbar/__examples__/__snapshots__/toolbar.stories.storyshot rename to x-pack/plugins/canvas/public/components/toolbar/__stories__/__snapshots__/toolbar.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx rename to x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/delete_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/delete_var.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/edit_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/edit_var.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/var_config.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/var_config.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/delete_var.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/delete_var.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/delete_var.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/delete_var.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/edit_var.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/edit_var.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/edit_var.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/edit_var.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/var_config.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/var_config.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/var_config.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/var_config.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/edit_menu.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/edit_menu.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/pdf_panel.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/pdf_panel.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/flyout.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/flyout.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/__snapshots__/view_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/__snapshots__/view_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/view_menu.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/view_menu.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/page.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/page.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/rendered_element.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/rendered_element.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/canvas.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/canvas.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/page.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/page.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/rendered_element.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/rendered_element.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/scrubber.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/scrubber.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/footer.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/footer.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/page_controls.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/page_controls.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/title.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/title.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/autoplay_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/autoplay_settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/toolbar_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/toolbar_settings.stories.tsx diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index c66be4a011f8c..b51a85edaa67b 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -77,7 +77,7 @@ jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( - '../public/components/workpad_header/share_menu/flyout/__examples__/flyout.stories', + '../public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories', () => { return 'Disabled Panel'; } From 9075b6e1cd246d2f4eec79e98ecadc4edf08fdfe Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 3 Aug 2020 11:15:49 +0100 Subject: [PATCH 041/115] [Security Solution] Disable bulk actions for immutable timeline templates (#73687) * disablebulk actions for immutable timeline templates * make immutable timelines not selectable * hide selected count if timeline status is immutable Co-authored-by: Elastic Machine --- .../components/open_timeline/index.tsx | 2 + .../open_timeline/open_timeline.test.tsx | 135 +++++++++++++++++- .../open_timeline/open_timeline.tsx | 53 ++++--- .../open_timeline_modal_body.test.tsx | 3 +- .../timelines_table/actions_columns.test.tsx | 29 ++++ .../open_timeline/timelines_table/index.tsx | 5 +- .../components/open_timeline/types.ts | 3 + 7 files changed, 204 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 188b8979f613c..4c5db80a6c916 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -326,6 +326,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortField={sortField} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} + timelineStatus={timelineStatus} timelineFilter={timelineTabs} title={title} totalSearchResultsCount={totalCount} @@ -356,6 +357,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortField={sortField} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} + timelineStatus={timelineStatus} timelineFilter={timelineFilters} title={title} totalSearchResultsCount={totalCount} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 57a6431a06b90..9de3242c5e303 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -17,7 +17,7 @@ import { TimelinesTableProps } from './timelines_table'; import { mockTimelineResults } from '../../../common/mock/timeline_results'; import { OpenTimeline } from './open_timeline'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/lib/kibana'); @@ -50,6 +50,7 @@ describe('OpenTimeline', () => { sortField: DEFAULT_SORT_FIELD, title, timelineType: TimelineType.default, + timelineStatus: TimelineStatus.active, templateTimelineFilter: [
], totalSearchResultsCount: mockSearchResults.length, }); @@ -263,4 +264,136 @@ describe('OpenTimeline', () => { `Showing: ${mockResults.length} timelines with "How was your day?"` ); }); + + test("it should render bulk actions if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); + }); + + test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']); + }); + + test("it should render selected count if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true); + }); + + test("it should not render bulk actions if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(false); + }); + + test("it should not render a selectable timeline table if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate']); + }); + + test("it should not render selected count if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(false); + }); + + test("it should render bulk actions if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); + }); + + test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']); + }); + + test("it should render selected count if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index d839a1deddf21..c9495c46d4acf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiBasicTable } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { ImportDataModal } from '../../../common/components/import_data_modal'; import { UtilityBarGroup, @@ -55,6 +55,7 @@ export const OpenTimeline = React.memo( setImportDataModalToggle, sortField, timelineType = TimelineType.default, + timelineStatus, timelineFilter, templateTimelineFilter, totalSearchResultsCount, @@ -140,19 +141,23 @@ export const OpenTimeline = React.memo( }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); const actionTimelineToShow = useMemo(() => { - const timelineActions: ActionTimelineToShow[] = [ - 'createFrom', - 'duplicate', - 'export', - 'selectable', - ]; + const timelineActions: ActionTimelineToShow[] = ['createFrom', 'duplicate']; - if (onDeleteSelected != null && deleteTimelines != null) { + if (timelineStatus !== TimelineStatus.immutable) { + timelineActions.push('export'); + timelineActions.push('selectable'); + } + + if ( + onDeleteSelected != null && + deleteTimelines != null && + timelineStatus !== TimelineStatus.immutable + ) { timelineActions.push('delete'); } return timelineActions; - }, [onDeleteSelected, deleteTimelines]); + }, [onDeleteSelected, deleteTimelines, timelineStatus]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); @@ -206,20 +211,24 @@ export const OpenTimeline = React.memo( - - - {timelineType === TimelineType.template - ? i18n.SELECTED_TEMPLATES(selectedItems.length) - : i18n.SELECTED_TIMELINES(selectedItems.length)} - - - {i18n.BATCH_ACTIONS} - + {timelineStatus !== TimelineStatus.immutable && ( + <> + + {timelineType === TimelineType.template + ? i18n.SELECTED_TEMPLATES(selectedItems.length) + : i18n.SELECTED_TIMELINES(selectedItems.length)} + + + {i18n.BATCH_ACTIONS} + + + )} {i18n.REFRESH} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 12df17ceba666..9632b0e6ecea4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -17,7 +17,7 @@ import { TimelinesTableProps } from '../timelines_table'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineModalBody } from './open_timeline_modal_body'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; jest.mock('../../../../common/lib/kibana'); @@ -48,6 +48,7 @@ describe('OpenTimelineModal', () => { sortDirection: DEFAULT_SORT_DIRECTION, sortField: DEFAULT_SORT_FIELD, timelineType: TimelineType.default, + timelineStatus: TimelineStatus.active, templateTimelineFilter: [
], title, totalSearchResultsCount: mockSearchResults.length, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx index eddfdf6e01df2..52b7a4293e847 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import '../../../../common/mock/match_media'; + import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; import { TimelinesTableProps } from '.'; @@ -233,4 +234,32 @@ describe('#getActionsColumns', () => { expect(enableExportTimelineDownloader).toBeCalledWith(mockResults[0]); }); + + test('it should not render "export timeline" if it is not included', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['createFrom', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toEqual(false); + }); + + test('it should not render "delete timeline" if it is not included', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['createFrom', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toEqual(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx index 2d3672b15dd10..d2fba696d9d54 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx @@ -24,7 +24,7 @@ import { getActionsColumns } from './actions_columns'; import { getCommonColumns } from './common_columns'; import { getExtendedColumns } from './extended_columns'; import { getIconHeaderColumns } from './icon_header_columns'; -import { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline'; +import { TimelineTypeLiteralWithNull, TimelineStatus } from '../../../../../common/types/timeline'; // there are a number of type mismatches across this file const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -159,7 +159,8 @@ export const TimelinesTable = React.memo( }; const selection = { - selectable: (timelineResult: OpenTimelineResult) => timelineResult.savedObjectId != null, + selectable: (timelineResult: OpenTimelineResult) => + timelineResult.savedObjectId != null && timelineResult.status !== TimelineStatus.immutable, selectableMessage: (selectable: boolean) => !selectable ? i18n.MISSING_SAVED_OBJECT_ID : undefined, onSelectionChange, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index eb5a03baad88c..8950f814d6965 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -14,6 +14,7 @@ import { TimelineStatus, TemplateTimelineTypeLiteral, RowRendererId, + TimelineStatusLiteralWithNull, } from '../../../../common/types/timeline'; /** The users who added a timeline to favorites */ @@ -174,6 +175,8 @@ export interface OpenTimelineProps { sortField: string; /** this affects timeline's behaviour like editable / duplicatible */ timelineType: TimelineTypeLiteralWithNull; + /* active or immutable */ + timelineStatus: TimelineStatusLiteralWithNull; /** when timelineType === template, templatetimelineFilter is a JSX.Element */ templateTimelineFilter: JSX.Element[] | null; /** timeline / timeline template */ From 1b2233a5a69ff0fae714da1cc60934e0acf0df6d Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 3 Aug 2020 12:16:39 +0200 Subject: [PATCH 042/115] [ Functional Test ] Unskip X-Pack "Advanced Settings - spaces feature controls" suite (#73823) Co-authored-by: Elastic Machine --- .../feature_controls/advanced_settings_spaces.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index c8adb3ce67d55..f1da94febc631 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -14,8 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const config = getService('config'); - // FLAKY: https://github.com/elastic/kibana/issues/57413 - describe.skip('spaces feature controls', () => { + describe('spaces feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); From d19883f624d8cbe654a6ba91e12edcf4722c88e3 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 3 Aug 2020 11:44:40 +0100 Subject: [PATCH 043/115] [ML] Adding combined job and datafeed JSON editing (#72117) * [ML] Fixing edit datafeed usablility issues * updates * add json editing to all wizard steps * removing unused include * adding comments * updating text * text update * wrapping preview in useCallback Co-authored-by: Elastic Machine --- .../datafeed_preview.tsx | 119 ++++++++++++++++++ .../datafeed_preview_flyout.tsx | 88 ++----------- .../common/datafeed_preview_flyout/index.ts | 1 + .../json_editor_flyout/json_editor_flyout.tsx | 112 ++++++++++++++--- .../components/datafeed_step/datafeed.tsx | 18 +-- .../job_details_step/job_details.tsx | 14 ++- .../pick_fields_step/pick_fields.tsx | 42 +++---- .../pages/components/summary_step/summary.tsx | 15 +-- 8 files changed, 262 insertions(+), 147 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx new file mode 100644 index 0000000000000..0dd802855ea67 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect, useMemo, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiLoadingSpinner, + EuiButton, +} from '@elastic/eui'; + +import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; +import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; +import { mlJobService } from '../../../../../../services/job_service'; +import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; + +export const DatafeedPreview: FC<{ + combinedJob: CombinedJob | null; + heightOffset?: number; +}> = ({ combinedJob, heightOffset = 0 }) => { + // the ace editor requires a fixed height + const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ + heightOffset, + ]); + const [loading, setLoading] = useState(false); + const [previewJsonString, setPreviewJsonString] = useState(''); + const [outOfDate, setOutOfDate] = useState(false); + const [combinedJobString, setCombinedJobString] = useState(''); + + useEffect(() => { + try { + if (combinedJob !== null) { + if (combinedJobString === '') { + // first time, set the string and load the preview + loadDataPreview(); + } else { + setOutOfDate(JSON.stringify(combinedJob) !== combinedJobString); + } + } + } catch (error) { + // fail silently + } + }, [combinedJob]); + + const loadDataPreview = useCallback(async () => { + setPreviewJsonString(''); + if (combinedJob === null) { + return; + } + + setLoading(true); + setCombinedJobString(JSON.stringify(combinedJob)); + + if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { + try { + const resp = await mlJobService.searchPreview(combinedJob); + const data = resp.aggregations + ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) + : resp.hits.hits; + + setPreviewJsonString(JSON.stringify(data, null, 2)); + } catch (error) { + setPreviewJsonString(JSON.stringify(error, null, 2)); + } + setLoading(false); + setOutOfDate(false); + } else { + const errorText = i18n.translate( + 'xpack.ml.newJob.wizard.datafeedPreviewFlyout.datafeedDoesNotExistLabel', + { + defaultMessage: 'Datafeed does not exist', + } + ); + setPreviewJsonString(errorText); + } + }, [combinedJob]); + + return ( + + + + +
+ +
+
+
+ + {outOfDate && ( + + Refresh + + )} + +
+ + {loading === true ? ( + + + + + + + ) : ( + + )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx index 03be38adfbbe0..d35083ec6e479 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useContext, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { Fragment, FC, useState, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, @@ -13,18 +12,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, - EuiTitle, EuiFlyoutBody, - EuiSpacer, - EuiLoadingSpinner, } from '@elastic/eui'; -import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; -import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; + import { JobCreatorContext } from '../../job_creator_context'; -import { mlJobService } from '../../../../../../services/job_service'; -import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; +import { DatafeedPreview } from './datafeed_preview'; -const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { HIDDEN, READONLY, @@ -36,50 +29,11 @@ interface Props { export const DatafeedPreviewFlyout: FC = ({ isDisabled }) => { const { jobCreator } = useContext(JobCreatorContext); const [showFlyout, setShowFlyout] = useState(false); - const [previewJsonString, setPreviewJsonString] = useState(''); - const [loading, setLoading] = useState(false); function toggleFlyout() { setShowFlyout(!showFlyout); } - useEffect(() => { - if (showFlyout === true) { - loadDataPreview(); - } - }, [showFlyout]); - - async function loadDataPreview() { - setLoading(true); - setPreviewJsonString(''); - const combinedJob: CombinedJob = { - ...jobCreator.jobConfig, - datafeed_config: jobCreator.datafeedConfig, - }; - - if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { - try { - const resp = await mlJobService.searchPreview(combinedJob); - const data = resp.aggregations - ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) - : resp.hits.hits; - - setPreviewJsonString(JSON.stringify(data, null, 2)); - } catch (error) { - setPreviewJsonString(JSON.stringify(error, null, 2)); - } - setLoading(false); - } else { - const errorText = i18n.translate( - 'xpack.ml.newJob.wizard.datafeedPreviewFlyout.datafeedDoesNotExistLabel', - { - defaultMessage: 'Datafeed does not exist', - } - ); - setPreviewJsonString(errorText); - } - } - return ( @@ -87,12 +41,11 @@ export const DatafeedPreviewFlyout: FC = ({ isDisabled }) => { {showFlyout === true && isDisabled === false && ( setShowFlyout(false)} hideCloseButton size="m"> - @@ -127,28 +80,3 @@ const FlyoutButton: FC<{ isDisabled: boolean; onClick(): void }> = ({ isDisabled ); }; - -const Contents: FC<{ - title: string; - value: string; - loading: boolean; -}> = ({ title, value, loading }) => { - return ( - - -
{title}
-
- - {loading === true ? ( - - - - - - - ) : ( - - )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts index d52ed1364452a..e96f374213eb3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts @@ -4,3 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export { DatafeedPreviewFlyout } from './datafeed_preview_flyout'; +export { DatafeedPreview } from './datafeed_preview'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index dd5c8aa3e280a..29d55e6ae48e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useContext, useEffect } from 'react'; +import React, { Fragment, FC, useState, useContext, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -17,19 +17,21 @@ import { EuiTitle, EuiFlyoutBody, EuiSpacer, + EuiCallOut, } from '@elastic/eui'; import { collapseLiteralStrings } from '../../../../../../../../shared_imports'; -import { Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; +import { CombinedJob, Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; import { ML_EDITOR_MODE, MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { isValidJson } from '../../../../../../../../common/util/validation_utils'; import { JobCreatorContext } from '../../job_creator_context'; +import { DatafeedPreview } from '../datafeed_preview_flyout'; -const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { HIDDEN, READONLY, EDITABLE, } +const WARNING_CALLOUT_OFFSET = 100; interface Props { isDisabled: boolean; jobEditorMode: EDITOR_MODE; @@ -38,21 +40,38 @@ interface Props { export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafeedEditorMode }) => { const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const [showJsonFlyout, setShowJsonFlyout] = useState(false); + const [showChangedIndicesWarning, setShowChangedIndicesWarning] = useState(false); const [jobConfigString, setJobConfigString] = useState(jobCreator.formattedJobJson); const [datafeedConfigString, setDatafeedConfigString] = useState( jobCreator.formattedDatafeedJson ); const [saveable, setSaveable] = useState(false); + const [tempCombinedJob, setTempCombinedJob] = useState(null); useEffect(() => { setJobConfigString(jobCreator.formattedJobJson); setDatafeedConfigString(jobCreator.formattedDatafeedJson); }, [jobCreatorUpdated]); + useEffect(() => { + if (showJsonFlyout === true) { + // when the flyout opens, update the JSON + setJobConfigString(jobCreator.formattedJobJson); + setDatafeedConfigString(jobCreator.formattedDatafeedJson); + setTempCombinedJob({ + ...JSON.parse(jobCreator.formattedJobJson), + datafeed_config: JSON.parse(jobCreator.formattedDatafeedJson), + }); + + setShowChangedIndicesWarning(false); + } else { + setTempCombinedJob(null); + } + }, [showJsonFlyout]); + const editJsonMode = - jobEditorMode === EDITOR_MODE.HIDDEN || datafeedEditorMode === EDITOR_MODE.HIDDEN; - const flyOutSize = editJsonMode ? 'm' : 'l'; + jobEditorMode === EDITOR_MODE.EDITABLE || datafeedEditorMode === EDITOR_MODE.EDITABLE; const readOnlyMode = jobEditorMode === EDITOR_MODE.READONLY && datafeedEditorMode === EDITOR_MODE.READONLY; @@ -64,6 +83,14 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee function onJobChange(json: string) { setJobConfigString(json); const valid = isValidJson(json); + setTempCombinedJob( + valid + ? { + ...JSON.parse(json), + datafeed_config: JSON.parse(datafeedConfigString), + } + : null + ); setSaveable(valid); } @@ -73,12 +100,22 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee let valid = isValidJson(jsonValue); if (valid) { // ensure that the user hasn't altered the indices list in the json. - const { indices }: Datafeed = JSON.parse(jsonValue); + const datafeed: Datafeed = JSON.parse(jsonValue); const originalIndices = jobCreator.indices.sort(); valid = - originalIndices.length === indices.length && - originalIndices.every((value, index) => value === indices[index]); + originalIndices.length === datafeed.indices.length && + originalIndices.every((value, index) => value === datafeed.indices[index]); + setShowChangedIndicesWarning(valid === false); + + setTempCombinedJob({ + ...JSON.parse(jobConfigString), + datafeed_config: datafeed, + }); + } else { + setShowChangedIndicesWarning(false); + setTempCombinedJob(null); } + setSaveable(valid); } @@ -99,7 +136,7 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee /> {showJsonFlyout === true && isDisabled === false && ( - setShowJsonFlyout(false)} hideCloseButton size={flyOutSize}> + setShowJsonFlyout(false)} hideCloseButton size={'l'}> {jobEditorMode !== EDITOR_MODE.HIDDEN && ( @@ -110,19 +147,51 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee defaultMessage: 'Job configuration JSON', })} value={jobConfigString} + heightOffset={showChangedIndicesWarning ? WARNING_CALLOUT_OFFSET : 0} /> )} {datafeedEditorMode !== EDITOR_MODE.HIDDEN && ( - + <> + + {datafeedEditorMode === EDITOR_MODE.EDITABLE && ( + + + + )} + )} + {showChangedIndicesWarning && ( + <> + + + + + + )} @@ -183,7 +252,12 @@ const Contents: FC<{ value: string; editJson: boolean; onChange(s: string): void; -}> = ({ title, value, editJson, onChange }) => { + heightOffset?: number; +}> = ({ title, value, editJson, onChange, heightOffset = 0 }) => { + // the ace editor requires a fixed height + const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ + heightOffset, + ]); return ( @@ -192,7 +266,7 @@ const Contents: FC<{ = ({ setCurrentStep, isCurrentStep }) => { const { jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); @@ -48,18 +47,11 @@ export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) = setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} nextActive={nextActive}> - - - - - - - - +
)} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx index b0fb2e7267f7f..bff99ad7c5281 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx @@ -14,6 +14,8 @@ import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { AdvancedSection } from './components/advanced_section'; import { AdditionalSection } from './components/additional_section'; +import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; +import { isAdvancedJobCreator } from '../../../common/job_creator'; interface Props extends StepProps { advancedExpanded: boolean; @@ -30,7 +32,7 @@ export const JobDetailsStep: FC = ({ additionalExpanded, setAdditionalExpanded, }) => { - const { jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); + const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); useEffect(() => { @@ -70,7 +72,15 @@ export const JobDetailsStep: FC = ({ previous={() => setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} next={() => setCurrentStep(WIZARD_STEPS.VALIDATION)} nextActive={nextActive} - /> + > + {isAdvancedJobCreator(jobCreator) && ( + + )} + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx index 6f03b9a3c3321..2316383709164 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx @@ -6,23 +6,26 @@ import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { JobCreatorContext } from '../job_creator_context'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; -import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { SingleMetricView } from './components/single_metric_view'; import { MultiMetricView } from './components/multi_metric_view'; import { PopulationView } from './components/population_view'; import { AdvancedView } from './components/advanced_view'; import { CategorizationView } from './components/categorization_view'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; +import { + isSingleMetricJobCreator, + isMultiMetricJobCreator, + isPopulationJobCreator, + isCategorizationJobCreator, + isAdvancedJobCreator, +} from '../../../common/job_creator'; export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); - const jobType = jobCreator.type; useEffect(() => { setNextActive(jobValidator.isPickFieldsStepValid); @@ -32,25 +35,25 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) {isCurrentStep && ( - {jobType === JOB_TYPE.SINGLE_METRIC && ( + {isSingleMetricJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.MULTI_METRIC && ( + {isMultiMetricJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.POPULATION && ( + {isPopulationJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.ADVANCED && ( + {isAdvancedJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.CATEGORIZATION && ( + {isCategorizationJobCreator(jobCreator) && ( )} setCurrentStep( - jobCreator.type === JOB_TYPE.ADVANCED + isAdvancedJobCreator(jobCreator) ? WIZARD_STEPS.ADVANCED_CONFIGURE_DATAFEED : WIZARD_STEPS.TIME_RANGE ) @@ -58,19 +61,12 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) next={() => setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} nextActive={nextActive} > - {jobType === JOB_TYPE.ADVANCED && ( - - - - - - - - + {isAdvancedJobCreator(jobCreator) && ( + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 5ef59951c43cc..24d7fb9fc2a40 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -22,8 +22,6 @@ import { JobCreatorContext } from '../job_creator_context'; import { JobRunner } from '../../../common/job_runner'; import { mlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; -import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { getErrorMessage } from '../../../../../../../common/util/errors'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; @@ -54,13 +52,14 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const [jobRunner, setJobRunner] = useState(null); const isAdvanced = isAdvancedJobCreator(jobCreator); + const jsonEditorMode = isAdvanced ? EDITOR_MODE.EDITABLE : EDITOR_MODE.READONLY; useEffect(() => { jobCreator.subscribeToProgress(setProgress); }, []); async function start() { - if (jobCreator.type === JOB_TYPE.ADVANCED) { + if (isAdvanced) { await startAdvanced(); } else { await startInline(); @@ -176,15 +175,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => 0} - jobEditorMode={EDITOR_MODE.READONLY} - datafeedEditorMode={EDITOR_MODE.READONLY} + jobEditorMode={jsonEditorMode} + datafeedEditorMode={jsonEditorMode} /> - {jobCreator.type === JOB_TYPE.ADVANCED ? ( - - - - ) : ( + {isAdvanced === false && ( Date: Mon, 3 Aug 2020 12:15:07 +0100 Subject: [PATCH 044/115] [ML] Add datafeed query reset button (#73958) * [ML] Add datafeed query reset button * changing id * adding translation * fix typo * default query refactor Co-authored-by: Elastic Machine --- .../components/reset_query/index.tsx | 7 ++ .../components/reset_query/reset_query.tsx | 75 +++++++++++++++++++ .../components/datafeed_step/datafeed.tsx | 2 + .../jobs/new_job/utils/new_job_utils.ts | 26 ++++--- 4 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/index.tsx create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/index.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/index.tsx new file mode 100644 index 0000000000000..151f600eafdbe --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ResetQueryButton } from './reset_query'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx new file mode 100644 index 0000000000000..17558368f117d --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiCodeBlock, + EuiSpacer, +} from '@elastic/eui'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { getDefaultDatafeedQuery } from '../../../../../utils/new_job_utils'; + +export const ResetQueryButton: FC = () => { + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); + const [confirmModalVisible, setConfirmModalVisible] = useState(false); + const [defaultQueryString] = useState(JSON.stringify(getDefaultDatafeedQuery(), null, 2)); + + const closeModal = () => setConfirmModalVisible(false); + const showModal = () => setConfirmModalVisible(true); + + function resetDatafeed() { + jobCreator.query = getDefaultDatafeedQuery(); + jobCreatorUpdate(); + closeModal(); + } + return ( + <> + {confirmModalVisible && ( + + + + + + + + {defaultQueryString} + + + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx index 4223be2a2e3c4..b9250c3ecdce5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx @@ -11,6 +11,7 @@ import { QueryInput } from './components/query'; import { QueryDelayInput } from './components/query_delay'; import { FrequencyInput } from './components/frequency'; import { ScrollSizeInput } from './components/scroll_size'; +import { ResetQueryButton } from './components/reset_query'; import { TimeField } from './components/time_field'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -46,6 +47,7 @@ export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) = + setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} nextActive={nextActive}> Date: Mon, 3 Aug 2020 14:49:17 +0200 Subject: [PATCH 045/115] [APM] Disable auto-refresh by default (#74068) --- .../DatePicker/__test__/DatePicker.test.tsx | 23 ++++++++----------- .../components/shared/DatePicker/index.tsx | 16 +------------ 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 36e33fba89fbb..2434d898389d8 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -64,21 +64,20 @@ describe('DatePicker', () => { }); beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); - it('should set default query params in the URL', () => { + it('sets default query params in the URL', () => { mountDatePicker(); expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledWith( expect.objectContaining({ - search: - 'rangeFrom=now-15m&rangeTo=now&refreshPaused=false&refreshInterval=10000', + search: 'rangeFrom=now-15m&rangeTo=now', }) ); }); - it('should add missing default value', () => { + it('adds missing default value', () => { mountDatePicker({ rangeTo: 'now', refreshInterval: 5000, @@ -86,13 +85,12 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledWith( expect.objectContaining({ - search: - 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000&refreshPaused=false', + search: 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000', }) ); }); - it('should not set default query params in the URL when values already defined', () => { + it('does not set default query params in the URL when values already defined', () => { mountDatePicker({ rangeFrom: 'now-1d', rangeTo: 'now', @@ -102,7 +100,7 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(0); }); - it('should update the URL when the date range changes', () => { + it('updates the URL when the date range changes', () => { const datePicker = mountDatePicker(); datePicker.find(EuiSuperDatePicker).props().onTimeChange({ start: 'updated-start', @@ -113,13 +111,12 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(2); expect(mockHistoryPush).toHaveBeenLastCalledWith( expect.objectContaining({ - search: - 'rangeFrom=updated-start&rangeTo=updated-end&refreshInterval=5000&refreshPaused=false', + search: 'rangeFrom=updated-start&rangeTo=updated-end', }) ); }); - it('should auto-refresh when refreshPaused is false', async () => { + it('enables auto-refresh when refreshPaused is false', async () => { jest.useFakeTimers(); const wrapper = mountDatePicker({ refreshPaused: false, @@ -132,7 +129,7 @@ describe('DatePicker', () => { wrapper.unmount(); }); - it('should NOT auto-refresh when refreshPaused is true', async () => { + it('disables auto-refresh when refreshPaused is true', async () => { jest.useFakeTimers(); mountDatePicker({ refreshPaused: true, refreshInterval: 1000 }); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index 5201d80de5a12..403a8cad854cd 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -14,11 +14,7 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { clearCache } from '../../../services/rest/callApi'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; -import { - TimePickerQuickRange, - TimePickerTimeDefaults, - TimePickerRefreshInterval, -} from './typings'; +import { TimePickerQuickRange, TimePickerTimeDefaults } from './typings'; function removeUndefinedAndEmptyProps(obj: T): Partial { return pickBy(obj, (value) => value !== undefined && !isEmpty(String(value))); @@ -36,19 +32,9 @@ export function DatePicker() { UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS ); - const timePickerRefreshIntervalDefaults = core.uiSettings.get< - TimePickerRefreshInterval - >(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); - const DEFAULT_VALUES = { rangeFrom: timePickerTimeDefaults.from, rangeTo: timePickerTimeDefaults.to, - refreshPaused: timePickerRefreshIntervalDefaults.pause, - /* - * Must be replaced by timePickerRefreshIntervalDefaults.value when this issue is fixed. - * https://github.com/elastic/kibana/issues/70562 - */ - refreshInterval: 10000, }; const commonlyUsedRanges = timePickerQuickRanges.map( From aada48e0deab04912956c842521635062eecf1fa Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 3 Aug 2020 15:39:26 +0100 Subject: [PATCH 046/115] [Security solution] Get all timeline (#72690) * get all timelines * update unit tests * fix cypress * rollback cypress rummer * add unit test Co-authored-by: Elastic Machine --- .../public/graphql/introspection.json | 36 ++- .../security_solution/public/graphql/types.ts | 11 +- .../server/graphql/timeline/resolvers.ts | 2 +- .../server/graphql/timeline/schema.gql.ts | 6 +- .../security_solution/server/graphql/types.ts | 13 +- .../timelines/find_timeline_by_filter.sh | 4 +- .../scripts/timelines/get_all_timelines.sh | 25 +- .../scripts/timelines/get_timeline_by_id.sh | 2 +- .../get_timeline_by_template_timeline_id.sh | 2 +- .../routes/__mocks__/import_timelines.ts | 18 ++ .../routes/__mocks__/request_responses.ts | 4 +- ...ute.test.ts => get_timeline_route.test.ts} | 28 ++- ...e_by_id_route.ts => get_timeline_route.ts} | 31 ++- .../schemas/get_timeline_by_id_schema.ts | 11 +- .../routes/utils/check_timelines_status.ts | 2 +- .../timeline/routes/utils/export_timelines.ts | 4 +- .../timeline/routes/utils/get_timelines.ts | 34 --- .../timeline/routes/utils/import_timelines.ts | 28 ++- .../server/lib/timeline/saved_object.test.ts | 230 +++++++++++++++++- .../server/lib/timeline/saved_object.ts | 40 +-- .../security_solution/server/routes/index.ts | 4 +- 21 files changed, 383 insertions(+), 152 deletions(-) rename x-pack/plugins/security_solution/server/lib/timeline/routes/{get_timeline_by_id_route.test.ts => get_timeline_route.test.ts} (66%) rename x-pack/plugins/security_solution/server/lib/timeline/routes/{get_timeline_by_id_route.ts => get_timeline_route.ts} (67%) delete mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 84096e242cbbd..7b20873bf63cc 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -229,7 +229,11 @@ { "name": "pageInfo", "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", "ofType": null } + }, "defaultValue": null }, { @@ -10905,13 +10909,21 @@ { "name": "pageIndex", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "defaultValue": null }, { "name": "pageSize", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "defaultValue": null } ], @@ -13142,24 +13154,6 @@ "interfaces": null, "enumValues": null, "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "TemplateTimelineType", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "elastic", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null } ], "directives": [ diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 90d1b8bd54df5..f7d2c81f536be 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -102,9 +102,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex?: Maybe; + pageIndex: number; - pageSize?: Maybe; + pageSize: number; } export interface SortTimeline { @@ -423,11 +423,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2324,7 +2319,7 @@ export interface GetOneTimelineQueryArgs { id: string; } export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts index f4a18a40f7d4b..9bd544f6942cf 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts @@ -50,7 +50,7 @@ export const createTimelineResolvers = ( return libs.timeline.getAllTimeline( req, args.onlyUserFavorite || null, - args.pageInfo || null, + args.pageInfo, args.search || null, args.sort || null, args.status || null, diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 58a13a7115b72..573539e1bb54f 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -178,8 +178,8 @@ export const timelineSchema = gql` } input PageInfoTimeline { - pageIndex: Float - pageSize: Float + pageIndex: Float! + pageSize: Float! } enum SortFieldTimeline { @@ -316,7 +316,7 @@ export const timelineSchema = gql` extend type Query { getOneTimeline(id: ID!): TimelineResult! - getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! + getAllTimeline(pageInfo: PageInfoTimeline!, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! } extend type Mutation { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index ca0732816aa4d..fa55af351651e 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -104,9 +104,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex?: Maybe; + pageIndex: number; - pageSize?: Maybe; + pageSize: number; } export interface SortTimeline { @@ -425,11 +425,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2326,7 +2321,7 @@ export interface GetOneTimelineQueryArgs { id: string; } export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; @@ -2802,7 +2797,7 @@ export namespace QueryResolvers { TContext = SiemContext > = Resolver; export interface GetAllTimelineArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh index 3dd8e7f1097f4..f3b8a81f4086a 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh @@ -14,13 +14,13 @@ STATUS=${1:-active} TIMELINE_TYPE=${2:-default} # Example get all timelines: -# ./timelines/find_timeline_by_filter.sh active +# sh ./timelines/find_timeline_by_filter.sh active # Example get all prepackaged timeline templates: # ./timelines/find_timeline_by_filter.sh immutable template # Example get all custom timeline templates: -# ./timelines/find_timeline_by_filter.sh active template +# sh ./timelines/find_timeline_by_filter.sh active template curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh index 335d1b8c86696..05a9e0bd1ac97 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh @@ -9,28 +9,11 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_all_timelines.sh +# Example: sh ./timelines/get_all_timelines.sh + curl -s -k \ -H "Content-Type: application/json" \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST "${KIBANA_URL}${SPACE_URL}/api/solutions/security/graphql" \ - -d '{ - "operationName": "GetAllTimeline", - "variables": { - "onlyUserFavorite": false, - "pageInfo": { - "pageIndex": null, - "pageSize": null - }, - "search": "", - "sort": { - "sortField": "updated", - "sortOrder": "desc" - }, - "status": "active", - "timelineType": null - }, - "query": "query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n" -}' | jq . - + -X GET "${KIBANA_URL}${SPACE_URL}/api/timeline" \ + | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh index 0c0694c0591f9..13184ac6c6d56 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_timeline_by_id.sh {timeline_id} +# Example: sh ./timelines/get_timeline_by_id.sh {timeline_id} curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh index 36862b519130b..87eddfbe6b9d4 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} +# Example: sh ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 0b10018de5bba..245146dda183f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -1198,3 +1198,21 @@ export const mockCheckTimelinesStatusAfterInstallResult = { }, ], }; + +export const mockSavedObject = { + type: 'siem-ui-timeline', + id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + attributes: { + savedQueryId: null, + + status: 'immutable', + + excludedRowRendererIds: [], + ...mockGetTemplateTimelineValue, + }, + references: [], + updated_at: '2020-07-21T12:03:08.901Z', + version: 'WzAsMV0=', + namespaces: ['default'], + score: 0.9444616, +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index e3aeff280678f..c5d69398b7f0c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -175,11 +175,11 @@ export const cleanDraftTimelinesRequest = (timelineType: TimelineType) => }, }); -export const getTimelineByIdRequest = (query: GetTimelineByIdSchemaQuery) => +export const getTimelineRequest = (query?: GetTimelineByIdSchemaQuery) => requestMock.create({ method: 'get', path: TIMELINE_URL, - query, + query: query ?? {}, }); export const installPrepackedTimelinesRequest = () => diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts similarity index 66% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts index 30528f8563ab8..6f99739ae2e2b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts @@ -10,19 +10,24 @@ import { requestContextMock, createMockConfig, } from '../../detection_engine/routes/__mocks__'; +import { getAllTimeline } from '../saved_object'; import { mockGetCurrentUser } from './__mocks__/import_timelines'; -import { getTimelineByIdRequest } from './__mocks__/request_responses'; +import { getTimelineRequest } from './__mocks__/request_responses'; import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; -import { getTimelineByIdRoute } from './get_timeline_by_id_route'; +import { getTimelineRoute } from './get_timeline_route'; jest.mock('./utils/create_timelines', () => ({ getTimeline: jest.fn(), getTemplateTimeline: jest.fn(), })); -describe('get timeline by id', () => { +jest.mock('../saved_object', () => ({ + getAllTimeline: jest.fn(), +})); + +describe('get timeline', () => { let server: ReturnType; let securitySetup: SecurityPluginSetup; let { context } = requestContextMock.createTools(); @@ -41,15 +46,12 @@ describe('get timeline by id', () => { authz: {}, } as unknown) as SecurityPluginSetup; - getTimelineByIdRoute(server.router, createMockConfig(), securitySetup); + getTimelineRoute(server.router, createMockConfig(), securitySetup); }); test('should call getTemplateTimeline if templateTimelineId is given', async () => { const templateTimelineId = '123'; - await server.inject( - getTimelineByIdRequest({ template_timeline_id: templateTimelineId }), - context - ); + await server.inject(getTimelineRequest({ template_timeline_id: templateTimelineId }), context); expect((getTemplateTimeline as jest.Mock).mock.calls[0][1]).toEqual(templateTimelineId); }); @@ -57,8 +59,16 @@ describe('get timeline by id', () => { test('should call getTimeline if id is given', async () => { const id = '456'; - await server.inject(getTimelineByIdRequest({ id }), context); + await server.inject(getTimelineRequest({ id }), context); expect((getTimeline as jest.Mock).mock.calls[0][1]).toEqual(id); }); + + test('should call getAllTimeline if nither templateTimelineId nor id is given', async () => { + (getAllTimeline as jest.Mock).mockResolvedValue({ totalCount: 3 }); + + await server.inject(getTimelineRequest(), context); + + expect(getAllTimeline as jest.Mock).toHaveBeenCalledTimes(2); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts similarity index 67% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts index c4957b9d4b9e2..f36adb648cc03 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts @@ -17,8 +17,10 @@ import { buildSiemResponse, transformError } from '../../detection_engine/routes import { buildFrameworkRequest } from './utils/common'; import { getTimelineByIdSchemaQuery } from './schemas/get_timeline_by_id_schema'; import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { getAllTimeline } from '../saved_object'; +import { TimelineStatus } from '../../../../common/types/timeline'; -export const getTimelineByIdRoute = ( +export const getTimelineRoute = ( router: IRouter, config: ConfigType, security: SetupPlugins['security'] @@ -34,12 +36,33 @@ export const getTimelineByIdRoute = ( async (context, request, response) => { try { const frameworkRequest = await buildFrameworkRequest(context, security, request); - const { template_timeline_id: templateTimelineId, id } = request.query; + const query = request.query ?? {}; + const { template_timeline_id: templateTimelineId, id } = query; let res = null; - if (templateTimelineId != null) { + if (templateTimelineId != null && id == null) { res = await getTemplateTimeline(frameworkRequest, templateTimelineId); - } else if (id != null) { + } else if (templateTimelineId == null && id != null) { res = await getTimeline(frameworkRequest, id); + } else if (templateTimelineId == null && id == null) { + const tempResult = await getAllTimeline( + frameworkRequest, + false, + { pageSize: 1, pageIndex: 1 }, + null, + null, + TimelineStatus.active, + null + ); + + res = await getAllTimeline( + frameworkRequest, + false, + { pageSize: tempResult?.totalCount ?? 0, pageIndex: 1 }, + null, + null, + TimelineStatus.active, + null + ); } return response.ok({ body: res ?? {} }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts index 2c6098bc75500..65c956ed60440 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import * as rt from 'io-ts'; +import { unionWithNullType } from '../../../../../common/utility_types'; -export const getTimelineByIdSchemaQuery = rt.partial({ - template_timeline_id: rt.string, - id: rt.string, -}); +export const getTimelineByIdSchemaQuery = unionWithNullType( + rt.partial({ + template_timeline_id: rt.string, + id: rt.string, + }) +); export type GetTimelineByIdSchemaQuery = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts index 2ce2c37d4fa31..b5aa24336b2d7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts @@ -35,7 +35,7 @@ export const checkTimelinesStatus = async ( try { readStream = await getReadables(dataPath); - timeline = await getExistingPrepackagedTimelines(frameworkRequest, false); + timeline = await getExistingPrepackagedTimelines(frameworkRequest); } catch (err) { return { timelinesToInstall: [], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts index 6f194c3b8538e..79ebf6280a19e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts @@ -20,7 +20,7 @@ import { FrameworkRequest } from '../../../framework'; import * as noteLib from '../../../note/saved_object'; import * as pinnedEventLib from '../../../pinned_event/saved_object'; -import { getTimelines } from '../../saved_object'; +import { getSelectedTimelines } from '../../saved_object'; const getGlobalEventNotesByTimelineId = (currentNotes: NoteSavedObject[]): ExportedNotes => { const initialNotes: ExportedNotes = { @@ -55,7 +55,7 @@ const getTimelinesFromObjects = async ( request: FrameworkRequest, ids?: string[] | null ): Promise> => { - const { timelines, errors } = await getTimelines(request, ids); + const { timelines, errors } = await getSelectedTimelines(request, ids); const exportedIds = timelines.map((t) => t.savedObjectId); const [notes, pinnedEvents] = await Promise.all([ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts deleted file mode 100644 index 1dac773ad6fde..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkRequest } from '../../../framework'; -import { getTimelines as getSelectedTimelines } from '../../saved_object'; -import { TimelineSavedObject } from '../../../../../common/types/timeline'; - -export const getTimelines = async ( - frameworkRequest: FrameworkRequest, - ids: string[] -): Promise<{ timeline: TimelineSavedObject[] | null; error: string | null }> => { - try { - const timelines = await getSelectedTimelines(frameworkRequest, ids); - const existingTimelineIds = timelines.timelines.map((timeline) => timeline.savedObjectId); - const errorMsg = timelines.errors.reduce( - (acc, curr) => (acc ? `${acc}, ${curr.message}` : curr.message), - '' - ); - if (existingTimelineIds.length > 0) { - const message = existingTimelineIds.join(', '); - return { - timeline: timelines.timelines, - error: errorMsg ? `${message} found, ${errorMsg}` : null, - }; - } else { - return { timeline: null, error: errorMsg }; - } - } catch (e) { - return e.message; - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts index 996dc5823691d..1fea11f01bcc5 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts @@ -77,6 +77,24 @@ export const timelineSavedObjectOmittedFields = [ 'version', ]; +export const setTimeline = ( + parsedTimelineObject: Partial, + parsedTimeline: ImportedTimeline, + isTemplateTimeline: boolean +) => { + return { + ...parsedTimelineObject, + status: + parsedTimeline.status === TimelineStatus.draft + ? TimelineStatus.active + : parsedTimeline.status ?? TimelineStatus.active, + templateTimelineVersion: isTemplateTimeline + ? parsedTimeline.templateTimelineVersion ?? 1 + : null, + templateTimelineId: isTemplateTimeline ? parsedTimeline.templateTimelineId ?? uuid.v4() : null, + }; +}; + const CHUNK_PARSED_OBJECT_SIZE = 10; const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; @@ -151,15 +169,7 @@ export const importTimelines = async ( // create timeline / timeline template newTimeline = await createTimelines({ frameworkRequest, - timeline: { - ...parsedTimelineObject, - status: - status === TimelineStatus.draft - ? TimelineStatus.active - : status ?? TimelineStatus.active, - templateTimelineVersion: isTemplateTimeline ? templateTimelineVersion : null, - templateTimelineId: isTemplateTimeline ? templateTimelineId ?? uuid.v4() : null, - }, + timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline), pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], isImmutable, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts index 3c4343b642891..0ef83bb84c4c3 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts @@ -3,8 +3,30 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { FrameworkRequest } from '../framework'; +import { mockGetTimelineValue, mockSavedObject } from './routes/__mocks__/import_timelines'; -import { convertStringToBase64 } from './saved_object'; +import { + convertStringToBase64, + getExistingPrepackagedTimelines, + getAllTimeline, + AllTimelinesResponse, +} from './saved_object'; +import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; +import { getNotesByTimelineId } from '../note/saved_object'; +import { getAllPinnedEventsByTimelineId } from '../pinned_event/saved_object'; + +jest.mock('./convert_saved_object_to_savedtimeline', () => ({ + convertSavedObjectToSavedTimeline: jest.fn(), +})); + +jest.mock('../note/saved_object', () => ({ + getNotesByTimelineId: jest.fn().mockResolvedValue([]), +})); + +jest.mock('../pinned_event/saved_object', () => ({ + getAllPinnedEventsByTimelineId: jest.fn().mockResolvedValue([]), +})); describe('saved_object', () => { describe('convertStringToBase64', () => { @@ -22,4 +44,210 @@ describe('saved_object', () => { expect(convertStringToBase64('')).toBe(''); }); }); + + describe('getExistingPrepackagedTimelines', () => { + let mockFindSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + + beforeEach(() => { + mockFindSavedObject = jest.fn().mockResolvedValue({ saved_objects: [], total: 0 }); + mockRequest = ({ + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + find: mockFindSavedObject, + }, + }, + }, + }, + } as unknown) as FrameworkRequest; + }); + + afterEach(() => { + mockFindSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should send correct options if countsOnly is true', async () => { + const contsOnly = true; + await getExistingPrepackagedTimelines(mockRequest, contsOnly); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options if countsOnly is false', async () => { + const contsOnly = false; + await getExistingPrepackagedTimelines(mockRequest, contsOnly); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options if pageInfo is given', async () => { + const contsOnly = false; + const pageInfo = { + pageSize: 10, + pageIndex: 1, + }; + await getExistingPrepackagedTimelines(mockRequest, contsOnly, pageInfo); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 10, + type: 'siem-ui-timeline', + }); + }); + }); + + describe('getAllTimeline', () => { + let mockFindSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + const pageInfo = { + pageSize: 10, + pageIndex: 1, + }; + let result = (null as unknown) as AllTimelinesResponse; + beforeEach(async () => { + (convertSavedObjectToSavedTimeline as jest.Mock).mockReturnValue(mockGetTimelineValue); + mockFindSavedObject = jest + .fn() + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValueOnce({ saved_objects: [], total: 0 }) + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValue({ saved_objects: [], total: 0 }); + mockRequest = ({ + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + find: mockFindSavedObject, + }, + }, + }, + }, + } as unknown) as FrameworkRequest; + + result = await getAllTimeline(mockRequest, false, pageInfo, null, null, null, null); + }); + + afterEach(() => { + mockFindSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should send correct options if no filters applys', async () => { + expect(mockFindSavedObject.mock.calls[0][0]).toEqual({ + filter: 'not siem-ui-timeline.attributes.status: draft', + page: pageInfo.pageIndex, + perPage: pageInfo.pageSize, + type: 'siem-ui-timeline', + sortField: undefined, + sortOrder: undefined, + search: undefined, + searchFields: ['title', 'description'], + }); + }); + + test('should send correct options for counts of default timelines', async () => { + expect(mockFindSavedObject.mock.calls[1][0]).toEqual({ + filter: + 'not siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of timeline templates', async () => { + expect(mockFindSavedObject.mock.calls[2][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of Elastic prebuilt templates', async () => { + expect(mockFindSavedObject.mock.calls[3][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of custom templates', async () => { + expect(mockFindSavedObject.mock.calls[4][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of favorite timeline', async () => { + expect(mockFindSavedObject.mock.calls[5][0]).toEqual({ + filter: + 'not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + search: ' dXNlcm5hbWU=', + searchFields: ['title', 'description', 'favorite.keySearch'], + type: 'siem-ui-timeline', + }); + }); + + test('should call getNotesByTimelineId', async () => { + expect((getNotesByTimelineId as jest.Mock).mock.calls[0][1]).toEqual(mockSavedObject.id); + }); + + test('should call getAllPinnedEventsByTimelineId', async () => { + expect((getAllPinnedEventsByTimelineId as jest.Mock).mock.calls[0][1]).toEqual( + mockSavedObject.id + ); + }); + + test('should retuen correct result', async () => { + expect(result).toEqual({ + totalCount: 1, + customTemplateTimelineCount: 0, + defaultTimelineCount: 0, + elasticTemplateTimelineCount: 1, + favoriteCount: 0, + templateTimelineCount: 1, + timeline: [ + { + ...mockGetTimelineValue, + noteIds: [], + pinnedEventIds: [], + eventIdToNoteIds: [], + favorite: [], + notes: [], + pinnedEventsSaveObject: [], + }, + ], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts index 6bc0ca64ae33f..23ea3e6213469 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts @@ -41,7 +41,7 @@ interface ResponseTimelines { totalCount: number; } -interface AllTimelinesResponse extends ResponseTimelines { +export interface AllTimelinesResponse extends ResponseTimelines { defaultTimelineCount: number; templateTimelineCount: number; elasticTemplateTimelineCount: number; @@ -63,7 +63,7 @@ export interface Timeline { getAllTimeline: ( request: FrameworkRequest, onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, + pageInfo: PageInfoTimeline, search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, @@ -152,17 +152,18 @@ const getTimelineTypeFilter = ( export const getExistingPrepackagedTimelines = async ( request: FrameworkRequest, countsOnly?: boolean, - pageInfo?: PageInfoTimeline | null + pageInfo?: PageInfoTimeline ): Promise<{ totalCount: number; timeline: TimelineSavedObject[]; }> => { - const queryPageInfo = countsOnly - ? { - perPage: 1, - page: 1, - } - : pageInfo ?? {}; + const queryPageInfo = + countsOnly && pageInfo == null + ? { + perPage: 1, + page: 1, + } + : { perPage: pageInfo?.pageSize, page: pageInfo?.pageIndex } ?? {}; const elasticTemplateTimelineOptions = { type: timelineSavedObjectType, ...queryPageInfo, @@ -175,7 +176,7 @@ export const getExistingPrepackagedTimelines = async ( export const getAllTimeline = async ( request: FrameworkRequest, onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, + pageInfo: PageInfoTimeline, search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, @@ -183,13 +184,13 @@ export const getAllTimeline = async ( ): Promise => { const options: SavedObjectsFindOptions = { type: timelineSavedObjectType, - perPage: pageInfo?.pageSize ?? undefined, - page: pageInfo?.pageIndex ?? undefined, + perPage: pageInfo.pageSize, + page: pageInfo.pageIndex, search: search != null ? search : undefined, searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], - filter: getTimelineTypeFilter(timelineType, status), + filter: getTimelineTypeFilter(timelineType ?? null, status ?? null), sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; @@ -220,7 +221,7 @@ export const getAllTimeline = async ( searchFields: ['title', 'description', 'favorite.keySearch'], perPage: 1, page: 1, - filter: getTimelineTypeFilter(timelineType, TimelineStatus.active), + filter: getTimelineTypeFilter(timelineType ?? null, TimelineStatus.active), }; const result = await Promise.all([ @@ -496,7 +497,6 @@ const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObje ]); }) ); - return { totalCount: savedObjects.total, timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => @@ -532,14 +532,20 @@ export const timelineWithReduxProperties = ( pinnedEventsSaveObject: pinnedEvents, }); -export const getTimelines = async (request: FrameworkRequest, timelineIds?: string[] | null) => { +export const getSelectedTimelines = async ( + request: FrameworkRequest, + timelineIds?: string[] | null +) => { const savedObjectsClient = request.context.core.savedObjects.client; let exportedIds = timelineIds; if (timelineIds == null || timelineIds.length === 0) { const { timeline: savedAllTimelines } = await getAllTimeline( request, false, - null, + { + pageIndex: 1, + pageSize: timelineIds?.length ?? 0, + }, null, null, TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 37a97c03ad332..000bd875930f9 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -37,7 +37,7 @@ import { cleanDraftTimelinesRoute } from '../lib/timeline/routes/clean_draft_tim import { SetupPlugins } from '../plugin'; import { ConfigType } from '../config'; import { installPrepackedTimelinesRoute } from '../lib/timeline/routes/install_prepacked_timelines_route'; -import { getTimelineByIdRoute } from '../lib/timeline/routes/get_timeline_by_id_route'; +import { getTimelineRoute } from '../lib/timeline/routes/get_timeline_route'; export const initRoutes = ( router: IRouter, @@ -70,7 +70,7 @@ export const initRoutes = ( importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security); getDraftTimelinesRoute(router, config, security); - getTimelineByIdRoute(router, config, security); + getTimelineRoute(router, config, security); cleanDraftTimelinesRoute(router, config, security); installPrepackedTimelinesRoute(router, config, security); From 8c6d655f17a6985d32943456442b77463c8ca0b2 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 3 Aug 2020 07:44:57 -0700 Subject: [PATCH 047/115] [Search] Set keep_alive parameter in async search (#73712) * [Search] Set keep_alive parameter in async search * Revert accidental change * Add batched_reduce_size Co-authored-by: Elastic Machine --- .../server/search/es_search_strategy.test.ts | 15 +++++++++++++++ .../server/search/es_search_strategy.ts | 11 +++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index faa4f2ee499e5..4fd1e889ba1a5 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -113,4 +113,19 @@ describe('ES search strategy', () => { expect(method).toBe('POST'); expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); }); + + it('sets wait_for_completion_timeout and keep_alive in the request', async () => { + mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + + const params = { index: 'foo-*', body: {} }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('transport.request'); + const { query } = mockApiCaller.mock.calls[0][1]; + expect(query).toHaveProperty('wait_for_completion_timeout'); + expect(query).toHaveProperty('keep_alive'); + }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 358335a2a4d60..1f7b6a5f9aace 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -80,8 +80,15 @@ async function asyncSearch( const method = request.id ? 'GET' : 'POST'; const path = encodeURI(request.id ? `/_async_search/${request.id}` : `/${index}/_async_search`); - // Wait up to 1s for the response to return - const query = toSnakeCase({ waitForCompletionTimeout: '100ms', ...queryParams }); + // Only report partial results every 64 shards; this should be reduced when we actually display partial results + const batchedReduceSize = request.id ? undefined : 64; + + const query = toSnakeCase({ + waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return + keepAlive: '1m', // Extend the TTL for this search request by one minute + ...(batchedReduceSize && { batchedReduceSize }), + ...queryParams, + }); const { id, response, is_partial, is_running } = (await caller( 'transport.request', From 426651ec61c2004380ab9641d78ec69344199e9f Mon Sep 17 00:00:00 2001 From: Jimmy Kuang Date: Mon, 3 Aug 2020 08:05:15 -0700 Subject: [PATCH 048/115] Added this.name in replace of New Watch string - isNew Create else Save (#73982) * Added this.name in replace of New Watch string * Address precommit_hook fix * Run node scripts/precommit_hook --fix * Run node scripts/precommit_hook --fix * Run node scripts/precommit_hook --fix * Update watch.isNew logic, address xpack jest check, and fix precommit hook * Fix elint issues * Used Translation - removed xpack.watcher.models.baseWatch.displayName and xpack.securitySolution.auditd.failedxLoginTooManyTimes * Used Translation - removed xpack.watcher.models.baseWatch.displayName * Updated SaveWatch function - separate create and saved displayName and Eslint fix * Added back xpack.securitySolution.auditd.failedxLoginTooManyTimesDescription * Reverse xpack.securitySolution.auditd.inDescription back to in * Remove x typo in failedx Co-authored-by: Elastic Machine --- .../translations/translations/ja-JP.json | 3 +-- .../translations/translations/zh-CN.json | 1 - .../application/models/watch/base_watch.js | 6 +----- .../sections/watch_edit/watch_edit_actions.ts | 19 +++++++++++++------ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e2f59f3fa910a..f13aa374e549c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19304,8 +19304,7 @@ "xpack.watcher.models.baseAction.simulateMessage": "アクション {id} のシミュレーションが完了しました", "xpack.watcher.models.baseAction.typeName": "アクション", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "不明なアクションタイプ {type} を作成しようとしました。", - "xpack.watcher.models.baseWatch.displayName": "新規ウォッチ", - "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", + "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", "xpack.watcher.models.baseWatch.selectMessageText": "新規ウォッチをセットアップします。", "xpack.watcher.models.baseWatch.typeName": "ウォッチ", "xpack.watcher.models.baseWatch.watchJsonPropertyMissingBadRequestMessage": "json 引数には {watchJson} プロパティが含まれている必要があります", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 316d3247d19d5..83e290b8c241a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19311,7 +19311,6 @@ "xpack.watcher.models.baseAction.simulateMessage": "已成功模拟操作 {id}", "xpack.watcher.models.baseAction.typeName": "操作", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "尝试创建的操作类型 {type} 未知。", - "xpack.watcher.models.baseWatch.displayName": "新建监视", "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 参数必须包含 {id} 属性", "xpack.watcher.models.baseWatch.selectMessageText": "设置新监视。", "xpack.watcher.models.baseWatch.typeName": "监视", diff --git a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js index eaced3e27c8a0..6b7d693bb308e 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js +++ b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js @@ -79,11 +79,7 @@ export class BaseWatch { }; get displayName() { - if (this.isNew) { - return i18n.translate('xpack.watcher.models.baseWatch.displayName', { - defaultMessage: 'New Watch', - }); - } else if (this.name) { + if (this.name) { return this.name; } else { return this.id; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts b/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts index 2d62bca75c1a1..36dfdb55b4ab6 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts @@ -66,12 +66,19 @@ export async function saveWatch(watch: BaseWatch, toasts: ToastsSetup): Promise< try { await createWatch(watch); toasts.addSuccess( - i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { - defaultMessage: "Saved '{watchDisplayName}'", - values: { - watchDisplayName: watch.displayName, - }, - }) + watch.isNew + ? i18n.translate('xpack.watcher.sections.watchEdit.json.createSuccessNotificationText', { + defaultMessage: "Created '{watchDisplayName}'", + values: { + watchDisplayName: watch.displayName, + }, + }) + : i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { + defaultMessage: "Saved '{watchDisplayName}'", + values: { + watchDisplayName: watch.displayName, + }, + }) ); goToWatchList(); } catch (error) { From a25feb6d575aabb4bb1ca0662fc5552c999f33f5 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Mon, 3 Aug 2020 17:07:33 +0200 Subject: [PATCH 049/115] [ML] Functional tests - stabilize waiting for AD, DFA, TFM job list refresh (#74064) With this PR, the job list refresh for anomaly detection, data frame analytics and transforms is waiting for the refresh loading indicator to disappear before moving on. --- .../refresh_analytics_list_button.tsx | 2 +- .../refresh_jobs_list_button.js | 2 +- .../refresh_transform_list_button.tsx | 2 +- .../services/ml/data_frame_analytics_table.ts | 9 ++++++++- x-pack/test/functional/services/ml/job_table.ts | 9 ++++++++- .../services/transform/transform_table.ts | 13 ++++++++++++- 6 files changed, 31 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx index f54cc4621eccf..e988640d2eae5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx @@ -15,7 +15,7 @@ export const RefreshAnalyticsListButton: FC = () => { const { refresh } = useRefreshAnalyticsList({ isLoading: setIsLoading }); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js index ecb8626867887..7c55ed01f432a 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export const RefreshJobsListButton = ({ onRefreshClick, isRefreshing }) => ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx index f8a1f84937326..f886b0b461482 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx @@ -20,7 +20,7 @@ export const RefreshTransformListButton: FC = ({ diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index f452c9cce7a1a..d315f9eb77210 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -62,8 +62,15 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F return rows; } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~mlAnalyticsRefreshListButton', { timeout: 10 * 1000 }); + await testSubjects.existOrFail('mlAnalyticsRefreshListButton loaded', { timeout: 30 * 1000 }); + } + public async refreshAnalyticsTable() { - await testSubjects.click('mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForAnalyticsToLoad(); } diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index a72d9c204060b..58a1afad88e11 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -141,8 +141,15 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte }); } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~mlRefreshJobListButton', { timeout: 10 * 1000 }); + await testSubjects.existOrFail('mlRefreshJobListButton loaded', { timeout: 30 * 1000 }); + } + public async refreshJobList() { - await testSubjects.click('mlRefreshJobListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlRefreshJobListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForJobsToLoad(); } diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 453dca904b605..37d8b6e51072f 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -95,8 +95,19 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~transformRefreshTransformListButton', { + timeout: 10 * 1000, + }); + await testSubjects.existOrFail('transformRefreshTransformListButton loaded', { + timeout: 30 * 1000, + }); + } + public async refreshTransformList() { - await testSubjects.click('transformRefreshTransformListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~transformRefreshTransformListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForTransformsToLoad(); } From 346bab35a36d3f46780fafc1390bb30d4ff5eca1 Mon Sep 17 00:00:00 2001 From: Eric Davis Date: Mon, 3 Aug 2020 11:17:07 -0400 Subject: [PATCH 050/115] Kibana issue #73932 - enrollment flyout changes (#74008) Co-authored-by: Elastic Machine --- .../components/enrollment_instructions/manual/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx index fe11c4cb08d13..a77de9369277b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx @@ -32,7 +32,7 @@ export const ManualInstructions: React.FunctionComponent = ({ const macOsLinuxTarCommand = `./elastic-agent enroll ${enrollArgs} ./elastic-agent run`; - const linuxDebRpmCommand = `./elastic-agent enroll ${enrollArgs} + const linuxDebRpmCommand = `elastic-agent enroll ${enrollArgs} systemctl enable elastic-agent systemctl start elastic-agent`; @@ -44,7 +44,7 @@ systemctl start elastic-agent`; From 234e6f130d40909a74da5a14672349e929b65599 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 3 Aug 2020 09:37:30 -0600 Subject: [PATCH 051/115] change 'Add from Visualize library' button text to 'Add from Kibana' (#74002) Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/i18n/components.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 03d6ade7bea69..71e3386d821f1 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -15,7 +15,7 @@ export const ComponentStrings = { }), getTitleText: () => i18n.translate('xpack.canvas.embedObject.titleText', { - defaultMessage: 'Add from Visualize library', + defaultMessage: 'Add from Kibana', }), }, AdvancedFilter: { @@ -1308,7 +1308,7 @@ export const ComponentStrings = { }), getEmbedObjectMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { - defaultMessage: 'Add from Visualize library', + defaultMessage: 'Add from Kibana', }), getFilterMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { From b9e5ae9c777f6e4d2007fd4abb315cc1b05eafd2 Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Mon, 3 Aug 2020 11:53:52 -0400 Subject: [PATCH 052/115] [Security Solution] Filter endpoint hosts by agent status (#71882) --- .../server/endpoint/routes/metadata/index.ts | 30 +++++- .../endpoint/routes/metadata/metadata.test.ts | 51 +++++++++- .../routes/metadata/query_builders.test.ts | 4 +- .../routes/metadata/query_builders.ts | 76 ++++++++------- .../metadata/support/agent_status.test.ts | 96 +++++++++++++++++++ .../routes/metadata/support/agent_status.ts | 47 +++++++++ .../apis/metadata.ts | 30 ++++-- 7 files changed, 286 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 084f892369b51..161a31e2ec934 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -21,6 +21,7 @@ import { EndpointAppContext } from '../../types'; import { AgentService } from '../../../../../ingest_manager/server'; import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models'; import { findAllUnenrolledAgentIds } from './support/unenroll'; +import { findAgentIDsByStatus } from './support/agent_status'; interface HitSource { _source: HostMetadata; @@ -52,6 +53,21 @@ const getLogger = (endpointAppContext: EndpointAppContext): Logger => { return endpointAppContext.logFactory.get('metadata'); }; +/* Filters that can be applied to the endpoint fetch route */ +export const endpointFilters = schema.object({ + kql: schema.nullable(schema.string()), + host_status: schema.nullable( + schema.arrayOf( + schema.oneOf([ + schema.literal(HostStatus.ONLINE.toString()), + schema.literal(HostStatus.OFFLINE.toString()), + schema.literal(HostStatus.UNENROLLING.toString()), + schema.literal(HostStatus.ERROR.toString()), + ]) + ) + ), +}); + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const logger = getLogger(endpointAppContext); router.post( @@ -76,10 +92,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp ]) ) ), - /** - * filter to be applied, it could be a kql expression or discrete filter to be implemented - */ - filter: schema.nullable(schema.oneOf([schema.string()])), + filters: endpointFilters, }) ), }, @@ -103,12 +116,21 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp context.core.savedObjects.client ); + const statusIDs = req.body?.filters?.host_status?.length + ? await findAgentIDsByStatus( + agentService, + context.core.savedObjects.client, + req.body?.filters?.host_status + ) + : undefined; + const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, metadataIndexPattern, { unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), + statusAgentIDs: statusIDs, } ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index f3b832de9a786..29624b35d5c9e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -27,7 +27,7 @@ import { HostStatus, } from '../../../../common/endpoint/types'; import { SearchResponse } from 'elasticsearch'; -import { registerEndpointRoutes } from './index'; +import { registerEndpointRoutes, endpointFilters } from './index'; import { createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -170,7 +170,7 @@ describe('test endpoint route', () => { }, ], - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); @@ -395,6 +395,53 @@ describe('test endpoint route', () => { }); }); +describe('Filters Schema Test', () => { + it('accepts a single host status', () => { + expect( + endpointFilters.validate({ + host_status: ['error'], + }) + ).toBeTruthy(); + }); + + it('accepts multiple host status filters', () => { + expect( + endpointFilters.validate({ + host_status: ['offline', 'unenrolling'], + }) + ).toBeTruthy(); + }); + + it('rejects invalid statuses', () => { + expect(() => + endpointFilters.validate({ + host_status: ['foobar'], + }) + ).toThrowError(); + }); + + it('accepts a KQL string', () => { + expect( + endpointFilters.validate({ + kql: 'whatever.field', + }) + ).toBeTruthy(); + }); + + it('accepts KQL + status', () => { + expect( + endpointFilters.validate({ + kql: 'thing.var', + host_status: ['online'], + }) + ).toBeTruthy(); + }); + + it('accepts no filters', () => { + expect(endpointFilters.validate({})).toBeTruthy(); + }); +}); + function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse { return ({ took: 15, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index 266d522e8a41d..e9eb7093a7631 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -127,7 +127,7 @@ describe('query builder', () => { it('test default query params for all endpoints metadata when body filter is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( @@ -201,7 +201,7 @@ describe('query builder', () => { const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: { - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index f6385d2710047..ba9be96201dbe 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -9,6 +9,7 @@ import { EndpointAppContext } from '../../types'; export interface QueryBuilderOptions { unenrolledAgentIds?: string[]; + statusAgentIDs?: string[]; } export async function kibanaRequestToMetadataListESQuery( @@ -22,7 +23,11 @@ export async function kibanaRequestToMetadataListESQuery( const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { - query: buildQueryBody(request, queryBuilderOptions?.unenrolledAgentIds!), + query: buildQueryBody( + request, + queryBuilderOptions?.unenrolledAgentIds!, + queryBuilderOptions?.statusAgentIDs! + ), collapse: { field: 'host.id', inner_hits: { @@ -76,47 +81,52 @@ async function getPagingProperties( function buildQueryBody( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, - unerolledAgentIds: string[] | undefined + unerolledAgentIds: string[] | undefined, + statusAgentIDs: string[] | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { - const filterUnenrolledAgents = unerolledAgentIds && unerolledAgentIds.length > 0; - if (typeof request?.body?.filter === 'string') { - const kqlQuery = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); - return { - bool: { - must: filterUnenrolledAgents - ? [ - { - bool: { - must_not: { - terms: { - 'elastic.agent.id': unerolledAgentIds, - }, - }, - }, - }, - { - ...kqlQuery, - }, - ] - : [ - { - ...kqlQuery, - }, - ], - }, - }; - } - return filterUnenrolledAgents - ? { - bool: { + const filterUnenrolledAgents = + unerolledAgentIds && unerolledAgentIds.length > 0 + ? { must_not: { terms: { 'elastic.agent.id': unerolledAgentIds, }, }, + } + : null; + const filterStatusAgents = statusAgentIDs + ? { + must: { + terms: { + 'elastic.agent.id': statusAgentIDs, + }, }, } + : null; + + const idFilter = { + bool: { + ...filterUnenrolledAgents, + ...filterStatusAgents, + }, + }; + + if (request?.body?.filters?.kql) { + const kqlQuery = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(request.body.filters.kql) + ); + const q = []; + if (filterUnenrolledAgents || filterStatusAgents) { + q.push(idFilter); + } + q.push({ ...kqlQuery }); + return { + bool: { must: q }, + }; + } + return filterUnenrolledAgents || filterStatusAgents + ? idFilter : { match_all: {}, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts new file mode 100644 index 0000000000000..a4b6b0750ec10 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { findAgentIDsByStatus } from './agent_status'; +import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { AgentService } from '../../../../../../ingest_manager/server/services'; +import { createMockAgentService } from '../../../mocks'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; +import { AgentStatusKueryHelper } from '../../../../../../ingest_manager/common/services'; + +describe('test filtering endpoint hosts by agent status', () => { + let mockSavedObjectClient: jest.Mocked; + let mockAgentService: jest.Mocked; + beforeEach(() => { + mockSavedObjectClient = savedObjectsClientMock.create(); + mockAgentService = createMockAgentService(); + }); + + it('will accept a valid status condition', async () => { + mockAgentService.listAgents.mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 0, + page: 1, + perPage: 10, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['online']); + expect(result).toBeDefined(); + }); + + it('will filter for offline hosts', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => + Promise.resolve({ + agents: [({ id: 'id1' } as unknown) as Agent, ({ id: 'id2' } as unknown) as Agent], + total: 2, + page: 1, + perPage: 2, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 2, + perPage: 2, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['offline']); + const offlineKuery = AgentStatusKueryHelper.buildKueryForOfflineAgents(); + expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect.stringContaining(offlineKuery) + ); + expect(result).toBeDefined(); + expect(result).toEqual(['id1', 'id2']); + }); + + it('will filter for multiple statuses', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => + Promise.resolve({ + agents: [({ id: 'A' } as unknown) as Agent, ({ id: 'B' } as unknown) as Agent], + total: 2, + page: 1, + perPage: 2, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 2, + perPage: 2, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, [ + 'unenrolling', + 'error', + ]); + const unenrollKuery = AgentStatusKueryHelper.buildKueryForUnenrollingAgents(); + const errorKuery = AgentStatusKueryHelper.buildKueryForErrorAgents(); + expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect.stringContaining(`${unenrollKuery} OR ${errorKuery}`) + ); + expect(result).toBeDefined(); + expect(result).toEqual(['A', 'B']); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts new file mode 100644 index 0000000000000..86f6d1a9a65e2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { AgentService } from '../../../../../../ingest_manager/server'; +import { AgentStatusKueryHelper } from '../../../../../../ingest_manager/common/services'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; +import { HostStatus } from '../../../../../common/endpoint/types'; + +const STATUS_QUERY_MAP = new Map([ + [HostStatus.ONLINE.toString(), AgentStatusKueryHelper.buildKueryForOnlineAgents()], + [HostStatus.OFFLINE.toString(), AgentStatusKueryHelper.buildKueryForOfflineAgents()], + [HostStatus.ERROR.toString(), AgentStatusKueryHelper.buildKueryForErrorAgents()], + [HostStatus.UNENROLLING.toString(), AgentStatusKueryHelper.buildKueryForUnenrollingAgents()], +]); + +export async function findAgentIDsByStatus( + agentService: AgentService, + soClient: SavedObjectsClientContract, + status: string[], + pageSize: number = 1000 +): Promise { + const helpers = status.map((s) => STATUS_QUERY_MAP.get(s)); + const searchOptions = (pageNum: number) => { + return { + page: pageNum, + perPage: pageSize, + showInactive: true, + kuery: `(fleet-agents.packages : "endpoint" AND (${helpers.join(' OR ')}))`, + }; + }; + + let page = 1; + + const result: string[] = []; + let hasMore = true; + + while (hasMore) { + const agents = await agentService.listAgents(soClient, searchOptions(page++)); + result.push(...agents.agents.map((agent: Agent) => agent.id)); + hasMore = agents.agents.length > 0; + } + return result; +} 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 719327e5f9b79..3afa9f397a2ea 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 @@ -119,7 +119,11 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') - .send({ filter: 'not host.ip:10.46.229.234' }) + .send({ + filters: { + kql: 'not host.ip:10.46.229.234', + }, + }) .expect(200); expect(body.total).to.eql(2); expect(body.hosts.length).to.eql(2); @@ -141,7 +145,9 @@ export default function ({ getService }: FtrProviderContext) { page_index: 0, }, ], - filter: `not host.ip:${notIncludedIp}`, + filters: { + kql: `not host.ip:${notIncludedIp}`, + }, }) .expect(200); expect(body.total).to.eql(2); @@ -166,7 +172,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.os.Ext.variant:${variantValue}`, + filters: { + kql: `host.os.Ext.variant:${variantValue}`, + }, }) .expect(200); expect(body.total).to.eql(2); @@ -185,7 +193,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.ip:${targetEndpointIp}`, + filters: { + kql: `host.ip:${targetEndpointIp}`, + }, }) .expect(200); expect(body.total).to.eql(1); @@ -204,7 +214,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `not Endpoint.policy.applied.status:success`, + filters: { + kql: `not Endpoint.policy.applied.status:success`, + }, }) .expect(200); const statuses: Set = new Set( @@ -223,7 +235,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `elastic.agent.id:${targetElasticAgentId}`, + filters: { + kql: `elastic.agent.id:${targetElasticAgentId}`, + }, }) .expect(200); expect(body.total).to.eql(1); @@ -243,7 +257,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: '', + filters: { + kql: '', + }, }) .expect(200); expect(body.total).to.eql(numberOfHostsInFixture); From 5cabe9c95dd0e7ae2e6181c2d062268eb3fef601 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 3 Aug 2020 10:57:32 -0500 Subject: [PATCH 053/115] [Ingest Manager] improve error handling of package install (#73728) * refactor to add assets refs to SO before install * get ingest pipeline refs by looping through datasets because names are diff than path dir * fix bug and improve error handling * bump package number from merge conflict * add integration test for the epm-package saved object * accidentally pasted line of code * rename errors for consistency * pass custom error when IngestManagerError * rename package from outdated to update --- .../plugins/ingest_manager/server/errors.ts | 8 +++ .../server/routes/epm/handlers.ts | 21 ++++--- .../elasticsearch/ingest_pipeline/install.ts | 49 +++++++++------ .../epm/elasticsearch/template/install.ts | 18 +++--- .../services/epm/kibana/assets/install.ts | 59 +++++++----------- .../server/services/epm/packages/install.ts | 47 +++++++++------ .../server/services/epm/packages/remove.ts | 6 +- .../server/services/epm/registry/index.ts | 3 +- .../apis/epm/index.js | 17 ++++++ .../apis/epm/install_errors.ts | 51 ++++++++++++++++ .../apis/epm/install_remove_assets.ts | 60 +++++++++++++++++++ .../apis/epm/list.ts | 2 +- .../0.1.0/dataset/test/fields/fields.yml | 16 +++++ .../update/0.1.0/dataset/test/manifest.yml | 9 +++ .../test_packages/update/0.1.0/docs/README.md | 3 + .../test_packages/update/0.1.0/manifest.yml | 20 +++++++ .../0.2.0/dataset/test/fields/fields.yml | 16 +++++ .../update/0.2.0/dataset/test/manifest.yml | 9 +++ .../test_packages/update/0.2.0/docs/README.md | 3 + .../test_packages/update/0.2.0/manifest.yml | 20 +++++++ .../apis/index.js | 7 +-- 21 files changed, 343 insertions(+), 101 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/index.js create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors.ts index ee03b3faf79d1..401211409ebf7 100644 --- a/x-pack/plugins/ingest_manager/server/errors.ts +++ b/x-pack/plugins/ingest_manager/server/errors.ts @@ -15,9 +15,17 @@ export class IngestManagerError extends Error { export const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { return 502; // Bad Gateway + } + if (error instanceof PackageNotFoundError) { + return 404; + } + if (error instanceof PackageOutdatedError) { + return 400; } else { return 400; // Bad Request } }; export class RegistryError extends IngestManagerError {} +export class PackageNotFoundError extends IngestManagerError {} +export class PackageOutdatedError extends IngestManagerError {} diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index f54e61280b98a..f47234fb20118 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -32,6 +32,7 @@ import { getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; +import { IngestManagerError, getHTTPResponseCode } from '../../errors'; export const getCategoriesHandler: RequestHandler< undefined, @@ -165,23 +166,25 @@ export const installPackageHandler: RequestHandler isPipeline(path)); - if (datasets) { - const pipelines = datasets.reduce>>((acc, dataset) => { - if (dataset.ingest_pipeline) { - acc.push( - installPipelinesForDataset({ - dataset, - callCluster, - paths: pipelinePaths, - pkgVersion: registryPackage.version, - }) - ); - } - return acc; - }, []); - const pipelinesToSave = await Promise.all(pipelines).then((results) => results.flat()); - return saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelinesToSave); - } - return []; + // get and save pipeline refs before installing pipelines + const pipelineRefs = datasets.reduce((acc, dataset) => { + const filteredPaths = pipelinePaths.filter((path) => isDatasetPipeline(path, dataset.path)); + const pipelineObjectRefs = filteredPaths.map((path) => { + const { name } = getNameAndExtension(path); + const nameForInstallation = getPipelineNameForInstallation({ + pipelineName: name, + dataset, + packageVersion: registryPackage.version, + }); + return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; + }); + acc.push(...pipelineObjectRefs); + return acc; + }, []); + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelineRefs); + const pipelines = datasets.reduce>>((acc, dataset) => { + if (dataset.ingest_pipeline) { + acc.push( + installPipelinesForDataset({ + dataset, + callCluster, + paths: pipelinePaths, + pkgVersion: registryPackage.version, + }) + ); + } + return acc; + }, []); + return await Promise.all(pipelines).then((results) => results.flat()); }; export function rewriteIngestPipeline( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index 436a6a1bdc55d..2a3120f064904 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -41,6 +41,16 @@ export const installTemplates = async ( ); // build templates per dataset from yml files const datasets = registryPackage.datasets; + if (!datasets) return []; + // get template refs to save + const installedTemplateRefs = datasets.map((dataset) => ({ + id: generateTemplateName(dataset), + type: ElasticsearchAssetType.indexTemplate, + })); + + // add package installation's references to index templates + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); + if (datasets) { const installTemplatePromises = datasets.reduce>>((acc, dataset) => { acc.push( @@ -55,14 +65,6 @@ export const installTemplates = async ( const res = await Promise.all(installTemplatePromises); const installedTemplates = res.flat(); - // get template refs to save - const installedTemplateRefs = installedTemplates.map((template) => ({ - id: template.templateName, - type: ElasticsearchAssetType.indexTemplate, - })); - - // add package installation's references to index templates - await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); return installedTemplates; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index a3fe444b19b1a..5741764164b83 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -11,14 +11,8 @@ import { } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import * as Registry from '../../registry'; -import { - AssetType, - KibanaAssetType, - AssetReference, - KibanaAssetReference, -} from '../../../../types'; -import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; -import { getInstallationObject, savedObjectTypes } from '../../packages'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { savedObjectTypes } from '../../packages'; type SavedObjectToBe = Required & { type: AssetType }; export type ArchiveAsset = Pick< @@ -28,7 +22,7 @@ export type ArchiveAsset = Pick< type: AssetType; }; -export async function getKibanaAsset(key: string) { +export async function getKibanaAsset(key: string): Promise { const buffer = Registry.getAsset(key); // cache values are buffers. convert to string / JSON @@ -51,31 +45,18 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; - paths: string[]; + kibanaAssets: ArchiveAsset[]; isUpdate: boolean; -}): Promise { - const { savedObjectsClient, paths, pkgName, isUpdate } = options; - - if (isUpdate) { - // delete currently installed kibana saved objects and installation references - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedKibanaRefs = installedPkg?.attributes.installed_kibana; - - if (installedKibanaRefs?.length) { - await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedKibanaRefs); - await deleteKibanaInstalledRefs(savedObjectsClient, pkgName, installedKibanaRefs); - } - } +}): Promise { + const { savedObjectsClient, kibanaAssets } = options; - // install the new assets and save installation references + // install the assets const kibanaAssetTypes = Object.values(KibanaAssetType); const installedAssets = await Promise.all( kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + installKibanaSavedObjects({ savedObjectsClient, assetType, kibanaAssets }) ) ); - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array return installedAssets.flat(); } export const deleteKibanaInstalledRefs = async ( @@ -92,21 +73,25 @@ export const deleteKibanaInstalledRefs = async ( installed_kibana: installedAssetsToSave, }); }; - +export async function getKibanaAssets(paths: string[]) { + const isKibanaAssetType = (path: string) => Registry.pathParts(path).type in KibanaAssetType; + const filteredPaths = paths.filter(isKibanaAssetType); + const kibanaAssets = await Promise.all(filteredPaths.map((path) => getKibanaAsset(path))); + return kibanaAssets; +} async function installKibanaSavedObjects({ savedObjectsClient, assetType, - paths, + kibanaAssets, }: { savedObjectsClient: SavedObjectsClientContract; assetType: KibanaAssetType; - paths: string[]; + kibanaAssets: ArchiveAsset[]; }) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); + const isSameType = (asset: ArchiveAsset) => assetType === asset.type; + const filteredKibanaAssets = kibanaAssets.filter((asset) => isSameType(asset)); const toBeSavedObjects = await Promise.all( - kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + filteredKibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) ); if (toBeSavedObjects.length === 0) { @@ -115,13 +100,11 @@ async function installKibanaSavedObjects({ const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { overwrite: true, }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; + return createResults.saved_objects; } } -function toAssetReference({ id, type }: SavedObject) { +export function toAssetReference({ id, type }: SavedObject) { const reference: AssetReference = { id, type: type as KibanaAssetType }; return reference; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index a69daae6e0410..4d51689b872e1 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -5,7 +5,6 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; -import Boom from 'boom'; import semver from 'semver'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { @@ -25,8 +24,15 @@ import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { installKibanaAssets } from '../kibana/assets/install'; +import { + installKibanaAssets, + getKibanaAssets, + toAssetReference, + ArchiveAsset, +} from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; +import { deleteKibanaSavedObjectsAssets } from './remove'; +import { PackageOutdatedError } from '../../../errors'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -97,7 +103,7 @@ export async function installPackage(options: { // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets const latestPackage = await Registry.fetchFindLatestPackage(pkgName); if (semver.lt(pkgVersion, latestPackage.version)) - throw Boom.badRequest('Cannot install or update to an out-of-date package'); + throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); @@ -124,12 +130,23 @@ export async function installPackage(options: { toSaveESIndexPatterns, }); } - const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); + const kibanaAssets = await getKibanaAssets(paths); + if (installedPkg) + await deleteKibanaSavedObjectsAssets( + savedObjectsClient, + installedPkg.attributes.installed_kibana + ); + // save new kibana refs before installing the assets + const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssets + ); const installKibanaAssetsPromise = installKibanaAssets({ savedObjectsClient, pkgName, - paths, + kibanaAssets, isUpdate, }); @@ -169,21 +186,14 @@ export async function installPackage(options: { ); } - // get template refs to save const installedTemplateRefs = installedTemplates.map((template) => ({ id: template.templateName, type: ElasticsearchAssetType.indexTemplate, })); - - const [installedKibanaAssets] = await Promise.all([ - installKibanaAssetsPromise, - installIndexPatternPromise, - ]); - - await saveInstalledKibanaRefs(savedObjectsClient, pkgName, installedKibanaAssets); + await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); // update to newly installed version when all assets are successfully installed if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - return [...installedKibanaAssets, ...installedPipelines, ...installedTemplateRefs]; + return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs]; } const updateVersion = async ( savedObjectsClient: SavedObjectsClientContract, @@ -230,15 +240,16 @@ export async function createInstallation(options: { return [...installedKibana, ...installedEs]; } -export const saveInstalledKibanaRefs = async ( +export const saveKibanaAssetsRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - installedAssets: KibanaAssetReference[] + kibanaAssets: ArchiveAsset[] ) => { + const assetRefs = kibanaAssets.map(toAssetReference); await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_kibana: installedAssets, + installed_kibana: assetRefs, }); - return installedAssets; + return assetRefs; }; export const saveInstalledEsRefs = async ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 81bc5847e6c0e..1acf2131dcb01 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -102,10 +102,12 @@ async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): P export async function deleteKibanaSavedObjectsAssets( savedObjectsClient: SavedObjectsClientContract, - installedObjects: AssetReference[] + installedRefs: AssetReference[] ) { + if (!installedRefs.length) return; + const logger = appContextService.getLogger(); - const deletePromises = installedObjects.map(({ id, type }) => { + const deletePromises = installedRefs.map(({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index c7f2df38fe41a..c701762e50b50 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -22,6 +22,7 @@ import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; +import { PackageNotFoundError } from '../../../errors'; export { ArchiveEntry } from './extract'; @@ -76,7 +77,7 @@ export async function fetchFindLatestPackage(packageName: string): Promise { + loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./file')); + //loadTestFile(require.resolve('./template')); + loadTestFile(require.resolve('./ilm')); + loadTestFile(require.resolve('./install_overrides')); + loadTestFile(require.resolve('./install_remove_assets')); + loadTestFile(require.resolve('./install_errors')); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts new file mode 100644 index 0000000000000..8acb11b00b57d --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + + describe('package error handling', async () => { + skipIfNoDockerRegistry(providerContext); + it('should return 404 if package does not exist', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(404); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'nonexistent', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 400 if trying to update/install to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/update-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'update', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 9ca8ebf136078..35058de0684b2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -108,6 +108,54 @@ export default function (providerContext: FtrProviderContext) { }); expect(resSearch.id).equal('sample_search'); }); + it('should have created the correct saved object', async function () { + const res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + expect(res.attributes).eql({ + installed_kibana: [ + { + id: 'sample_dashboard', + type: 'dashboard', + }, + { + id: 'sample_dashboard2', + type: 'dashboard', + }, + { + id: 'sample_search', + type: 'search', + }, + { + id: 'sample_visualization', + type: 'visualization', + }, + ], + installed_es: [ + { + id: 'logs-all_assets.test_logs-0.1.0', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs', + type: 'index_template', + }, + { + id: 'metrics-all_assets.test_metrics', + type: 'index_template', + }, + ], + es_index_patterns: { + test_logs: 'logs-all_assets.test_logs-*', + test_metrics: 'metrics-all_assets.test_metrics-*', + }, + name: 'all_assets', + version: '0.1.0', + internal: false, + removable: true, + }); + }); }); describe('uninstalls all assets when uninstalling a package', async () => { @@ -192,6 +240,18 @@ export default function (providerContext: FtrProviderContext) { } expect(resSearch.response.data.statusCode).equal(404); }); + it('should have removed the saved object', async function () { + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts index 98b26c1c04ebb..20414fcb90521 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; const listResponse = await fetchPackageList(); - expect(listResponse.response.length).to.be(13); + expect(listResponse.response.length).to.be(14); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml new file mode 100644 index 0000000000000..12a9a03c1337b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md new file mode 100644 index 0000000000000..13ef3f4fa9152 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml new file mode 100644 index 0000000000000..b12f1bfbd3b7e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: update +title: Package update test +description: This is a test package for updating a package +version: 0.1.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml new file mode 100644 index 0000000000000..12a9a03c1337b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md new file mode 100644 index 0000000000000..8e26522d86839 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml new file mode 100644 index 0000000000000..11dbdc102dce8 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: update +title: Package update test +description: This is a test package for updating a package +version: 0.2.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index d21b80bd6eed7..72121b2164bfd 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -12,12 +12,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./fleet/index')); // EPM - loadTestFile(require.resolve('./epm/list')); - loadTestFile(require.resolve('./epm/file')); - //loadTestFile(require.resolve('./epm/template')); - loadTestFile(require.resolve('./epm/ilm')); - loadTestFile(require.resolve('./epm/install_overrides')); - loadTestFile(require.resolve('./epm/install_remove_assets')); + loadTestFile(require.resolve('./epm/index')); // Package configs loadTestFile(require.resolve('./package_config/create')); From 909c9826ff87e522e29626403103f854089f3d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Mon, 3 Aug 2020 18:27:28 +0200 Subject: [PATCH 054/115] [Metrics UI] Fix typo on view selector in metrics explorer (#74084) --- .../infra/public/components/saved_views/toolbar_control.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 2e06ee55189d9..83fe233553351 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -176,7 +176,7 @@ export function SavedViewsToolbarControls(props: Props) { {currentView ? currentView.name : i18n.translate('xpack.infra.savedView.unknownView', { - defaultMessage: 'No view seleted', + defaultMessage: 'No view selected', })} From 0739a3453dcc27fdd422916f098bc263a0a84c4a Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 3 Aug 2020 18:29:08 +0200 Subject: [PATCH 055/115] [SIEM] Adds threshold rule creation Cypress test (#74065) * adds threshold rule creation cypress test * fixes type chek error Co-authored-by: Elastic Machine --- .../alerts_detection_rules_threshold.spec.ts | 174 ++++++++++++++++++ .../security_solution/cypress/objects/rule.ts | 21 +++ .../cypress/screens/create_new_rule.ts | 8 + .../cypress/screens/rule_details.ts | 2 + .../cypress/tasks/create_new_rule.ts | 40 +++- 5 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts new file mode 100644 index 0000000000000..10f9ebb5623df --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { newThresholdRule } from '../objects/rule'; + +import { + CUSTOM_RULES_BTN, + RISK_SCORE, + RULE_NAME, + RULES_ROW, + RULES_TABLE, + SEVERITY, +} from '../screens/alerts_detection_rules'; +import { + ABOUT_FALSE_POSITIVES, + ABOUT_INVESTIGATION_NOTES, + ABOUT_MITRE, + ABOUT_RISK, + ABOUT_RULE_DESCRIPTION, + ABOUT_SEVERITY, + ABOUT_STEP, + ABOUT_TAGS, + ABOUT_URLS, + DEFINITION_CUSTOM_QUERY, + DEFINITION_INDEX_PATTERNS, + DEFINITION_THRESHOLD, + DEFINITION_TIMELINE, + DEFINITION_STEP, + INVESTIGATION_NOTES_MARKDOWN, + INVESTIGATION_NOTES_TOGGLE, + RULE_ABOUT_DETAILS_HEADER_TOGGLE, + RULE_NAME_HEADER, + SCHEDULE_LOOPBACK, + SCHEDULE_RUNS, + SCHEDULE_STEP, +} from '../screens/rule_details'; + +import { + goToManageAlertsDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, +} from '../tasks/alerts'; +import { + changeToThreeHundredRowsPerPage, + filterByCustomRules, + goToCreateNewRule, + goToRuleDetails, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/alerts_detection_rules'; +import { + createAndActivateRule, + fillAboutRuleAndContinue, + fillDefineThresholdRuleAndContinue, + selectThresholdRuleType, +} from '../tasks/create_new_rule'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS_URL } from '../urls/navigation'; + +describe('Detection rules, threshold', () => { + before(() => { + esArchiverLoad('timeline'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Creates and activates a new threshold rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRuleAndContinue(newThresholdRule); + fillAboutRuleAndContinue(newThresholdRule); + createAndActivateRule(); + + cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 1; + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); + + filterByCustomRules(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); + }); + cy.get(RULE_NAME).invoke('text').should('eql', newThresholdRule.name); + cy.get(RISK_SCORE).invoke('text').should('eql', newThresholdRule.riskScore); + cy.get(SEVERITY).invoke('text').should('eql', newThresholdRule.severity); + cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + let expectedUrls = ''; + newThresholdRule.referenceUrls.forEach((url) => { + expectedUrls = expectedUrls + url; + }); + let expectedFalsePositives = ''; + newThresholdRule.falsePositivesExamples.forEach((falsePositive) => { + expectedFalsePositives = expectedFalsePositives + falsePositive; + }); + let expectedTags = ''; + newThresholdRule.tags.forEach((tag) => { + expectedTags = expectedTags + tag; + }); + let expectedMitre = ''; + newThresholdRule.mitre.forEach((mitre) => { + expectedMitre = expectedMitre + mitre.tactic; + mitre.techniques.forEach((technique) => { + expectedMitre = expectedMitre + technique; + }); + }); + const expectedIndexPatterns = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ]; + + cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newThresholdRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newThresholdRule.description); + cy.get(ABOUT_STEP).eq(ABOUT_SEVERITY).invoke('text').should('eql', newThresholdRule.severity); + cy.get(ABOUT_STEP).eq(ABOUT_RISK).invoke('text').should('eql', newThresholdRule.riskScore); + cy.get(ABOUT_STEP).eq(ABOUT_URLS).invoke('text').should('eql', expectedUrls); + cy.get(ABOUT_STEP) + .eq(ABOUT_FALSE_POSITIVES) + .invoke('text') + .should('eql', expectedFalsePositives); + cy.get(ABOUT_STEP).eq(ABOUT_MITRE).invoke('text').should('eql', expectedMitre); + cy.get(ABOUT_STEP).eq(ABOUT_TAGS).invoke('text').should('eql', expectedTags); + + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); + + cy.get(DEFINITION_INDEX_PATTERNS).then((patterns) => { + cy.wrap(patterns).each((pattern, index) => { + cy.wrap(pattern).invoke('text').should('eql', expectedIndexPatterns[index]); + }); + }); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_CUSTOM_QUERY) + .invoke('text') + .should('eql', `${newThresholdRule.customQuery} `); + cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_THRESHOLD) + .invoke('text') + .should( + 'eql', + `Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}` + ); + + cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); + cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index a30fddc3c3a69..aeadc34c6e49c 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -33,6 +33,11 @@ export interface CustomRule { timelineId: string; } +export interface ThresholdRule extends CustomRule { + thresholdField: string; + threshold: string; +} + export interface MachineLearningRule { machineLearningJob: string; anomalyScoreThreshold: string; @@ -72,6 +77,22 @@ export const newRule: CustomRule = { timelineId: '0162c130-78be-11ea-9718-118a926974a4', }; +export const newThresholdRule: ThresholdRule = { + customQuery: 'host.name:*', + name: 'New Rule Test', + description: 'The new rule description.', + severity: 'High', + riskScore: '17', + tags: ['test', 'newRule'], + referenceUrls: ['https://www.google.com/', 'https://elastic.co/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [mitre1, mitre2], + note: '# test markdown', + timelineId: '0162c130-78be-11ea-9718-118a926974a4', + thresholdField: 'host.name', + threshold: '10', +}; + export const machineLearningRule: MachineLearningRule = { machineLearningJob: 'linux_anomalous_network_service', anomalyScoreThreshold: '20', diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index bc0740554bc52..af4fe7257ae5b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -27,6 +27,8 @@ export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; +export const INPUT = '[data-test-subj="input"]'; + export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; @@ -64,3 +66,9 @@ export const SEVERITY_DROPDOWN = export const TAGS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxSearchInput"]'; + +export const THRESHOLD_FIELD_SELECTION = '.euiFilterSelectItem'; + +export const THRESHOLD_INPUT_AREA = '[data-test-subj="thresholdInput"]'; + +export const THRESHOLD_TYPE = '[data-test-subj="thresholdRuleType"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index ec57e142125da..1c0102382ab6b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -26,6 +26,8 @@ export const ANOMALY_SCORE = 1; export const DEFINITION_CUSTOM_QUERY = 1; +export const DEFINITION_THRESHOLD = 4; + export const DEFINITION_TIMELINE = 3; export const DEFINITION_INDEX_PATTERNS = diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 88ae582b58891..de9d343bc91f7 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -3,7 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CustomRule, MachineLearningRule, machineLearningRule } from '../objects/rule'; + +import { + CustomRule, + MachineLearningRule, + machineLearningRule, + ThresholdRule, +} from '../objects/rule'; import { ABOUT_CONTINUE_BTN, ANOMALY_THRESHOLD_INPUT, @@ -15,6 +21,7 @@ import { DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, + INPUT, INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, @@ -30,6 +37,9 @@ import { SCHEDULE_CONTINUE_BUTTON, SEVERITY_DROPDOWN, TAGS_INPUT, + THRESHOLD_FIELD_SELECTION, + THRESHOLD_INPUT_AREA, + THRESHOLD_TYPE, } from '../screens/create_new_rule'; import { TIMELINE } from '../screens/timeline'; @@ -39,7 +49,9 @@ export const createAndActivateRule = () => { cy.get(CREATE_AND_ACTIVATE_BTN).should('not.exist'); }; -export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) => { +export const fillAboutRuleAndContinue = ( + rule: CustomRule | MachineLearningRule | ThresholdRule +) => { cy.get(RULE_NAME_INPUT).type(rule.name, { force: true }); cy.get(RULE_DESCRIPTION_INPUT).type(rule.description, { force: true }); @@ -80,18 +92,28 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); }; -export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { - cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); +export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timelineId)).click(); cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; -export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { - cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); - cy.get(TIMELINE(rule.timelineId)).click(); +export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { + const thresholdField = 0; + const threshold = 1; + + cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); + cy.get(THRESHOLD_INPUT_AREA) + .find(INPUT) + .then((inputs) => { + cy.wrap(inputs[thresholdField]).type(rule.thresholdField); + cy.get(THRESHOLD_FIELD_SELECTION).click({ force: true }); + cy.wrap(inputs[threshold]).clear().type(rule.threshold); + }); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); @@ -111,3 +133,7 @@ export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRu export const selectMachineLearningRuleType = () => { cy.get(MACHINE_LEARNING_TYPE).click({ force: true }); }; + +export const selectThresholdRuleType = () => { + cy.get(THRESHOLD_TYPE).click({ force: true }); +}; From df41c9b274ee6e472742842f5704f256d9d9ee38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 3 Aug 2020 18:05:39 +0100 Subject: [PATCH 056/115] [Telemetry] Data: Report dataset info only if there is known metadata (#71419) * [Telemetry] Data: Report dataset information only if there is known metadata * Handle data-stream indices (.ds-*) Co-authored-by: Elastic Machine --- .../get_data_telemetry.test.ts | 55 ++++++++++++- .../get_data_telemetry/get_data_telemetry.ts | 77 +++++++++++++------ 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts index 8bffc5d012a74..ad19def160200 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -75,7 +75,7 @@ describe('get_data_telemetry', () => { { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name // New Indexing strategy: everything can be inferred from the constant_keyword values { - name: 'logs-nginx.access-default-000001', + name: '.ds-logs-nginx.access-default-000001', datasetName: 'nginx.access', datasetType: 'logs', shipper: 'filebeat', @@ -84,7 +84,7 @@ describe('get_data_telemetry', () => { sizeInBytes: 1000, }, { - name: 'logs-nginx.access-default-000002', + name: '.ds-logs-nginx.access-default-000002', datasetName: 'nginx.access', datasetType: 'logs', shipper: 'filebeat', @@ -92,6 +92,42 @@ describe('get_data_telemetry', () => { docCount: 1000, sizeInBytes: 60, }, + { + name: '.ds-traces-something-default-000002', + datasetName: 'something', + datasetType: 'traces', + packageName: 'some-package', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + { + name: '.ds-metrics-something.else-default-000002', + datasetName: 'something.else', + datasetType: 'metrics', + managedBy: 'ingest-manager', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + // Filter out if it has datasetName and datasetType but none of the shipper, packageName or managedBy === 'ingest-manager' + { + name: 'some-index-that-should-not-show', + datasetName: 'should-not-show', + datasetType: 'logs', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + { + name: 'other-index-that-should-not-show', + datasetName: 'should-not-show-either', + datasetType: 'metrics', + managedBy: 'me', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, ]) ).toStrictEqual([ { @@ -138,6 +174,21 @@ describe('get_data_telemetry', () => { doc_count: 2000, size_in_bytes: 1060, }, + { + dataset: { name: 'something', type: 'traces' }, + package: { name: 'some-package' }, + index_count: 1, + ecs_index_count: 1, + doc_count: 1000, + size_in_bytes: 60, + }, + { + dataset: { name: 'something.else', type: 'metrics' }, + index_count: 1, + ecs_index_count: 1, + doc_count: 1000, + size_in_bytes: 60, + }, ]); }); }); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts index cf906bc5c86cf..079f510bb256a 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -36,6 +36,9 @@ export interface DataTelemetryDocument extends DataTelemetryBasePayload { name?: string; type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s }; + package?: { + name: string; + }; shipper?: string; pattern_name?: DataPatternName; } @@ -44,6 +47,8 @@ export type DataTelemetryPayload = DataTelemetryDocument[]; export interface DataTelemetryIndex { name: string; + packageName?: string; // Populated by Ingest Manager at `_meta.package.name` + managedBy?: string; // Populated by Ingest Manager at `_meta.managed_by` datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword shipper?: string; // To be obtained from `_meta.beat` if it's set @@ -58,6 +63,7 @@ export interface DataTelemetryIndex { type AtLeastOne }> = Partial & U[keyof U]; type DataDescriptor = AtLeastOne<{ + packageName: string; datasetName: string; datasetType: string; shipper: string; @@ -67,17 +73,28 @@ type DataDescriptor = AtLeastOne<{ function findMatchingDescriptors({ name, shipper, + packageName, + managedBy, datasetName, datasetType, }: DataTelemetryIndex): DataDescriptor[] { // If we already have the data from the indices' mappings... - if ([shipper, datasetName, datasetType].some(Boolean)) { + if ( + [shipper, packageName].some(Boolean) || + (managedBy === 'ingest-manager' && [datasetType, datasetName].some(Boolean)) + ) { return [ { ...(shipper && { shipper }), + ...(packageName && { packageName }), ...(datasetName && { datasetName }), ...(datasetType && { datasetType }), - } as AtLeastOne<{ datasetName: string; datasetType: string; shipper: string }>, // Using casting here because TS doesn't infer at least one exists from the if clause + } as AtLeastOne<{ + packageName: string; + datasetName: string; + datasetType: string; + shipper: string; + }>, // Using casting here because TS doesn't infer at least one exists from the if clause ]; } @@ -122,6 +139,7 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe ({ name }) => !( name.startsWith('.') && + !name.startsWith('.ds-') && // data_stream-related indices can be included !startingDotPatternsUntilTheFirstAsterisk.find((pattern) => name.startsWith(pattern)) ) ); @@ -130,10 +148,17 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe for (const indexCandidate of indexCandidates) { const matchingDescriptors = findMatchingDescriptors(indexCandidate); - for (const { datasetName, datasetType, shipper, patternName } of matchingDescriptors) { - const key = `${datasetName}-${datasetType}-${shipper}-${patternName}`; + for (const { + datasetName, + datasetType, + packageName, + shipper, + patternName, + } of matchingDescriptors) { + const key = `${datasetName}-${datasetType}-${packageName}-${shipper}-${patternName}`; acc.set(key, { ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...(packageName && { package: { name: packageName } }), ...(shipper && { shipper }), ...(patternName && { pattern_name: patternName }), ...increaseCounters(acc.get(key), indexCandidate), @@ -165,6 +190,12 @@ interface IndexMappings { mappings: { _meta?: { beat?: string; + + // Ingest Manager provided metadata + package?: { + name?: string; + }; + managed_by?: string; // Typically "ingest-manager" }; properties: { dataset?: { @@ -195,7 +226,7 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { try { const index = [ ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern), - '*-*-*-*', // Include new indexing strategy indices {type}-{dataset}-{namespace}-{rollover_counter} + '*-*-*', // Include data-streams aliases `{type}-{dataset}-{namespace}` ]; const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([ // GET */_mapping?filter_path=*.mappings._meta.beat,*.mappings.properties.ecs.properties.version.type,*.mappings.properties.dataset.properties.type.value,*.mappings.properties.dataset.properties.name.value @@ -204,16 +235,17 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { filterPath: [ // _meta.beat tells the shipper '*.mappings._meta.beat', + // _meta.package.name tells the Ingest Manager's package + '*.mappings._meta.package.name', + // _meta.managed_by is usually populated by Ingest Manager for the UI to identify it + '*.mappings._meta.managed_by', // Does it have `ecs.version` in the mappings? => It follows the ECS conventions '*.mappings.properties.ecs.properties.version.type', - // Disable the fields below because they are still pending to be confirmed: - // https://github.com/elastic/ecs/pull/845 - // TODO: Re-enable when the final fields are confirmed - // // If `dataset.type` is a `constant_keyword`, it can be reported as a type - // '*.mappings.properties.dataset.properties.type.value', - // // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset - // '*.mappings.properties.dataset.properties.name.value', + // If `dataset.type` is a `constant_keyword`, it can be reported as a type + '*.mappings.properties.dataset.properties.type.value', + // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset + '*.mappings.properties.dataset.properties.name.value', ], }), // GET /_stats/docs,store?level=indices&filter_path=indices.*.total @@ -227,24 +259,25 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices }); const indices = indexNames.map((name) => { - const isECS = !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type; - const shipper = indexMappings[name]?.mappings?._meta?.beat; - const datasetName = indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value; - const datasetType = indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value; + const baseIndexInfo = { + name, + isECS: !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type, + shipper: indexMappings[name]?.mappings?._meta?.beat, + packageName: indexMappings[name]?.mappings?._meta?.package?.name, + managedBy: indexMappings[name]?.mappings?._meta?.managed_by, + datasetName: indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value, + datasetType: indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value, + }; const stats = (indexStats?.indices || {})[name]; if (stats) { return { - name, - datasetName, - datasetType, - shipper, - isECS, + ...baseIndexInfo, docCount: stats.total?.docs?.count, sizeInBytes: stats.total?.store?.size_in_bytes, }; } - return { name, datasetName, datasetType, shipper, isECS }; + return baseIndexInfo; }); return buildDataTelemetryPayload(indices); } catch (e) { From e82f14af75df99417921f7092702c0edf895eeea Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 3 Aug 2020 13:07:39 -0400 Subject: [PATCH 057/115] fix bug when clicking on start a new case (#74092) --- .../recent_cases/no_cases/index.test.tsx | 49 +++++++++++++++++++ .../recent_cases/no_cases/index.tsx | 3 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx new file mode 100644 index 0000000000000..99902a31975d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { useKibana } from '../../../../common/lib/kibana'; +import '../../../../common/mock/match_media'; +import { createUseKibanaMock, TestProviders } from '../../../../common/mock'; +import { NoCases } from '.'; + +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mock; + +let navigateToApp: jest.Mock; + +describe('RecentCases', () => { + beforeEach(() => { + jest.resetAllMocks(); + navigateToApp = jest.fn(); + const kibanaMock = createUseKibanaMock()(); + useKibanaMock.mockReturnValue({ + ...kibanaMock, + services: { + application: { + navigateToApp, + getUrlForApp: jest.fn(), + }, + }, + }); + }); + + it('if no cases, you should be able to create a case by clicking on the link "start a new case"', () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="no-cases-create-case"]`).first().simulate('click'); + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { + path: + "/create?timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx index 40969a6e1df4a..875a678f32226 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx @@ -21,7 +21,7 @@ const NoCasesComponent = () => { const goToCreateCase = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { path: getCreateCaseUrl(search), }); }, @@ -30,6 +30,7 @@ const NoCasesComponent = () => { const newCaseLink = useMemo( () => ( {` ${i18n.START_A_NEW_CASE}`} From 6337cbf16397881cc39f60ad37a0d79d431b610e Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 3 Aug 2020 19:34:01 +0200 Subject: [PATCH 058/115] updates exceptions 'Add to clipboard' copy for comments (#74103) Co-authored-by: Elastic Machine --- .../public/common/components/exceptions/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index b826c1e49f274..e68b903266428 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -176,7 +176,7 @@ export const ADD_COMMENT_PLACEHOLDER = i18n.translate( export const ADD_TO_CLIPBOARD = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addToClipboard', { - defaultMessage: 'Add to clipboard', + defaultMessage: 'Comment', } ); From 89f1ac49870d06624eca635b82c83bce4f0545df Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 3 Aug 2020 19:07:03 +0100 Subject: [PATCH 059/115] fix default timeline's tab (#74051) --- .../components/open_timeline/index.test.tsx | 172 ++++++++++++++++-- .../open_timeline/use_timeline_types.tsx | 7 +- 2 files changed, 161 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 75b6413bf08f9..43fd57fcfc5bf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -4,29 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; import { mount } from 'enzyme'; import { MockedProvider } from 'react-apollo/test-utils'; -import React from 'react'; - // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; +import { useHistory, useParams } from 'react-router-dom'; + import '../../../common/mock/match_media'; +import { SecurityPageName } from '../../../app/types'; +import { TimelineType } from '../../../../common/types/timeline'; + import { TestProviders, apolloClient } from '../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../common/mock/timeline_results'; +import { getTimelineTabsUrl } from '../../../common/components/link_to'; + import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; +import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; import { StatefulOpenTimeline } from '.'; -import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; +import { TimelineTabsStyle } from './types'; +import { + useTimelineTypes, + UseTimelineTypesArgs, + UseTimelineTypesResult, +} from './use_timeline_types'; -import { useParams } from 'react-router-dom'; -import { TimelineType } from '../../../../common/types/timeline'; +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); -jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/components/link_to'); + return { + ...originalModule, + useParams: jest.fn(), + useHistory: jest.fn(), + }; +}); jest.mock('./helpers', () => { const originalModule = jest.requireActual('./helpers'); @@ -41,24 +60,31 @@ jest.mock('../../containers/all', () => { return { ...originalModule, useGetAllTimeline: jest.fn(), - getAllTimeline: originalModule.getAllTimeline, }; }); -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); +jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/link_to'); +jest.mock('../../../common/components/link_to', () => { + const originalModule = jest.requireActual('../../../common/components/link_to'); return { ...originalModule, - useParams: jest.fn(), - useHistory: jest.fn().mockReturnValue([]), + getTimelineTabsUrl: jest.fn(), + useFormatUrl: jest.fn().mockReturnValue({ formatUrl: jest.fn(), search: 'urlSearch' }), }; }); describe('StatefulOpenTimeline', () => { const title = 'All Timelines / Open Timelines'; + let mockHistory: History[]; beforeEach(() => { - (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.default, + pageName: SecurityPageName.timelines, + }); + mockHistory = []; + (useHistory as jest.Mock).mockReturnValue(mockHistory); ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), timelines: getAllTimeline( @@ -71,6 +97,13 @@ describe('StatefulOpenTimeline', () => { }); }); + afterEach(() => { + (getTimelineTabsUrl as jest.Mock).mockClear(); + (useParams as jest.Mock).mockClear(); + (useHistory as jest.Mock).mockClear(); + mockHistory = []; + }); + test('it has the expected initial state', () => { const wrapper = mount( @@ -101,6 +134,109 @@ describe('StatefulOpenTimeline', () => { }); }); + describe("Template timelines' tab", () => { + test("should land on correct timelines' tab with url timelines/default", () => { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.default); + }); + + test("should land on correct timelines' tab with url timelines/template", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.template, + pageName: SecurityPageName.timelines, + }); + + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.template); + }); + + test("should land on correct templates' tab after switching tab", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.template, + pageName: SecurityPageName.timelines, + }); + + const wrapper = mount( + + + + + + ); + wrapper + .find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}-${TimelineType.template}"]`) + .first() + .simulate('click'); + act(() => { + expect(history.length).toBeGreaterThan(0); + }); + }); + + test("should selecting correct timelines' filter", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: 'mockTabName', + pageName: SecurityPageName.case, + }); + + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.default); + }); + + test('should not change url after switching filter', () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: 'mockTabName', + pageName: SecurityPageName.case, + }); + + const wrapper = mount( + + + + + + ); + wrapper + .find( + `[data-test-subj="open-timeline-modal-body-${TimelineTabsStyle.filter}-${TimelineType.template}"]` + ) + .first() + .simulate('click'); + act(() => { + expect(mockHistory.length).toEqual(0); + }); + }); + }); + describe('#onQueryChange', () => { test('it updates the query state with the expected trimmed value when the user enters a query', () => { const wrapper = mount( @@ -482,9 +618,13 @@ describe('StatefulOpenTimeline', () => { ); - expect(wrapper.find('[data-test-subj="open-timeline-modal-body-filters"]').exists()).toEqual( - true - ); + expect( + wrapper + .find( + `[data-test-subj="open-timeline-modal-body-${TimelineTabsStyle.filter}-${TimelineType.default}"]` + ) + .exists() + ).toEqual(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 55afe845cdfb3..1ffa626b01311 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -33,7 +33,9 @@ export const useTimelineTypes = ({ const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [timelineType, setTimelineTypes] = useState( - tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null + tabName === TimelineType.default || tabName === TimelineType.template + ? tabName + : TimelineType.default ); const goToTimeline = useCallback( @@ -114,6 +116,7 @@ export const useTimelineTypes = ({ {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( { return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( Date: Mon, 3 Aug 2020 21:41:34 +0300 Subject: [PATCH 060/115] [Security Solutions][Case] Fix quote handling (#74093) --- .../components/add_comment/index.test.tsx | 10 +- .../cases/components/add_comment/index.tsx | 150 ++++++++++-------- .../components/user_action_tree/index.tsx | 21 +-- 3 files changed, 98 insertions(+), 83 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 88969c3ae5fb3..f697ce443f2c5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { AddComment } from '.'; +import { AddComment, AddCommentRefObject } from '.'; import { TestProviders } from '../../../common/mock'; import { getFormMock } from '../__mock__/form'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; @@ -60,9 +60,11 @@ const defaultPostCommment = { isError: false, postComment, }; + const sampleData = { comment: 'what a cool comment', }; + describe('AddComment ', () => { const formHookMock = getFormMock(sampleData); @@ -122,16 +124,18 @@ describe('AddComment ', () => { ).toBeTruthy(); }); - it('should insert a quote if one is available', () => { + it('should insert a quote', () => { const sampleQuote = 'what a cool quote'; + const ref = React.createRef(); mount( - + ); + ref.current!.addQuote(sampleQuote); expect(formHookMock.setFieldValue).toBeCalledWith( 'comment', `${sampleData.comment}\n\n${sampleQuote}` diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index a54cf142c18b7..87bd7bb247056 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; import { CommentRequest } from '../../../../../case/common/api'; @@ -30,88 +30,98 @@ const initialCommentValue: CommentRequest = { comment: '', }; +export interface AddCommentRefObject { + addQuote: (quote: string) => void; +} + interface AddCommentProps { caseId: string; disabled?: boolean; - insertQuote: string | null; onCommentSaving?: () => void; onCommentPosted: (newCase: Case) => void; showLoading?: boolean; } -export const AddComment = React.memo( - ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { - const { isLoading, postComment } = usePostComment(caseId); - const { form } = useForm({ - defaultValue: initialCommentValue, - options: { stripEmptyFields: false }, - schema, - }); - const { getFormData, setFieldValue, reset, submit } = form; - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - form, - 'comment' - ); +export const AddComment = React.memo( + forwardRef( + ({ caseId, disabled, showLoading = true, onCommentPosted, onCommentSaving }, ref) => { + const { isLoading, postComment } = usePostComment(caseId); + const { form } = useForm({ + defaultValue: initialCommentValue, + options: { stripEmptyFields: false }, + schema, + }); + const { getFormData, setFieldValue, reset, submit } = form; + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( + form, + 'comment' + ); + + const addQuote = useCallback( + (quote) => { + const { comment } = getFormData(); + setFieldValue('comment', `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`); + }, + [getFormData, setFieldValue] + ); - useEffect(() => { - if (insertQuote !== null) { - const { comment } = getFormData(); - setFieldValue('comment', `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}`); - } - }, [getFormData, insertQuote, setFieldValue]); + useImperativeHandle(ref, () => ({ + addQuote, + })); - const handleTimelineClick = useTimelineClick(); + const handleTimelineClick = useTimelineClick(); - const onSubmit = useCallback(async () => { - const { isValid, data } = await submit(); - if (isValid) { - if (onCommentSaving != null) { - onCommentSaving(); + const onSubmit = useCallback(async () => { + const { isValid, data } = await submit(); + if (isValid) { + if (onCommentSaving != null) { + onCommentSaving(); + } + postComment(data, onCommentPosted); + reset(); } - postComment(data, onCommentPosted); - reset(); - } - }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); + }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); - return ( - - {isLoading && showLoading && } -
- - {i18n.ADD_COMMENT} - - ), - topRightContent: ( - - ), - }} - /> - -
- ); - } + return ( + + {isLoading && showLoading && } +
+ + {i18n.ADD_COMMENT} + + ), + topRightContent: ( + + ), + }} + /> + +
+ ); + } + ) ); AddComment.displayName = 'AddComment'; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 0c1da8694bf1a..733e3db3c25e6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -14,7 +14,7 @@ import * as i18n from '../case_view/translations'; import { Case, CaseUserActions } from '../../containers/types'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../../common/lib/kibana'; -import { AddComment } from '../add_comment'; +import { AddComment, AddCommentRefObject } from '../add_comment'; import { getLabelTitle } from './helpers'; import { UserActionItem } from './user_action_item'; import { UserActionMarkdown } from './user_action_markdown'; @@ -58,12 +58,12 @@ export const UserActionTree = React.memo( }: UserActionTreeProps) => { const { commentId } = useParams(); const handlerTimeoutId = useRef(0); + const addCommentRef = useRef(null); const [initLoading, setInitLoading] = useState(true); const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); const { isLoadingIds, patchComment } = useUpdateComment(); const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); - const [insertQuote, setInsertQuote] = useState(null); const handleManageMarkdownEditId = useCallback( (id: string) => { if (!manageMarkdownEditIds.includes(id)) { @@ -111,14 +111,17 @@ export const UserActionTree = React.memo( window.clearTimeout(handlerTimeoutId.current); }, 2400); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [handlerTimeoutId.current] + [handlerTimeoutId] ); const handleManageQuote = useCallback( (quote: string) => { const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); - setInsertQuote(`> ${addCarrots} \n`); + + if (addCommentRef && addCommentRef.current) { + addCommentRef.current.addQuote(`> ${addCarrots} \n`); + } + handleOutlineComment('add-comment'); }, [handleOutlineComment] @@ -152,14 +155,13 @@ export const UserActionTree = React.memo( ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [caseData.id, handleUpdate, insertQuote, userCanCrud] + [caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId] ); useEffect(() => { @@ -169,8 +171,7 @@ export const UserActionTree = React.memo( handleOutlineComment(commentId); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [commentId, initLoading, isLoadingUserActions, isLoadingIds]); + }, [commentId, initLoading, isLoadingUserActions, isLoadingIds, handleOutlineComment]); return ( <> Date: Mon, 3 Aug 2020 19:42:28 +0100 Subject: [PATCH 061/115] fix legend's color (#74115) --- .../components/charts/barchart.test.tsx | 65 +++++++++++++++++++ .../common/components/charts/barchart.tsx | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 8617388f4ffb5..64c8fde87a6bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -356,6 +356,71 @@ describe.each(chartDataSets)('BarChart with stackByField', () => { }); }); +describe.each(chartDataSets)('BarChart with custom color', () => { + let wrapper: ReactWrapper; + + const data = [ + { + key: 'python.exe', + value: [ + { + x: 1586754900000, + y: 9675, + g: 'python.exe', + }, + ], + color: '#1EA591', + }, + { + key: 'kernel', + value: [ + { + x: 1586754900000, + y: 8708, + g: 'kernel', + }, + { + x: 1586757600000, + y: 9282, + g: 'kernel', + }, + ], + color: '#000000', + }, + { + key: 'sshd', + value: [ + { + x: 1586754900000, + y: 5907, + g: 'sshd', + }, + ], + color: '#ffffff', + }, + ]; + + const expectedColors = ['#1EA591', '#000000', '#ffffff']; + + const stackByField = 'process.name'; + + beforeAll(() => { + wrapper = mount( + + + + + + ); + }); + + expectedColors.forEach((color, i) => { + test(`it renders the expected legend color ${color} for legend item ${i}`, () => { + expect(wrapper.find(`div [color="${color}"]`).exists()).toBe(true); + }); + }); +}); + describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', (data) => { let shallowWrapper: ShallowWrapper; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx index fba8c3faa9237..cafb0095431f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx @@ -133,7 +133,7 @@ export const BarChartComponent: React.FC = ({ () => barChart != null && stackByField != null ? barChart.map((d, i) => ({ - color: d.color ?? i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + color: d.color ?? (i < defaultLegendColors.length ? defaultLegendColors[i] : undefined), dataProviderId: escapeDataProviderId( `draggable-legend-item-${uuid.v4()}-${stackByField}-${d.key}` ), From 4d6b8ce45df4a207d071f0f5c049e1962ef1822a Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 3 Aug 2020 15:05:12 -0400 Subject: [PATCH 062/115] [Monitoring] Different messaging for CPU usage alert on cloud (#74082) * Different messaging for cloud * Fix type issues --- .../monitoring/server/alerts/base_alert.ts | 5 +- .../alerts/cluster_health_alert.test.ts | 9 ++-- .../server/alerts/cpu_usage_alert.test.ts | 44 ++++++++++++++++-- .../server/alerts/cpu_usage_alert.ts | 46 ++++++++++--------- ...asticsearch_version_mismatch_alert.test.ts | 9 ++-- .../kibana_version_mismatch_alert.test.ts | 9 ++-- .../alerts/license_expiration_alert.test.ts | 9 ++-- .../logstash_version_mismatch_alert.test.ts | 9 ++-- .../server/alerts/nodes_changed_alert.test.ts | 6 ++- x-pack/plugins/monitoring/server/plugin.ts | 11 ++++- x-pack/plugins/monitoring/server/types.ts | 2 + 11 files changed, 112 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index cac57f599633d..016acf2737f9b 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -50,6 +50,7 @@ export class BaseAlert { protected getLogger!: (...scopes: string[]) => Logger; protected config!: MonitoringConfig; protected kibanaUrl!: string; + protected isCloud: boolean = false; protected defaultParams: CommonAlertParams | {} = {}; public get paramDetails() { return {}; @@ -82,13 +83,15 @@ export class BaseAlert { monitoringCluster: ILegacyCustomClusterClient, getLogger: (...scopes: string[]) => Logger, config: MonitoringConfig, - kibanaUrl: string + kibanaUrl: string, + isCloud: boolean ) { this.getUiSettingsService = getUiSettingsService; this.monitoringCluster = monitoringCluster; this.config = config; this.kibanaUrl = kibanaUrl; this.getLogger = getLogger; + this.isCloud = isCloud; } public getAlertType(): AlertType { diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index f25179fa63c2f..4b083787f58cb 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -112,7 +112,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -175,7 +176,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -223,7 +225,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts index 2596252c92d11..c330e977e53d8 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -116,7 +116,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -214,7 +215,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -286,7 +288,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -352,7 +355,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -436,7 +440,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -564,5 +569,34 @@ describe('CpuUsageAlert', () => { }, ]); }); + + it('should fire with different messaging for cloud', async () => { + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl, + true + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + action: `[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify CPU levels across affected nodes.', + clusterName, + count, + nodes: `${nodeName}:${cpuUsage.toFixed(2)}`, + state: 'firing', + }); + }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 4742f55487045..afe5abcf1ebd7 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -322,29 +322,31 @@ export class CpuUsageAlert extends BaseAlert { ',' )})`; const action = `[${fullActionText}](${url})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, + values: { + count: firingCount, + clusterName: cluster.clusterName, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, + values: { + count: firingCount, + clusterName: cluster.clusterName, + action, + }, + } + ); instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', - { - defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', - { - defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - action, - }, - } - ), + internalShortMessage, + internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage, state: FIRING, nodes: firingNodes, count: firingCount, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 50bf40825c515..ed300c211215b 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -115,7 +115,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -166,7 +167,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -214,7 +216,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 1a76fae9fc420..dd3b37b5755e7 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -118,7 +118,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -168,7 +169,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -216,7 +218,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index 0f677dcc9c120..e2f21b34efe21 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -122,7 +122,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -195,7 +196,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -243,7 +245,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index f29c199b3f1e1..fbb4a01d5b4ed 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -115,7 +115,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -165,7 +166,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -213,7 +215,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index d45d404b38304..4b3e3d2d6cb6d 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -128,7 +128,8 @@ describe('NodesChangedAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -180,7 +181,8 @@ describe('NodesChangedAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 86022a0e863d5..ed091d4b8d7a7 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -126,10 +126,17 @@ export class Plugin { const coreStart = (await core.getStartServices())[0]; return coreStart.uiSettings; }; - + const isCloud = Boolean(plugins.cloud?.isCloudEnabled); const alerts = AlertsFactory.getAll(); for (const alert of alerts) { - alert.initializeAlertType(getUiSettingsService, cluster, this.getLogger, config, kibanaUrl); + alert.initializeAlertType( + getUiSettingsService, + cluster, + this.getLogger, + config, + kibanaUrl, + isCloud + ); plugins.alerts.registerType(alert.getAlertType()); } diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 1e7a5acb33644..a0ef6d3e2d984 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -17,6 +17,7 @@ import { InfraPluginSetup } from '../../infra/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; +import { CloudSetup } from '../../cloud/server'; export interface MonitoringLicenseService { refresh: () => Promise; @@ -44,6 +45,7 @@ export interface PluginsSetup { features: FeaturesPluginSetupContract; alerts: AlertingPluginSetupContract; infra: InfraPluginSetup; + cloud: CloudSetup; } export interface PluginsStart { From 4758cb1b4dcf7d7583b01f2f23ddb4746bf94a2c Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 3 Aug 2020 12:17:38 -0700 Subject: [PATCH 063/115] [Search] Fix telemetry collection to not send extra requests (#73382) * [Search] Fix telemetry collection to not send extra requests * Update tracker to collect duration from ES response * Fix type check * Update docs and types * Fix typescript Co-authored-by: Liza K --- ...-plugins-data-server.isearchsetup.usage.md | 2 +- .../kibana-plugin-plugins-data-server.md | 2 + ...-plugin-plugins-data-server.searchusage.md | 19 +++++++ ...gins-data-server.searchusage.trackerror.md | 15 ++++++ ...ns-data-server.searchusage.tracksuccess.md | 22 ++++++++ ...lugin-plugins-data-server.usageprovider.md | 22 ++++++++ .../collectors/create_usage_collector.test.ts | 14 ------ .../collectors/create_usage_collector.ts | 16 ------ .../data/public/search/collectors/types.ts | 2 - .../data/public/search/search_interceptor.ts | 9 +--- src/plugins/data/server/index.ts | 2 + .../data/server/search/collectors/index.ts | 20 ++++++++ .../data/server/search/collectors/routes.ts | 50 ------------------- .../data/server/search/collectors/usage.ts | 23 ++++----- .../search/es_search/es_search_strategy.ts | 27 ++++++---- src/plugins/data/server/search/index.ts | 2 + .../data/server/search/search_service.ts | 12 +++-- src/plugins/data/server/search/types.ts | 2 +- src/plugins/data/server/server.api.md | 33 ++++++++---- x-pack/plugins/data_enhanced/kibana.json | 2 +- .../public/search/search_interceptor.test.ts | 3 -- .../public/search/search_interceptor.ts | 3 -- x-pack/plugins/data_enhanced/server/plugin.ts | 15 ++++-- .../server/search/es_search_strategy.ts | 39 +++++++++++++-- 24 files changed, 214 insertions(+), 142 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md create mode 100644 src/plugins/data/server/search/collectors/index.ts delete mode 100644 src/plugins/data/server/search/collectors/routes.ts diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md index 85abd9d9dba98..1a94a709cc214 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md @@ -9,5 +9,5 @@ Used internally for telemetry Signature: ```typescript -usage: SearchUsage; +usage?: SearchUsage; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 6bf481841f334..1bcd575803f88 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -27,6 +27,7 @@ | [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally | | [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | | +| [usageProvider(core)](./kibana-plugin-plugins-data-server.usageprovider.md) | | ## Interfaces @@ -49,6 +50,7 @@ | [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) | | | [Query](./kibana-plugin-plugins-data-server.query.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-server.refreshinterval.md) | | +| [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) | | | [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | ## Variables diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md new file mode 100644 index 0000000000000..d867509e915b6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) + +## SearchUsage interface + +Signature: + +```typescript +export interface SearchUsage +``` + +## Methods + +| Method | Description | +| --- | --- | +| [trackError()](./kibana-plugin-plugins-data-server.searchusage.trackerror.md) | | +| [trackSuccess(duration)](./kibana-plugin-plugins-data-server.searchusage.tracksuccess.md) | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md new file mode 100644 index 0000000000000..212133588f62d --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) > [trackError](./kibana-plugin-plugins-data-server.searchusage.trackerror.md) + +## SearchUsage.trackError() method + +Signature: + +```typescript +trackError(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md new file mode 100644 index 0000000000000..b58f440c7dccf --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) > [trackSuccess](./kibana-plugin-plugins-data-server.searchusage.tracksuccess.md) + +## SearchUsage.trackSuccess() method + +Signature: + +```typescript +trackSuccess(duration: number): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| duration | number | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md new file mode 100644 index 0000000000000..ad5c61b5c85a1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [usageProvider](./kibana-plugin-plugins-data-server.usageprovider.md) + +## usageProvider() function + +Signature: + +```typescript +export declare function usageProvider(core: CoreSetup): SearchUsage; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | + +Returns: + +`SearchUsage` + diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts index a9ca9efb8b7e1..aaaac5ae6ff7c 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts @@ -90,18 +90,4 @@ describe('Search Usage Collector', () => { SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT ); }); - - test('tracks response errors', async () => { - const duration = 10; - await usageCollector.trackError(duration); - expect(mockCoreSetup.http.post).toBeCalled(); - expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage'); - }); - - test('tracks response duration', async () => { - const duration = 5; - await usageCollector.trackSuccess(duration); - expect(mockCoreSetup.http.post).toBeCalled(); - expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage'); - }); }); diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.ts b/src/plugins/data/public/search/collectors/create_usage_collector.ts index cb1b2b65c17c8..7adb0c3caa675 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.ts @@ -72,21 +72,5 @@ export const createUsageCollector = ( SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT ); }, - trackError: async (duration: number) => { - return core.http.post('/api/search/usage', { - body: JSON.stringify({ - eventType: 'error', - duration, - }), - }); - }, - trackSuccess: async (duration: number) => { - return core.http.post('/api/search/usage', { - body: JSON.stringify({ - eventType: 'success', - duration, - }), - }); - }, }; }; diff --git a/src/plugins/data/public/search/collectors/types.ts b/src/plugins/data/public/search/collectors/types.ts index bb85532fd3ab5..3e98f901eb0c3 100644 --- a/src/plugins/data/public/search/collectors/types.ts +++ b/src/plugins/data/public/search/collectors/types.ts @@ -31,6 +31,4 @@ export interface SearchUsageCollector { trackLongQueryPopupShown: () => Promise; trackLongQueryDialogDismissed: () => Promise; trackLongQueryRunBeyondTimeout: () => Promise; - trackError: (duration: number) => Promise; - trackSuccess: (duration: number) => Promise; } diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 84e24114a9e6c..21586374d1e51 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -18,7 +18,7 @@ */ import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs'; -import { finalize, filter, tap } from 'rxjs/operators'; +import { finalize, filter } from 'rxjs/operators'; import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public'; import { getCombinedSignal, AbortError } from '../../common/utils'; import { IEsSearchRequest, IEsSearchResponse } from '../../common/search'; @@ -123,13 +123,6 @@ export class SearchInterceptor { this.pendingCount$.next(++this.pendingCount); return this.runSearch(request, combinedSignal).pipe( - tap({ - next: (e) => { - if (this.deps.usageCollector) { - this.deps.usageCollector.trackSuccess(e.rawResponse.took); - } - }, - }), finalize(() => { this.pendingCount$.next(--this.pendingCount); cleanup(); diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 461b21e1cc980..1f3d7fbcb9f0f 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -170,6 +170,8 @@ export { ISearchStart, getDefaultSearchParams, getTotalLoaded, + usageProvider, + SearchUsage, } from './search'; // Search namespace diff --git a/src/plugins/data/server/search/collectors/index.ts b/src/plugins/data/server/search/collectors/index.ts new file mode 100644 index 0000000000000..417dc1c2012d3 --- /dev/null +++ b/src/plugins/data/server/search/collectors/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { usageProvider, SearchUsage } from './usage'; diff --git a/src/plugins/data/server/search/collectors/routes.ts b/src/plugins/data/server/search/collectors/routes.ts deleted file mode 100644 index 38fb517e3c3f6..0000000000000 --- a/src/plugins/data/server/search/collectors/routes.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { schema } from '@kbn/config-schema'; -import { CoreSetup } from '../../../../../core/server'; -import { DataPluginStart } from '../../plugin'; -import { SearchUsage } from './usage'; - -export function registerSearchUsageRoute( - core: CoreSetup, - usage: SearchUsage -): void { - const router = core.http.createRouter(); - - router.post( - { - path: '/api/search/usage', - validate: { - body: schema.object({ - eventType: schema.string(), - duration: schema.number(), - }), - }, - }, - async (context, request, res) => { - const { eventType, duration } = request.body; - - if (eventType === 'success') usage.trackSuccess(duration); - if (eventType === 'error') usage.trackError(duration); - - return res.ok(); - } - ); -} diff --git a/src/plugins/data/server/search/collectors/usage.ts b/src/plugins/data/server/search/collectors/usage.ts index c43c572c2edbb..e1be92aa13c37 100644 --- a/src/plugins/data/server/search/collectors/usage.ts +++ b/src/plugins/data/server/search/collectors/usage.ts @@ -18,19 +18,18 @@ */ import { CoreSetup } from 'kibana/server'; -import { DataPluginStart } from '../../plugin'; import { Usage } from './register'; const SAVED_OBJECT_ID = 'search-telemetry'; export interface SearchUsage { - trackError(duration: number): Promise; + trackError(): Promise; trackSuccess(duration: number): Promise; } -export function usageProvider(core: CoreSetup): SearchUsage { +export function usageProvider(core: CoreSetup): SearchUsage { const getTracker = (eventType: keyof Usage) => { - return async (duration: number) => { + return async (duration?: number) => { const repository = await core .getStartServices() .then(([coreStart]) => coreStart.savedObjects.createInternalRepository()); @@ -52,17 +51,17 @@ export function usageProvider(core: CoreSetup): SearchU attributes[eventType]++; - const averageDuration = - (duration + (attributes.averageDuration ?? 0)) / - ((attributes.errorCount ?? 0) + (attributes.successCount ?? 0)); - - const newAttributes = { ...attributes, averageDuration }; + // Only track the average duration for successful requests + if (eventType === 'successCount') { + attributes.averageDuration = + ((duration ?? 0) + (attributes.averageDuration ?? 0)) / (attributes.successCount ?? 1); + } try { if (doesSavedObjectExist) { - await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, newAttributes); + await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, attributes); } else { - await repository.create(SAVED_OBJECT_ID, newAttributes, { id: SAVED_OBJECT_ID }); + await repository.create(SAVED_OBJECT_ID, attributes, { id: SAVED_OBJECT_ID }); } } catch (e) { // Version conflict error, swallow @@ -71,7 +70,7 @@ export function usageProvider(core: CoreSetup): SearchU }; return { - trackError: getTracker('errorCount'), + trackError: () => getTracker('errorCount')(), trackSuccess: getTracker('successCount'), }; } diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index b8010f735c327..78ead6df1a44e 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -20,11 +20,13 @@ import { first } from 'rxjs/operators'; import { SharedGlobalConfig, Logger } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { Observable } from 'rxjs'; +import { SearchUsage } from '../collectors/usage'; import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded } from '..'; export const esSearchStrategyProvider = ( config$: Observable, - logger: Logger + logger: Logger, + usage?: SearchUsage ): ISearchStrategy => { return { search: async (context, request, options) => { @@ -43,15 +45,22 @@ export const esSearchStrategyProvider = ( ...request.params, }; - const rawResponse = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'search', - params, - options - )) as SearchResponse; + try { + const rawResponse = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( + 'search', + params, + options + )) as SearchResponse; - // The above query will either complete or timeout and throw an error. - // There is no progress indication on this api. - return { rawResponse, ...getTotalLoaded(rawResponse._shards) }; + if (usage) usage.trackSuccess(rawResponse.took); + + // The above query will either complete or timeout and throw an error. + // There is no progress indication on this api. + return { rawResponse, ...getTotalLoaded(rawResponse._shards) }; + } catch (e) { + if (usage) usage.trackError(); + throw e; + } }, }; }; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 67789fcbf56b4..cea2714671f0b 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -20,3 +20,5 @@ export { ISearchStrategy, ISearchOptions, ISearchSetup, ISearchStart } from './types'; export { getDefaultSearchParams, getTotalLoaded } from './es_search'; + +export { usageProvider, SearchUsage } from './collectors'; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index bbd0671754749..9dc47369567af 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -32,7 +32,6 @@ import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; import { searchTelemetry } from '../saved_objects'; -import { registerSearchUsageRoute } from './collectors/routes'; import { IEsSearchRequest } from '../../common'; interface StrategyMap { @@ -51,9 +50,15 @@ export class SearchService implements Plugin { core: CoreSetup, { usageCollection }: { usageCollection?: UsageCollectionSetup } ): ISearchSetup { + const usage = usageCollection ? usageProvider(core) : undefined; + this.registerSearchStrategy( ES_SEARCH_STRATEGY, - esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$, this.logger) + esSearchStrategyProvider( + this.initializerContext.config.legacy.globalConfig$, + this.logger, + usage + ) ); core.savedObjects.registerType(searchTelemetry); @@ -61,10 +66,7 @@ export class SearchService implements Plugin { registerUsageCollector(usageCollection, this.initializerContext); } - const usage = usageProvider(core); - registerSearchRoute(core); - registerSearchUsageRoute(core, usage); return { registerSearchStrategy: this.registerSearchStrategy, usage }; } diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 25dc890e0257d..76afd7e8c951c 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -40,7 +40,7 @@ export interface ISearchSetup { /** * Used internally for telemetry */ - usage: SearchUsage; + usage?: SearchUsage; } export interface ISearchStart { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index d35a6a5bbb9a9..013034c79d3f3 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -32,6 +32,7 @@ import { ClusterRerouteParams } from 'elasticsearch'; import { ClusterStateParams } from 'elasticsearch'; import { ClusterStatsParams } from 'elasticsearch'; import { ConfigOptions } from 'elasticsearch'; +import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CountParams } from 'elasticsearch'; import { CreateDocumentParams } from 'elasticsearch'; import { DeleteDocumentByQueryParams } from 'elasticsearch'; @@ -537,8 +538,7 @@ export interface ISearchOptions { // @public (undocumented) export interface ISearchSetup { registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; - // Warning: (ae-forgotten-export) The symbol "SearchUsage" needs to be exported by the entry point index.d.ts - usage: SearchUsage; + usage?: SearchUsage; } // Warning: (ae-missing-release-tag) "ISearchStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -727,6 +727,16 @@ export const search: { }; }; +// Warning: (ae-missing-release-tag) "SearchUsage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SearchUsage { + // (undocumented) + trackError(): Promise; + // (undocumented) + trackSuccess(duration: number): Promise; +} + // Warning: (ae-missing-release-tag) "shouldReadFieldFromDocValues" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -778,6 +788,11 @@ export const UI_SETTINGS: { readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; }; +// Warning: (ae-missing-release-tag) "usageProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function usageProvider(core: CoreSetup_2): SearchUsage; + // Warnings were encountered during analysis: // @@ -802,13 +817,13 @@ export const UI_SETTINGS: { // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:178:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:179:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:180:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:181:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:182:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:183:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:180:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:181:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:182:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:183:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index f0baa84afca32..637af39339e27 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -8,7 +8,7 @@ "requiredPlugins": [ "data" ], - "optionalPlugins": ["kibanaReact", "kibanaUtils"], + "optionalPlugins": ["kibanaReact", "kibanaUtils", "usageCollection"], "server": true, "ui": true, "requiredBundles": ["kibanaReact", "kibanaUtils"] diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 9bd1ffddeaca8..639f56d0cafca 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -52,8 +52,6 @@ describe('EnhancedSearchInterceptor', () => { trackLongQueryPopupShown: jest.fn(), trackLongQueryDialogDismissed: jest.fn(), trackLongQueryRunBeyondTimeout: jest.fn(), - trackError: jest.fn(), - trackSuccess: jest.fn(), }; searchInterceptor = new EnhancedSearchInterceptor( @@ -458,7 +456,6 @@ describe('EnhancedSearchInterceptor', () => { expect(next.mock.calls[1][0]).toStrictEqual(timedResponses[1].value); expect(error).not.toHaveBeenCalled(); expect(mockUsageCollector.trackLongQueryRunBeyondTimeout).toBeCalledTimes(1); - expect(mockUsageCollector.trackSuccess).toBeCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index d1ed410065248..927dc91f365b7 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -89,9 +89,6 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { // If the response indicates it is complete, stop polling and complete the observable if (!response.is_running) { - if (this.deps.usageCollector && response.rawResponse) { - this.deps.usageCollector.trackSuccess(response.rawResponse.took); - } return EMPTY; } diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 9c3a0edf7e733..0e9731a414119 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -12,11 +12,17 @@ import { Logger, } from '../../../../src/core/server'; import { ES_SEARCH_STRATEGY } from '../../../../src/plugins/data/common'; -import { PluginSetup as DataPluginSetup } from '../../../../src/plugins/data/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, + usageProvider, +} from '../../../../src/plugins/data/server'; import { enhancedEsSearchStrategyProvider } from './search'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; interface SetupDependencies { data: DataPluginSetup; + usageCollection?: UsageCollectionSetup; } export class EnhancedDataServerPlugin implements Plugin { @@ -26,12 +32,15 @@ export class EnhancedDataServerPlugin implements Plugin, deps: SetupDependencies) { + const usage = deps.usageCollection ? usageProvider(core) : undefined; + deps.data.search.registerSearchStrategy( ES_SEARCH_STRATEGY, enhancedEsSearchStrategyProvider( this.initializerContext.config.legacy.globalConfig$, - this.logger + this.logger, + usage ) ); } diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 1f7b6a5f9aace..d2a8384b1f882 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -19,20 +19,34 @@ import { getDefaultSearchParams, getTotalLoaded, ISearchStrategy, + SearchUsage, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; import { shimHitsTotal } from './shim_hits_total'; +import { IEsSearchResponse } from '../../../../../src/plugins/data/common/search/es_search'; -export interface AsyncSearchResponse { +interface AsyncSearchResponse { id: string; is_partial: boolean; is_running: boolean; response: SearchResponse; } +interface EnhancedEsSearchResponse extends IEsSearchResponse { + is_partial: boolean; + is_running: boolean; +} + +function isEnhancedEsSearchResponse( + response: IEsSearchResponse +): response is EnhancedEsSearchResponse { + return response.hasOwnProperty('is_partial') && response.hasOwnProperty('is_running'); +} + export const enhancedEsSearchStrategyProvider = ( config$: Observable, - logger: Logger + logger: Logger, + usage?: SearchUsage ): ISearchStrategy => { const search = async ( context: RequestHandlerContext, @@ -45,9 +59,24 @@ export const enhancedEsSearchStrategyProvider = ( const defaultParams = getDefaultSearchParams(config); const params = { ...defaultParams, ...request.params }; - return request.indexType === 'rollup' - ? rollupSearch(caller, { ...request, params }, options) - : asyncSearch(caller, { ...request, params }, options); + try { + const response = + request.indexType === 'rollup' + ? await rollupSearch(caller, { ...request, params }, options) + : await asyncSearch(caller, { ...request, params }, options); + + if ( + usage && + (!isEnhancedEsSearchResponse(response) || (!response.is_partial && !response.is_running)) + ) { + usage.trackSuccess(response.rawResponse.took); + } + + return response; + } catch (e) { + if (usage) usage.trackError(); + throw e; + } }; const cancel = async (context: RequestHandlerContext, id: string) => { From 18e2daa2fb55afb8294cf6c0ca24d69bf09710dd Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Mon, 3 Aug 2020 21:19:42 +0200 Subject: [PATCH 064/115] Fix API Keys functional test in cloud (#74075) * check admin description * split test logic --- x-pack/test/functional/apps/api_keys/home_page.ts | 15 +++++++++++---- .../test/functional/page_objects/api_keys_page.ts | 4 ++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 553cceef573e1..0c4097a1d5c4e 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -11,6 +11,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'apiKeys']); const log = getService('log'); const security = getService('security'); + const testSubjects = getService('testSubjects'); describe('Home page', function () { before(async () => { @@ -32,10 +33,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('Loads the app', async () => { await security.testUser.setRoles(['test_api_keys']); log.debug('Checking for section header'); - const headerText = await pageObjects.apiKeys.noAPIKeysHeading(); - expect(headerText).to.be('No API keys'); - const goToConsoleButton = await pageObjects.apiKeys.getGoToConsoleButton(); - expect(await goToConsoleButton.isDisplayed()).to.be(true); + const headers = await testSubjects.findAll('noApiKeysHeader'); + if (headers.length > 0) { + expect(await headers[0].getVisibleText()).to.be('No API keys'); + const goToConsoleButton = await pageObjects.apiKeys.getGoToConsoleButton(); + expect(await goToConsoleButton.isDisplayed()).to.be(true); + } else { + // page may already contain EiTable with data, then check API Key Admin text + const description = await pageObjects.apiKeys.getApiKeyAdminDesc(); + expect(description).to.be('You are an API Key administrator.'); + } }); }); }; diff --git a/x-pack/test/functional/page_objects/api_keys_page.ts b/x-pack/test/functional/page_objects/api_keys_page.ts index 17f4df74921bc..fa10c5a574c09 100644 --- a/x-pack/test/functional/page_objects/api_keys_page.ts +++ b/x-pack/test/functional/page_objects/api_keys_page.ts @@ -14,6 +14,10 @@ export function ApiKeysPageProvider({ getService }: FtrProviderContext) { return await testSubjects.getVisibleText('noApiKeysHeader'); }, + async getApiKeyAdminDesc() { + return await testSubjects.getVisibleText('apiKeyAdminDescriptionCallOut'); + }, + async getGoToConsoleButton() { return await testSubjects.find('goToConsoleButton'); }, From 447db183e94fac36026bb27f69f3e44ac88fa694 Mon Sep 17 00:00:00 2001 From: Eric Davis Date: Mon, 3 Aug 2020 15:56:07 -0400 Subject: [PATCH 065/115] Change / to \ for Windows Enrollment command in Fleet (#74132) --- .../components/enrollment_instructions/manual/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx index a77de9369277b..8ea236b2dd6c3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx @@ -36,7 +36,7 @@ export const ManualInstructions: React.FunctionComponent = ({ systemctl enable elastic-agent systemctl start elastic-agent`; - const windowsCommand = `./elastic-agent enroll ${enrollArgs} + const windowsCommand = `.\elastic-agent enroll ${enrollArgs} ./install-service-elastic-agent.ps1`; return ( From 60494255e898ce69712b825299d8da55535588f2 Mon Sep 17 00:00:00 2001 From: debadair Date: Mon, 3 Aug 2020 14:29:47 -0700 Subject: [PATCH 066/115] [DOCS] Remove the ILM tutorial, which is now in the ES Ref (#73580) --- .../example-index-lifecycle-policy.asciidoc | 179 ------------------ docs/user/management.asciidoc | 2 - 2 files changed, 181 deletions(-) delete mode 100644 docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc diff --git a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc b/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc deleted file mode 100644 index 0097bf8c648f0..0000000000000 --- a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc +++ /dev/null @@ -1,179 +0,0 @@ -[role="xpack"] - -[[example-using-index-lifecycle-policy]] -=== Tutorial: Use {ilm-init} to manage {filebeat} time-based indices - -With {ilm} ({ilm-init}), you can create policies that perform actions automatically -on indices as they age and grow. {ilm-init} policies help you to manage -performance, resilience, and retention of your data during its lifecycle. This tutorial shows -you how to use {kib}’s *Index Lifecycle Policies* to modify and create {ilm-init} -policies. You can learn more about all of the actions, benefits, and lifecycle -phases in the {ref}/overview-index-lifecycle-management.html[{ilm-init} overview]. - - -[discrete] -[[example-using-index-lifecycle-policy-scenario]] -==== Scenario - -You’re tasked with sending syslog files to an {es} cluster. This -log data has the following data retention guidelines: - -* Keep logs on hot data nodes for 30 days -* Roll over to a new index if the size reaches 50GB -* After 30 days: -** Move the logs to warm data nodes -** Set {ref}/glossary.html#glossary-replica-shard[replica shards] to 1 -** {ref}/indices-forcemerge.html[Force merge] multiple index segments to free up the space used by deleted documents -* Delete logs after 90 days - - -[discrete] -[[example-using-index-lifecycle-policy-prerequisites]] -==== Prerequisites - -To complete this tutorial, you'll need: - -* An {es} cluster with hot and warm nodes configured for shard allocation -awareness. If you’re using {cloud}/ec-getting-started-templates-hot-warm.html[{ess}], -choose the hot-warm architecture deployment template. - -+ -For a self-managed cluster, add node attributes as described for {ref}/shard-allocation-filtering.html[shard allocation filtering] -to label data nodes as hot or warm. This step is required to migrate shards between -nodes configured with specific hardware for the hot or warm phases. -+ -For example, you can set this in your `elasticsearch.yml` for each data node: -+ -[source,yaml] --------------------------------------------------------------------------------- -node.attr.data: "warm" --------------------------------------------------------------------------------- - -* A server with {filebeat} installed and configured to send logs to the `elasticsearch` -output as described in {filebeat-ref}/filebeat-getting-started.html[Getting Started with {filebeat}]. - -[discrete] -[[example-using-index-lifecycle-policy-view-fb-ilm-policy]] -==== View the {filebeat} {ilm-init} policy - -{filebeat} includes a default {ilm-init} policy that enables rollover. {ilm-init} -is enabled automatically if you’re using the default `filebeat.yml` and index template. - -To view the default policy in {kib}, open the menu, go to *Stack Management > Data > Index Lifecycle Policies*, -search for _filebeat_, and choose the _filebeat-version_ policy. - -This policy initiates the rollover action when the index size reaches 50GB or -becomes 30 days old. - -[role="screenshot"] -image::images/tutorial-ilm-hotphaserollover-default.png["Default policy"] - - -[float] -==== Modify the policy - -The default policy is enough to prevent the creation of many tiny daily indices. -You can modify the policy to meet more complex requirements. - -. Activate the warm phase. - -+ -. Set either of the following options to control when the index moves to the warm phase: - -** Provide a value for *Timing for warm phase*. Setting this to *15* keeps the -indices on hot nodes for a range of 15-45 days, depending on when the initial -rollover occurred. - -** Enable *Move to warm phase on rollover*. The index might move to the warm phase -more quickly than intended if it reaches the *Maximum index size* before the -the *Maximum age*. - -. In the *Select a node attribute to control shard allocation* dropdown, select -*data:warm(2)* to migrate shards to warm data nodes. - -. Change *Number of replicas* to *1*. - -. Enable *Force merge data* and set *Number of segments* to *1*. -+ -NOTE: When rollover is enabled in the hot phase, action timing in the other phases -is based on the rollover date. - -+ -[role="screenshot"] -image::images/tutorial-ilm-modify-default-warm-phase-rollover.png["Modify to add warm phase"] - -. Activate the delete phase and set *Timing for delete phase* to *90* days. -+ -[role="screenshot"] -image::images/tutorial-ilm-delete-rollover.png["Add a delete phase"] - -[float] -==== Create a custom policy - -If meeting a specific retention time period is most important, you can create a -custom policy. For this option, you will use {filebeat} daily indices without -rollover. - -. To create a custom policy, open the menu, go to *Stack Management > Data > Index Lifecycle Policies*, then click -*Create policy*. - -. Activate the warm phase and configure it as follows: -+ -|=== -|*Setting* |*Value* - -|Timing for warm phase -|30 days from index creation - -|Node attribute -|`data:warm` - -|Number of replicas -|1 - -|Force merge data -|enable - -|Number of segments -|1 -|=== - -+ -[role="screenshot"] -image::images/tutorial-ilm-custom-policy.png["Modify the custom policy to add a warm phase"] - - -+ -. Activate the delete phase and set the timing. -+ -|=== -|*Setting* |*Value* -|Timing for delete phase -|90 -|=== - -+ -[role="screenshot"] -image::images/tutorial-ilm-delete-phase-creation.png["Delete phase"] - -. To configure the index to use the new policy, open the menu, then go to *Stack Management > Data > Index Lifecycle -Policies*. - -.. Find your {ilm-init} policy. -.. Click the *Actions* link next to your policy name. -.. Choose *Add policy to index template*. -.. Select your {filebeat} index template name from the *Index template* list. For example, `filebeat-7.5.x`. -.. Click *Add Policy* to save the changes. - -+ -NOTE: If you initially used the default {filebeat} {ilm-init} policy, you will -see a notice that the template already has a policy associated with it. Confirm -that you want to overwrite that configuration. - -+ -+ -TIP: When you change the policy associated with the index template, the active -index will continue to use the policy it was associated with at index creation -unless you manually update it. The next new index will use the updated policy. -For more reasons that your {ilm-init} policy changes might be delayed, see -{ref}/update-lifecycle-policy.html#update-lifecycle-policy[Update Lifecycle Policy]. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 1704a80847652..bc96463f6efba 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -190,8 +190,6 @@ include::{kib-repo-dir}/management/index-lifecycle-policies/manage-policy.asciid include::{kib-repo-dir}/management/index-lifecycle-policies/add-policy-to-index.asciidoc[] -include::{kib-repo-dir}/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc[] - include::{kib-repo-dir}/management/managing-indices.asciidoc[] include::{kib-repo-dir}/management/ingest-pipelines/ingest-pipelines.asciidoc[] From 06412ae770820e6c6049e56aaccc1119d6e6f156 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Mon, 3 Aug 2020 15:43:34 -0600 Subject: [PATCH 067/115] Migrate timepicker:timeDefaults ui setting to the data plugin. (#73750) --- .../kibana/server/ui_setting_defaults.js | 14 -------------- .../state_sync/connect_to_query_state.test.ts | 2 +- .../query/timefilter/timefilter_service.ts | 2 +- .../query_bar_top_row.test.tsx | 2 +- .../query_string_input/query_bar_top_row.tsx | 2 +- src/plugins/data/server/ui_settings.ts | 18 ++++++++++++++++++ .../lens/public/app_plugin/app.test.tsx | 2 +- x-pack/plugins/monitoring/public/plugin.ts | 2 +- .../translations/translations/ja-JP.json | 6 +++--- .../translations/translations/zh-CN.json | 4 ++-- 10 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index e1dadb0a24de1..625c2c02510db 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -258,20 +258,6 @@ export function getUiSettingDefaults() { 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', }), }, - 'timepicker:timeDefaults': { - name: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsTitle', { - defaultMessage: 'Time filter defaults', - }), - value: `{ - "from": "now-15m", - "to": "now" -}`, - type: 'json', - description: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsText', { - defaultMessage: 'The timefilter selection to use when Kibana is started without one', - }), - requiresPageReload: true, - }, 'theme:darkMode': { name: i18n.translate('kbn.advancedSettings.darkModeTitle', { defaultMessage: 'Dark mode', 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 307d1fe1b2b0b..2053e0b94b21a 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 @@ -50,7 +50,7 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { return true; case UI_SETTINGS.SEARCH_QUERY_LANGUAGE: return 'kuery'; - case 'timepicker:timeDefaults': + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: return { from: 'now-15m', to: 'now' }; case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index df2fbc8e5a8f3..35b46de5f21b2 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -35,7 +35,7 @@ export interface TimeFilterServiceDependencies { export class TimefilterService { public setup({ uiSettings, storage }: TimeFilterServiceDependencies): TimefilterSetup { const timefilterConfig = { - timeDefaults: uiSettings.get('timepicker:timeDefaults'), + timeDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS), refreshIntervalDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; const history = new TimeHistory(storage); diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index 5f2d4c00cd6b6..879ff6708068e 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -51,7 +51,7 @@ startMock.uiSettings.get.mockImplementation((key: string) => { return 'MMM D, YYYY @ HH:mm:ss.SSS'; case UI_SETTINGS.HISTORY_LIMIT: return 10; - case 'timepicker:timeDefaults': + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: return { from: 'now-15m', to: 'now', diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 86bf30ba0e374..05249d46a1c50 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -94,7 +94,7 @@ export function QueryBarTopRow(props: Props) { } function getDateRange() { - const defaultTimeSetting = uiSettings!.get('timepicker:timeDefaults'); + const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); return { from: props.dateRangeFrom || defaultTimeSetting.from, to: props.dateRangeTo || defaultTimeSetting.to, diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index e825ef7f6c945..763a086d7688d 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -526,6 +526,24 @@ export function getUiSettings(): Record> { value: schema.number(), }), }, + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + name: i18n.translate('data.advancedSettings.timepicker.timeDefaultsTitle', { + defaultMessage: 'Time filter defaults', + }), + value: `{ + "from": "now-15m", + "to": "now" +}`, + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.timeDefaultsText', { + defaultMessage: 'The timefilter selection to use when Kibana is started without one', + }), + requiresPageReload: true, + schema: schema.object({ + from: schema.string(), + to: schema.string(), + }), + }, [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: { name: i18n.translate('data.advancedSettings.timepicker.quickRangesTitle', { defaultMessage: 'Time filter quick ranges', diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index b30a586487009..f92343183a700 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -196,7 +196,7 @@ describe('Lens App', () => { core.uiSettings.get.mockImplementation( jest.fn((type) => { - if (type === 'timepicker:timeDefaults') { + if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { return { from: 'now-7d', to: 'now' }; } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { return 'kuery'; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index cfac5e195a127..88b36b9572fc7 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -128,7 +128,7 @@ export class MonitoringPlugin UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS, JSON.stringify(refreshInterval) ); - uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); + uiSettings.overrideLocalDefault(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS, JSON.stringify(time)); } private getExternalConfig() { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f13aa374e549c..3287352699f2f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -698,6 +698,8 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "時間フィルターのデフォルト更新間隔「値」はミリ秒で指定する必要があります。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "タイムピッカーの更新間隔", "data.advancedSettings.timepicker.thisWeek": "今週", + "data.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", + "data.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", @@ -2791,8 +2793,6 @@ "kbn.advancedSettings.storeUrlTitle": "セッションストレージに URL を格納", "kbn.advancedSettings.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", "kbn.advancedSettings.themeVersionTitle": "テーマバージョン", - "kbn.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", - "kbn.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", @@ -19304,7 +19304,7 @@ "xpack.watcher.models.baseAction.simulateMessage": "アクション {id} のシミュレーションが完了しました", "xpack.watcher.models.baseAction.typeName": "アクション", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "不明なアクションタイプ {type} を作成しようとしました。", - "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", + "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", "xpack.watcher.models.baseWatch.selectMessageText": "新規ウォッチをセットアップします。", "xpack.watcher.models.baseWatch.typeName": "ウォッチ", "xpack.watcher.models.baseWatch.watchJsonPropertyMissingBadRequestMessage": "json 引数には {watchJson} プロパティが含まれている必要があります", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 83e290b8c241a..9598ab33ffa25 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -698,6 +698,8 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "时间筛选的默认刷新时间间隔。需要使用毫秒单位指定“值”。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "时间筛选刷新时间间隔", "data.advancedSettings.timepicker.thisWeek": "本周", + "data.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", + "data.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", @@ -2792,8 +2794,6 @@ "kbn.advancedSettings.storeUrlTitle": "将 URL 存储在会话存储中", "kbn.advancedSettings.themeVersionText": "在用于 Kibana 当前和下一版本的主题间切换。需要刷新页面,才能应用设置。", "kbn.advancedSettings.themeVersionTitle": "主题版本", - "kbn.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", - "kbn.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", From 584804047a96f01d591001bd8313e0996299533e Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 3 Aug 2020 15:13:27 -0700 Subject: [PATCH 068/115] [Search] Send ID in path, rather than body (#62889) * [Search] Send ID in path, rather than body * Fix typo in merge * Update docs * Revert to using POST instead of GET * Revert accidental change Co-authored-by: Elastic Machine --- ...ugin-plugins-data-public.searchinterceptor.md | 2 +- ...ns-data-public.searchinterceptor.runsearch.md | 4 ++-- src/plugins/data/public/public.api.md | 2 +- .../data/public/search/search_interceptor.ts | 16 +++++++--------- src/plugins/data/server/search/routes.ts | 11 +++++++---- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index 4d2fac0287035..30e980b5ffc54 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -35,7 +35,7 @@ export declare class SearchInterceptor | Method | Modifiers | Description | | --- | --- | --- | -| [runSearch(request, combinedSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | +| [runSearch(request, signal)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates the pendingCount when the request is started/finalized. | | [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md index 385d4f6a238d4..3601a00c48cf3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md @@ -7,7 +7,7 @@ Signature: ```typescript -protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Observable; +protected runSearch(request: IEsSearchRequest, signal: AbortSignal): Observable; ``` ## Parameters @@ -15,7 +15,7 @@ protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Obs | Parameter | Type | Description | | --- | --- | --- | | request | IEsSearchRequest | | -| combinedSignal | AbortSignal | | +| signal | AbortSignal | | Returns: diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d6812a4aa4524..2cfdab80123ed 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1661,7 +1661,7 @@ export class SearchInterceptor { // (undocumented) protected readonly requestTimeout?: number | undefined; // (undocumented) - protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Observable; + protected runSearch(request: IEsSearchRequest, signal: AbortSignal): Observable; search(request: IEsSearchRequest, options?: ISearchOptions): Observable; // (undocumented) protected setupTimers(options?: ISearchOptions): { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 21586374d1e51..e6eca16c5ca4b 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -92,16 +92,14 @@ export class SearchInterceptor { protected runSearch( request: IEsSearchRequest, - combinedSignal: AbortSignal + signal: AbortSignal ): Observable { - return from( - this.deps.http.fetch({ - path: `/internal/search/es`, - method: 'POST', - body: JSON.stringify(request), - signal: combinedSignal, - }) - ); + const { id, ...searchRequest } = request; + const path = id != null ? `/internal/search/es/${id}` : '/internal/search/es'; + const method = 'POST'; + const body = JSON.stringify(id != null ? {} : searchRequest); + const response = this.deps.http.fetch({ path, method, body, signal }); + return from(response); } /** diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index bf1982a1f7fb2..32d8f8c1b09e0 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -27,9 +27,12 @@ export function registerSearchRoute(core: CoreSetup): v router.post( { - path: '/internal/search/{strategy}', + path: '/internal/search/{strategy}/{id?}', validate: { - params: schema.object({ strategy: schema.string() }), + params: schema.object({ + strategy: schema.string(), + id: schema.maybe(schema.string()), + }), query: schema.object({}, { unknowns: 'allow' }), @@ -38,13 +41,13 @@ export function registerSearchRoute(core: CoreSetup): v }, async (context, request, res) => { const searchRequest = request.body; - const { strategy } = request.params; + const { strategy, id } = request.params; const signal = getRequestAbortedSignal(request.events.aborted$); const [, , selfStart] = await core.getStartServices(); try { - const response = await selfStart.search.search(context, searchRequest, { + const response = await selfStart.search.search(context, id ? { id } : searchRequest, { signal, strategy, }); From cd1b1b3a9575c4de4b503ef7f3f329b7602c9443 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 3 Aug 2020 16:15:43 -0600 Subject: [PATCH 069/115] [Maps] convert dynamic icon style to TS (#73903) * [Maps] convert dynmaic icon style to TS * tslint * fix snapsshot name changes caused by file name changes * revert change that is not part of PR Co-authored-by: Elastic Machine --- ...test.js.snap => vector_icon.test.tsx.snap} | 0 .../{breaked_legend.js => breaked_legend.tsx} | 44 +++++++--- .../legend/{category.js => category.tsx} | 15 +++- .../{circle_icon.js => circle_icon.tsx} | 4 +- .../legend/{line_icon.js => line_icon.tsx} | 4 +- .../{ordinal_legend.js => ordinal_legend.tsx} | 80 ++++++++++--------- .../{polygon_icon.js => polygon_icon.tsx} | 4 +- .../{symbol_icon.js => symbol_icon.tsx} | 27 ++++--- ...ctor_icon.test.js => vector_icon.test.tsx} | 0 .../{vector_icon.js => vector_icon.tsx} | 19 +++-- ...tyle_legend.js => vector_style_legend.tsx} | 10 ++- .../properties/dynamic_icon_property.test.tsx | 12 ++- ..._property.js => dynamic_icon_property.tsx} | 26 +++--- .../properties/dynamic_style_property.tsx | 14 +--- .../vector/properties/style_property.ts | 8 +- 15 files changed, 161 insertions(+), 106 deletions(-) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/{vector_icon.test.js.snap => vector_icon.test.tsx.snap} (100%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{breaked_legend.js => breaked_legend.tsx} (69%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{category.js => category.tsx} (82%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{circle_icon.js => circle_icon.tsx} (92%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{line_icon.js => line_icon.tsx} (77%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{ordinal_legend.js => ordinal_legend.tsx} (70%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{polygon_icon.js => polygon_icon.tsx} (78%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{symbol_icon.js => symbol_icon.tsx} (83%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{vector_icon.test.js => vector_icon.test.tsx} (100%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{vector_icon.js => vector_icon.tsx} (79%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{vector_style_legend.js => vector_style_legend.tsx} (74%) rename x-pack/plugins/maps/public/classes/styles/vector/properties/{dynamic_icon_property.js => dynamic_icon_property.tsx} (79%) diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx similarity index 69% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx index 7e8e6896ef9ce..9d5bf85005ae1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx @@ -4,35 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component, ReactElement } from 'react'; import _ from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { Category } from './category'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; + const EMPTY_VALUE = ''; -export class BreakedLegend extends React.Component { - state = { +interface Break { + color: string; + label: ReactElement | string; + symbolId: string; +} + +interface Props { + style: IDynamicStyleProperty; + breaks: Break[]; + isLinesOnly: boolean; + isPointsOnly: boolean; +} + +interface State { + label: string; +} + +export class BreakedLegend extends Component { + private _isMounted: boolean = false; + + state: State = { label: EMPTY_VALUE, }; componentDidMount() { this._isMounted = true; - this._loadParams(); + this._loadLabel(); } componentDidUpdate() { - this._loadParams(); + this._loadLabel(); } componentWillUnmount() { this._isMounted = false; } - async _loadParams() { - const label = await this.props.style.getField().getLabel(); - const newState = { label }; - if (this._isMounted && !_.isEqual(this.state, newState)) { - this.setState(newState); + async _loadLabel() { + const field = this.props.style.getField(); + if (!field) { + return; + } + const label = await field.getLabel(); + if (this._isMounted && !_.isEqual(this.state.label, label)) { + this.setState({ label }); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx similarity index 82% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx index cfdbd728c2217..02ca4645dd8cd 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { VECTOR_STYLES } from '../../../../../../common/constants'; +import React, { ReactElement } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { VECTOR_STYLES } from '../../../../../../common/constants'; import { VectorIcon } from './vector_icon'; -export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, symbolId }) { +interface Props { + styleName: VECTOR_STYLES; + label: ReactElement | string; + color: string; + isLinesOnly: boolean; + isPointsOnly: boolean; + symbolId: string; +} + +export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, symbolId }: Props) { function renderIcon() { if (styleName === VECTOR_STYLES.LABEL_COLOR) { return ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx similarity index 92% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx index 5efba64360f23..0056a2cba02cd 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { CSSProperties } from 'react'; -export const CircleIcon = ({ style }) => ( +export const CircleIcon = ({ style }: { style: CSSProperties }) => ( ( +export const LineIcon = ({ style }: { style: CSSProperties }) => ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx similarity index 70% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx index 478d96962e47b..a99548b6af7bf 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Component, Fragment } from 'react'; import _ from 'lodash'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +// @ts-expect-error import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row'; import { VECTOR_STYLES } from '../../../../../../common/constants'; -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { CircleIcon } from './circle_icon'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; function getLineWidthIcons() { const defaultStyle = { @@ -37,41 +39,50 @@ function getSymbolSizeIcons() { } const EMPTY_VALUE = ''; -export class OrdinalLegend extends React.Component { - constructor() { - super(); - this._isMounted = false; - this.state = { - label: EMPTY_VALUE, - }; - } +interface Props { + style: IDynamicStyleProperty; +} - async _loadParams() { - const label = await this.props.style.getField().getLabel(); - const newState = { label }; - if (this._isMounted && !_.isEqual(this.state, newState)) { - this.setState(newState); - } - } +interface State { + label: string; +} - _formatValue(value) { - if (value === EMPTY_VALUE) { - return value; - } - return this.props.style.formatField(value); +export class OrdinalLegend extends Component { + private _isMounted: boolean = false; + + state: State = { + label: EMPTY_VALUE, + }; + + componentDidMount() { + this._isMounted = true; + this._loadLabel(); } componentDidUpdate() { - this._loadParams(); + this._loadLabel(); } componentWillUnmount() { this._isMounted = false; } - componentDidMount() { - this._isMounted = true; - this._loadParams(); + async _loadLabel() { + const field = this.props.style.getField(); + if (!field) { + return; + } + const label = await field.getLabel(); + if (this._isMounted && !_.isEqual(this.state.label, label)) { + this.setState({ label }); + } + } + + _formatValue(value: string | number) { + if (value === EMPTY_VALUE) { + return value; + } + return this.props.style.formatField(value); } _renderRangeLegendHeader() { @@ -115,21 +126,16 @@ export class OrdinalLegend extends React.Component { const fieldMeta = this.props.style.getRangeFieldMeta(); - let minLabel = EMPTY_VALUE; - let maxLabel = EMPTY_VALUE; + let minLabel: string | number = EMPTY_VALUE; + let maxLabel: string | number = EMPTY_VALUE; if (fieldMeta) { - const range = { min: fieldMeta.min, max: fieldMeta.max }; - const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE)); + const min = this._formatValue(_.get(fieldMeta, 'min', EMPTY_VALUE)); minLabel = - this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange - ? `< ${min}` - : min; + this.props.style.isFieldMetaEnabled() && fieldMeta.isMinOutsideStdRange ? `< ${min}` : min; - const max = this._formatValue(_.get(range, 'max', EMPTY_VALUE)); + const max = this._formatValue(_.get(fieldMeta, 'max', EMPTY_VALUE)); maxLabel = - this.props.style.isFieldMetaEnabled() && range && range.isMaxOutsideStdRange - ? `> ${max}` - : max; + this.props.style.isFieldMetaEnabled() && fieldMeta.isMaxOutsideStdRange ? `> ${max}` : max; } return ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx similarity index 78% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx index 4210b59f0d676..09241d538a0fe 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { CSSProperties } from 'react'; -export const PolygonIcon = ({ style }) => ( +export const PolygonIcon = ({ style }: { style: CSSProperties }) => ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx similarity index 83% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx index ea3886c600be9..c5d41ae2b1a9b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx @@ -5,13 +5,24 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - +// @ts-expect-error import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; -export class SymbolIcon extends Component { - state = { - imgDataUrl: undefined, +interface Props { + symbolId: string; + fill?: string; + stroke?: string; +} + +interface State { + imgDataUrl: string | null; +} + +export class SymbolIcon extends Component { + private _isMounted: boolean = false; + + state: State = { + imgDataUrl: null, }; componentDidMount() { @@ -62,9 +73,3 @@ export class SymbolIcon extends Component { ); } } - -SymbolIcon.propTypes = { - symbolId: PropTypes.string.isRequired, - fill: PropTypes.string, - stroke: PropTypes.string, -}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx similarity index 79% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx index e255dceda856e..d68bbdae2c17e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx @@ -5,14 +5,21 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import { CircleIcon } from './circle_icon'; import { LineIcon } from './line_icon'; import { PolygonIcon } from './polygon_icon'; import { SymbolIcon } from './symbol_icon'; -export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, symbolId }) { +interface Props { + fillColor?: string; + isPointsOnly: boolean; + isLinesOnly: boolean; + strokeColor?: string; + symbolId?: string; +} + +export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, symbolId }: Props) { if (isLinesOnly) { const style = { stroke: strokeColor, @@ -44,11 +51,3 @@ export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, /> ); } - -VectorIcon.propTypes = { - fillColor: PropTypes.string, - isPointsOnly: PropTypes.bool.isRequired, - isLinesOnly: PropTypes.bool.isRequired, - strokeColor: PropTypes.string, - symbolId: PropTypes.string, -}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx similarity index 74% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx index 88eb4109627e5..4d50c632bfd6d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx @@ -5,8 +5,16 @@ */ import React from 'react'; +import { IStyleProperty } from '../../properties/style_property'; -export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }) { +interface Props { + isLinesOnly: boolean; + isPointsOnly: boolean; + styles: Array>; + symbolId: string; +} + +export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }: Props) { const legendRows = []; for (let i = 0; i < styles.length; i++) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index 132c0b3f27603..81513818bc0ec 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -21,15 +21,21 @@ import { DynamicIconProperty } from './dynamic_icon_property'; import { mockField, MockLayer } from './__tests__/test_util'; import { IconDynamicOptions } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; +import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; const makeProperty = (options: Partial, field: IField = mockField) => { + const defaultOptions: IconDynamicOptions = { + iconPaletteId: null, + fieldMetaOptions: { isEnabled: false }, + }; + const mockVectorLayer = (new MockLayer() as unknown) as IVectorLayer; return new DynamicIconProperty( - { ...options, fieldMetaOptions: { isEnabled: false } }, + { ...defaultOptions, ...options }, VECTOR_STYLES.ICON, field, - new MockLayer(), + mockVectorLayer, () => { - return (x: string) => x + '_format'; + return (value: string | number | undefined) => value + '_format'; } ); }; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx similarity index 79% rename from x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx index 665317569e5e8..0d152534aba6a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx @@ -6,13 +6,17 @@ import _ from 'lodash'; import React from 'react'; +import { EuiTextColor } from '@elastic/eui'; +import { Map as MbMap } from 'mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; +// @ts-expect-error import { getIconPalette, getMakiIconId, getMakiSymbolAnchor } from '../symbol_utils'; import { BreakedLegend } from '../components/legend/breaked_legend'; import { getOtherCategoryLabel, assignCategoriesToPalette } from '../style_util'; -import { EuiTextColor } from '@elastic/eui'; +import { LegendProps } from './style_property'; +import { IconDynamicOptions } from '../../../../../common/descriptor_types'; -export class DynamicIconProperty extends DynamicStyleProperty { +export class DynamicIconProperty extends DynamicStyleProperty { isOrdinal() { return false; } @@ -26,7 +30,7 @@ export class DynamicIconProperty extends DynamicStyleProperty { return palette.length; } - syncIconWithMb(symbolLayerId, mbMap, iconPixelSize) { + syncIconWithMb(symbolLayerId: string, mbMap: MbMap, iconPixelSize: number) { if (this._isIconDynamicConfigComplete()) { mbMap.setLayoutProperty( symbolLayerId, @@ -64,11 +68,11 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); } - _getMbIconImageExpression(iconPixelSize) { + _getMbIconImageExpression(iconPixelSize: number) { const { stops, fallbackSymbolId } = this._getPaletteStops(); if (stops.length < 1 || !fallbackSymbolId) { - //occurs when no data + // occurs when no data return null; } @@ -79,16 +83,16 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); if (fallbackSymbolId) { - mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); //last item is fallback style for anything that does not match provided stops + mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); // last item is fallback style for anything that does not match provided stops } - return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; + return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops]; } _getMbIconAnchorExpression() { const { stops, fallbackSymbolId } = this._getPaletteStops(); if (stops.length < 1 || !fallbackSymbolId) { - //occurs when no data + // occurs when no data return null; } @@ -99,16 +103,16 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); if (fallbackSymbolId) { - mbStops.push(getMakiSymbolAnchor(fallbackSymbolId)); //last item is fallback style for anything that does not match provided stops + mbStops.push(getMakiSymbolAnchor(fallbackSymbolId)); // last item is fallback style for anything that does not match provided stops } - return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; + return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops]; } _isIconDynamicConfigComplete() { return this._field && this._field.isValid(); } - renderLegendDetailRow({ isPointsOnly, isLinesOnly }) { + renderLegendDetailRow({ isPointsOnly, isLinesOnly }: LegendProps) { const { stops, fallbackSymbolId } = this._getPaletteStops(); const breaks = []; stops.forEach(({ stop, style }) => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index 216fde595af32..f666a9aad7804 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -46,7 +46,7 @@ export interface IDynamicStyleProperty extends IStyleProperty { getValueSuggestions(query: string): Promise; } -type fieldFormatter = (value: string | undefined) => string; +type fieldFormatter = (value: string | number | undefined) => string | number; export class DynamicStyleProperty extends AbstractStyleProperty implements IDynamicStyleProperty { @@ -69,12 +69,10 @@ export class DynamicStyleProperty extends AbstractStyleProperty this._getFieldFormatter = getFieldFormatter; } - // ignore TS error about "Type '(query: string) => Promise | never[]' is not assignable to type '(query: string) => Promise'." - // @ts-expect-error - getValueSuggestions = (query: string) => { + getValueSuggestions = async (query: string) => { return this._field === null ? [] - : this._field.getSource().getValueSuggestions(this._field, query); + : await this._field.getSource().getValueSuggestions(this._field, query); }; _getStyleMetaDataRequestId(fieldName: string) { @@ -306,7 +304,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty }; } - formatField(value: string | undefined): string { + formatField(value: string | number | undefined): string | number { if (this.getField()) { const fieldName = this.getFieldName(); const fieldFormatter = this._getFieldFormatter(fieldName); @@ -316,10 +314,6 @@ export class DynamicStyleProperty extends AbstractStyleProperty } } - renderLegendDetailRow() { - return null; - } - renderFieldMetaPopover(onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void) { if (!this.supportsFieldMeta()) { return null; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts index 7a0ed4fb3e962..ec52f6a0f7283 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts @@ -11,7 +11,7 @@ import { getVectorStyleLabel } from '../components/get_vector_style_label'; import { FieldMetaOptions } from '../../../../../common/descriptor_types'; import { VECTOR_STYLES } from '../../../../../common/constants'; -type LegendProps = { +export type LegendProps = { isPointsOnly: boolean; isLinesOnly: boolean; symbolId?: string; @@ -20,7 +20,7 @@ type LegendProps = { export interface IStyleProperty { isDynamic(): boolean; isComplete(): boolean; - formatField(value: string | undefined): string; + formatField(value: string | number | undefined): string | number; getStyleName(): VECTOR_STYLES; getOptions(): T; renderLegendDetailRow(legendProps: LegendProps): ReactElement | null; @@ -53,7 +53,7 @@ export class AbstractStyleProperty implements IStyleProperty { return true; } - formatField(value: string | undefined): string { + formatField(value: string | number | undefined): string | number { // eslint-disable-next-line eqeqeq return value == undefined ? '' : value; } @@ -66,7 +66,7 @@ export class AbstractStyleProperty implements IStyleProperty { return this._options; } - renderLegendDetailRow() { + renderLegendDetailRow({ isPointsOnly, isLinesOnly }: LegendProps): ReactElement | null { return null; } From e00d7d279ade2ac6fedaa31f539b6b90863083e5 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 3 Aug 2020 16:03:13 -0700 Subject: [PATCH 070/115] [Reporting] Handle error gracefully when browser driver unexpectedly exits or crashes. (#71989) * [Reporting] Handle page error event directly with Puppeteer * clean up logger.error that was stringifying error * use fromEvent * better handling and error messaging if browser was closed in the middle of the screenshot pipeline * fix pdf functional api tests * fix i18n error * fix jest * fix ts * fix i18n * tweaks * ok to throw error in callback * fix ts Co-authored-by: Elastic Machine --- .../chromium/driver/chromium_driver.ts | 27 +- .../browsers/chromium/driver_factory/index.ts | 28 +- .../server/browsers/chromium/index.ts | 16 + .../csv_from_savedobject/create_job.ts | 4 +- .../printable_pdf/lib/generate_pdf.ts | 3 +- .../lib/screenshots/check_browser_open.ts | 21 + .../server/lib/screenshots/observable.test.ts | 35 +- .../server/lib/screenshots/observable.ts | 7 +- .../create_mock_browserdriverfactory.ts | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../canvas_disallowed_url/data.json.gz | Bin 0 -> 883 bytes .../canvas_disallowed_url/mappings.json | 2216 +++++++++++++++++ .../test/reporting_api_integration/config.js | 9 + .../reporting/index.ts | 1 + .../reporting/network_policy.ts | 51 + 16 files changed, 2358 insertions(+), 63 deletions(-) create mode 100644 x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts create mode 100644 x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz create mode 100644 x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json create mode 100644 x-pack/test/reporting_api_integration/reporting/network_policy.ts diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index eb16a9d6de1a8..494f7ab0a28db 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -9,6 +9,7 @@ import { map, truncate } from 'lodash'; import open from 'opn'; import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; +import { getDisallowedOutgoingUrlError } from '../'; import { LevelLogger } from '../../../lib'; import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; import { ConditionalHeaders, ElementPosition } from '../../../types'; @@ -76,6 +77,9 @@ export class HeadlessChromiumDriver { }); } + /* + * Call Page.goto and wait to see the Kibana DOM content + */ public async open( url: string, { @@ -113,6 +117,16 @@ export class HeadlessChromiumDriver { logger.info(`handled ${this.interceptedCount} page requests`); } + /* + * Let modules poll if Chrome is still running so they can short circuit if needed + */ + public isPageOpen() { + return !this.page.isClosed(); + } + + /* + * Call Page.screenshot and return a base64-encoded string of the image + */ public async screenshot(elementPosition: ElementPosition): Promise { const { boundingClientRect, scroll } = elementPosition; const screenshot = await this.page.screenshot({ @@ -220,18 +234,13 @@ export class HeadlessChromiumDriver { // We should never ever let file protocol requests go through if (!allowed || !this.allowRequest(interceptedUrl)) { - logger.error(`Got bad URL: "${interceptedUrl}", closing browser.`); await client.send('Fetch.failRequest', { errorReason: 'Aborted', requestId, }); this.page.browser().close(); - throw new Error( - i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', { - defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}", exiting`, - values: { interceptedUrl }, - }) - ); + logger.error(getDisallowedOutgoingUrlError(interceptedUrl)); + return; } if (this._shouldUseCustomHeaders(conditionalHeaders.conditions, interceptedUrl)) { @@ -292,9 +301,9 @@ export class HeadlessChromiumDriver { } if (!allowed || !this.allowRequest(interceptedUrl)) { - logger.error(`Got disallowed URL "${interceptedUrl}", closing browser.`); this.page.browser().close(); - throw new Error(`Received disallowed URL in response: ${interceptedUrl}`); + logger.error(getDisallowedOutgoingUrlError(interceptedUrl)); + throw getDisallowedOutgoingUrlError(interceptedUrl); } }); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 157d109e9e27e..809bfb57dd4fa 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -19,6 +19,7 @@ import { import * as Rx from 'rxjs'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; +import { getChromiumDisconnectedError } from '../'; import { BROWSER_TYPE } from '../../../../common/constants'; import { CaptureConfig } from '../../../../server/types'; import { LevelLogger } from '../../../lib'; @@ -115,13 +116,19 @@ export class HeadlessChromiumDriverFactory { logger.debug(`Browser page driver created`); } catch (err) { - observer.error(new Error(`Error spawning Chromium browser: [${err}]`)); + observer.error(new Error(`Error spawning Chromium browser!`)); + observer.error(err); throw err; } const childProcess = { async kill() { - await browser.close(); + try { + await browser.close(); + } catch (err) { + // do not throw + logger.error(err); + } }, }; const { terminate$ } = safeChildProcess(logger, childProcess); @@ -167,7 +174,8 @@ export class HeadlessChromiumDriverFactory { // the unsubscribe function isn't `async` so we're going to make our best effort at // deleting the userDataDir and if it fails log an error. del(userDataDir, { force: true }).catch((error) => { - logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`); + logger.error(`error deleting user data directory at [${userDataDir}]!`); + logger.error(error); }); }); }); @@ -219,7 +227,7 @@ export class HeadlessChromiumDriverFactory { mergeMap((err) => { return Rx.throwError( i18n.translate('xpack.reporting.browsers.chromium.errorDetected', { - defaultMessage: 'Reporting detected an error: {err}', + defaultMessage: 'Reporting encountered an error: {err}', values: { err: err.toString() }, }) ); @@ -230,7 +238,7 @@ export class HeadlessChromiumDriverFactory { mergeMap((err) => { return Rx.throwError( i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { - defaultMessage: `Reporting detected an error on the page: {err}`, + defaultMessage: `Reporting encountered an error on the page: {err}`, values: { err: err.toString() }, }) ); @@ -238,15 +246,7 @@ export class HeadlessChromiumDriverFactory { ); const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( - mergeMap(() => - Rx.throwError( - new Error( - i18n.translate('xpack.reporting.browsers.chromium.chromiumClosed', { - defaultMessage: `Reporting detected that Chromium has closed.`, - }) - ) - ) - ) + mergeMap(() => Rx.throwError(getChromiumDisconnectedError())) ); return Rx.merge(pageError$, uncaughtExceptionPageError$, browserDisconnect$); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/index.ts index cebcd228b01c3..29eb51dff21da 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { BrowserDownload } from '../'; import { CaptureConfig } from '../../../server/types'; import { LevelLogger } from '../../lib'; @@ -15,3 +16,18 @@ export const chromium: BrowserDownload = { createDriverFactory: (binaryPath: string, captureConfig: CaptureConfig, logger: LevelLogger) => new HeadlessChromiumDriverFactory(binaryPath, captureConfig, logger), }; + +export const getChromiumDisconnectedError = () => + new Error( + i18n.translate('xpack.reporting.screencapture.browserWasClosed', { + defaultMessage: 'Browser was closed unexpectedly! Check the server logs for more info.', + }) + ); + +export const getDisallowedOutgoingUrlError = (interceptedUrl: string) => + new Error( + i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', { + defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}". Failing the request and closing the browser.`, + values: { interceptedUrl }, + }) + ); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts index e7fb0c6e2cb99..9acfc6d8c608d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts @@ -105,9 +105,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory if (errPayload.statusCode === 404) { throw notFound(errPayload.message); } - if (err.stack) { - logger.error(err.stack); - } + logger.error(err); throw new Error(`Unable to create a job from saved object data! Error: ${err}`); }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index f2ce423566c46..2e0292e1f9ab5 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -90,7 +90,8 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { logger.debug(`PDF buffer byte length: ${buffer?.byteLength || 0}`); tracker.endGetBuffer(); } catch (err) { - logger.error(`Could not generate the PDF buffer! ${err}`); + logger.error(`Could not generate the PDF buffer!`); + logger.error(err); } tracker.end(); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts b/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts new file mode 100644 index 0000000000000..1b5e73648c2f4 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HeadlessChromiumDriver } from '../../browsers'; +import { getChromiumDisconnectedError } from '../../browsers/chromium'; + +/* + * Call this function within error-handling `catch` blocks while setup and wait + * for the Kibana URL to be ready for screenshot. This detects if a block of + * code threw an exception because the page is closed or crashed. + * + * Also call once after `setup$` fires in the screenshot pipeline + */ +export const checkPageIsOpen = (browser: HeadlessChromiumDriver) => { + if (!browser.isPageOpen()) { + throw getChromiumDisconnectedError(); + } +}; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 1b72be6c92f43..0ad41cd904853 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -281,7 +281,7 @@ describe('Screenshot Observable Pipeline', () => { `); }); - it('recovers if exit$ fires a timeout signal', async () => { + it('observes page exit', async () => { // mocks const mockGetCreatePage = (driver: HeadlessChromiumDriver) => jest @@ -311,38 +311,7 @@ describe('Screenshot Observable Pipeline', () => { }).toPromise(); }; - await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object {}, - "position": Object { - "boundingClientRect": Object { - "height": 200, - "left": 0, - "top": 0, - "width": 200, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": "Instant timeout has fired!", - "screenshots": Array [ - Object { - "base64EncodedData": "allyourBase64", - "description": undefined, - "title": undefined, - }, - ], - "timeRange": null, - }, - ] - `); + await expect(getScreenshot()).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`); }); it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index ab4dabf9ed2c2..c6d3d826c88fb 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -24,6 +24,7 @@ import { ScreenshotResults, ScreenshotsObservableFn, } from '../../types'; +import { checkPageIsOpen } from './check_browser_open'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; @@ -68,7 +69,6 @@ export function screenshotsObservableFactory( return Rx.from(urls).pipe( concatMap((url, index) => { const setup$: Rx.Observable = Rx.of(1).pipe( - takeUntil(exit$), mergeMap(() => { // If we're moving to another page in the app, we'll want to wait for the app to tell us // it's loaded the next page. @@ -117,14 +117,19 @@ export function screenshotsObservableFactory( })); }), catchError((err) => { + checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it + logger.error(err); return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); }) ); return setup$.pipe( + takeUntil(exit$), mergeMap( async (data: ScreenSetupData): Promise => { + checkPageIsOpen(driver); // re-check that the browser has not closed + const elements = data.elementsPositionAndAttributes ? data.elementsPositionAndAttributes : getDefaultElementPosition(layout.getViewport(1)); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index db10d96db2263..08313e6892f3c 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -129,6 +129,7 @@ export const createMockBrowserDriverFactory = async ( mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate; mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot; mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open; + mockBrowserDriver.isPageOpen = () => true; mockBrowserDriverFactory.createPage = opts.getCreatePage ? opts.getCreatePage(mockBrowserDriver) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3287352699f2f..6058644a84356 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14321,7 +14321,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "その名前のリモートクラスターはありません。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "ES からレスポンスが返らず、クラスターを編集できません。", "xpack.reporting.breadcrumb": "レポート", - "xpack.reporting.browsers.chromium.chromiumClosed": "レポート生成時に Chromium の終了を検出しました。", "xpack.reporting.browsers.chromium.errorDetected": "レポート生成時にエラーを検出しました: {err}", "xpack.reporting.browsers.chromium.pageErrorDetected": "レポート生成時にページでエラーを検出しました: {err}", "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "無許可の着信 URL を受信しました: 「{interceptedUrl}」、終了します", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9598ab33ffa25..09cbbba248fd5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14326,7 +14326,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "没有该名称的远程集群。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "无法编辑集群,ES 未返回任何响应。", "xpack.reporting.breadcrumb": "报告", - "xpack.reporting.browsers.chromium.chromiumClosed": "Reporting 检测到 Chromium 已关闭。", "xpack.reporting.browsers.chromium.errorDetected": "Reporting 检测到错误:{err}", "xpack.reporting.browsers.chromium.pageErrorDetected": "Reporting 在页面上检测到错误:{err}", "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "接收到禁止的传出 URL:“{interceptedUrl}”,正在退出", diff --git a/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c434eee5dd8d35749fce7d082a802c159e70af14 GIT binary patch literal 883 zcmV-(1C0D1iwFqD#}Hov17u-zVJ>QOZ*BmkR@;u-HV}RHR|x7$i$q$Ix{wqQv?z)` z7JVohAaxM%CRAZW8?X!sON=Q^lfjv`Hvgs&~GwK36TpP4+=<2L;{eL zOq6v1W;LZ54M2!V!9mE$iaIL-FfX{=c-DxEEDJz)W}NVFd(G=S%o8eBN)d8OT;?Gs zMnZ`YtP3HEP!J_BW?>-AM2+(>CBcQtH_<8CNXMX55{I5dN(wiEuv~=Fxgu+Z0+5sv zDY=Qx84aU@d4|0b6Z?AvgyZC1RxV)d^IJeoQiGF1V z6`l5aug}XnepJojXq_KMU;#Jdd(&9Ocs3s;B^9V?OWx>G|WDFFu%l z8n!nfbh}9I?^c3{M7feE5wJu_4%}OaBAz7_6N&RIV{SkMddH*v(yBW`$DD~rpMswG zeJ92%mvca54b!h2+p^B{-Bx{Krt!VDE_dm1)fEDA750`v+%*ToeCXem8N&>=FVPsM zAO-P;#;97k!xyVGZb{s}5CXoNeHx^HAAe%@^%pGpa0%;WIPcE8-RUd7J$hX?hl { + before(async () => { + await esArchiver.load(archive); // includes a canvas worksheet with an offending image URL + }); + + after(async () => { + await esArchiver.unload(archive); + }); + + it('should fail job when page voilates the network policy', async () => { + const downloadPath = await reportingAPI.postJob( + `/api/reporting/generate/printablePdf?jobParams=(layout:(dimensions:(height:720,width:1080),id:preserve_layout),objectType:'canvas%20workpad',relativeUrls:!(%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fworkpad-e7464259-0b75-4b8c-81c8-8422b15ff201%2Fpage%2F1),title:'My%20Canvas%20Workpad')` + ); + + // Retry the download URL until a "failed" response status is returned + const fails$: Rx.Observable = Rx.interval(100).pipe( + switchMap(() => supertest.get(downloadPath).then((response) => response.body)), + filter(({ statusCode }) => statusCode === 500), + map(({ message }) => message), + first(), + timeout(15000) + ); + + const reportFailed = await fails$.toPromise(); + + expect(reportFailed).to.match(/Reporting generation failed: Error:/); + }); + }); +} From 6f7bfd6a04146da160cbd54019db3f00be92974e Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 3 Aug 2020 16:08:03 -0700 Subject: [PATCH 071/115] Closes #74031 by adding nowrap style to the link. (#74175) --- .../shared/Links/apm/AnomalyDetectionSetupLink.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx index a80dcca4a107d..0c209b0aca917 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx @@ -30,7 +30,10 @@ export function AnomalyDetectionSetupLink() { const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); return ( - + {ANOMALY_DETECTION_LINK_LABEL} From c7202e55cf77e5029e8f9a49d8195c00b78e35a1 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 3 Aug 2020 16:20:58 -0700 Subject: [PATCH 072/115] [kbn/optimizer] remove unneeded DisallowSyntaxPlugin (#74178) Co-authored-by: spalger --- .../disallowed_syntax.ts | 194 ------------------ .../disallowed_syntax_plugin.ts | 73 ------- .../common/disallowed_syntax_plugin/index.ts | 20 -- packages/kbn-optimizer/src/common/index.ts | 1 - packages/kbn-optimizer/src/index.ts | 1 - .../src/worker/webpack.config.ts | 3 +- 6 files changed, 1 insertion(+), 291 deletions(-) delete mode 100644 packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts delete mode 100644 packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts delete mode 100644 packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts deleted file mode 100644 index aba4451622dcd..0000000000000 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import estree from 'estree'; - -export interface DisallowedSyntaxCheck { - name: string; - nodeType: estree.Node['type'] | Array; - test?: (n: any) => boolean | void; -} - -export const checks: DisallowedSyntaxCheck[] = [ - /** - * es2015 - */ - // https://github.com/estree/estree/blob/master/es2015.md#functions - { - name: '[es2015] generator function', - nodeType: ['FunctionDeclaration', 'FunctionExpression'], - test: (n: estree.FunctionDeclaration | estree.FunctionExpression) => !!n.generator, - }, - // https://github.com/estree/estree/blob/master/es2015.md#forofstatement - { - name: '[es2015] for-of statement', - nodeType: 'ForOfStatement', - }, - // https://github.com/estree/estree/blob/master/es2015.md#variabledeclaration - { - name: '[es2015] let/const variable declaration', - nodeType: 'VariableDeclaration', - test: (n: estree.VariableDeclaration) => n.kind === 'let' || n.kind === 'const', - }, - // https://github.com/estree/estree/blob/master/es2015.md#expressions - { - name: '[es2015] `super`', - nodeType: 'Super', - }, - // https://github.com/estree/estree/blob/master/es2015.md#expressions - { - name: '[es2015] ...spread', - nodeType: 'SpreadElement', - }, - // https://github.com/estree/estree/blob/master/es2015.md#arrowfunctionexpression - { - name: '[es2015] arrow function expression', - nodeType: 'ArrowFunctionExpression', - }, - // https://github.com/estree/estree/blob/master/es2015.md#yieldexpression - { - name: '[es2015] `yield` expression', - nodeType: 'YieldExpression', - }, - // https://github.com/estree/estree/blob/master/es2015.md#templateliteral - { - name: '[es2015] template literal', - nodeType: 'TemplateLiteral', - }, - // https://github.com/estree/estree/blob/master/es2015.md#patterns - { - name: '[es2015] destructuring', - nodeType: ['ObjectPattern', 'ArrayPattern', 'AssignmentPattern'], - }, - // https://github.com/estree/estree/blob/master/es2015.md#classes - { - name: '[es2015] class', - nodeType: [ - 'ClassDeclaration', - 'ClassExpression', - 'ClassBody', - 'MethodDefinition', - 'MetaProperty', - ], - }, - - /** - * es2016 - */ - { - name: '[es2016] exponent operator', - nodeType: 'BinaryExpression', - test: (n: estree.BinaryExpression) => n.operator === '**', - }, - { - name: '[es2016] exponent assignment', - nodeType: 'AssignmentExpression', - test: (n: estree.AssignmentExpression) => n.operator === '**=', - }, - - /** - * es2017 - */ - // https://github.com/estree/estree/blob/master/es2017.md#function - { - name: '[es2017] async function', - nodeType: ['FunctionDeclaration', 'FunctionExpression'], - test: (n: estree.FunctionDeclaration | estree.FunctionExpression) => n.async, - }, - // https://github.com/estree/estree/blob/master/es2017.md#awaitexpression - { - name: '[es2017] await expression', - nodeType: 'AwaitExpression', - }, - - /** - * es2018 - */ - // https://github.com/estree/estree/blob/master/es2018.md#statements - { - name: '[es2018] for-await-of statements', - nodeType: 'ForOfStatement', - test: (n: estree.ForOfStatement) => n.await, - }, - // https://github.com/estree/estree/blob/master/es2018.md#expressions - { - name: '[es2018] object spread properties', - nodeType: 'ObjectExpression', - test: (n: estree.ObjectExpression) => n.properties.some((p) => p.type === 'SpreadElement'), - }, - // https://github.com/estree/estree/blob/master/es2018.md#template-literals - { - name: '[es2018] tagged template literal with invalid escape', - nodeType: 'TemplateElement', - test: (n: estree.TemplateElement) => n.value.cooked === null, - }, - // https://github.com/estree/estree/blob/master/es2018.md#patterns - { - name: '[es2018] rest properties', - nodeType: 'ObjectPattern', - test: (n: estree.ObjectPattern) => n.properties.some((p) => p.type === 'RestElement'), - }, - - /** - * es2019 - */ - // https://github.com/estree/estree/blob/master/es2019.md#catchclause - { - name: '[es2019] catch clause without a binding', - nodeType: 'CatchClause', - test: (n: estree.CatchClause) => !n.param, - }, - - /** - * es2020 - */ - // https://github.com/estree/estree/blob/master/es2020.md#bigintliteral - { - name: '[es2020] bigint literal', - nodeType: 'Literal', - test: (n: estree.Literal) => typeof n.value === 'bigint', - }, - - /** - * webpack transforms import/export in order to support tree shaking and async imports - * - * // https://github.com/estree/estree/blob/master/es2020.md#importexpression - * { - * name: '[es2020] import expression', - * nodeType: 'ImportExpression', - * }, - * // https://github.com/estree/estree/blob/master/es2020.md#exportalldeclaration - * { - * name: '[es2020] export all declaration', - * nodeType: 'ExportAllDeclaration', - * }, - * - */ -]; - -export const checksByNodeType = new Map(); -for (const check of checks) { - const nodeTypes = Array.isArray(check.nodeType) ? check.nodeType : [check.nodeType]; - for (const nodeType of nodeTypes) { - if (!checksByNodeType.has(nodeType)) { - checksByNodeType.set(nodeType, []); - } - checksByNodeType.get(nodeType)!.push(check); - } -} diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts deleted file mode 100644 index 8fb7559f3e22f..0000000000000 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import webpack from 'webpack'; -import acorn from 'acorn'; -import * as AcornWalk from 'acorn-walk'; - -import { checksByNodeType, DisallowedSyntaxCheck } from './disallowed_syntax'; -import { parseFilePath } from '../parse_path'; - -export class DisallowedSyntaxPlugin { - apply(compiler: webpack.Compiler) { - compiler.hooks.normalModuleFactory.tap(DisallowedSyntaxPlugin.name, (factory) => { - factory.hooks.parser.for('javascript/auto').tap(DisallowedSyntaxPlugin.name, (parser) => { - parser.hooks.program.tap(DisallowedSyntaxPlugin.name, (program: acorn.Node) => { - const module = parser.state?.current; - if (!module || !module.resource) { - return; - } - - const resource: string = module.resource; - const { dirs } = parseFilePath(resource); - - if (!dirs.includes('node_modules')) { - return; - } - - const failedChecks = new Set(); - - AcornWalk.full(program, (node) => { - const checks = checksByNodeType.get(node.type as any); - if (!checks) { - return; - } - - for (const check of checks) { - if (!check.test || check.test(node)) { - failedChecks.add(check); - } - } - }); - - if (!failedChecks.size) { - return; - } - - // throw an error to trigger a parse failure, causing this module to be reported as invalid - throw new Error( - `disallowed syntax found in file ${resource}:\n - ${Array.from(failedChecks) - .map((c) => c.name) - .join('\n - ')}` - ); - }); - }); - }); - } -} diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts deleted file mode 100644 index ca5ba1b90fe95..0000000000000 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './disallowed_syntax_plugin'; diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index 89cde2c1cd064..5f17a9b38f9fb 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -27,6 +27,5 @@ export * from './ts_helpers'; export * from './rxjs_helpers'; export * from './array_helpers'; export * from './event_stream_helpers'; -export * from './disallowed_syntax_plugin'; export * from './parse_path'; export * from './theme_tags'; diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 29922944e8817..39cf2120baf0a 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -20,5 +20,4 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; -export * from './common/disallowed_syntax_plugin'; export * from './report_optimizer_stats'; diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index ae5d2b5fb3292..820b13629697d 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -29,7 +29,7 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import { Bundle, BundleRefs, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; +import { Bundle, BundleRefs, WorkerConfig, parseDirPath } from '../common'; import { BundleRefsPlugin } from './bundle_refs_plugin'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; @@ -70,7 +70,6 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: plugins: [ new CleanWebpackPlugin(), - new DisallowedSyntaxPlugin(), new BundleRefsPlugin(bundle, bundleRefs), ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], From 47d6be3b7ecc4dedb232615d3aa9d7fa4f16bbf2 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Mon, 3 Aug 2020 18:36:01 -0500 Subject: [PATCH 073/115] [Metrics UI] Fix pluralization in Alert Preview (#74160) --- .../public/alerting/common/components/alert_preview.tsx | 7 ++----- x-pack/plugins/translations/translations/ja-JP.json | 4 ++-- x-pack/plugins/translations/translations/zh-CN.json | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index d5b50fce38718..9d28ef71a5518 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -162,11 +162,10 @@ export const AlertPreview: React.FC = (props) => { {' '} @@ -187,11 +186,9 @@ export const AlertPreview: React.FC = (props) => { {showNoDataResults && previewResult.resultTotals.noData ? ( {previewResult.resultTotals.noData}, - plural: previewResult.resultTotals.noData !== 1 ? 's' : '', }} /> ) : null}{' '} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6058644a84356..544f6cc6ec342 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8830,9 +8830,9 @@ "xpack.infra.metrics.alertFlyout.alertOnNoData": "データがない場合に通知する", "xpack.infra.metrics.alertFlyout.alertPreviewError": "このアラート条件をプレビューするときにエラーが発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "一部のデータを評価するときにエラーが発生しました。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} {groupName}{plural}", + "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} {groupName}", "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "すべてを対象にする", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データがない{were} {noData}結果{plural}がありました。", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データがない {noData}結果がありました。", "xpack.infra.metrics.alertFlyout.alertPreviewResult": "このアラートは{firedTimes}回発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "過去{lookback}", "xpack.infra.metrics.alertFlyout.conditions": "条件", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 09cbbba248fd5..2ee85fc87cc72 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8832,9 +8832,9 @@ "xpack.infra.metrics.alertFlyout.alertOnNoData": "没数据时提醒我", "xpack.infra.metrics.alertFlyout.alertPreviewError": "尝试预览此告警条件时发生错误", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "尝试评估部分数据时发生错误。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} 个{groupName}{plural}", + "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} 个{groupName}", "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "在", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在{were} {noData} 个无数据结果{plural}。", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在 {noData} 个无数据结果。", "xpack.infra.metrics.alertFlyout.alertPreviewResult": "此告警将发生 {firedTimes}", "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "在过去 {lookback}。", "xpack.infra.metrics.alertFlyout.conditions": "条件", From 3ea9e607e5481bea06cab6b206e6c98d5ed88aed Mon Sep 17 00:00:00 2001 From: Ross Wolf <31489089+rw-access@users.noreply.github.com> Date: Mon, 3 Aug 2020 16:38:03 -0700 Subject: [PATCH 074/115] [Detection Rules] Add 7.9 rules (#74177) --- .../collection_cloudtrail_logging_created.json | 5 ++++- ...and_and_control_certutil_network_connection.json | 3 ++- ...dential_access_attempted_bypass_of_okta_mfa.json | 5 ++++- ...tial_access_aws_iam_assume_role_brute_force.json | 5 ++++- ...redential_access_credential_dumping_msbuild.json | 3 ++- ...redential_access_iam_user_addition_to_group.json | 5 ++++- ...ccess_okta_brute_force_or_password_spraying.json | 5 ++++- ...ential_access_secretsmanager_getsecretvalue.json | 9 ++++++--- .../credential_access_tcpdump_activity.json | 3 ++- ...he_hidden_file_attribute_with_via_attribexe.json | 3 ++- ...ion_attempt_to_disable_iptables_or_firewall.json | 3 ++- ...e_evasion_attempt_to_disable_syslog_service.json | 3 ++- ...e16_or_base32_encoding_or_decoding_activity.json | 3 ++- ...vasion_base64_encoding_or_decoding_activity.json | 3 ++- ...defense_evasion_clearing_windows_event_logs.json | 3 ++- .../defense_evasion_cloudtrail_logging_deleted.json | 5 ++++- ...efense_evasion_cloudtrail_logging_suspended.json | 5 ++++- .../defense_evasion_cloudwatch_alarm_deletion.json | 5 ++++- ...efense_evasion_config_service_rule_deletion.json | 5 ++++- ...ense_evasion_configuration_recorder_stopped.json | 5 ++++- ...asion_delete_volume_usn_journal_with_fsutil.json | 3 ++- ...asion_deleting_backup_catalogs_with_wbadmin.json | 3 ++- ...asion_deletion_of_bash_command_line_history.json | 3 ++- .../defense_evasion_disable_selinux_attempt.json | 3 ++- ...n_disable_windows_firewall_rules_with_netsh.json | 3 ++- .../defense_evasion_ec2_flow_log_deletion.json | 5 ++++- .../defense_evasion_ec2_network_acl_deletion.json | 5 ++++- ...ion_encoding_or_decoding_files_via_certutil.json | 3 ++- ...ion_execution_msbuild_started_by_office_app.json | 3 ++- ...evasion_execution_msbuild_started_by_script.json | 3 ++- ...execution_msbuild_started_by_system_process.json | 3 ++- ...e_evasion_execution_msbuild_started_renamed.json | 3 ++- ...on_execution_msbuild_started_unusal_process.json | 3 ++- .../defense_evasion_file_deletion_via_shred.json | 3 ++- .../defense_evasion_file_mod_writable_dir.json | 3 ++- ...defense_evasion_guardduty_detector_deletion.json | 5 ++++- ...e_evasion_hex_encoding_or_decoding_activity.json | 3 ++- .../defense_evasion_hidden_file_dir_tmp.json | 3 ++- .../defense_evasion_kernel_module_removal.json | 3 ++- ...sion_misc_lolbin_connecting_to_the_internet.json | 3 ++- ...defense_evasion_modification_of_boot_config.json | 3 ++- ...se_evasion_s3_bucket_configuration_deletion.json | 5 ++++- ...on_volume_shadow_copy_deletion_via_vssadmin.json | 3 ++- ...vasion_volume_shadow_copy_deletion_via_wmic.json | 3 ++- .../defense_evasion_waf_acl_deletion.json | 5 ++++- ...nse_evasion_waf_rule_or_rule_group_deletion.json | 5 ++++- .../discovery_kernel_module_enumeration.json | 3 ++- .../discovery_net_command_system_account.json | 3 ++- .../discovery_virtual_machine_fingerprinting.json | 3 ++- .../discovery_whoami_commmand.json | 3 ++- .../rules/prepackaged_rules/elastic_endpoint.json | 13 ++++++++++--- ...n_command_prompt_connecting_to_the_internet.json | 3 ++- ...ecution_command_shell_started_by_powershell.json | 3 ++- .../execution_command_shell_started_by_svchost.json | 3 ++- ...ecutable_program_connecting_to_the_internet.json | 3 ++- .../execution_local_service_commands.json | 3 ++- ...xecution_msbuild_making_network_connections.json | 3 ++- .../execution_mshta_making_network_connections.json | 3 ++- .../prepackaged_rules/execution_msxsl_network.json | 3 ++- .../prepackaged_rules/execution_perl_tty_shell.json | 3 ++- .../execution_psexec_lateral_movement_command.json | 3 ++- .../execution_python_tty_shell.json | 3 ++- ...r_server_program_connecting_to_the_internet.json | 3 ++- .../execution_script_executing_powershell.json | 3 ++- ...xecution_suspicious_ms_office_child_process.json | 3 ++- ...ecution_suspicious_ms_outlook_child_process.json | 3 ++- .../execution_suspicious_pdf_reader.json | 3 ++- ...ion_unusual_network_connection_via_rundll32.json | 3 ++- ...xecution_unusual_process_network_connection.json | 3 ++- .../execution_via_net_com_assemblies.json | 3 ++- .../execution_via_system_manager.json | 5 ++++- .../exfiltration_ec2_snapshot_change_activity.json | 5 ++++- .../rules/prepackaged_rules/external_alerts.json | 8 ++++++-- .../impact_attempt_to_revoke_okta_api_token.json | 5 ++++- .../impact_cloudtrail_logging_updated.json | 5 ++++- .../impact_cloudwatch_log_group_deletion.json | 5 ++++- .../impact_cloudwatch_log_stream_deletion.json | 5 ++++- .../impact_ec2_disable_ebs_encryption.json | 5 ++++- .../impact_iam_deactivate_mfa_device.json | 5 ++++- .../impact_iam_group_deletion.json | 5 ++++- .../impact_possible_okta_dos_attack.json | 5 ++++- .../impact_rds_cluster_deletion.json | 5 ++++- .../impact_rds_instance_cluster_stoppage.json | 5 ++++- .../initial_access_console_login_root.json | 5 ++++- .../initial_access_password_recovery.json | 5 ++++- ...s_suspicious_activity_reported_by_okta_user.json | 5 ++++- ...ral_movement_direct_outbound_smb_connection.json | 3 ++- ...l_movement_telnet_network_activity_external.json | 3 ++- ...l_movement_telnet_network_activity_internal.json | 3 ++- .../prepackaged_rules/linux_hping_activity.json | 3 ++- .../prepackaged_rules/linux_iodine_activity.json | 3 ++- .../prepackaged_rules/linux_mknod_activity.json | 3 ++- .../linux_netcat_network_connection.json | 3 ++- .../prepackaged_rules/linux_nmap_activity.json | 3 ++- .../prepackaged_rules/linux_nping_activity.json | 3 ++- .../linux_process_started_in_temp_directory.json | 3 ++- .../prepackaged_rules/linux_socat_activity.json | 3 ++- .../prepackaged_rules/linux_strace_activity.json | 3 ++- .../okta_attempt_to_deactivate_okta_mfa_rule.json | 5 ++++- .../okta_attempt_to_delete_okta_policy.json | 5 ++++- .../okta_attempt_to_modify_okta_mfa_rule.json | 5 ++++- .../okta_attempt_to_modify_okta_network_zone.json | 5 ++++- .../okta_attempt_to_modify_okta_policy.json | 5 ++++- ...modify_or_delete_application_sign_on_policy.json | 5 ++++- .../okta_threat_detected_by_okta_threatinsight.json | 5 ++++- ...nistrator_privileges_assigned_to_okta_group.json | 5 ++++- .../persistence_adobe_hijack_persistence.json | 3 ++- ...ersistence_attempt_to_create_okta_api_token.json | 5 ++++- ...mpt_to_deactivate_mfa_for_okta_user_account.json | 5 ++++- ...rsistence_attempt_to_deactivate_okta_policy.json | 5 ++++- ..._to_reset_mfa_factors_for_okta_user_account.json | 5 ++++- .../persistence_ec2_network_acl_creation.json | 5 ++++- .../persistence_iam_group_creation.json | 5 ++++- .../persistence_kernel_module_activity.json | 3 ++- .../persistence_local_scheduled_task_commands.json | 3 ++- .../persistence_rds_cluster_creation.json | 5 ++++- .../persistence_shell_activity_by_web_server.json | 3 ++- .../persistence_system_shells_via_services.json | 3 ++- .../persistence_user_account_creation.json | 3 ++- ...privilege_escalation_root_login_without_mfa.json | 5 ++++- ...ivilege_escalation_setgid_bit_set_via_chmod.json | 3 ++- ...ivilege_escalation_setuid_bit_set_via_chmod.json | 3 ++- .../privilege_escalation_sudoers_file_mod.json | 3 ++- ...rivilege_escalation_uac_bypass_event_viewer.json | 3 ++- ...escalation_unusual_parentchild_relationship.json | 3 ++- ...privilege_escalation_updateassumerolepolicy.json | 5 ++++- 126 files changed, 364 insertions(+), 131 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json index ee39661ee9b10..acc6f7724d0b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json index 4132d03c27854..25274928aa2b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies certutil.exe making a network connection. Adversaries could abuse certutil.exe to download a certificate, or malware, from a remote URL.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json index eb8523b797ddf..a2267755c7376 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json index ddc9e91782136..4c79be6fe9045 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json @@ -21,7 +21,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json index a2936f3f09519..6be1f037f967e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json index ecbf268550b6c..5b73f849dddc5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json index 87f20525203f6..2cf24d54b7268 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json index f570b7fb3e946..ef20746fb1d80 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json @@ -21,12 +21,15 @@ "https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html", "http://detectioninthe.cloud/credential_access/access_secret_in_secrets_manager/" ], - "risk_score": 21, + "risk_score": 73, "rule_id": "a00681e3-9ed6-447c-ab2c-be648821c622", - "severity": "low", + "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Data Protection", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json index 9abbe3de148dd..d5b069f7b81e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json @@ -7,7 +7,8 @@ "Some normal use of this command may originate from server or network administrators engaged in network troubleshooting." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json index 861821d24b73c..b22b74ebc53bc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json @@ -4,7 +4,8 @@ ], "description": "Adversaries can add the 'hidden' attribute to files to hide them from the user in an attempt to evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json index 431d133845f0e..e2ba81da917b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to disable the iptables or firewall service in an attempt to affect how a host is allowed to receive or send network traffic.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json index 13dd405c79326..4f4a9aacd79aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to disable the syslog service in an attempt to an attempt to disrupt event logging and evade detection by security controls.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json index 67fb0b2e6755a..5bcc4a00ccd82 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json index f60dede360b4b..a17fd6d2702dd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json index 7c6ede8df7346..cf09bc512916f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json @@ -4,7 +4,8 @@ ], "description": "Identifies attempts to clear Windows event log stores. This is often done by attackers in an attempt to evade detection or destroy forensic evidence on a system.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json index 78f4c9e853f64..b5e76b6ebfa36 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json index f412ad9b2e2fd..6ba9503edc260 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json index b76ea0944f855..3d31eead43c8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json index 353067e6db833..22ceb35dfc856 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json index b70aa5cd11b52..95e357e56fe3b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json index ba9f43651e32f..0c82444dd9397 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the fsutil.exe to delete the volume USNJRNL. This technique is used by attackers to eliminate evidence of files created during post-exploitation activities.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json index 79c2d4c25b7d5..c76c5f20fa88b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the wbadmin.exe to delete the backup catalog. Ransomware and other malware may do this to prevent system recovery.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json index b9727e18dddcf..b38ed94e132e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to clear the bash command line history in an attempt to evade detection or forensic investigations.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json index e8f5f1a8de1c5..229a03de39600 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json @@ -4,7 +4,8 @@ ], "description": "Identifies potential attempts to disable Security-Enhanced Linux (SELinux), which is a Linux kernel security feature to support access control policies. Adversaries may disable security tools to avoid possible detection of their tools and activities.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json index 2b45f059ec8d9..4800e87c180e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the netsh.exe to disable or weaken the local firewall. Attackers will use this command line tool to disable the firewall during troubleshooting or to enable network mobility.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json index a1b0ec0f01d2a..809a9a187937d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json index 21ce4e498ccaf..8467b87f9983c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json index 056de9e5c003e..075dd13d9819b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json @@ -4,7 +4,8 @@ ], "description": "Identifies the use of certutil.exe to encode or decode data. CertUtil is a native Windows component which is part of Certificate Services. CertUtil is often abused by attackers to encode or decode base64 data for stealthier command and control or exfiltration.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json index 814caee4e888a..133863f8e2148 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. It is quite unusual for this program to be started by an Office application like Word or Excel." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json index 6426f8722df3d..85d348bb14be0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json index b27dfced0f4f6..38482c0a70fc9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json index d7da758e57c6d..7db683caf2bb2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json index 30d482e9b9569..1c4666955dde0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. If a build system triggers this rule it can be exempted by process, user or host name." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json index 4aad56abd0534..c375ea7b19b37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json @@ -4,7 +4,8 @@ ], "description": "Malware or other files dropped or created on a system by an adversary may leave traces behind as to what was done within a network and how. Adversaries may remove these files over the course of an intrusion to keep their footprint low or remove them at the end as part of the post-intrusion cleanup process.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json index c630ad1eecec0..22090e1a241e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json @@ -7,7 +7,8 @@ "Certain programs or applications may modify files or change ownership in writable directories. These can be exempted by username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json index 989eff90aaf02..c2590a2f062cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json index 3c1ea7ee229c9..00491937e9aae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json index 7202d9be3b8c3..16a398011fc53 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json @@ -7,7 +7,8 @@ "Certain tools may create hidden temporary files or directories upon installation or as part of their normal behavior. These events can be filtered by the process arguments, username, or process name values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json index f055ee44efb39..11781cb719599 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json @@ -7,7 +7,8 @@ "There is usually no reason to remove modules, but some buggy modules require it. These can be exempted by username. Note that some Linux distributions are not built to support the removal of modules at all." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json index afa1467b15074..7d931725fa6eb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json @@ -4,7 +4,8 @@ ], "description": "Binaries signed with trusted digital certificates can execute on Windows systems protected by digital signature validation. Adversaries may use these binaries to 'live off the land' and execute malicious files that could bypass application allowlists and signature validation.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json index 801b60a2572e2..1bffe7a1cfc24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of bcdedit.exe to delete boot configuration data. This tactic is sometimes used as by malware or an attacker as a destructive technique.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json index b1e8d0cd0d3e1..13d0eb267f640 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json @@ -28,7 +28,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json index 3166cc23ae726..f3cc5c2eec8a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of vssadmin.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json index 730879684a811..334276142ca42 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of wmic.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json index b2092dc78b012..ef7667e34be3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json index ccec76b7f7974..1e8e1bfa42246 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json index 14472f02280a3..0e4bea426c591 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json @@ -7,7 +7,8 @@ "Security tools and device drivers may run these programs in order to enumerate kernel modules. Use of these programs by ordinary users is uncommon. These can be exempted by process name or username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json index a2fe82c43b15a..6ac2bbf355961 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json @@ -4,7 +4,8 @@ ], "description": "Identifies the SYSTEM account using the Net utility. The Net utility is a component of the Windows operating system. It is used in command line operations for control of users, groups, services, and network connections.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json index 94f09f73b454e..e73aa5f4566a7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json @@ -7,7 +7,8 @@ "Certain tools or automated software may enumerate hardware information. These tools can be exempted via user name or process arguments to eliminate potential noise." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json index a7833c4a01751..0017186787139 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json @@ -7,7 +7,8 @@ "Security testing tools and frameworks may run this command. Some normal use of this command may originate from automation tools and frameworks." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json index 05601ec8ffb4c..1466b4526815b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json @@ -1,5 +1,7 @@ { - "author": ["Elastic"], + "author": [ + "Elastic" + ], "description": "Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Elastic Endpoint alerts.", "enabled": true, "exceptions_list": [ @@ -11,7 +13,9 @@ } ], "from": "now-10m", - "index": ["logs-endpoint.alerts-*"], + "index": [ + "logs-endpoint.alerts-*" + ], "language": "kuery", "license": "Elastic License", "max_signals": 10000, @@ -54,7 +58,10 @@ "value": "99" } ], - "tags": ["Elastic", "Endpoint"], + "tags": [ + "Elastic", + "Endpoint" + ], "timestamp_override": "event.ingested", "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json index 97197be498a8d..0ba6480fe42a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json @@ -7,7 +7,8 @@ "Administrators may use the command prompt for regular administrative tasks. It's important to baseline your environment for network connections being made from the command prompt to determine any abnormal use of this tool." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json index 832ca1e1e7d39..2d3edb0f5f6cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json @@ -4,7 +4,8 @@ ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from PowerShell.exe.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json index e92ee45c0f3b6..3a4b4915f3c8b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json @@ -4,7 +4,8 @@ ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json index c75f77301e531..a2eb76b9831f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json @@ -4,7 +4,8 @@ ], "description": "Compiled HTML files (.chm) are commonly distributed as part of the Microsoft HTML Help system. Adversaries may conceal malicious code in a CHM file and deliver it to a victim for execution. CHM content is loaded by the HTML Help executable program (hh.exe).", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json index 9b50d99761ad2..e43ab9de86ef7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of sc.exe to create, modify, or start services on remote hosts. This could be indicative of adversary lateral movement but will be noisy if commonly done by admins.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json index 192e35df1da3f..9d480259d49de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json @@ -4,7 +4,8 @@ ], "description": "Identifies MsBuild.exe making outbound network connections. This may indicate adversarial activity as MsBuild is often leveraged by adversaries to execute code and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json index cb098086e3324..cdef5f16e5cd7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json @@ -4,7 +4,8 @@ ], "description": "Identifies mshta.exe making a network connection. This may indicate adversarial activity as mshta.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json index 9f1d2fc62fadf..d501bda08c3a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json @@ -4,7 +4,8 @@ ], "description": "Identifies msxsl.exe making a network connection. This may indicate adversarial activity as msxsl.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json index db96fe1bc1b50..e82b42869e44d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json @@ -4,7 +4,8 @@ ], "description": "Identifies when a terminal (tty) is spawned via Perl. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json index a5ac6cffd2376..e4c84fd3c3b83 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json @@ -7,7 +7,8 @@ "PsExec is a dual-use tool that can be used for benign or malicious activity. It's important to baseline your environment to determine the amount of noise to expect from this tool." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json index 59be6da19e93f..3aa9ac20bba9e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json @@ -4,7 +4,8 @@ ], "description": "Identifies when a terminal (tty) is spawned via Python. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json index 262313782fe33..0a1ba97bd01ea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json @@ -7,7 +7,8 @@ "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json index 6f9170f476d90..7305247192f57 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json @@ -4,7 +4,8 @@ ], "description": "Identifies a PowerShell process launched by either cscript.exe or wscript.exe. Observing Windows scripting processes executing a PowerShell script, may be indicative of malicious activity.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json index 1b5fd4e1f502d..7ff8eb9424d5f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of frequently targeted Microsoft Office applications (Word, PowerPoint, Excel). These child processes are often launched during exploitation of Office applications or from documents with malicious macros.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json index f874b7e3f8e80..e923407765f8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of Microsoft Outlook. These child processes are often associated with spear phishing activity.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json index 35206d130ea5f..24a744ce30832 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of PDF reader applications. These child processes are often launched via exploitation of PDF applications or social engineering.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json index 43f1f8a5c9c61..529f2199e46dc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json @@ -4,7 +4,8 @@ ], "description": "Identifies unusual instances of rundll32.exe making outbound network connections. This may indicate adversarial activity and may identify malicious DLLs.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json index b49d1b358cb8d..69a25b3b24bac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies network activity from unexpected system applications. This may indicate adversarial activity as these applications are often leveraged by adversaries to execute code and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json index 2c141da80e797..cae5d1b7e0f1f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json @@ -4,7 +4,8 @@ ], "description": "RegSvcs.exe and RegAsm.exe are Windows command line utilities that are used to register .NET Component Object Model (COM) assemblies. Adversaries can use RegSvcs.exe and RegAsm.exe to proxy execution of code through a trusted Windows utility.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json index a9f8ee1af8bf6..5e3e44604e9b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json index 25711afbb4c66..81d82670e794a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json index 8b627c48d2904..6bc14f4e5af8a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json @@ -1,5 +1,7 @@ { - "author": ["Elastic"], + "author": [ + "Elastic" + ], "description": "Generates a detection alert for each external alert written to the configured indices. Enabling this rule allows you to immediately begin investigating external alerts in the app.", "index": [ "apm-*-transaction*", @@ -51,7 +53,9 @@ "value": "99" } ], - "tags": ["Elastic"], + "tags": [ + "Elastic" + ], "timestamp_override": "event.ingested", "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json index 27e50313c8f82..ee434efa019d6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json index 0bafa56c9af49..2de24a8155254 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json index 74b5e0d93c441..9fe0d97ceda32 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json index 59c659117c098..085acb9a2fb13 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json index 10a1989ad6423..f75e4d15f1e6a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json @@ -26,7 +26,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Data Protection", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json index 4aa0b355171fe..68ad10977f4d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json index 25b300d33cce1..aab2deff3a266 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json index 9ca8b7ed21acb..abcc6f65fbc67 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json index e8343f1b7b7c6..b0615cf032386 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json index 8c4387e60d281..d77533e5183ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json index 829d87c1964c9..026e1e549b574 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json @@ -24,7 +24,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json index 7429c69fc3174..bd20be0924d05 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json index 25bf7dd287d05..2344346c8d61d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json index b4850e77ae719..8a68b26abad20 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies unexpected processes making network connections over port 445. Windows File Sharing is typically implemented over Server Message Block (SMB), which communicates between hosts using port 445. When legitimate, these network connections are established by the kernel. Processes making 445/tcp connections may be port scanners, exploits, or suspicious user-level processes moving laterally.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json index 27e5da09452e7..2ea75dbd758cb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json @@ -7,7 +7,8 @@ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json index 0273800c18d52..4379759608aba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json @@ -7,7 +7,8 @@ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json index a842d8ef952ff..24104439cd0ec 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json @@ -7,7 +7,8 @@ "Normal use of hping is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json index c1ce773c2aa44..73bf20a5a175e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json @@ -7,7 +7,8 @@ "Normal use of Iodine is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json index 98b262edfe6f6..1895caf4dea81 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json @@ -7,7 +7,8 @@ "Mknod is a Linux system program. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json index 30d34f245c6d2..ac46bcbdbc083 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json @@ -7,7 +7,8 @@ "Netcat is a dual-use tool that can be used for benign or malicious activity. Netcat is included in some Linux distributions so its presence is not necessarily suspicious. Some normal use of this program, while uncommon, may originate from scripts, automation tools, and frameworks." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json index 57f5fe57b0e0b..2825dc28ad18f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json @@ -7,7 +7,8 @@ "Security testing tools and frameworks may run `Nmap` in the course of security auditing. Some normal use of this command may originate from security engineers and network or server administrators. Use of nmap by ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json index 086492edeb8ad..234a09e9607b9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json @@ -7,7 +7,8 @@ "Some normal use of this command may originate from security engineers and network or server administrators, but this is usually not routine or unannounced. Use of `Nping` by non-engineers or ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json index 09680fcf8e996..759622804444e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json @@ -7,7 +7,8 @@ "Build systems, like Jenkins, may start processes in the `/tmp` directory. These can be exempted by name or by username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json index 057d8ba9859a8..cd38aff3f2164 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json @@ -7,7 +7,8 @@ "Socat is a dual-use tool that can be used for benign or malicious activity. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json index 3dd18c8242a5e..7fcb9f915c560 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json @@ -7,7 +7,8 @@ "Strace is a dual-use tool that can be used for benign or malicious activity. Some normal use of this command may originate from developers or SREs engaged in debugging or system call tracing." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json index 1d15db83bb18e..b4c2d6522fb01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json index 6df2ed6cb34a4..f64db94fbc7b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json index e276166f6130b..30e52eed86110 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json index bdfe7d25092ba..18a72a331219e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Network", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json index e3e0d5fef7b2f..b9c6e390effd9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json index ad21ebe065f8c..786fdd1ac16c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json index e92cf3d67d313..06089272f0e8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json index d5f3995fb8bcc..a9e6d2feef813 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json index c5d8e50d3dba7..3392a1bff23b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json @@ -4,7 +4,8 @@ ], "description": "Detects writing executable files that will be automatically launched by Adobe on launch.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json index 5f6c006c5d177..49b9a7501a3aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json index d3a66ef8d9c77..f289e8341a0d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json index 7104cace1c5d9..9393f7e4ef515 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json index c38f71d8e00a6..09aeed65f1ef0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json index 99bb07fe9660e..229286d4e234d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json @@ -27,7 +27,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json index 9b2478b97fb38..b62384f5bd76a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json index 48ed65caceda7..e76379d171bf7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json @@ -7,7 +7,8 @@ "Security tools and device drivers may run these programs in order to load legitimate kernel modules. Use of these programs by ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json index b99690f78b2b4..b9e7f941ee5df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json @@ -7,7 +7,8 @@ "Legitimate scheduled tasks may be created during installation of new software." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json index 94a695a97a27a..830d2d956125b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json @@ -27,7 +27,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json index 24ea80e10f5e3..0cf6fcdb3875a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json @@ -7,7 +7,8 @@ "Network monitoring or management products may have a web server component that runs shell commands as part of normal behavior." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json index c3684006a49e5..59715dae441f4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json @@ -4,7 +4,8 @@ ], "description": "Windows services typically run as SYSTEM and can be used as a privilege escalation opportunity. Malware or penetration testers may run a shell as a service to gain SYSTEM permissions.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json index 5704f6d14bfec..7465751d5cd49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json @@ -4,7 +4,8 @@ ], "description": "Identifies attempts to create new local users. This is sometimes done by attackers to increase access to a system or domain.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json index 74c5376100b2b..aff6df969d90b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json index 3738c04346e6e..9550eea6ca6aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json @@ -4,7 +4,8 @@ ], "description": "An adversary may add the setgid bit to a file or directory in order to run a file with the privileges of the owning group. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setgid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json index 58dcd2d671f52..343426953add6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json @@ -4,7 +4,8 @@ ], "description": "An adversary may add the setuid bit to a file or directory in order to run a file with the privileges of the owning user. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setuid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json index 9850d4d908b69..44b50c74bafe6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json @@ -4,7 +4,8 @@ ], "description": "A sudoers file specifies the commands that users or groups can run and from which terminals. Adversaries can take advantage of these configurations to execute commands as other users or spawn processes with higher privileges.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json index d8b59804fecdf..50692dae3856f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json @@ -4,7 +4,8 @@ ], "description": "Identifies User Account Control (UAC) bypass via eventvwr.exe. Attackers bypass UAC to stealthily execute code with elevated permissions.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index bc80953d0aa61..8f938c0ceee6d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -4,7 +4,8 @@ ], "description": "Identifies Windows programs run from unexpected parent processes. This could indicate masquerading or other strange activity on a system.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json index 7ce54b00f211c..e271f855e4424 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { From 63500c89c35135854fdb66c2b47b02ebeb57f29a Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 3 Aug 2020 18:41:20 -0500 Subject: [PATCH 075/115] Do not display our app URL field on read-only view (#74183) This is a "hidden" field added as metadata during rule creation. It is omitted from all other views, but in the case of an error response during rule creation, the read-only view will display this field. In lieu of any broad changes here, this simply hides that field from the (really, any) read-only view of that data. --- .../detections/components/rules/description_step/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 0b341050fa9d5..47c12d1934174 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -213,6 +213,8 @@ export const getDescriptionItem = ( } else if (field === 'ruleType') { const ruleType: RuleType = get(field, data); return buildRuleTypeDescription(label, ruleType); + } else if (field === 'kibanaSiemAppUrl') { + return []; } const description: string = get(field, data); From 64126b425e230a1dbcd6bcada6355c3bc9cbc817 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 3 Aug 2020 20:01:11 -0400 Subject: [PATCH 076/115] [Security Solution][Resolver] Handle info and change events for children (#74007) * Handle info and change events for children * Disabling tests for children search_after * Addressing comments --- .../common/endpoint/models/event.test.ts | 44 ++++-- .../common/endpoint/models/event.ts | 18 ++- .../routes/resolver/queries/children.ts | 31 ++++- .../resolver/utils/children_helper.test.ts | 4 +- .../routes/resolver/utils/children_helper.ts | 4 +- .../routes/resolver/utils/pagination.ts | 43 +++++- .../apis/index.ts | 1 + .../apis/resolver/children.ts | 129 ++++++++++++++++++ .../apis/resolver/tree.ts | 12 +- 9 files changed, 251 insertions(+), 35 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts index 62f923aa6d793..6e6e0f443015b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { EndpointDocGenerator } from '../generate_data'; -import { descriptiveName, isStart } from './event'; +import { descriptiveName, isProcessRunning } from './event'; import { ResolverEvent } from '../types'; describe('Generated documents', () => { @@ -42,52 +42,66 @@ describe('Generated documents', () => { }); }); - describe('Start events', () => { - it('is a start event when event.type is a string', () => { + describe('Process running events', () => { + it('is a running event when event.type is a string', () => { const event: ResolverEvent = generator.generateEvent({ eventType: 'start', }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is a start event when event.type is an array of strings', () => { + it('is a running event when event.type is an array of strings', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['start'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is a start event when event.type is an array of strings and contains start', () => { + it('is a running event when event.type is an array of strings and contains start', () => { let event: ResolverEvent = generator.generateEvent({ eventType: ['bogus', 'start', 'creation'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); event = generator.generateEvent({ eventType: ['start', 'bogus'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is not a start event when event.type is not start', () => { + it('is not a running event when event.type is only and end type', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['end'], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); }); - it('is not a start event when event.type is empty', () => { + it('is not a running event when event.type is empty', () => { const event: ResolverEvent = generator.generateEvent({ eventType: [], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); }); - it('is not a start event when event.type is bogus', () => { + it('is not a running event when event.type is bogus', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['bogus'], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); + }); + + it('is a running event when event.type contains info', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['info'], + }); + expect(isProcessRunning(event)).toBeTruthy(); + }); + + it('is a running event when event.type contains change', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['bogus', 'change'], + }); + expect(isProcessRunning(event)).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index 216b5cc028588..1168b5edb6ffd 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -9,16 +9,26 @@ export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEven return (event as LegacyEndpointEvent).endgame !== undefined; } -export function isStart(event: ResolverEvent): boolean { +export function isProcessRunning(event: ResolverEvent): boolean { if (isLegacyEvent(event)) { - return event.event?.type === 'process_start' || event.event?.action === 'fork_event'; + return ( + event.event?.type === 'process_start' || + event.event?.action === 'fork_event' || + event.event?.type === 'already_running' + ); } if (Array.isArray(event.event.type)) { - return event.event.type.includes('start'); + return ( + event.event.type.includes('start') || + event.event.type.includes('change') || + event.event.type.includes('info') + ); } - return event.event.type === 'start'; + return ( + event.event.type === 'start' || event.event.type === 'change' || event.event.type === 'info' + ); } export function eventTimestamp(event: ResolverEvent): string | undefined | number { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts index d99533e23f2c2..902d287a09e42 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts @@ -22,7 +22,13 @@ export class ChildrenQuery extends ResolverQuery { } protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { + const paginationFields = this.pagination.buildQueryFields('endgame.serial_event_id'); return { + collapse: { + field: 'endgame.unique_pid', + }, + size: paginationFields.size, + sort: paginationFields.sort, query: { bool: { filter: [ @@ -42,7 +48,7 @@ export class ChildrenQuery extends ResolverQuery { bool: { should: [ { - term: { 'event.type': 'process_start' }, + terms: { 'event.type': ['process_start', 'already_running'] }, }, { term: { 'event.action': 'fork_event' }, @@ -53,12 +59,30 @@ export class ChildrenQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('endgame.serial_event_id'), }; } protected query(entityIDs: string[]): JsonObject { + const paginationFields = this.pagination.buildQueryFieldsAsInterface('event.id'); return { + /** + * Using collapse here will only return a single event per occurrence of a process.entity_id. The events are sorted + * based on timestamp in ascending order so it will be the first event that ocurred. The actual type of event that + * we receive for this query doesn't really matter (whether it is a start, info, or exec for a particular entity_id). + * All this is trying to accomplish is removing duplicate events that indicate a process existed for a node. We + * only need to know that a process existed and it's it's ancestry array and the process.entity_id fields because + * we will use it to query for the next set of descendants. + * + * The reason it is important to only receive 1 event per occurrence of a process.entity_id is it allows us to avoid + * ES 10k limit most of the time. If instead we received multiple events with the same process.entity_id that would + * reduce the maximum number of unique children processes we could retrieve in a single query. + */ + collapse: { + field: 'process.entity_id', + }, + // do not set the search_after field because collapse does not work with it + size: paginationFields.size, + sort: paginationFields.sort, query: { bool: { filter: [ @@ -93,12 +117,11 @@ export class ChildrenQuery extends ResolverQuery { term: { 'event.kind': 'event' }, }, { - term: { 'event.type': 'start' }, + terms: { 'event.type': ['start', 'info', 'change'] }, }, ], }, }, - ...this.pagination.buildQueryFields('event.id'), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index 01dd59b2611d9..78e4219aad75c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -10,12 +10,12 @@ import { TreeNode, } from '../../../../../common/endpoint/generate_data'; import { ChildrenNodesHelper } from './children_helper'; -import { eventId, isStart } from '../../../../../common/endpoint/models/event'; +import { eventId, isProcessRunning } from '../../../../../common/endpoint/models/event'; function getStartEvents(events: Event[]): Event[] { const startEvents: Event[] = []; for (const event of events) { - if (isStart(event)) { + if (isProcessRunning(event)) { startEvents.push(event); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index d3ca7a54c16d2..ef487897e3b4e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -7,7 +7,7 @@ import { entityId, parentEntityId, - isStart, + isProcessRunning, getAncestryAsArray, } from '../../../../../common/endpoint/models/event'; import { @@ -99,7 +99,7 @@ export class ChildrenNodesHelper { for (const event of startEvents) { const parentID = parentEntityId(event); const entityID = entityId(event); - if (parentID && entityID && isStart(event)) { + if (parentID && entityID && isProcessRunning(event)) { // don't actually add the start event to the node, because that'll be done in // a different call const childNode = this.getOrCreateChildNode(entityID); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index 150f56cbd70cc..8e160fe5fc72a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -8,11 +8,29 @@ import { ResolverEvent } from '../../../../../common/endpoint/types'; import { eventId } from '../../../../../common/endpoint/models/event'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; +type SortFields = [ + { + '@timestamp': string; + }, + { [x: string]: string } +]; + +type SearchAfterFields = [number, string]; + interface PaginationCursor { timestamp: number; eventID: string; } +/** + * Interface for defining the returned pagination information. + */ +export interface PaginationFields { + sort: SortFields; + size: number; + searchAfter?: SearchAfterFields; +} + /** * This class handles constructing pagination cursors that resolver can use to return additional events in subsequent * queries. It also constructs an aggregation query to determine the totals for other queries. This class should be used @@ -100,6 +118,22 @@ export class PaginationBuilder { return new PaginationBuilder(limit); } + /** + * Helper for creates an object for adding the pagination fields to a query + * + * @param tiebreaker a unique field to use as the tiebreaker for the search_after + * @returns an object containing the pagination information + */ + buildQueryFieldsAsInterface(tiebreaker: string): PaginationFields { + const sort: SortFields = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; + let searchAfter: SearchAfterFields | undefined; + if (this.timestamp && this.eventID) { + searchAfter = [this.timestamp, this.eventID]; + } + + return { sort, size: this.size, searchAfter }; + } + /** * Creates an object for adding the pagination fields to a query * @@ -108,10 +142,11 @@ export class PaginationBuilder { */ buildQueryFields(tiebreaker: string): JsonObject { const fields: JsonObject = {}; - fields.sort = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; - fields.size = this.size; - if (this.timestamp && this.eventID) { - fields.search_after = [this.timestamp, this.eventID] as Array; + const pagination = this.buildQueryFieldsAsInterface(tiebreaker); + fields.sort = pagination.sort; + fields.size = pagination.size; + if (pagination.searchAfter) { + fields.search_after = pagination.searchAfter; } return fields; } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 9cdef1c938890..90db224f31839 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -28,6 +28,7 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider }); loadTestFile(require.resolve('./resolver/entity_id')); loadTestFile(require.resolve('./resolver/tree')); + loadTestFile(require.resolve('./resolver/children')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts new file mode 100644 index 0000000000000..cde1a3616b620 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { entityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; +import { PaginationBuilder } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/utils/pagination'; +import { ChildrenQuery } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/queries/children'; +import { + ResolverTree, + ResolverEvent, +} from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + Event, + EndpointDocGenerator, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { InsertedEvents } from '../../services/resolver'; + +export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const resolver = getService('resolverGenerator'); + const generator = new EndpointDocGenerator('resolver'); + const es = getService('es'); + + describe('Resolver children edge cases', () => { + describe('info and exec children', () => { + let origin: Event; + let infoEvent: Event; + let startEvent: Event; + let execEvent: Event; + let genData: InsertedEvents; + + before(async () => { + // Construct the following tree: + // Origin -> infoEvent -> startEvent -> execEvent + origin = generator.generateEvent(); + infoEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['info'], + }); + + startEvent = generator.generateEvent({ + parentEntityID: infoEvent.process.entity_id, + ancestry: [infoEvent.process.entity_id, origin.process.entity_id], + eventType: ['start'], + }); + + execEvent = generator.generateEvent({ + parentEntityID: startEvent.process.entity_id, + ancestry: [startEvent.process.entity_id, infoEvent.process.entity_id], + eventType: ['change'], + }); + genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('finds all the children of the origin', async () => { + const { body }: { body: ResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process.entity_id}?children=100`) + .expect(200); + expect(body.children.childNodes.length).to.be(3); + expect(body.children.childNodes[0].entityID).to.be(infoEvent.process.entity_id); + expect(body.children.childNodes[1].entityID).to.be(startEvent.process.entity_id); + expect(body.children.childNodes[2].entityID).to.be(execEvent.process.entity_id); + }); + }); + + describe('duplicate process running events', () => { + let origin: Event; + let startEvent: Event; + let infoEvent: Event; + let execEvent: Event; + let genData: InsertedEvents; + + before(async () => { + // Construct the following tree: + // Origin -> (infoEvent, startEvent, execEvent are all for the same node) + origin = generator.generateEvent(); + startEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['start'], + }); + + infoEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + entityID: startEvent.process.entity_id, + eventType: ['info'], + }); + + execEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['change'], + entityID: startEvent.process.entity_id, + }); + genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('only retrieves the start event for the child node', async () => { + const childrenQuery = new ChildrenQuery( + PaginationBuilder.createBuilder(100), + eventsIndexPattern + ); + // [1] here gets the body portion of the array + const [, query] = childrenQuery.buildMSearch(origin.process.entity_id); + const { body } = await es.search>({ body: query }); + expect(body.hits.hits.length).to.be(1); + + const event = body.hits.hits[0]._source; + expect(entityId(event)).to.be(startEvent.process.entity_id); + expect(event.event?.type).to.eql(['start']); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 3527e7e575c99..758e26fd5eece 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -566,7 +566,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ).to.eql(93932); }); - it('returns no values when there is no more data', async () => { + // The children api does not support pagination currently + it.skip('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( @@ -577,7 +578,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.eql(null); }); - it('returns the first page of information when the cursor is invalid', async () => { + // The children api does not support pagination currently + it.skip('returns the first page of information when the cursor is invalid', async () => { const { body }: { body: ResolverChildren } = await supertest .get( `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=blah` @@ -639,7 +641,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.not.eql(null); }); - it('paginates the children', async () => { + // children api does not support pagination currently + it.skip('paginates the children', async () => { // this gets a node should have 3 children which were created in succession so that the timestamps // are ordered correctly to be retrieved in a single call const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; @@ -668,7 +671,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.be(null); }); - it('gets all children in two queries', async () => { + // children api does not support pagination currently + it.skip('gets all children in two queries', async () => { // should get all the children of the origin let { body }: { body: ResolverChildren } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=3`) From 4031f557012a9410fe2aa06add3e60b27b608864 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 3 Aug 2020 18:25:12 -0600 Subject: [PATCH 077/115] [Security Solution] Fixes scroll issues related to the sticky header (#74062) ## [Security Solution] Fixes scroll issues related to the sticky header Fixes scrolling issues related to the sticky header. The Security solution hid the app navigation links (e.g. `Overview`, `Detections`, `Hosts` ...) in the sticky header when the page was scrolled (to maximize the available vertical space), but in recent `7.9` BCs, sometimes this [created issues while scrolling](https://github.com/elastic/kibana/issues/73882). With the introduction of Full Screen mode in Timeline-based views, it's no longer necessary to hide the app navigation links while scrolling. (The navigation links are hidden when Timeline-based views are placed into full screen mode.) Fixes: https://github.com/elastic/kibana/issues/73882 ## Desk testing Desk tested in: - Chrome `84.0.4147.105` - Firefox `79.0` - Safari `13.1.2` --- .../public/app/home/index.tsx | 49 ++++------- .../components/alerts_viewer/alerts_table.tsx | 3 - .../common/components/alerts_viewer/index.tsx | 22 +---- .../events_viewer/events_viewer.tsx | 24 +++-- .../common/components/events_viewer/index.tsx | 70 ++++++++------- .../filters_global.test.tsx.snap | 23 +++-- .../filters_global/filters_global.test.tsx | 88 +++---------------- .../filters_global/filters_global.tsx | 60 ++++--------- .../common/components/header_global/index.tsx | 43 ++++++--- .../common/components/inspect/index.tsx | 1 + .../public/common/components/page/index.tsx | 28 +++++- .../common/components/wrapper_page/index.tsx | 23 +++-- .../containers/use_full_screen/index.tsx | 19 +++- .../common/hooks/use_global_header_portal.tsx | 19 ++++ .../components/alerts_table/index.tsx | 3 - .../detection_engine/detection_engine.tsx | 31 +------ .../detection_engine/rules/details/index.tsx | 32 +------ .../public/hosts/pages/details/index.tsx | 10 +-- .../public/hosts/pages/hosts.tsx | 10 +-- .../navigation/events_query_tab_body.tsx | 23 +---- .../public/network/pages/ip_details/index.tsx | 7 +- .../public/network/pages/network.tsx | 10 +-- .../public/overview/pages/overview.tsx | 7 +- .../flyout/__snapshots__/index.test.tsx.snap | 1 - .../components/flyout/index.test.tsx | 16 ++-- .../timelines/components/flyout/index.tsx | 10 +-- .../pane/__snapshots__/index.test.tsx.snap | 1 - .../components/flyout/pane/index.test.tsx | 29 +----- .../components/flyout/pane/index.tsx | 1 - .../components/graph_overlay/index.tsx | 8 +- .../components/timeline/body/helpers.ts | 35 -------- .../components/timeline/body/index.test.tsx | 2 - .../components/timeline/body/index.tsx | 10 +-- .../timeline/body/stateful_body.tsx | 4 - .../components/timeline/footer/index.tsx | 2 +- 35 files changed, 264 insertions(+), 460 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 41b9252c67b8a..f230566fa08e6 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -7,7 +7,6 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { useThrottledResizeObserver } from '../../common/components/utils'; import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout } from '../../timelines/components/flyout'; import { HeaderGlobal } from '../../common/components/header_global'; @@ -19,43 +18,29 @@ import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; -const WrappedByAutoSizer = styled.div` +const SecuritySolutionAppWrapper = styled.div` + display: flex; + flex-direction: column; height: 100%; + width: 100%; `; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper'; const Main = styled.main` - height: 100%; + position: relative; + overflow: auto; + flex: 1; `; + Main.displayName = 'Main'; const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) -/** the global Kibana navigation at the top of every page */ -export const globalHeaderHeightPx = 48; - -const calculateFlyoutHeight = ({ - globalHeaderSize, - windowHeight, -}: { - globalHeaderSize: number; - windowHeight: number; -}): number => Math.max(0, windowHeight - globalHeaderSize); - interface HomePageProps { children: React.ReactNode; } -export const HomePage: React.FC = ({ children }) => { - const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); - const flyoutHeight = useMemo( - () => - calculateFlyoutHeight({ - globalHeaderSize: globalHeaderHeightPx, - windowHeight, - }), - [windowHeight] - ); +const HomePageComponent: React.FC = ({ children }) => { const { signalIndexExists, signalIndexName } = useSignalIndex(); const indexToAdd = useMemo(() => { @@ -69,7 +54,7 @@ export const HomePage: React.FC = ({ children }) => { const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd); return ( - +
@@ -78,11 +63,7 @@ export const HomePage: React.FC = ({ children }) => { {indicesExist && showTimeline && ( <> - + )} @@ -91,8 +72,10 @@ export const HomePage: React.FC = ({ children }) => {
-
+ ); }; -HomePage.displayName = 'HomePage'; +HomePageComponent.displayName = 'HomePage'; + +export const HomePage = React.memo(HomePageComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index e30560f6c8147..841a1ef09ede6 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -58,7 +58,6 @@ const defaultAlertsFilters: Filter[] = [ interface Props { timelineId: TimelineIdLiteral; endDate: string; - eventsViewerBodyHeight?: number; startDate: string; pageFilters?: Filter[]; } @@ -66,7 +65,6 @@ interface Props { const AlertsTableComponent: React.FC = ({ timelineId, endDate, - eventsViewerBodyHeight, startDate, pageFilters = [], }) => { @@ -93,7 +91,6 @@ const AlertsTableComponent: React.FC = ({ pageFilters={alertsFilter} defaultModel={alertsDefaultModel} end={endDate} - height={eventsViewerBodyHeight} id={timelineId} start={startDate} /> diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 832b14f00159a..633135d63ac33 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -5,17 +5,9 @@ */ import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; -import { useWindowSize } from 'react-use'; -import { globalHeaderHeightPx } from '../../../app/home'; -import { DEFAULT_NUMBER_FORMAT, FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { useFullScreen } from '../../containers/use_full_screen'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../events_viewer/events_viewer'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; import { AlertsComponentsProps } from './types'; import { AlertsTable } from './alerts_table'; @@ -45,7 +37,6 @@ export const AlertsView = ({ // eslint-disable-next-line react-hooks/exhaustive-deps [] ); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( () => ({ @@ -79,17 +70,6 @@ export const AlertsView = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index e836e2e20432a..436386077e725 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { BrowserFields, DocValueFields } from '../../containers/source'; @@ -50,18 +50,18 @@ const TitleText = styled.span` margin-right: 12px; `; -const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; - const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` + display: flex; + flex-direction: column; + ${({ $isFullScreen }) => $isFullScreen && - css` + ` border: 0; box-shadow: none; padding-top: 0; padding-bottom: 0; - `} - max-width: 100%; + `} `; const TitleFlexGroup = styled(EuiFlexGroup)` @@ -70,7 +70,10 @@ const TitleFlexGroup = styled(EuiFlexGroup)` const EventsContainerLoading = styled.div` width: 100%; - overflow: auto; + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; `; /** @@ -78,9 +81,7 @@ const EventsContainerLoading = styled.div` * from being unmounted, to preserve the state of the component */ const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` - ${({ show }) => css` - ${show ? '' : 'visibility: hidden;'}; - `} + ${({ show }) => (show ? '' : 'visibility: hidden;')} `; interface Props { @@ -119,7 +120,6 @@ const EventsViewerComponent: React.FC = ({ end, filters, headerFilterGroup, - height = DEFAULT_EVENTS_VIEWER_HEIGHT, id, indexPattern, isLive, @@ -277,7 +277,6 @@ const EventsViewerComponent: React.FC = ({ docValueFields={docValueFields} id={id} isEventViewer={true} - height={height} sort={sort} toggleColumn={toggleColumn} /> @@ -326,7 +325,6 @@ export const EventsViewer = React.memo( prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && prevProps.headerFilterGroup === nextProps.headerFilterGroup && - prevProps.height === nextProps.height && prevProps.id === nextProps.id && deepEqual(prevProps.indexPattern, nextProps.indexPattern) && prevProps.isLive === nextProps.isLive && diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index c402116ee2714..1563eab6039a6 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import styled from 'styled-components'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -23,12 +24,20 @@ import { useUiSetting } from '../../lib/kibana'; import { EventsViewer } from './events_viewer'; import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { InspectButtonContainer } from '../inspect'; +import { useFullScreen } from '../../containers/use_full_screen'; + +const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; + +const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` + height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : `${DEFAULT_EVENTS_VIEWER_HEIGHT}px`)}; + display: flex; + width: 100%; +`; export interface OwnProps { defaultIndices?: string[]; defaultModel: SubsetTimelineModel; end: string; - height?: number; id: string; start: string; headerFilterGroup?: React.ReactNode; @@ -49,7 +58,6 @@ const StatefulEventsViewerComponent: React.FC = ({ excludedRowRendererIds, filters, headerFilterGroup, - height, id, isLive, itemsPerPage, @@ -74,6 +82,8 @@ const StatefulEventsViewerComponent: React.FC = ({ 'events_viewer' ); + const { globalFullScreen } = useFullScreen(); + useEffect(() => { if (createTimeline != null) { createTimeline({ @@ -121,33 +131,34 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); return ( - - - + + + + + ); }; @@ -219,7 +230,6 @@ export const StatefulEventsViewer = connector( prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && - prevProps.height === nextProps.height && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap index 35fe74abff284..994e98d8619a1 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap @@ -1,13 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendering renders correctly 1`] = ` -} > - - + + +

+ Additional filters here. +

+
+
+ `; diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx index 9fda60b1f289d..d9032092744a1 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx @@ -6,7 +6,6 @@ import { mount, ReactWrapper, shallow } from 'enzyme'; import React from 'react'; -import { StickyContainer } from 'react-sticky'; import '../../mock/match_media'; import { FiltersGlobal } from './filters_global'; @@ -15,7 +14,7 @@ import { TestProviders } from '../../mock/test_providers'; describe('rendering', () => { test('renders correctly', () => { const wrapper = shallow( - +

{'Additional filters here.'}

); @@ -23,101 +22,40 @@ describe('rendering', () => { expect(wrapper).toMatchSnapshot(); }); - describe('full screen mode', () => { + describe('when show is true (the default)', () => { let wrapper: ReactWrapper; beforeEach(() => { wrapper = mount( - - -

{'Filter content'}

-
-
+ +

{'Filter content'}

+
); }); - test('it does NOT render the sticky container', () => { - expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe( - false - ); - }); - - test('it renders the non-sticky container', () => { - expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(true); - }); - test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => { expect( - wrapper.find('[data-test-subj="non-sticky-global-container"]').first() - ).not.toHaveStyleRule('display', 'none'); - }); - }); - - describe('non-full screen mode', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - wrapper = mount( - - - -

{'Filter content'}

-
-
-
- ); - }); - - test('it renders the sticky container', () => { - expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe( - true - ); - }); - - test('it does NOT render the non-sticky container', () => { - expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(false); - }); - - test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => { - expect( - wrapper.find('[data-test-subj="sticky-filters-global-container"]').first() + wrapper.find('[data-test-subj="filters-global-container"]').first() ).not.toHaveStyleRule('display', 'none'); }); }); describe('when show is false', () => { - test('in full screen mode it renders the container with a `display: none` style', () => { + test('it renders the container with a `display: none` style', () => { const wrapper = mount( - - -

{'Filter content'}

-
-
+ +

{'Filter content'}

+
); - expect( - wrapper.find('[data-test-subj="non-sticky-global-container"]').first() - ).toHaveStyleRule('display', 'none'); - }); - - test('in non-full screen mode it renders the container with a `display: none` style', () => { - const wrapper = mount( - - - -

{'Filter content'}

-
-
-
+ expect(wrapper.find('[data-test-subj="filters-global-container"]').first()).toHaveStyleRule( + 'display', + 'none' ); - - expect( - wrapper.find('[data-test-subj="sticky-filters-global-container"]').first() - ).toHaveStyleRule('display', 'none'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx index 80e7209492fa5..c324b812a9ec2 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx @@ -4,38 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; -import { Sticky } from 'react-sticky'; import styled, { css } from 'styled-components'; +import { InPortal } from 'react-reverse-portal'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; +import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { gutterTimeline } from '../../lib/helpers'; -const offsetChrome = 49; - -const disableSticky = `screen and (max-width: ${euiLightVars.euiBreakpoints.s})`; -const disableStickyMq = window.matchMedia(disableSticky); - -const Wrapper = styled.aside<{ isSticky?: boolean }>` - height: ${FILTERS_GLOBAL_HEIGHT}px; +const Wrapper = styled.aside` position: relative; z-index: ${({ theme }) => theme.eui.euiZNavigation}; background: ${({ theme }) => theme.eui.euiColorEmptyShade}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - padding: ${({ theme }) => theme.eui.paddingSizes.m} ${gutterTimeline} ${({ theme }) => - theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; - - ${({ isSticky }) => - isSticky && - css` - top: ${offsetChrome}px !important; - `} - - @media only ${disableSticky} { - position: static !important; - z-index: ${({ theme }) => theme.eui.euiZContent} !important; - } + padding: ${({ theme }) => theme.eui.paddingSizes.m} ${gutterTimeline} + ${({ theme }) => theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; `; Wrapper.displayName = 'Wrapper'; @@ -47,33 +29,21 @@ const FiltersGlobalContainer = styled.header<{ show: boolean }>` FiltersGlobalContainer.displayName = 'FiltersGlobalContainer'; -const NO_STYLE: React.CSSProperties = {}; - export interface FiltersGlobalProps { children: React.ReactNode; - globalFullScreen: boolean; show?: boolean; } -export const FiltersGlobal = React.memo( - ({ children, globalFullScreen, show = true }) => - globalFullScreen ? ( - - - {children} - +export const FiltersGlobal = React.memo(({ children, show = true }) => { + const { globalHeaderPortalNode } = useGlobalHeaderPortal(); + + return ( + + + {children} - ) : ( - - {({ style, isSticky }) => ( - - - {children} - - - )} - - ) -); + + ); +}); FiltersGlobal.displayName = 'FiltersGlobal'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index a1e7293ce974b..fbc3d62768d00 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -7,29 +7,31 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { pickBy } from 'lodash/fp'; import React, { useCallback } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; +import { OutPortal } from 'react-reverse-portal'; import { gutterTimeline } from '../../lib/helpers'; import { navTabs } from '../../../app/home/home_navigations'; +import { useFullScreen } from '../../containers/use_full_screen'; import { SecurityPageName } from '../../../app/types'; import { getAppOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; import { useWithSource } from '../../containers/source'; -import { useFullScreen } from '../../containers/use_full_screen'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { useKibana } from '../../lib/kibana'; import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; +import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { LinkAnchor } from '../links'; -const Wrapper = styled.header<{ show: boolean }>` - ${({ show, theme }) => css` +const Wrapper = styled.header<{ $globalFullScreen: boolean }>` + ${({ $globalFullScreen, theme }) => ` background: ${theme.eui.euiColorEmptyShade}; border-bottom: ${theme.eui.euiBorderThin}; - padding: ${theme.eui.paddingSizes.m} ${gutterTimeline} ${theme.eui.paddingSizes.m} - ${theme.eui.paddingSizes.l}; - ${show ? '' : 'display: none;'}; + padding-top: ${$globalFullScreen ? theme.eui.paddingSizes.s : theme.eui.paddingSizes.m}; + width: 100%; + z-index: ${theme.eui.euiZNavigation}; `} `; Wrapper.displayName = 'Wrapper'; @@ -39,11 +41,24 @@ const FlexItem = styled(EuiFlexItem)` `; FlexItem.displayName = 'FlexItem'; +const FlexGroup = styled(EuiFlexGroup)<{ $globalFullScreen: boolean }>` + ${({ $globalFullScreen, theme }) => ` + border-bottom: ${theme.eui.euiBorderThin}; + margin-bottom: 1px; + padding-bottom: 4px; + padding-left: ${theme.eui.paddingSizes.l}; + padding-right: ${gutterTimeline}; + ${$globalFullScreen ? 'display: none;' : ''} + `} +`; +FlexGroup.displayName = 'FlexGroup'; + interface HeaderGlobalProps { hideDetectionEngine?: boolean; } export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { const { indicesExist } = useWithSource(); + const { globalHeaderPortalNode } = useGlobalHeaderPortal(); const { globalFullScreen } = useFullScreen(); const search = useGetUrlSearch(navTabs.overview); const { navigateToApp } = useKibana().services.application; @@ -56,8 +71,13 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine ); return ( - - + + <> @@ -100,7 +120,10 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - + +
+ +
); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index 3dc120b3d874d..435f3f6e349d0 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -20,6 +20,7 @@ import * as i18n from './translations'; export const BUTTON_CLASS = 'inspectButtonComponent'; export const InspectButtonContainer = styled.div<{ show?: boolean }>` + width: 100%; display: flex; flex-grow: 1; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 8bf0690bfd0ad..6c49ce7453ce9 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -62,15 +62,39 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar z-index: 9950; } - /** applies a "toggled" button style to the Full Screen button */ + /* applies a "toggled" button style to the Full Screen button */ .${FULL_SCREEN_TOGGLED_CLASS_NAME} { ${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`}; } - .${SCROLLING_DISABLED_CLASS_NAME} body { + body { overflow-y: hidden; } + #kibana-body { + height: 100%; + + > .content { + height: 100%; + + > .app-wrapper { + height: 100%; + + > .app-wrapper-panel { + height: 100%; + + > .application { + height: 100%; + + > div { + height: 100%; + } + } + } + } + } + } + .${SCROLLING_DISABLED_CLASS_NAME} #kibana-body { overflow-y: hidden; } diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx index 03f9b43678003..373c1f7aaec75 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx @@ -12,12 +12,9 @@ import { useFullScreen } from '../../containers/use_full_screen'; import { gutterTimeline } from '../../lib/helpers'; import { AppGlobalStyle } from '../page/index'; -const Wrapper = styled.div<{ noPadding?: boolean }>` - padding: ${(props) => - props.noPadding - ? '0' - : `${props.theme.eui.paddingSizes.l} ${gutterTimeline} ${props.theme.eui.paddingSizes.l} - ${props.theme.eui.paddingSizes.l}`}; +const Wrapper = styled.div` + padding: ${({ theme }) => + `${theme.eui.paddingSizes.l} ${gutterTimeline} ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l}`}; &.siemWrapperPage--restrictWidthDefault, &.siemWrapperPage--restrictWidthCustom { box-sizing: content-box; @@ -27,6 +24,14 @@ const Wrapper = styled.div<{ noPadding?: boolean }>` &.siemWrapperPage--restrictWidthDefault { max-width: 1000px; } + + &.siemWrapperPage--fullHeight { + height: 100%; + } + + &.siemWrapperPage--noPadding { + padding: 0; + } `; Wrapper.displayName = 'Wrapper'; @@ -46,13 +51,15 @@ const WrapperPageComponent: React.FC = ({ style, noPadding, }) => { - const { setGlobalFullScreen } = useFullScreen(); + const { globalFullScreen, setGlobalFullScreen } = useFullScreen(); useEffect(() => { setGlobalFullScreen(false); // exit full screen mode on page load }, [setGlobalFullScreen]); const classes = classNames(className, { siemWrapperPage: true, + 'siemWrapperPage--noPadding': noPadding, + 'siemWrapperPage--fullHeight': globalFullScreen, 'siemWrapperPage--restrictWidthDefault': restrictWidth && typeof restrictWidth === 'boolean' && restrictWidth === true, 'siemWrapperPage--restrictWidthCustom': restrictWidth && typeof restrictWidth !== 'boolean', @@ -66,7 +73,7 @@ const WrapperPageComponent: React.FC = ({ } return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx index aa0d90a216035..32591fb032436 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx @@ -11,6 +11,22 @@ import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../common/constants'; import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; +export const resetScroll = () => { + setTimeout(() => { + window.scrollTo(0, 0); + + const kibanaBody = document.querySelector('#kibana-body'); + if (kibanaBody != null) { + kibanaBody.scrollTop = 0; + } + + const pageContainer = document.querySelector('[data-test-subj="pageContainer"]'); + if (pageContainer != null) { + pageContainer.scrollTop = 0; + } + }, 0); +}; + export const useFullScreen = () => { const dispatch = useDispatch(); const globalFullScreen = useSelector(inputsSelectors.globalFullScreenSelector) ?? false; @@ -20,9 +36,10 @@ export const useFullScreen = () => { (fullScreen: boolean) => { if (fullScreen) { document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME); + resetScroll(); } else { document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME); - setTimeout(() => window.scrollTo(0, 0), 0); + resetScroll(); } dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen })); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx new file mode 100644 index 0000000000000..546d2615fe6ae --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; +import { createPortalNode } from 'react-reverse-portal'; + +/** + * A singleton portal for rendering content in the global header + */ +const globalHeaderPortalNodeSingleton = createPortalNode(); + +export const useGlobalHeaderPortal = () => { + const [globalHeaderPortalNode] = useState(globalHeaderPortalNodeSingleton); + + return { globalHeaderPortalNode }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index ab95e433d92f3..d93bad29f3348 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -61,7 +61,6 @@ interface OwnProps { timelineId: TimelineIdLiteral; canUserCRUD: boolean; defaultFilters?: Filter[]; - eventsViewerBodyHeight?: number; hasIndexWrite: boolean; from: string; loading: boolean; @@ -88,7 +87,6 @@ export const AlertsTableComponent: React.FC = ({ clearEventsLoading, clearSelected, defaultFilters, - eventsViewerBodyHeight, from, globalFilters, globalQuery, @@ -451,7 +449,6 @@ export const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={to} headerFilterGroup={headerFilterGroup} - height={eventsViewerBodyHeight} id={timelineId} start={from} utilityBar={utilityBarCallback} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index c114e4519df10..d76da592e1c81 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -7,12 +7,9 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; -import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; -import { useWindowSize } from 'react-use'; import { useHistory } from 'react-router-dom'; -import { globalHeaderHeightPx } from '../../../app/home'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { useGlobalTime } from '../../../common/containers/use_global_time'; @@ -34,7 +31,6 @@ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; import { useUserInfo } from '../../components/user_info'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../common/components/events_viewer/events_viewer'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; @@ -43,14 +39,8 @@ import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unau import * as i18n from './translations'; import { LinkButton } from '../../../common/components/links'; import { useFormatUrl } from '../../../common/components/link_to'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import { Display } from '../../../hosts/pages/display'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -64,7 +54,6 @@ export const DetectionEnginePageComponent: React.FC = ({ setAbsoluteRangeDatePicker, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); const { loading: userInfoLoading, @@ -157,12 +146,9 @@ export const DetectionEnginePageComponent: React.FC = ({ {hasEncryptionKey != null && !hasEncryptionKey && } {hasIndexWrite != null && !hasIndexWrite && } {indicesExist ? ( - + <> - + @@ -210,17 +196,6 @@ export const DetectionEnginePageComponent: React.FC = ({ loading={loading} hasIndexWrite={hasIndexWrite ?? false} canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)} - eventsViewerBodyHeight={ - globalFullScreen - ? getEventsViewerBodyHeight({ - footerHeight, - headerHeight: EVENTS_VIEWER_HEADER_HEIGHT, - kibanaChromeHeight: globalHeaderHeightPx, - otherContentHeight: FILTERS_GLOBAL_HEIGHT, - windowHeight, - }) - : MIN_EVENTS_VIEWER_BODY_HEIGHT - } from={from} defaultFilters={alertsTableDefaultFilters} showBuildingBlockAlerts={showBuildingBlockAlerts} @@ -229,7 +204,7 @@ export const DetectionEnginePageComponent: React.FC = ({ to={to} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 789469e981fb4..4327ef96c93a7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -21,11 +21,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; -import { useWindowSize } from 'react-use'; -import { globalHeaderHeightPx } from '../../../../../app/home'; import { TimelineId } from '../../../../../../common/types/timeline'; import { UpdateDateRange } from '../../../../../common/components/charts/common'; import { FiltersGlobal } from '../../../../../common/components/filters_global'; @@ -66,7 +63,6 @@ import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../../../common/components/events_viewer/events_viewer'; import { inputsSelectors } from '../../../../../common/store/inputs'; import { State } from '../../../../../common/store'; import { InputsRange } from '../../../../../common/store/inputs/model'; @@ -81,15 +77,10 @@ import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; -import { DEFAULT_INDEX_PATTERN, FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../shared_imports'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../../../timelines/components/timeline/footer'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; @@ -167,7 +158,6 @@ export const RuleDetailsPageComponent: FC = ({ const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); // TODO: Refactor license check + hasMlAdminPermissions to common check @@ -364,12 +354,9 @@ export const RuleDetailsPageComponent: FC = ({ {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions(canUserCRUD) && } {indicesExist ? ( - + <> - + @@ -507,17 +494,6 @@ export const RuleDetailsPageComponent: FC = ({ timelineId={TimelineId.detectionsRulesDetailsPage} canUserCRUD={canUserCRUD ?? false} defaultFilters={alertDefaultFilters} - eventsViewerBodyHeight={ - globalFullScreen - ? getEventsViewerBodyHeight({ - footerHeight, - headerHeight: EVENTS_VIEWER_HEADER_HEIGHT, - kibanaChromeHeight: globalHeaderHeightPx, - otherContentHeight: FILTERS_GLOBAL_HEIGHT, - windowHeight, - }) - : MIN_EVENTS_VIEWER_BODY_HEIGHT - } hasIndexWrite={hasIndexWrite ?? false} from={from} loading={loading} @@ -542,7 +518,7 @@ export const RuleDetailsPageComponent: FC = ({ )} {ruleDetailTab === RuleDetailTabs.failures && } - + ) : ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index b6c1727ee6afa..34840b2826626 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -8,7 +8,6 @@ import { EuiHorizontalRule, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { SecurityPageName } from '../../../app/types'; import { UpdateDateRange } from '../../../common/components/charts/common'; @@ -102,12 +101,9 @@ const HostDetailsComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -210,7 +206,7 @@ const HostDetailsComponent = React.memo( setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 1d0b73f80a69a..e4e69443c510d 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -8,7 +8,6 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { useParams } from 'react-router-dom'; import { SecurityPageName } from '../../app/types'; @@ -96,12 +95,9 @@ export const HostsComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -156,7 +152,7 @@ export const HostsComponent = React.memo( hostsPagePath={hostsPagePath} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index 962c227d6b670..cea987db485f4 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -6,7 +6,6 @@ import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import { useWindowSize } from 'react-use'; import { TimelineId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; @@ -22,15 +21,7 @@ import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { - getEventsViewerBodyHeight, - getInvestigateInResolverAction, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; -import { globalHeaderHeightPx } from '../../../app/home'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../common/components/events_viewer/events_viewer'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; +import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; @@ -71,7 +62,6 @@ export const EventsQueryTabBody = ({ }: HostsComponentsQueryProps) => { const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); useEffect(() => { initializeTimeline({ @@ -108,17 +98,6 @@ export const EventsQueryTabBody = ({ {indicesExist ? ( - - + <> + @@ -260,7 +259,7 @@ export const IPDetailsComponent: React.FC - + ) : ( diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index f516f2a2de346..601bae89f7a4d 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -9,7 +9,6 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; import { esQuery } from '../../../../../../src/plugins/data/public'; import { SecurityPageName } from '../../app/types'; @@ -104,12 +103,9 @@ const NetworkComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -180,7 +176,7 @@ const NetworkComponent = React.memo( )} - + ) : ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 1b743c259555a..520fd6c459705 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { Query, Filter } from 'src/plugins/data/public'; import styled from 'styled-components'; @@ -70,8 +69,8 @@ const OverviewComponent: React.FC = ({ return ( <> {indicesExist ? ( - - + <> + @@ -140,7 +139,7 @@ const OverviewComponent: React.FC = ({ - + ) : ( )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap index 4bf0033bcb430..46c9fbb524066 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap @@ -2,7 +2,6 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` ({ StatefulTimeline: () =>
, })); -const testFlyoutHeight = 980; const usersViewing = ['elastic']; describe('Flyout', () => { @@ -39,7 +38,7 @@ describe('Flyout', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper.find('Flyout')).toMatchSnapshot(); @@ -48,7 +47,7 @@ describe('Flyout', () => { test('it renders the default flyout state as a button', () => { const wrapper = mount( - + ); @@ -69,7 +68,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -94,7 +93,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -117,7 +116,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -127,7 +126,7 @@ describe('Flyout', () => { test('it hides the data providers badge when the timeline does NOT have data providers', () => { const wrapper = mount( - + ); @@ -152,7 +151,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -167,7 +166,6 @@ describe('Flyout', () => { ` Visible.displayName = 'Visible'; interface OwnProps { - flyoutHeight: number; timelineId: string; usersViewing: string[]; } @@ -44,7 +43,7 @@ interface OwnProps { type Props = OwnProps & ProsFromRedux; export const FlyoutComponent = React.memo( - ({ dataProviders, flyoutHeight, show = true, showTimeline, timelineId, usersViewing, width }) => { + ({ dataProviders, show = true, showTimeline, timelineId, usersViewing, width }) => { const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ showTimeline, timelineId, @@ -57,12 +56,7 @@ export const FlyoutComponent = React.memo( return ( <> - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap index d30fd6f31012c..f24ef3448d03f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -2,7 +2,6 @@ exports[`Pane renders correctly against snapshot 1`] = ` { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( - + {'I am a child of flyout'} @@ -33,12 +27,7 @@ describe('Pane', () => { test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { const wrapper = mount( - + {'I am a child of flyout'} @@ -50,12 +39,7 @@ describe('Pane', () => { test('it should render a resize handle', () => { const wrapper = mount( - + {'I am a child of flyout'} @@ -67,12 +51,7 @@ describe('Pane', () => { test('it should render children', () => { const wrapper = mount( - + {'I am a mock body'} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 3f842bcc2eb68..7528468ef6522 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -22,7 +22,6 @@ const minWidthPixels = 550; // do not allow the flyout to shrink below this widt const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view interface FlyoutPaneComponentProps { children: React.ReactNode; - flyoutHeight: number; onClose: () => void; timelineId: string; width: number; diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 97d1d11395c7f..ededf70152968 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -34,8 +34,8 @@ import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal' import * as i18n from './translations'; -const OverlayContainer = styled.div<{ bodyHeight?: number }>` - height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; +const OverlayContainer = styled.div` + height: 100%; width: 100%; display: flex; flex-direction: column; @@ -50,7 +50,6 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` `; interface OwnProps { - bodyHeight?: number; graphEventId?: string; timelineId: string; timelineType: TimelineType; @@ -97,7 +96,6 @@ const Navigation = ({ ); const GraphOverlayComponent = ({ - bodyHeight, graphEventId, status, timelineId, @@ -140,7 +138,7 @@ const GraphOverlayComponent = ({ ]); return ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts index 73b5a58ef7b65..b62888fbf8427 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts @@ -135,38 +135,3 @@ export const getInvestigateInResolverAction = ({ dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })), width: DEFAULT_ICON_BUTTON_WIDTH, }); - -/** - * The minimum height of a timeline-based events viewer body, as seen in several - * views, e.g. `Detections`, `Events`, `External events`, etc - */ -export const MIN_EVENTS_VIEWER_BODY_HEIGHT = 500; // px - -interface GetEventsViewerBodyHeightParams { - /** the height of the header, e.g. the section containing "`Showing n event / alerts`, and `Open` / `In progress` / `Closed` filters" */ - headerHeight: number; - /** the height of the footer, e.g. "`25 of 100 events / alerts`, `Load More`, `Updated n minutes ago`" */ - footerHeight: number; - /** the height of the global Kibana chrome, common throughout the app */ - kibanaChromeHeight: number; - /** the (combined) height of other non-events viewer content, e.g. the global search / filter bar in full screen mode */ - otherContentHeight: number; - /** the full height of the window */ - windowHeight: number; -} - -export const getEventsViewerBodyHeight = ({ - footerHeight, - headerHeight, - kibanaChromeHeight, - otherContentHeight, - windowHeight, -}: GetEventsViewerBodyHeightParams) => { - if (windowHeight === 0 || !isFinite(windowHeight)) { - return MIN_EVENTS_VIEWER_BODY_HEIGHT; - } - - const combinedHeights = kibanaChromeHeight + otherContentHeight + headerHeight + footerHeight; - - return Math.max(MIN_EVENTS_VIEWER_BODY_HEIGHT, windowHeight - combinedHeights); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 5a98263cbd3fd..456c1ee541471 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -22,7 +22,6 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; import { TimelineType } from '../../../../../common/types/timeline'; -const testBodyHeight = 700; const mockGetNotesByIds = (eventId: string[]) => []; const mockSort: Sort = { columnId: '@timestamp', @@ -65,7 +64,6 @@ describe('Body', () => { data: mockTimelineData, docValueFields: [], eventIdToNoteIds: {}, - height: testBodyHeight, id: 'timeline-test', isSelectAllChecked: false, getNotesByIds: mockGetNotesByIds, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index e971dc6c8e1e2..6f578ffe3e956 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -42,7 +42,6 @@ export interface BodyProps { docValueFields: DocValueFields[]; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; - height?: number; id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -85,7 +84,6 @@ export const Body = React.memo( eventIdToNoteIds, getNotesByIds, graphEventId, - height, id, isEventViewer = false, isSelectAllChecked, @@ -129,17 +127,11 @@ export const Body = React.memo( return ( <> {graphEventId && ( - + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 550a4adf713c6..15fa13b1a08f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -42,7 +42,6 @@ interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; docValueFields: DocValueFields[]; - height?: number; id: string; isEventViewer?: boolean; sort: Sort; @@ -63,7 +62,6 @@ const StatefulBodyComponent = React.memo( docValueFields, eventIdToNoteIds, excludedRowRendererIds, - height, id, isEventViewer = false, isSelectAllChecked, @@ -199,7 +197,6 @@ const StatefulBodyComponent = React.memo( eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} graphEventId={graphEventId} - height={height} id={id} isEventViewer={isEventViewer} isSelectAllChecked={isSelectAllChecked} @@ -234,7 +231,6 @@ const StatefulBodyComponent = React.memo( prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && - prevProps.height === nextProps.height && prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index 4e1595eef984c..3169201b12c7b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -68,7 +68,7 @@ const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ( height: `${height}px`, }, }))` - flex: 0; + flex: 0 0 auto; `; FooterContainer.displayName = 'FooterContainer'; From 3cce216f2e5c3c1167723414309ae78506f9c614 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 3 Aug 2020 19:24:48 -0700 Subject: [PATCH 078/115] [Ingest Manager] Improve agent config list -> detail UI transition (#73566) * Improve agent config detail page loading state * Show ID instead of loading icon * Fix types --- .../agent_config/details_page/index.tsx | 261 +++++++++--------- 1 file changed, 133 insertions(+), 128 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 0e65cb80f07c4..b87bd66cce0ea 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -10,7 +10,6 @@ import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, - EuiCallOut, EuiText, EuiSpacer, EuiButtonEmpty, @@ -24,7 +23,7 @@ import styled from 'styled-components'; import { AgentConfig, AgentConfigDetailsDeployAgentAction } from '../../../types'; import { PAGE_ROUTING_PATHS } from '../../../constants'; import { useGetOneAgentConfig, useLink, useBreadcrumbs, useCore } from '../../../hooks'; -import { Loading } from '../../../components'; +import { Loading, Error } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; import { LinkedAgentCount, AgentConfigActionMenu } from '../components'; @@ -109,97 +108,98 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { }, [routeState, navigateToApp]); const headerRightContent = useMemo( - () => ( - - {[ - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.revision', { - defaultMessage: 'Revision', - }), - content: agentConfig?.revision ?? 0, - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.package_configs', { - defaultMessage: 'Integrations', - }), - content: ( - - ), - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.usedBy', { - defaultMessage: 'Used by', - }), - content: ( - - ), - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.lastUpdated', { - defaultMessage: 'Last updated on', - }), - content: - (agentConfig && ( - + agentConfig ? ( + + {[ + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.revision', { + defaultMessage: 'Revision', + }), + content: agentConfig?.revision ?? 0, + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.package_configs', { + defaultMessage: 'Integrations', + }), + content: ( + - )) || - '', - }, - { isDivider: true }, - { - content: agentConfig && ( - { - history.push(getPath('configuration_details', { configId: newAgentConfig.id })); - }} - enrollmentFlyoutOpenByDefault={openEnrollmentFlyoutOpenByDefault} - onCancelEnrollment={ - routeState && routeState.onDoneNavigateTo - ? enrollmentCancelClickHandler - : undefined - } - /> - ), - }, - ].map((item, index) => ( - - {item.isDivider ?? false ? ( - - ) : item.label ? ( - - - {item.label} - - - {item.content} - - - ) : ( - item.content - )} - - ))} - - ), + ), + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.usedBy', { + defaultMessage: 'Used by', + }), + content: ( + + ), + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.lastUpdated', { + defaultMessage: 'Last updated on', + }), + content: + (agentConfig && ( + + )) || + '', + }, + { isDivider: true }, + { + content: agentConfig && ( + { + history.push(getPath('configuration_details', { configId: newAgentConfig.id })); + }} + enrollmentFlyoutOpenByDefault={openEnrollmentFlyoutOpenByDefault} + onCancelEnrollment={ + routeState && routeState.onDoneNavigateTo + ? enrollmentCancelClickHandler + : undefined + } + /> + ), + }, + ].map((item, index) => ( + + {item.isDivider ?? false ? ( + + ) : item.label ? ( + + + {item.label} + + + {item.content} + + + ) : ( + item.content + )} + + ))} + + ) : undefined, /* eslint-disable-next-line react-hooks/exhaustive-deps */ [agentConfig, configId, agentStatus] ); @@ -225,45 +225,50 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { ]; }, [getHref, configId, tabId]); - if (redirectToAgentConfigList) { - return ; - } + const content = useMemo(() => { + if (redirectToAgentConfigList) { + return ; + } - if (isLoading) { - return ; - } + if (isLoading) { + return ; + } - if (error) { - return ( - - -

- {error.message} -

-
-
- ); - } + if (error) { + return ( + + } + error={error} + /> + ); + } - if (!agentConfig) { - return ( - - + } + error={i18n.translate('xpack.ingestManager.configDetails.configNotFoundErrorTitle', { + defaultMessage: "Config '{id}' not found", + values: { + id: configId, + }, + })} /> - - ); - } + ); + } + + return ; + }, [agentConfig, configId, error, isLoading, redirectToAgentConfigList]); return ( @@ -273,7 +278,7 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { rightColumn={headerRightContent} tabs={(headerTabs as unknown) as EuiTabProps[]} > - + {content} From fb3370d99b2b8be5ba49b36b301fd9efd4502447 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 4 Aug 2020 09:17:03 +0200 Subject: [PATCH 079/115] updates file picker label (#74136) --- .../components/value_lists_management_modal/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts index 7063dca2341ca..9beebdfb923dc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts @@ -13,7 +13,7 @@ export const MODAL_TITLE = i18n.translate('xpack.securitySolution.lists.uploadVa export const FILE_PICKER_LABEL = i18n.translate( 'xpack.securitySolution.lists.uploadValueListDescription', { - defaultMessage: 'Upload single value lists to use while writing rules or rule exceptions.', + defaultMessage: 'Upload single value lists to use while writing rule exceptions.', } ); From e57f4d2b25ac95183e876ea0c305138ab119ea26 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 4 Aug 2020 10:50:32 +0300 Subject: [PATCH 080/115] [KP] update ES client migration guide (#73241) * add note about error format and FTR service * add notice about lack of response typings * more detailes example of search response type usage --- src/core/MIGRATION_EXAMPLES.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index d630fec652a37..3f34742e44861 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -1082,7 +1082,7 @@ const { body } = await client.asInternalUser.get({ id: 'id' }); const { body } = await client.asInternalUser.get({ id: 'id' }); ``` -- the returned error types changed +- the returned error types changed There are no longer specific errors for every HTTP status code (such as `BadRequest` or `NotFound`). A generic `ResponseError` with the specific `statusCode` is thrown instead. @@ -1097,6 +1097,7 @@ try { if(e instanceof errors.NotFound) { // do something } + if(e.status === 401) {} } ``` @@ -1115,6 +1116,7 @@ try { if(e.name === 'ResponseError' && e.statusCode === 404) { // do something } + if(e.statusCode === 401) {...} } ``` @@ -1178,6 +1180,30 @@ const request = client.asCurrentUser.ping({}, { }); ``` +- the new client doesn't provide exhaustive typings for the response object yet. You might have to copy +response type definitions from the Legacy Elasticsearch library until https://github.com/elastic/elasticsearch-js/pull/970 merged. + +```ts +// platform provides a few typings for internal purposes +import { SearchResponse } from 'src/core/server'; +type SearchSource = {...}; +type SearchBody = SearchResponse; +const { body } = await client.search(...); +interface Info {...} +const { body } = await client.info(...); +``` + +- Functional tests are subject to migration to the new client as well. +before: +```ts +const client = getService('legacyEs'); +``` + +after: +```ts +const client = getService('es'); +``` + Please refer to the [Breaking changes list](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/breaking-changes.html) for more information about the changes between the legacy and new client. From fc04ef915269a754f35e2eba64a2088e29f1ab30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 4 Aug 2020 11:46:49 +0100 Subject: [PATCH 081/115] Observabilty overview APM section doesn't fully collapse (#74224) --- .../public/pages/overview/index.tsx | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 32bdb00577bd2..8870bcbc9fa38 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -113,44 +113,46 @@ export function OverviewPage({ routeParams }: Props) { {/* Data sections */} {showDataSections && ( - - {hasData.infra_logs && ( - - - - )} - {hasData.infra_metrics && ( - - - - )} - {hasData.apm && ( - - - - )} - {hasData.uptime && ( - - - - )} - + + + {hasData.infra_logs && ( + + + + )} + {hasData.infra_metrics && ( + + + + )} + {hasData.apm && ( + + + + )} + {hasData.uptime && ( + + + + )} + + )} {/* Empty sections */} From 8e7d0b86cccce9052434f94e6cc369dab817964d Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 4 Aug 2020 07:36:21 -0400 Subject: [PATCH 082/115] [SECURITY] Bug do not allow saving when opening a timeline (#74139) * only saved timeline when you are duplicating them * fix unit test --- .../public/timelines/components/open_timeline/helpers.test.ts | 1 + .../public/timelines/components/open_timeline/helpers.ts | 2 +- .../public/timelines/store/timeline/actions.ts | 1 + .../security_solution/public/timelines/store/timeline/epic.ts | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index f4bd17005fed7..ac6c61b33b35e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -967,6 +967,7 @@ describe('helpers', () => { expect(dispatchAddTimeline).toHaveBeenCalledWith({ id: 'timeline-1', + savedTimeline: true, timeline: mockTimelineModel, }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index af289f94c9a0d..27920fa297a48 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -374,7 +374,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli ruleNote, }: UpdateTimeline): (() => void) => () => { dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); - dispatch(dispatchAddTimeline({ id, timeline })); + dispatch(dispatchAddTimeline({ id, timeline, savedTimeline: duplicate })); if ( timeline.kqlQuery != null && timeline.kqlQuery.filterQuery != null && diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index faeef432ea422..ebee0f5cae9a6 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -104,6 +104,7 @@ export const updateTimeline = actionCreator<{ export const addTimeline = actionCreator<{ id: string; timeline: TimelineModel; + savedTimeline?: boolean; }>('ADD_TIMELINE'); export const setInsertTimeline = actionCreator('SET_INSERT_TIMELINE'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 7757794c6dc9a..ad849c3a995b3 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -6,6 +6,7 @@ import { get, + getOr, has, merge as mergeObject, set, @@ -172,7 +173,7 @@ export const createTimelineEpic = (): Epic< myEpicTimelineId.setTimelineVersion(addNewTimeline.version); myEpicTimelineId.setTemplateTimelineId(addNewTimeline.templateTimelineId); myEpicTimelineId.setTemplateTimelineVersion(addNewTimeline.templateTimelineVersion); - return true; + return getOr(false, 'payload.savedTimeline', action); } else if ( timelineActionsType.includes(action.type) && !timelineObj.isLoading && From 5c741a1e0056d46612c6e6633adef3dc1173472c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 4 Aug 2020 07:37:22 -0400 Subject: [PATCH 083/115] reset datepicker of timeline (#74210) --- .../properties/use_create_timeline.test.tsx | 46 ++++++++++++++++++- .../properties/use_create_timeline.tsx | 25 ++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index 8f548f16cf1d6..5b3bc72fc37ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -4,18 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { act as actDom } from 'react-dom/test-utils'; + import { renderHook, act } from '@testing-library/react-hooks'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { TimelineType } from '../../../../../common/types/timeline'; import { TestProviders } from '../../../../common/mock'; import { useCreateTimelineButton } from './use_create_timeline'; +const mockDispatch = jest.fn(); + jest.mock('react-redux', () => { const actual = jest.requireActual('react-redux'); return { ...actual, - useDispatch: jest.fn(), + useDispatch: () => mockDispatch, + useSelector: () => ({ + kind: 'relative', + fromStr: 'now-24h', + toStr: 'now', + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + }), }; }); @@ -65,4 +76,35 @@ describe('useCreateTimelineButton', () => { expect(wrapper.find('[data-test-subj="timeline-new"]').exists()).toBeTruthy(); }); }); + + test('Make sure that timeline reset to the global date picker', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } + ); + + await waitForNextUpdate(); + const button = result.current.getButton({ outline: false, title: 'mock title' }); + actDom(() => { + const wrapper = mount(button); + wrapper.update(); + + wrapper.find('[data-test-subj="timeline-new"]').first().simulate('click'); + + expect(mockDispatch.mock.calls[0][0].type).toEqual( + 'x-pack/security_solution/local/timeline/CREATE_TIMELINE' + ); + expect(mockDispatch.mock.calls[1][0].type).toEqual( + 'x-pack/security_solution/local/inputs/ADD_GLOBAL_LINK_TO' + ); + expect(mockDispatch.mock.calls[2][0].type).toEqual( + 'x-pack/security_solution/local/inputs/ADD_TIMELINE_LINK_TO' + ); + expect(mockDispatch.mock.calls[3][0].type).toEqual( + 'x-pack/security_solution/local/inputs/SET_RELATIVE_RANGE_DATE_PICKER' + ); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index f418491ac4e47..97f3b1df011ff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { timelineActions } from '../../../store/timeline'; @@ -14,6 +14,7 @@ import { TimelineType, TimelineTypeLiteral, } from '../../../../../common/types/timeline'; +import { inputsActions, inputsSelectors } from '../../../../common/store/inputs'; export const useCreateTimelineButton = ({ timelineId, @@ -26,13 +27,12 @@ export const useCreateTimelineButton = ({ }) => { const dispatch = useDispatch(); const { timelineFullScreen, setTimelineFullScreen } = useFullScreen(); - + const globalTimeRange = useSelector(inputsSelectors.globalTimeRangeSelector); const createTimeline = useCallback( ({ id, show }) => { if (id === TimelineId.active && timelineFullScreen) { setTimelineFullScreen(false); } - dispatch( timelineActions.createTimeline({ id, @@ -41,8 +41,25 @@ export const useCreateTimelineButton = ({ timelineType, }) ); + dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); + dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' })); + if (globalTimeRange.kind === 'absolute') { + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + ...globalTimeRange, + id: 'timeline', + }) + ); + } else if (globalTimeRange.kind === 'relative') { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + ...globalTimeRange, + id: 'timeline', + }) + ); + } }, - [dispatch, setTimelineFullScreen, timelineFullScreen, timelineType] + [dispatch, globalTimeRange, setTimelineFullScreen, timelineFullScreen, timelineType] ); const handleButtonClick = useCallback(() => { From 53131313df49ee7eeac08e856e81eb6e617ac7e5 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 4 Aug 2020 08:19:02 -0400 Subject: [PATCH 084/115] [kbn-storybook] Kill tsdocgen for immediate perf improvement (#74059) * [kbn-storybook] Kill tsdocgen for immediate perf improvement * Addressing feedback --- .../storybook_config/webpack.config.js | 241 +++++++++--------- 1 file changed, 117 insertions(+), 124 deletions(-) diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 0a9977463aee8..7b43d106417b2 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -19,6 +19,7 @@ const { parse, resolve } = require('path'); const webpack = require('webpack'); +const webpackMerge = require('webpack-merge'); const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); @@ -26,145 +27,137 @@ const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); const { currentConfig } = require('../../../built_assets/storybook/current.config'); // Extend the Storybook Webpack config with some customizations -module.exports = async ({ config }) => { - // Find and alter the CSS rule to replace the Kibana public path string with a path - // to the route we've added in middleware.js - const cssRule = config.module.rules.find((rule) => rule.test.source.includes('.css$')); - cssRule.use.push({ - loader: 'string-replace-loader', - options: { - search: '__REPLACE_WITH_PUBLIC_PATH__', - replace: '/', - flags: 'g', - }, - }); - - // Include the React preset from Kibana for Storybook JS files. - config.module.rules.push({ - test: /\.js$/, - exclude: /node_modules/, - loaders: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }); - - config.module.rules.push({ - test: /\.(html|md|txt|tmpl)$/, - use: { - loader: 'raw-loader', - }, - }); - - // Handle Typescript files - config.module.rules.push({ - test: /\.tsx?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], +module.exports = async ({ config: storybookConfig }) => { + let config = { + module: { + rules: [ + // Include the React preset from Kibana for JS(X) and TS(X) + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + loaders: 'babel-loader', + options: { + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, }, - }, - ], - }); - - // Parse props data for .tsx files - config.module.rules.push({ - test: /\.tsx$/, - // Exclude example files, as we don't display props info for them - exclude: /\.examples.tsx$/, - use: [ - // Parse TS comments to create Props tables in the UI - require.resolve('react-docgen-typescript-loader'), - ], - }); - - // Enable SASS - config.module.rules.push({ - test: /\.scss$/, - exclude: /\.module.(s(a|c)ss)$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader', options: { importLoaders: 2 } }, - { - loader: 'postcss-loader', - options: { - config: { - path: require.resolve('@kbn/optimizer/postcss.config.js'), + { + test: /\.(html|md|txt|tmpl)$/, + use: { + loader: 'raw-loader', }, }, - }, - { - loader: 'resolve-url-loader', - options: { - // If you don't have arguments (_, __) to the join function, the - // resolve-url-loader fails with a loader misconfiguration error. - // - // eslint-disable-next-line no-unused-vars - join: (_, __) => (uri, base) => { - if (!base || !parse(base).dir.includes('legacy')) { - return null; - } + // Parse props data for .tsx files + // This is notoriously slow, and is making Storybook unusable. Disabling for now. + // See: https://github.com/storybookjs/storybook/issues/7998 + // + // { + // test: /\.tsx$/, + // // Exclude example files, as we don't display props info for them + // exclude: /\.stories.tsx$/, + // use: [ + // // Parse TS comments to create Props tables in the UI + // require.resolve('react-docgen-typescript-loader'), + // ], + // }, + { + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, + }, + }, + { + loader: 'resolve-url-loader', + options: { + // If you don't have arguments (_, __) to the join function, the + // resolve-url-loader fails with a loader misconfiguration error. + // + // eslint-disable-next-line no-unused-vars + join: (_, __) => (uri, base) => { + if (!base || !parse(base).dir.includes('legacy')) { + return null; + } - // URIs on mixins in src/legacy/public/styles need to be resolved. - if (uri.startsWith('ui/assets')) { - return resolve(REPO_ROOT, 'src/core/server/core_app/', uri.replace('ui/', '')); - } + // URIs on mixins in src/legacy/public/styles need to be resolved. + if (uri.startsWith('ui/assets')) { + return resolve(REPO_ROOT, 'src/core/server/core_app/', uri.replace('ui/', '')); + } - return null; - }, + return null; + }, + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') + )};\n`; + }, + sassOptions: { + includePaths: [resolve(REPO_ROOT, 'node_modules')], + }, + }, + }, + ], }, - }, - { - loader: 'sass-loader', - options: { - prependData(loaderContext) { - return `@import ${stringifyRequest( - loaderContext, - resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') - )};\n`; + ], + }, + plugins: [ + // Reference the built DLL file of static(ish) dependencies, which are removed + // during kbn:bootstrap and rebuilt if missing. + new webpack.DllReferencePlugin({ + manifest: resolve(DLL_DIST_DIR, 'manifest.json'), + context: REPO_ROOT, + }), + // Copy the DLL files to the Webpack build for use in the Storybook UI + + new CopyWebpackPlugin({ + patterns: [ + { + from: resolve(DLL_DIST_DIR, 'dll.js'), + to: 'dll.js', }, - sassOptions: { - includePaths: [resolve(REPO_ROOT, 'node_modules')], + { + from: resolve(DLL_DIST_DIR, 'dll.css'), + to: 'dll.css', }, - }, - }, + ], + }), ], - }); - - // Reference the built DLL file of static(ish) dependencies, which are removed - // during kbn:bootstrap and rebuilt if missing. - config.plugins.push( - new webpack.DllReferencePlugin({ - manifest: resolve(DLL_DIST_DIR, 'manifest.json'), - context: REPO_ROOT, - }) - ); + resolve: { + // Tell Webpack about the ts/x extensions + extensions: ['.ts', '.tsx', '.scss'], + }, + }; - // Copy the DLL files to the Webpack build for use in the Storybook UI - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ - { - from: resolve(DLL_DIST_DIR, 'dll.js'), - to: 'dll.js', - }, - { - from: resolve(DLL_DIST_DIR, 'dll.css'), - to: 'dll.css', - }, - ], - }) - ); + // Find and alter the CSS rule to replace the Kibana public path string with a path + // to the route we've added in middleware.js + const cssRule = storybookConfig.module.rules.find((rule) => rule.test.source.includes('.css$')); + cssRule.use.push({ + loader: 'string-replace-loader', + options: { + search: '__REPLACE_WITH_PUBLIC_PATH__', + replace: '/', + flags: 'g', + }, + }); - // Tell Webpack about the ts/x extensions - config.resolve.extensions.push('.ts', '.tsx', '.scss'); + config = webpackMerge(storybookConfig, config); // Load custom Webpack config specified by a plugin. if (currentConfig.webpackHook) { // eslint-disable-next-line import/no-dynamic-require - config = await require(currentConfig.webpackHook)({ config }); + return await require(currentConfig.webpackHook)({ config }); } return config; From c78da24708e55d22a1ea54e6de99f608a7e35b27 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 4 Aug 2020 14:44:16 +0200 Subject: [PATCH 085/115] [ Functional Tests ] TVSB charts flaky tests on index pattern switch fix (#74110) Co-authored-by: Elastic Machine --- test/functional/apps/visualize/_tsvb_chart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 7e1f88650cbba..f1082bf618b90 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -130,7 +130,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const fromTime = 'Oct 22, 2018 @ 00:00:00.000'; const toTime = 'Oct 28, 2018 @ 23:59:59.999'; // Sometimes popovers take some time to appear in Firefox (#71979) - await retry.try(async () => { + await retry.tryForTime(20000, async () => { await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.visualBuilder.setIndexPatternValue('kibana_sample_data_flights'); await PageObjects.visualBuilder.selectIndexPatternTimeField('timestamp'); From 73077c3d19cbece3449780ad97676f03df18867a Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 4 Aug 2020 06:54:47 -0600 Subject: [PATCH 086/115] Fixes one more spot where we forgot to add plumbing for the strict_date_optional_time (#74211) ## Summary Related closed issues: https://github.com/elastic/kibana/issues/58965 https://github.com/elastic/kibana/pull/70713 If you add a custom mapping and go to the hosts details page you will get an error toaster: Screen Shot 2020-08-03 at 7 53 16 PM If running local host you can configure your index patterns to use a custom one I setup with custom date time formats and a single record which can cause this: Screen Shot 2020-08-03 at 7 50 12 PM Then visit this URL and set your date time to go backwards by 1 year ```ts http://localhost:5601/app/security/hosts/app/security/hosts/MacBook-Pro.local/alerts ``` And with the fix you no longer get the error toaster. --- .../security_solution/server/lib/hosts/query.detail_host.dsl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts index ee0d98c45c443..10dcb7ee7e743 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts @@ -26,6 +26,7 @@ export const buildHostOverviewQuery = ({ { range: { [timestamp]: { + format: 'strict_date_optional_time', gte: from, lte: to, }, From 4f2209b209e3bbe460f2226a91b3dd437676e57d Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 4 Aug 2020 06:20:57 -0700 Subject: [PATCH 087/115] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20clean=20up?= =?UTF-8?q?=20and=20simplify=20replace=20panel=20input=20update=20(#74076)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/replace_panel_flyout.tsx | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx index e4a98ffac7a55..0000f63c48c2d 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx @@ -69,31 +69,33 @@ export class ReplacePanelFlyout extends React.Component { }; public onReplacePanel = async (savedObjectId: string, type: string, name: string) => { - const originalPanels = this.props.container.getInput().panels; - const filteredPanels = { ...originalPanels }; + const { panelToRemove, container } = this.props; + const { w, h, x, y } = (container.getInput().panels[ + panelToRemove.id + ] as DashboardPanelState).gridData; - const nnw = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.w; - const nnh = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.h; - const nnx = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.x; - const nny = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.y; - - // add the new view - const newObj = await this.props.container.addNewEmbeddable(type, { + const { id } = await container.addNewEmbeddable(type, { savedObjectId, }); - const finalPanels = _.cloneDeep(this.props.container.getInput().panels); - (finalPanels[newObj.id] as DashboardPanelState).gridData.w = nnw; - (finalPanels[newObj.id] as DashboardPanelState).gridData.h = nnh; - (finalPanels[newObj.id] as DashboardPanelState).gridData.x = nnx; - (finalPanels[newObj.id] as DashboardPanelState).gridData.y = nny; - - // delete the old view - delete finalPanels[this.props.panelToRemove.id]; - - // apply changes - this.props.container.updateInput({ panels: finalPanels }); - this.props.container.reload(); + const { [panelToRemove.id]: omit, ...panels } = container.getInput().panels; + + container.updateInput({ + panels: { + ...panels, + [id]: { + ...panels[id], + gridData: { + ...(panels[id] as DashboardPanelState).gridData, + w, + h, + x, + y, + }, + } as DashboardPanelState, + }, + }); + container.reload(); this.showToast(name); this.props.onClose(); From 23fedcf636aac1fd9af0858c9adf534f29db5418 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 4 Aug 2020 06:25:53 -0700 Subject: [PATCH 088/115] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20navigate=20from?= =?UTF-8?q?=20maps=20embeddable=20to=20app=20in=20SPA=20way=20(#74102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/maps/public/embeddable/map_embeddable.tsx | 2 ++ .../maps/public/embeddable/map_embeddable_factory.ts | 10 +++++++++- x-pack/plugins/maps/public/embeddable/types.ts | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 137b3a31b795b..616d06a5c7b19 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -81,6 +81,8 @@ export class MapEmbeddable extends Embeddable Date: Tue, 4 Aug 2020 15:56:18 +0200 Subject: [PATCH 089/115] [Security Solution] Fix Fullscreen view (#74236) --- x-pack/plugins/security_solution/public/app/home/index.tsx | 1 - .../security_solution/public/common/components/page/index.tsx | 1 + .../timelines/components/open_timeline/use_timeline_types.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index f230566fa08e6..7c287646ba7ac 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -27,7 +27,6 @@ const SecuritySolutionAppWrapper = styled.div` SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper'; const Main = styled.main` - position: relative; overflow: auto; flex: 1; `; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 6c49ce7453ce9..140429dc4abd7 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -73,6 +73,7 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar #kibana-body { height: 100%; + overflow-y: hidden; > .content { height: 100%; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 1ffa626b01311..ec02124ae43d3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -100,7 +100,7 @@ export const useTimelineTypes = ({ (tabId, tabStyle: TimelineTabsStyle) => { setTimelineTypes((prevTimelineTypes) => { if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) { - return null; + return tabId === TimelineType.default ? TimelineType.template : TimelineType.default; } else if (prevTimelineTypes !== tabId) { setTimelineTypes(tabId); } From 7208a5746abbab6d8377294a7b95701faa1aaf3f Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Tue, 4 Aug 2020 09:18:47 -0500 Subject: [PATCH 090/115] [Canvas] Fix Custom Element functional test (and remove skip) (#73727) * Remove skip of custom elements --- x-pack/test/functional/apps/canvas/custom_elements.ts | 4 ++-- x-pack/test/functional/page_objects/canvas_page.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index 20ad045d0a653..33db56751285e 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -19,8 +19,7 @@ export default function canvasCustomElementTest({ const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); - // FLAKY: https://github.com/elastic/kibana/issues/63339 - describe.skip('custom elements', function () { + describe('custom elements', function () { this.tags('skipFirefox'); before(async () => { @@ -66,6 +65,7 @@ export default function canvasCustomElementTest({ // ensure the custom element is the one expected and click it to add to the workpad const customElement = await find.byCssSelector('.canvasElementCard__wrapper'); const elementName = await customElement.findByCssSelector('.euiCard__title'); + expect(await elementName.getVisibleText()).to.contain('My New Element'); customElement.click(); diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index f08d1e6b7fef4..23b5057573b3b 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -8,10 +8,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; -export function CanvasPageProvider({ getService }: FtrProviderContext) { +export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const browser = getService('browser'); + const PageObjects = getPageObjects(['common']); return { async enterFullscreen() { @@ -58,6 +59,8 @@ export function CanvasPageProvider({ getService }: FtrProviderContext) { async openSavedElementsModal() { await testSubjects.click('add-element-button'); await testSubjects.click('saved-elements-menu-option'); + + await PageObjects.common.sleep(1000); // give time for modal animation to complete }, async closeSavedElementsModal() { await testSubjects.click('saved-elements-modal-close-button'); From f3fbb7d79f08a518292f32e67ffa9d99ef22606b Mon Sep 17 00:00:00 2001 From: Ben Skelker <54019610+benskelker@users.noreply.github.com> Date: Tue, 4 Aug 2020 17:42:51 +0300 Subject: [PATCH 091/115] [Security Solution]Updates exception options and setting text (#73769) * updates exception text * updates modal text Co-authored-by: Elastic Machine --- .../components/exceptions/add_exception_modal/translations.ts | 2 +- .../exceptions/edit_exception_modal/translations.ts | 2 +- .../public/common/components/exceptions/translations.ts | 4 ++-- .../public/detections/components/alerts_table/translations.ts | 2 +- .../detections/components/rules/step_about_rule/schema.tsx | 2 +- .../components/rules/step_about_rule/translations.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts index abc296e9c0e1a..3916284416707 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts @@ -56,7 +56,7 @@ export const ENDPOINT_QUARANTINE_TEXT = i18n.translate( 'xpack.securitySolution.exceptions.addException.endpointQuarantineText', { defaultMessage: - 'Any file in quarantine on any endpoint that matches the attribute(s) selected will automatically be restored to its original location. This exception will apply to any rule that is linked to the Global Endpoint Exception List.', + 'On all Endpoint hosts, quarantined files that match the exception are automatically restored to their original locations. This exception applies to all rules using Endpoint exceptions.', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts index c5b6fc8a6a9ae..09e0a75d21573 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts @@ -65,7 +65,7 @@ export const ENDPOINT_QUARANTINE_TEXT = i18n.translate( 'xpack.securitySolution.exceptions.editException.endpointQuarantineText', { defaultMessage: - 'Any file in quarantine on any endpoint that matches the attribute(s) selected will automatically be restored to its original location. This exception will apply to any rule that is linked to the Global Endpoint Exception List.', + 'On all Endpoint hosts, quarantined files that match the exception are automatically restored to their original locations. This exception applies to all rules using Endpoint exceptions.', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index e68b903266428..13e9d0df549f8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -77,14 +77,14 @@ export const ADD_EXCEPTION_LABEL = i18n.translate( export const ADD_TO_ENDPOINT_LIST = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addToEndpointListLabel', { - defaultMessage: 'Add to endpoint list', + defaultMessage: 'Add Endpoint exception', } ); export const ADD_TO_DETECTIONS_LIST = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel', { - defaultMessage: 'Add to detections list', + defaultMessage: 'Add rule exception', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index e5e8635b9e799..3d6c3dc0a7a8e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -118,7 +118,7 @@ export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( export const ACTION_ADD_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addException', { - defaultMessage: 'Add exception', + defaultMessage: 'Add rule exception', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index 20470d7bb924f..a3db8fe659d84 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -96,7 +96,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAssociatedToEndpointListLabel', { - defaultMessage: 'Associate rule to Global Endpoint Exception List', + defaultMessage: 'Add existing Endpoint exceptions to the rule', } ), labelAppend: OptionalFieldLabel, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index 939747717385c..f4d90d0596ede 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -30,7 +30,7 @@ export const ADD_FALSE_POSITIVE = i18n.translate( export const GLOBAL_ENDPOINT_EXCEPTION_LIST = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel', { - defaultMessage: 'Global endpoint exception list', + defaultMessage: 'Elastic Endpoint exceptions', } ); From 8e0ae5ffd12a6ee05cb782c221b269c2b7198580 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 4 Aug 2020 10:54:11 -0500 Subject: [PATCH 092/115] add force install option (#74172) --- .../server/routes/epm/handlers.ts | 9 +- .../server/services/epm/packages/install.ts | 11 ++- .../server/types/rest_spec/epm.ts | 5 + .../apis/epm/index.js | 2 +- .../apis/epm/install_errors.ts | 51 ---------- .../apis/epm/install_update.ts | 92 +++++++++++++++++++ .../0.1.0/dataset/test/fields/fields.yml | 0 .../0.1.0/dataset/test/manifest.yml | 0 .../0.1.0/docs/README.md | 0 .../0.1.0/manifest.yml | 6 +- .../0.2.0/dataset/test/fields/fields.yml | 0 .../0.2.0/dataset/test/manifest.yml | 0 .../0.2.0/docs/README.md | 0 .../0.2.0/manifest.yml | 6 +- .../0.3.0/dataset/test/fields/fields.yml | 16 ++++ .../0.3.0/dataset/test/manifest.yml | 9 ++ .../multiple_versions/0.3.0/docs/README.md | 3 + .../multiple_versions/0.3.0/manifest.yml | 20 ++++ 18 files changed, 166 insertions(+), 64 deletions(-) delete mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.1.0/dataset/test/fields/fields.yml (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.1.0/dataset/test/manifest.yml (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.1.0/docs/README.md (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.1.0/manifest.yml (67%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.2.0/dataset/test/fields/fields.yml (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.2.0/dataset/test/manifest.yml (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.2.0/docs/README.md (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.2.0/manifest.yml (67%) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index f47234fb20118..f8f39f6294260 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -146,9 +146,11 @@ export const getInfoHandler: RequestHandler> = async (context, request, response) => { +export const installPackageHandler: RequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { const logger = appContextService.getLogger(); const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; @@ -159,6 +161,7 @@ export const installPackageHandler: RequestHandler { - const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets const latestPackage = await Registry.fetchFindLatestPackage(pkgName); - if (semver.lt(pkgVersion, latestPackage.version)) + if (semver.lt(pkgVersion, latestPackage.version) && !force) throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index 08f47a8f1caaa..191014606f220 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -36,6 +36,11 @@ export const InstallPackageRequestSchema = { params: schema.object({ pkgkey: schema.string(), }), + body: schema.nullable( + schema.object({ + force: schema.boolean(), + }) + ), }; export const DeletePackageRequestSchema = { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index 92b41ca4102ee..1582f72dd1cd8 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -12,6 +12,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./ilm')); loadTestFile(require.resolve('./install_overrides')); loadTestFile(require.resolve('./install_remove_assets')); - loadTestFile(require.resolve('./install_errors')); + loadTestFile(require.resolve('./install_update')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts deleted file mode 100644 index 8acb11b00b57d..0000000000000 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; -import { skipIfNoDockerRegistry } from '../../helpers'; - -export default function (providerContext: FtrProviderContext) { - const { getService } = providerContext; - const kibanaServer = getService('kibanaServer'); - const supertest = getService('supertest'); - - describe('package error handling', async () => { - skipIfNoDockerRegistry(providerContext); - it('should return 404 if package does not exist', async function () { - await supertest - .post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`) - .set('kbn-xsrf', 'xxxx') - .expect(404); - let res; - try { - res = await kibanaServer.savedObjects.get({ - type: 'epm-package', - id: 'nonexistent', - }); - } catch (err) { - res = err; - } - expect(res.response.data.statusCode).equal(404); - }); - it('should return 400 if trying to update/install to an out-of-date package', async function () { - await supertest - .post(`/api/ingest_manager/epm/packages/update-0.1.0`) - .set('kbn-xsrf', 'xxxx') - .expect(400); - let res; - try { - res = await kibanaServer.savedObjects.get({ - type: 'epm-package', - id: 'update', - }); - } catch (err) { - res = err; - } - expect(res.response.data.statusCode).equal(404); - }); - }); -} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts new file mode 100644 index 0000000000000..9de6cd9118fe4 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('installing and updating scenarios', async () => { + skipIfNoDockerRegistry(providerContext); + after(async () => { + await deletePackage('multiple_versions-0.3.0'); + }); + + it('should return 404 if package does not exist', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(404); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'nonexistent', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 400 if trying to install an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'update', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 200 if trying to force install an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('should return 400 if trying to update to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.2.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + it('should return 200 if trying to force update to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.2.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('should return 200 if trying to update to the latest package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.3.0`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + await deletePackage('multiple_versions-0.3.0'); + }); + it('should return 200 if trying to install the latest package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.3.0`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/fields/fields.yml similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/fields/fields.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/manifest.yml similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/manifest.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/docs/README.md similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/docs/README.md diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml similarity index 67% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml index b12f1bfbd3b7e..32c626b115739 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: update -title: Package update test -description: This is a test package for updating a package +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a package version: 0.1.0 categories: [] release: beta diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/fields/fields.yml similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/fields/fields.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/manifest.yml similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/manifest.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/docs/README.md similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/docs/README.md diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml similarity index 67% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml index 11dbdc102dce8..773903a69e7f7 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: update -title: Package update test -description: This is a test package for updating a package +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a packagee version: 0.2.0 categories: [] release: beta diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml new file mode 100644 index 0000000000000..12a9a03c1337b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md new file mode 100644 index 0000000000000..8e26522d86839 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml new file mode 100644 index 0000000000000..49c85994d2c2c --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a package +version: 0.3.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' From b42f64d1a212cf038da9bd77b19bb05c1bc3320a Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 4 Aug 2020 16:57:53 +0100 Subject: [PATCH 093/115] [Security Solution] Load prepackage timelines by default (#74209) * load prepackage timelines by default * fix unit tests --- .../components/open_timeline/index.test.tsx | 18 +++++++- .../open_timeline_modal/index.test.tsx | 41 ++++++++++++++++++- .../open_timeline/use_timeline_status.tsx | 2 +- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 43fd57fcfc5bf..facdc392ff7ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -25,11 +25,10 @@ import { getTimelineTabsUrl } from '../../../common/components/link_to'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; +import { useTimelineStatus } from './use_timeline_status'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; - import { StatefulOpenTimeline } from '.'; - import { TimelineTabsStyle } from './types'; import { useTimelineTypes, @@ -75,9 +74,17 @@ jest.mock('../../../common/components/link_to', () => { }; }); +jest.mock('./use_timeline_status', () => { + return { + useTimelineStatus: jest.fn(), + }; +}); + describe('StatefulOpenTimeline', () => { const title = 'All Timelines / Open Timelines'; let mockHistory: History[]; + const mockInstallPrepackagedTimelines = jest.fn(); + beforeEach(() => { (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default, @@ -95,6 +102,13 @@ describe('StatefulOpenTimeline', () => { totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, refetch: jest.fn(), }); + ((useTimelineStatus as unknown) as jest.Mock).mockReturnValue({ + timelineStatus: null, + templateTimelineType: null, + templateTimelineFilter:
, + installPrepackagedTimelines: mockInstallPrepackagedTimelines, + }); + mockInstallPrepackagedTimelines.mockClear(); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx index 3017f553d59d5..5ce53607817eb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx @@ -12,10 +12,11 @@ import { ThemeProvider } from 'styled-components'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; + import { TestProviderWithoutDragAndDrop } from '../../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_results'; import { useGetAllTimeline, getAllTimeline } from '../../../containers/all'; - +import { useTimelineStatus } from '../use_timeline_status'; import { OpenTimelineModal } from '.'; jest.mock('../../../../common/lib/kibana'); @@ -39,8 +40,15 @@ jest.mock('../use_timeline_types', () => { }; }); +jest.mock('../use_timeline_status', () => { + return { + useTimelineStatus: jest.fn(), + }; +}); + describe('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockInstallPrepackagedTimelines = jest.fn(); beforeEach(() => { ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), @@ -52,6 +60,16 @@ describe('OpenTimelineModal', () => { totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, refetch: jest.fn(), }); + ((useTimelineStatus as unknown) as jest.Mock).mockReturnValue({ + timelineStatus: null, + templateTimelineType: null, + templateTimelineFilter:
, + installPrepackagedTimelines: mockInstallPrepackagedTimelines, + }); + }); + + afterEach(() => { + mockInstallPrepackagedTimelines.mockClear(); }); test('it renders the expected modal', async () => { @@ -76,4 +94,25 @@ describe('OpenTimelineModal', () => { { timeout: 10000 } ); }, 20000); + + test('it installs elastic prebuilt templates', async () => { + const wrapper = mount( + + + + + + + + ); + + await waitFor( + () => { + wrapper.update(); + + expect(mockInstallPrepackagedTimelines).toHaveBeenCalled(); + }, + { timeout: 10000 } + ); + }, 20000); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx index 8c4c686698c88..37bea3d713c08 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx @@ -102,7 +102,7 @@ export const useTimelineStatus = ({ }, [templateTimelineType, filters, isTemplateFilterEnabled, onFilterClicked]); const installPrepackagedTimelines = useCallback(async () => { - if (templateTimelineType === TemplateTimelineType.elastic) { + if (templateTimelineType !== TemplateTimelineType.custom) { await installPrepackedTimelines(); } }, [templateTimelineType]); From 89dba3927333fc2f6b60573a1e173d966510ee41 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 4 Aug 2020 17:13:32 +0100 Subject: [PATCH 094/115] Handle modifier keys (#74237) --- x-pack/plugins/infra/public/hooks/use_link_props.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index dec8eaae56f41..710011d3bdf3e 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -68,6 +68,9 @@ export const useLinkProps = ( const onClick = useMemo(() => { return (e: React.MouseEvent | React.MouseEvent) => { + if (e.defaultPrevented || isModifiedEvent(e)) { + return; + } e.preventDefault(); const navigate = () => { @@ -112,3 +115,6 @@ const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { ); } }; + +const isModifiedEvent = (event: any) => + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); From 2dea17a8d0eff4730335b776885699ee946548e9 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 4 Aug 2020 12:54:20 -0400 Subject: [PATCH 095/115] Using msearch for tree api endpoint (#73813) --- .../server/endpoint/routes/resolver/tree.ts | 40 +++++-------------- .../utils/children_lifecycle_query_handler.ts | 2 +- .../endpoint/routes/resolver/utils/fetch.ts | 2 +- .../apis/index.ts | 4 +- .../apis/resolver/entity_id.ts | 2 +- .../apis/resolver/index.ts | 16 ++++++++ .../apis/resolver/tree.ts | 2 +- 7 files changed, 30 insertions(+), 38 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts index 33011078ee823..02cddc3ddcf6e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts @@ -9,7 +9,6 @@ import { TypeOf } from '@kbn/config-schema'; import { eventsIndexPattern, alertsIndexPattern } from '../../../../common/endpoint/constants'; import { validateTree } from '../../../../common/endpoint/schema/resolver'; import { Fetcher } from './utils/fetch'; -import { Tree } from './utils/tree'; import { EndpointAppContext } from '../../types'; export function handleTree( @@ -17,42 +16,21 @@ export function handleTree( endpointAppContext: EndpointAppContext ): RequestHandler, TypeOf> { return async (context, req, res) => { - const { - params: { id }, - query: { - children, - ancestors, - events, - alerts, - afterAlert, - afterEvent, - afterChild, - legacyEndpointID: endpointID, - }, - } = req; try { const client = context.core.elasticsearch.legacy.client; - const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); + const fetcher = new Fetcher( + client, + req.params.id, + eventsIndexPattern, + alertsIndexPattern, + req.query.legacyEndpointID + ); - const [childrenNodes, ancestry, relatedEvents, relatedAlerts] = await Promise.all([ - fetcher.children(children, afterChild), - fetcher.ancestors(ancestors), - fetcher.events(events, afterEvent), - fetcher.alerts(alerts, afterAlert), - ]); - - const tree = new Tree(id, { - ancestry, - children: childrenNodes, - relatedEvents, - relatedAlerts, - }); - - const enrichedTree = await fetcher.stats(tree); + const tree = await fetcher.tree(req.query); return res.ok({ - body: enrichedTree.render(), + body: tree.render(), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts index 8aaf809405d63..ab610dc9776ca 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts @@ -66,6 +66,6 @@ export class ChildrenLifecycleQueryHandler implements SingleQueryHandler { await ingestManager.setup(); }); - loadTestFile(require.resolve('./resolver/entity_id')); - loadTestFile(require.resolve('./resolver/tree')); - loadTestFile(require.resolve('./resolver/children')); + loadTestFile(require.resolve('./resolver/index')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index 231871fae3d39..cb6c49e17c712 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -16,7 +16,7 @@ import { } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { InsertedEvents } from '../../services/resolver'; -export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); const generator = new EndpointDocGenerator('resolver'); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts new file mode 100644 index 0000000000000..dc9a1fab9ec02 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { loadTestFile } = providerContext; + + describe('Resolver tests', () => { + loadTestFile(require.resolve('./entity_id')); + loadTestFile(require.resolve('./children')); + loadTestFile(require.resolve('./tree')); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 758e26fd5eece..7b511c3be74b5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -230,7 +230,7 @@ const verifyLifecycleStats = ( } }; -export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const resolver = getService('resolverGenerator'); From 0ca8e99a9f62435e05f6af0b9af2586a3b02b3cd Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 4 Aug 2020 13:13:51 -0400 Subject: [PATCH 096/115] [CI] Add pipeline task queue framework and merge workers into one (#71268) --- .ci/Dockerfile | 38 +++ .ci/runbld_no_junit.yml | 2 +- .gitignore | 2 + Jenkinsfile | 44 +--- packages/kbn-dev-utils/src/run/help.test.ts | 6 +- src/dev/ci_setup/checkout_sibling_es.sh | 12 +- src/dev/ci_setup/setup_env.sh | 4 +- src/dev/notice/generate_notice_from_source.ts | 2 + tasks/test_jest.js | 6 +- test/scripts/checks/doc_api_changes.sh | 5 + test/scripts/checks/file_casing.sh | 5 + test/scripts/checks/i18n.sh | 5 + test/scripts/checks/licenses.sh | 5 + test/scripts/checks/lock_file_symlinks.sh | 5 + test/scripts/checks/telemetry.sh | 5 + test/scripts/checks/test_hardening.sh | 5 + test/scripts/checks/test_projects.sh | 5 + test/scripts/checks/ts_projects.sh | 5 + test/scripts/checks/type_check.sh | 5 + .../checks/verify_dependency_versions.sh | 5 + test/scripts/checks/verify_notice.sh | 5 + test/scripts/jenkins_accessibility.sh | 2 +- .../jenkins_build_kbn_sample_panel_action.sh | 0 test/scripts/jenkins_build_kibana.sh | 13 +- test/scripts/jenkins_build_plugins.sh | 12 + test/scripts/jenkins_ci_group.sh | 2 +- test/scripts/jenkins_firefox_smoke.sh | 2 +- test/scripts/jenkins_plugin_functional.sh | 15 ++ .../jenkins_security_solution_cypress.sh | 8 +- .../jenkins_setup_parallel_workspace.sh | 32 +++ test/scripts/jenkins_test_setup.sh | 6 + test/scripts/jenkins_test_setup_oss.sh | 15 +- test/scripts/jenkins_test_setup_xpack.sh | 15 +- test/scripts/jenkins_xpack_accessibility.sh | 2 +- test/scripts/jenkins_xpack_build_kibana.sh | 17 +- test/scripts/jenkins_xpack_build_plugins.sh | 14 + ...nkins_xpack_saved_objects_field_metrics.sh | 2 +- test/scripts/lint/eslint.sh | 5 + test/scripts/lint/sasslint.sh | 5 + test/scripts/test/api_integration.sh | 5 + test/scripts/test/jest_integration.sh | 5 + test/scripts/test/jest_unit.sh | 5 + test/scripts/test/karma_ci.sh | 5 + test/scripts/test/mocha.sh | 5 + test/scripts/test/safer_lodash_set.sh | 5 + test/scripts/test/xpack_jest_unit.sh | 6 + test/scripts/test/xpack_karma.sh | 6 + .../test/xpack_list_cyclic_dependency.sh | 6 + .../test/xpack_siem_cyclic_dependency.sh | 6 + vars/catchErrors.groovy | 11 +- vars/kibanaPipeline.groovy | 241 +++++++++++++++--- vars/task.groovy | 5 + vars/tasks.groovy | 119 +++++++++ vars/withTaskQueue.groovy | 154 +++++++++++ vars/workers.groovy | 12 +- .../canvas/storybook/storyshots.test.tsx | 7 + 56 files changed, 815 insertions(+), 131 deletions(-) create mode 100644 .ci/Dockerfile create mode 100755 test/scripts/checks/doc_api_changes.sh create mode 100755 test/scripts/checks/file_casing.sh create mode 100755 test/scripts/checks/i18n.sh create mode 100755 test/scripts/checks/licenses.sh create mode 100755 test/scripts/checks/lock_file_symlinks.sh create mode 100755 test/scripts/checks/telemetry.sh create mode 100755 test/scripts/checks/test_hardening.sh create mode 100755 test/scripts/checks/test_projects.sh create mode 100755 test/scripts/checks/ts_projects.sh create mode 100755 test/scripts/checks/type_check.sh create mode 100755 test/scripts/checks/verify_dependency_versions.sh create mode 100755 test/scripts/checks/verify_notice.sh mode change 100644 => 100755 test/scripts/jenkins_build_kbn_sample_panel_action.sh create mode 100755 test/scripts/jenkins_build_plugins.sh create mode 100755 test/scripts/jenkins_plugin_functional.sh mode change 100644 => 100755 test/scripts/jenkins_security_solution_cypress.sh create mode 100755 test/scripts/jenkins_setup_parallel_workspace.sh mode change 100644 => 100755 test/scripts/jenkins_test_setup.sh mode change 100644 => 100755 test/scripts/jenkins_test_setup_oss.sh mode change 100644 => 100755 test/scripts/jenkins_test_setup_xpack.sh create mode 100755 test/scripts/jenkins_xpack_build_plugins.sh create mode 100755 test/scripts/lint/eslint.sh create mode 100755 test/scripts/lint/sasslint.sh create mode 100755 test/scripts/test/api_integration.sh create mode 100755 test/scripts/test/jest_integration.sh create mode 100755 test/scripts/test/jest_unit.sh create mode 100755 test/scripts/test/karma_ci.sh create mode 100755 test/scripts/test/mocha.sh create mode 100755 test/scripts/test/safer_lodash_set.sh create mode 100755 test/scripts/test/xpack_jest_unit.sh create mode 100755 test/scripts/test/xpack_karma.sh create mode 100755 test/scripts/test/xpack_list_cyclic_dependency.sh create mode 100755 test/scripts/test/xpack_siem_cyclic_dependency.sh create mode 100644 vars/task.groovy create mode 100644 vars/tasks.groovy create mode 100644 vars/withTaskQueue.groovy diff --git a/.ci/Dockerfile b/.ci/Dockerfile new file mode 100644 index 0000000000000..d90d9f4710b5b --- /dev/null +++ b/.ci/Dockerfile @@ -0,0 +1,38 @@ +# NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. +# If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts + +ARG NODE_VERSION=10.21.0 + +FROM node:${NODE_VERSION} AS base + +RUN apt-get update && \ + apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ + libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ + libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ + libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget openjdk-8-jre && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ + && apt-get update \ + && apt-get install -y rsync jq bsdtar google-chrome-stable \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN LATEST_VAULT_RELEASE=$(curl -s https://api.github.com/repos/hashicorp/vault/tags | jq --raw-output .[0].name[1:]) \ + && curl -L https://releases.hashicorp.com/vault/${LATEST_VAULT_RELEASE}/vault_${LATEST_VAULT_RELEASE}_linux_amd64.zip -o vault.zip \ + && unzip vault.zip \ + && rm vault.zip \ + && chmod +x vault \ + && mv vault /usr/local/bin/vault + +RUN groupadd -r kibana && useradd -r -g kibana kibana && mkdir /home/kibana && chown kibana:kibana /home/kibana + +COPY ./bash_standard_lib.sh /usr/local/bin/bash_standard_lib.sh +RUN chmod +x /usr/local/bin/bash_standard_lib.sh + +COPY ./runbld /usr/local/bin/runbld +RUN chmod +x /usr/local/bin/runbld + +USER kibana diff --git a/.ci/runbld_no_junit.yml b/.ci/runbld_no_junit.yml index 67b5002c1c437..1bcb7e22a2648 100644 --- a/.ci/runbld_no_junit.yml +++ b/.ci/runbld_no_junit.yml @@ -3,4 +3,4 @@ profiles: - ".*": # Match any job tests: - junit-filename-pattern: "8d8bd494-d909-4e67-a052-7e8b5aaeb5e4" # A bogus path that should never exist + junit-filename-pattern: false diff --git a/.gitignore b/.gitignore index dfd02de7b1186..1d12ef2a9cff3 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ npm-debug.log* .tern-project .nyc_output .ci/pipeline-library/build/ +.ci/runbld +.ci/bash_standard_lib.sh .gradle # apm plugin diff --git a/Jenkinsfile b/Jenkinsfile index ad1d244c78874..3b68cde206573 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,49 +9,7 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) ciStats.trackBuild { catchError { retryable.enable() - parallel([ - 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), - 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), - 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), - 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), - 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), - 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), - 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), - 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), - 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), - 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), - 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), - 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), - 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), - 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), - // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), - ]), - 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), - 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), - 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), - 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), - 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), - 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), - 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), - 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), - 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), - 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), - 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), - 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), - 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), - 'xpack-securitySolutionCypress': { processNumber -> - whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/', 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx']) { - kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) - } - }, - - // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), - ]), - ]) + kibanaPipeline.allCiTasks() } } } diff --git a/packages/kbn-dev-utils/src/run/help.test.ts b/packages/kbn-dev-utils/src/run/help.test.ts index 27be7ad28b81a..300f1cba7eb7d 100644 --- a/packages/kbn-dev-utils/src/run/help.test.ts +++ b/packages/kbn-dev-utils/src/run/help.test.ts @@ -57,7 +57,7 @@ const barCommand: Command = { usage: 'bar [...names]', }; -describe('getHelp()', () => { +describe.skip('getHelp()', () => { it('returns the expected output', () => { expect( getHelp({ @@ -95,7 +95,7 @@ describe('getHelp()', () => { }); }); -describe('getCommandLevelHelp()', () => { +describe.skip('getCommandLevelHelp()', () => { it('returns the expected output', () => { expect( getCommandLevelHelp({ @@ -141,7 +141,7 @@ describe('getCommandLevelHelp()', () => { }); }); -describe('getHelpForAllCommands()', () => { +describe.skip('getHelpForAllCommands()', () => { it('returns the expected output', () => { expect( getHelpForAllCommands({ diff --git a/src/dev/ci_setup/checkout_sibling_es.sh b/src/dev/ci_setup/checkout_sibling_es.sh index 915759d4214f9..3832ec9b4076a 100755 --- a/src/dev/ci_setup/checkout_sibling_es.sh +++ b/src/dev/ci_setup/checkout_sibling_es.sh @@ -7,10 +7,11 @@ function checkout_sibling { targetDir=$2 useExistingParamName=$3 useExisting="$(eval "echo "\$$useExistingParamName"")" + repoAddress="https://github.com/" if [ -z ${useExisting:+x} ]; then if [ -d "$targetDir" ]; then - echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$PARENT_DIR]!" + echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$WORKSPACE]!" echo echo "Either define '${useExistingParamName}' or remove the existing '${project}' sibling." exit 1 @@ -21,8 +22,9 @@ function checkout_sibling { cloneBranch="" function clone_target_is_valid { + echo " -> checking for '${cloneBranch}' branch at ${cloneAuthor}/${project}" - if [[ -n "$(git ls-remote --heads "git@github.com:${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then + if [[ -n "$(git ls-remote --heads "${repoAddress}${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then return 0 else return 1 @@ -71,7 +73,7 @@ function checkout_sibling { fi echo " -> checking out '${cloneBranch}' branch from ${cloneAuthor}/${project}..." - git clone -b "$cloneBranch" "git@github.com:${cloneAuthor}/${project}.git" "$targetDir" --depth=1 + git clone -b "$cloneBranch" "${repoAddress}${cloneAuthor}/${project}.git" "$targetDir" --depth=1 echo " -> checked out ${project} revision: $(git -C "${targetDir}" rev-parse HEAD)" echo } @@ -87,12 +89,12 @@ function checkout_sibling { fi } -checkout_sibling "elasticsearch" "${PARENT_DIR}/elasticsearch" "USE_EXISTING_ES" +checkout_sibling "elasticsearch" "${WORKSPACE}/elasticsearch" "USE_EXISTING_ES" export TEST_ES_FROM=${TEST_ES_FROM:-snapshot} # Set the JAVA_HOME based on the Java property file in the ES repo # This assumes the naming convention used on CI (ex: ~/.java/java10) -ES_DIR="$PARENT_DIR/elasticsearch" +ES_DIR="$WORKSPACE/elasticsearch" ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 86927b694679a..72ec73ad810e6 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -53,6 +53,8 @@ export PARENT_DIR="$parentDir" kbnBranch="$(jq -r .branch "$KIBANA_DIR/package.json")" export KIBANA_PKG_BRANCH="$kbnBranch" +export WORKSPACE="${WORKSPACE:-$PARENT_DIR}" + ### ### download node ### @@ -162,7 +164,7 @@ export -f checks-reporter-with-killswitch source "$KIBANA_DIR/src/dev/ci_setup/load_env_keys.sh" -ES_DIR="$PARENT_DIR/elasticsearch" +ES_DIR="$WORKSPACE/elasticsearch" ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index 37bbcce72e497..0bef5bc5f32d4 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -49,8 +49,10 @@ export async function generateNoticeFromSource({ productName, directory, log }: ignore: [ '{node_modules,build,dist,data,built_assets}/**', 'packages/*/{node_modules,build,dist}/**', + 'src/plugins/*/{node_modules,build,dist}/**', 'x-pack/{node_modules,build,dist,data}/**', 'x-pack/packages/*/{node_modules,build,dist}/**', + 'x-pack/plugins/*/{node_modules,build,dist}/**', '**/target/**', ], }; diff --git a/tasks/test_jest.js b/tasks/test_jest.js index d8f51806e8ddc..810ed42324840 100644 --- a/tasks/test_jest.js +++ b/tasks/test_jest.js @@ -22,7 +22,7 @@ const { resolve } = require('path'); module.exports = function (grunt) { grunt.registerTask('test:jest', function () { const done = this.async(); - runJest(resolve(__dirname, '../scripts/jest.js')).then(done, done); + runJest(resolve(__dirname, '../scripts/jest.js'), ['--maxWorkers=10']).then(done, done); }); grunt.registerTask('test:jest_integration', function () { @@ -30,10 +30,10 @@ module.exports = function (grunt) { runJest(resolve(__dirname, '../scripts/jest_integration.js')).then(done, done); }); - function runJest(jestScript) { + function runJest(jestScript, args = []) { const serverCmd = { cmd: 'node', - args: [jestScript, '--ci'], + args: [jestScript, '--ci', ...args], opts: { stdio: 'inherit' }, }; diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh new file mode 100755 index 0000000000000..503d12b2f6d73 --- /dev/null +++ b/test/scripts/checks/doc_api_changes.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkDocApiChanges diff --git a/test/scripts/checks/file_casing.sh b/test/scripts/checks/file_casing.sh new file mode 100755 index 0000000000000..513664263791b --- /dev/null +++ b/test/scripts/checks/file_casing.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkFileCasing diff --git a/test/scripts/checks/i18n.sh b/test/scripts/checks/i18n.sh new file mode 100755 index 0000000000000..7a6fd46c46c76 --- /dev/null +++ b/test/scripts/checks/i18n.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:i18nCheck diff --git a/test/scripts/checks/licenses.sh b/test/scripts/checks/licenses.sh new file mode 100755 index 0000000000000..a08d7d07a24a1 --- /dev/null +++ b/test/scripts/checks/licenses.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:licenses diff --git a/test/scripts/checks/lock_file_symlinks.sh b/test/scripts/checks/lock_file_symlinks.sh new file mode 100755 index 0000000000000..1d43d32c9feb8 --- /dev/null +++ b/test/scripts/checks/lock_file_symlinks.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkLockfileSymlinks diff --git a/test/scripts/checks/telemetry.sh b/test/scripts/checks/telemetry.sh new file mode 100755 index 0000000000000..c74ec295b385c --- /dev/null +++ b/test/scripts/checks/telemetry.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:telemetryCheck diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh new file mode 100755 index 0000000000000..9184758577654 --- /dev/null +++ b/test/scripts/checks/test_hardening.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_hardening diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh new file mode 100755 index 0000000000000..5f9aafe80e10e --- /dev/null +++ b/test/scripts/checks/test_projects.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_projects diff --git a/test/scripts/checks/ts_projects.sh b/test/scripts/checks/ts_projects.sh new file mode 100755 index 0000000000000..d667c753baec2 --- /dev/null +++ b/test/scripts/checks/ts_projects.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkTsProjects diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh new file mode 100755 index 0000000000000..07c49638134be --- /dev/null +++ b/test/scripts/checks/type_check.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:typeCheck diff --git a/test/scripts/checks/verify_dependency_versions.sh b/test/scripts/checks/verify_dependency_versions.sh new file mode 100755 index 0000000000000..b73a71e7ff7fd --- /dev/null +++ b/test/scripts/checks/verify_dependency_versions.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:verifyDependencyVersions diff --git a/test/scripts/checks/verify_notice.sh b/test/scripts/checks/verify_notice.sh new file mode 100755 index 0000000000000..9f8343e540861 --- /dev/null +++ b/test/scripts/checks/verify_notice.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:verifyNotice diff --git a/test/scripts/jenkins_accessibility.sh b/test/scripts/jenkins_accessibility.sh index c122d71b58edb..fa7cbd41d7078 100755 --- a/test/scripts/jenkins_accessibility.sh +++ b/test/scripts/jenkins_accessibility.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_oss.sh checks-reporter-with-killswitch "Kibana accessibility tests" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/accessibility/config.ts; diff --git a/test/scripts/jenkins_build_kbn_sample_panel_action.sh b/test/scripts/jenkins_build_kbn_sample_panel_action.sh old mode 100644 new mode 100755 diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 2310a35f94f33..f449986713f97 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -2,13 +2,9 @@ source src/dev/ci_setup/setup_env.sh -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --oss \ - --filter '!alertingExample' \ - --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ - --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ - --verbose; +if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + ./test/scripts/jenkins_build_plugins.sh +fi # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -20,4 +16,7 @@ yarn run grunt functionalTests:ensureAllTestsInCiGroup; if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + + mkdir -p "$WORKSPACE/kibana-build-oss" + cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ fi diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh new file mode 100755 index 0000000000000..0c3ee4e3f261f --- /dev/null +++ b/test/scripts/jenkins_build_plugins.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --filter '!alertingExample' \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ + --workers 6 \ + --verbose diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 60d7f0406f4c9..2542d7032e83b 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -5,7 +5,7 @@ source test/scripts/jenkins_test_setup_oss.sh if [[ -z "$CODE_COVERAGE" ]]; then checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; - if [ "$CI_GROUP" == "1" ]; then + if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then source test/scripts/jenkins_build_kbn_sample_panel_action.sh yarn run grunt run:pluginFunctionalTestsRelease --from=source; yarn run grunt run:exampleFunctionalTestsRelease --from=source; diff --git a/test/scripts/jenkins_firefox_smoke.sh b/test/scripts/jenkins_firefox_smoke.sh index 2bba6e06d76d7..247ab360b7912 100755 --- a/test/scripts/jenkins_firefox_smoke.sh +++ b/test/scripts/jenkins_firefox_smoke.sh @@ -5,6 +5,6 @@ source test/scripts/jenkins_test_setup_oss.sh checks-reporter-with-killswitch "Firefox smoke test" \ node scripts/functional_tests \ --bail --debug \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --include-tag "includeFirefox" \ --config test/functional/config.firefox.js; diff --git a/test/scripts/jenkins_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh new file mode 100755 index 0000000000000..1d691d98982de --- /dev/null +++ b/test/scripts/jenkins_plugin_functional.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +cd test/plugin_functional/plugins/kbn_sample_panel_action; +if [[ ! -d "target" ]]; then + yarn build; +fi +cd -; + +pwd + +yarn run grunt run:pluginFunctionalTestsRelease --from=source; +yarn run grunt run:exampleFunctionalTestsRelease --from=source; +yarn run grunt run:interpreterFunctionalTestsRelease; diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh old mode 100644 new mode 100755 index 204911a3eedaa..a5a1a2103801f --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -1,12 +1,6 @@ #!/usr/bin/env bash -source test/scripts/jenkins_test_setup.sh - -installDir="$PARENT_DIR/install/kibana" -destDir="${installDir}-${CI_WORKER_NUMBER}" -cp -R "$installDir" "$destDir" - -export KIBANA_INSTALL_DIR="$destDir" +source test/scripts/jenkins_test_setup_xpack.sh echo " -> Running security solution cypress tests" cd "$XPACK_DIR" diff --git a/test/scripts/jenkins_setup_parallel_workspace.sh b/test/scripts/jenkins_setup_parallel_workspace.sh new file mode 100755 index 0000000000000..5274d05572e71 --- /dev/null +++ b/test/scripts/jenkins_setup_parallel_workspace.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -e + +CURRENT_DIR=$(pwd) + +# Copy everything except node_modules into the current workspace +rsync -a ${WORKSPACE}/kibana/* . --exclude node_modules +rsync -a ${WORKSPACE}/kibana/.??* . + +# Symlink all non-root, non-fixture node_modules into our new workspace +cd ${WORKSPACE}/kibana +find . -type d -name node_modules -not -path '*__fixtures__*' -not -path './node_modules*' -prune -print0 | xargs -0I % ln -s "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%" +find . -type d -wholename '*__fixtures__*node_modules' -not -path './node_modules*' -prune -print0 | xargs -0I % cp -R "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%" +cd "${CURRENT_DIR}" + +# Symlink all of the individual root-level node_modules into the node_modules/ directory +mkdir -p node_modules +ln -s ${WORKSPACE}/kibana/node_modules/* node_modules/ +ln -s ${WORKSPACE}/kibana/node_modules/.??* node_modules/ + +# Copy a few node_modules instead of symlinking them. They don't work correctly if symlinked +unlink node_modules/@kbn +unlink node_modules/css-loader +unlink node_modules/style-loader + +# packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts will fail if this is a symlink +unlink node_modules/val-loader + +cp -R ${WORKSPACE}/kibana/node_modules/@kbn node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/css-loader node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/style-loader node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/val-loader node_modules/ diff --git a/test/scripts/jenkins_test_setup.sh b/test/scripts/jenkins_test_setup.sh old mode 100644 new mode 100755 index 49ee8a6b526ca..05b88aa2dd0a2 --- a/test/scripts/jenkins_test_setup.sh +++ b/test/scripts/jenkins_test_setup.sh @@ -14,3 +14,9 @@ trap 'post_work' EXIT export TEST_BROWSER_HEADLESS=1 source src/dev/ci_setup/setup_env.sh + +# For parallel workspaces, we should copy the .es directory from the root, because it should already have downloaded snapshots in it +# This isn't part of jenkins_setup_parallel_workspace.sh just because not all tasks require ES +if [[ ! -d .es && -d "$WORKSPACE/kibana/.es" ]]; then + cp -R $WORKSPACE/kibana/.es ./ +fi diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh old mode 100644 new mode 100755 index 7bbb867526384..b7eac33f35176 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -2,10 +2,17 @@ source test/scripts/jenkins_test_setup.sh -if [[ -z "$CODE_COVERAGE" ]] ; then - installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)" - destDir=${installDir}-${CI_PARALLEL_PROCESS_NUMBER} - cp -R "$installDir" "$destDir" +if [[ -z "$CODE_COVERAGE" ]]; then + + destDir="build/kibana-build-oss" + if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" + fi + + if [[ ! -d $destDir ]]; then + mkdir -p $destDir + cp -pR "$WORKSPACE/kibana-build-oss/." $destDir/ + fi export KIBANA_INSTALL_DIR="$destDir" fi diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh old mode 100644 new mode 100755 index a72e9749ebbd5..74a3de77e3a76 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -3,11 +3,18 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - installDir="$PARENT_DIR/install/kibana" - destDir="${installDir}-${CI_PARALLEL_PROCESS_NUMBER}" - cp -R "$installDir" "$destDir" - export KIBANA_INSTALL_DIR="$destDir" + destDir="build/kibana-build-xpack" + if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" + fi + + if [[ ! -d $destDir ]]; then + mkdir -p $destDir + cp -pR "$WORKSPACE/kibana-build-xpack/." $destDir/ + fi + + export KIBANA_INSTALL_DIR="$(realpath $destDir)" cd "$XPACK_DIR" fi diff --git a/test/scripts/jenkins_xpack_accessibility.sh b/test/scripts/jenkins_xpack_accessibility.sh index a3c03dd780886..3afd4bfb76396 100755 --- a/test/scripts/jenkins_xpack_accessibility.sh +++ b/test/scripts/jenkins_xpack_accessibility.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_xpack.sh checks-reporter-with-killswitch "X-Pack accessibility tests" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/accessibility/config.ts; diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index c962b962b1e5e..2452e2f5b8c58 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -3,15 +3,9 @@ cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ - --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ - --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ - --verbose; +if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + ./test/scripts/jenkins_xpack_build_plugins.sh +fi # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -36,7 +30,10 @@ if [[ -z "$CODE_COVERAGE" ]] ; then cd "$KIBANA_DIR" node scripts/build --debug --no-oss linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" - installDir="$PARENT_DIR/install/kibana" + installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + + mkdir -p "$WORKSPACE/kibana-build-xpack" + cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ fi diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh new file mode 100755 index 0000000000000..3fd3d02de1304 --- /dev/null +++ b/test/scripts/jenkins_xpack_build_plugins.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --workers 12 \ + --verbose diff --git a/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh b/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh index d3ca8839a7dab..e3b0fe778bdfb 100755 --- a/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh +++ b/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_xpack.sh checks-reporter-with-killswitch "Capture Kibana Saved Objects field count metrics" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/saved_objects_field_count/config.ts; diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh new file mode 100755 index 0000000000000..c3211300b96c5 --- /dev/null +++ b/test/scripts/lint/eslint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:eslint diff --git a/test/scripts/lint/sasslint.sh b/test/scripts/lint/sasslint.sh new file mode 100755 index 0000000000000..b9c683bcb049e --- /dev/null +++ b/test/scripts/lint/sasslint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:sasslint diff --git a/test/scripts/test/api_integration.sh b/test/scripts/test/api_integration.sh new file mode 100755 index 0000000000000..152c97a3ca7df --- /dev/null +++ b/test/scripts/test/api_integration.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:apiIntegrationTests diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh new file mode 100755 index 0000000000000..73dbbddfb38f6 --- /dev/null +++ b/test/scripts/test/jest_integration.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_jest_integration diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh new file mode 100755 index 0000000000000..e25452698cebc --- /dev/null +++ b/test/scripts/test/jest_unit.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_jest diff --git a/test/scripts/test/karma_ci.sh b/test/scripts/test/karma_ci.sh new file mode 100755 index 0000000000000..e9985300ba19d --- /dev/null +++ b/test/scripts/test/karma_ci.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_karma_ci diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh new file mode 100755 index 0000000000000..43c00f0a09dcf --- /dev/null +++ b/test/scripts/test/mocha.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:mocha diff --git a/test/scripts/test/safer_lodash_set.sh b/test/scripts/test/safer_lodash_set.sh new file mode 100755 index 0000000000000..4d7f9c28210d1 --- /dev/null +++ b/test/scripts/test/safer_lodash_set.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_package_safer_lodash_set diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh new file mode 100755 index 0000000000000..93d70ec355391 --- /dev/null +++ b/test/scripts/test/xpack_jest_unit.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=10 diff --git a/test/scripts/test/xpack_karma.sh b/test/scripts/test/xpack_karma.sh new file mode 100755 index 0000000000000..9078f01f1b870 --- /dev/null +++ b/test/scripts/test/xpack_karma.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:karma diff --git a/test/scripts/test/xpack_list_cyclic_dependency.sh b/test/scripts/test/xpack_list_cyclic_dependency.sh new file mode 100755 index 0000000000000..493fe9f58d322 --- /dev/null +++ b/test/scripts/test/xpack_list_cyclic_dependency.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps diff --git a/test/scripts/test/xpack_siem_cyclic_dependency.sh b/test/scripts/test/xpack_siem_cyclic_dependency.sh new file mode 100755 index 0000000000000..b21301f25ad08 --- /dev/null +++ b/test/scripts/test/xpack_siem_cyclic_dependency.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps diff --git a/vars/catchErrors.groovy b/vars/catchErrors.groovy index 460a90b8ec0c0..2a1b55d832606 100644 --- a/vars/catchErrors.groovy +++ b/vars/catchErrors.groovy @@ -1,8 +1,15 @@ // Basically, this is a shortcut for catchError(catchInterruptions: false) {} // By default, catchError will swallow aborts/timeouts, which we almost never want +// Also, by wrapping it in an additional try/catch, we cut down on spam in Pipeline Steps def call(Map params = [:], Closure closure) { - params.catchInterruptions = false - return catchError(params, closure) + try { + closure() + } catch (ex) { + params.catchInterruptions = false + catchError(params) { + throw ex + } + } } return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 94bfe983b4653..5f3e8d1a0660d 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -2,20 +2,63 @@ def withPostBuildReporting(Closure closure) { try { closure() } finally { - catchErrors { - runErrorReporter() + def parallelWorkspaces = [] + try { + parallelWorkspaces = getParallelWorkspaces() + } catch(ex) { + print ex } catchErrors { - runbld.junit() + runErrorReporter([pwd()] + parallelWorkspaces) } catchErrors { publishJunit() } + + catchErrors { + def parallelWorkspace = "${env.WORKSPACE}/parallel" + if (fileExists(parallelWorkspace)) { + dir(parallelWorkspace) { + def workspaceTasks = [:] + + parallelWorkspaces.each { workspaceDir -> + workspaceTasks[workspaceDir] = { + dir(workspaceDir) { + catchErrors { + runbld.junit() + } + } + } + } + + if (workspaceTasks) { + parallel(workspaceTasks) + } + } + } + } } } +def getParallelWorkspaces() { + def workspaces = [] + def parallelWorkspace = "${env.WORKSPACE}/parallel" + if (fileExists(parallelWorkspace)) { + dir(parallelWorkspace) { + // findFiles only returns files if you use glob, so look for a file that should be in every valid workspace + workspaces = findFiles(glob: '*/kibana/package.json') + .collect { + // get the paths to the kibana directories for the parallel workspaces + return parallelWorkspace + '/' + it.path.tokenize('/').dropRight(1).join('/') + } + } + } + + return workspaces +} + def notifyOnError(Closure closure) { try { closure() @@ -35,36 +78,43 @@ def notifyOnError(Closure closure) { } } -def functionalTestProcess(String name, Closure closure) { - return { processNumber -> - def kibanaPort = "61${processNumber}1" - def esPort = "61${processNumber}2" - def esTransportPort = "61${processNumber}3" - def ingestManagementPackageRegistryPort = "61${processNumber}4" +def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { + // This can go away once everything that uses the deprecated workers.parallelProcesses() is moved to task queue + def parallelId = env.TASK_QUEUE_PROCESS_ID ?: env.CI_PARALLEL_PROCESS_NUMBER - withEnv([ - "CI_PARALLEL_PROCESS_NUMBER=${processNumber}", - "TEST_KIBANA_HOST=localhost", - "TEST_KIBANA_PORT=${kibanaPort}", - "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", - "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", - "TEST_ES_TRANSPORT_PORT=${esTransportPort}", - "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", - "IS_PIPELINE_JOB=1", - "JOB=${name}", - "KBN_NP_PLUGINS_BUILT=true", - ]) { - notifyOnError { - closure() - } - } + def kibanaPort = "61${parallelId}1" + def esPort = "61${parallelId}2" + def esTransportPort = "61${parallelId}3" + def ingestManagementPackageRegistryPort = "61${parallelId}4" + + withEnv([ + "CI_GROUP=${parallelId}", + "REMOVE_KIBANA_INSTALL_DIR=1", + "CI_PARALLEL_PROCESS_NUMBER=${parallelId}", + "TEST_KIBANA_HOST=localhost", + "TEST_KIBANA_PORT=${kibanaPort}", + "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", + "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", + "TEST_ES_TRANSPORT_PORT=${esTransportPort}", + "KBN_NP_PLUGINS_BUILT=true", + "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", + ] + additionalEnvs) { + closure() + } +} + +def functionalTestProcess(String name, Closure closure) { + return { + withFunctionalTestEnv(["JOB=${name}"], closure) } } def functionalTestProcess(String name, String script) { return functionalTestProcess(name) { - retryable(name) { - runbld(script, "Execute ${name}") + notifyOnError { + retryable(name) { + runbld(script, "Execute ${name}") + } } } } @@ -121,11 +171,17 @@ def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ 'target/kibana-*', + 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', 'target/junit/**/*', - 'test/**/screenshots/**/*.png', + 'target/test-suites-ci-plan.json', + 'test/**/screenshots/session/*.png', + 'test/**/screenshots/failure/*.png', + 'test/**/screenshots/diff/*.png', 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/**/*.png', + 'x-pack/test/**/screenshots/session/*.png', + 'x-pack/test/**/screenshots/failure/*.png', + 'x-pack/test/**/screenshots/diff/*.png', 'x-pack/test/functional/failure_debug/html/*.html', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', ] @@ -140,6 +196,12 @@ def withGcsArtifactUpload(workerName, closure) { ARTIFACT_PATTERNS.each { pattern -> uploadGcsArtifact(uploadPrefix, pattern) } + + dir(env.WORKSPACE) { + ARTIFACT_PATTERNS.each { pattern -> + uploadGcsArtifact(uploadPrefix, "parallel/*/kibana/${pattern}") + } + } } } }) @@ -152,6 +214,10 @@ def withGcsArtifactUpload(workerName, closure) { def publishJunit() { junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) + + dir(env.WORKSPACE) { + junit(testResults: 'parallel/*/kibana/target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) + } } def sendMail() { @@ -217,26 +283,36 @@ def doSetup() { } } -def buildOss() { +def buildOss(maxWorkers = '') { notifyOnError { - runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + } } } -def buildXpack() { +def buildXpack(maxWorkers = '') { notifyOnError { - runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + } } } def runErrorReporter() { + return runErrorReporter([pwd()]) +} + +def runErrorReporter(workspaces) { def status = buildUtils.getBuildStatus() def dryRun = status != "ABORTED" ? "" : "--no-github-update" + def globs = workspaces.collect { "'${it}/target/junit/**/*.xml'" }.join(" ") + bash( """ source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests ${dryRun} target/junit/**/*.xml + node scripts/report_failed_tests ${dryRun} ${globs} """, "Report failed tests, if necessary" ) @@ -275,6 +351,102 @@ def call(Map params = [:], Closure closure) { } } +// Creates a task queue using withTaskQueue, and copies the bootstrapped kibana repo into each process's workspace +// Note that node_modules are mostly symlinked to save time/space. See test/scripts/jenkins_setup_parallel_workspace.sh +def withCiTaskQueue(Map options = [:], Closure closure) { + def setupClosure = { + // This can't use runbld, because it expects the source to be there, which isn't yet + bash("${env.WORKSPACE}/kibana/test/scripts/jenkins_setup_parallel_workspace.sh", "Set up duplicate workspace for parallel process") + } + + def config = [parallel: 24, setup: setupClosure] + options + + withTaskQueue(config) { + closure.call() + } +} + +def scriptTask(description, script) { + return { + withFunctionalTestEnv { + notifyOnError { + runbld(script, description) + } + } + } +} + +def scriptTaskDocker(description, script) { + return { + withDocker(scriptTask(description, script)) + } +} + +def buildDocker() { + sh( + script: """ + cp /usr/local/bin/runbld .ci/ + cp /usr/local/bin/bash_standard_lib.sh .ci/ + cd .ci + docker build -t kibana-ci -f ./Dockerfile . + """, + label: 'Build CI Docker image' + ) +} + +def withDocker(Closure closure) { + docker + .image('kibana-ci') + .inside( + "-v /etc/runbld:/etc/runbld:ro -v '${env.JENKINS_HOME}:${env.JENKINS_HOME}' -v '/dev/shm/workspace:/dev/shm/workspace' --shm-size 2GB --cpus 4", + closure + ) +} + +def buildOssPlugins() { + runbld('./test/scripts/jenkins_build_plugins.sh', 'Build OSS Plugins') +} + +def buildXpackPlugins() { + runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins') +} + +def withTasks(Map params = [worker: [:]], Closure closure) { + catchErrors { + def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:]) + + workers.ci(config) { + withCiTaskQueue(parallel: 24) { + parallel([ + docker: { + retry(2) { + buildDocker() + } + }, + + // There are integration tests etc that require the plugins to be built first, so let's go ahead and build them before set up the parallel workspaces + ossPlugins: { buildOssPlugins() }, + xpackPlugins: { buildXpackPlugins() }, + ]) + + catchErrors { + closure() + } + } + } + } +} + +def allCiTasks() { + withTasks { + tasks.check() + tasks.lint() + tasks.test() + tasks.functionalOss() + tasks.functionalXpack() + } +} + def pipelineLibraryTests() { whenChanged(['vars/', '.ci/pipeline-library/']) { workers.base(size: 'flyweight', bootstrapped: false, ramDisk: false) { @@ -285,5 +457,4 @@ def pipelineLibraryTests() { } } - return this diff --git a/vars/task.groovy b/vars/task.groovy new file mode 100644 index 0000000000000..0c07b519b6fef --- /dev/null +++ b/vars/task.groovy @@ -0,0 +1,5 @@ +def call(Closure closure) { + withTaskQueue.addTask(closure) +} + +return this diff --git a/vars/tasks.groovy b/vars/tasks.groovy new file mode 100644 index 0000000000000..52641ce31f0be --- /dev/null +++ b/vars/tasks.groovy @@ -0,0 +1,119 @@ +def call(List closures) { + withTaskQueue.addTasks(closures) +} + +def check() { + tasks([ + kibanaPipeline.scriptTask('Check Telemetry Schema', 'test/scripts/checks/telemetry.sh'), + kibanaPipeline.scriptTask('Check TypeScript Projects', 'test/scripts/checks/ts_projects.sh'), + kibanaPipeline.scriptTask('Check Doc API Changes', 'test/scripts/checks/doc_api_changes.sh'), + kibanaPipeline.scriptTask('Check Types', 'test/scripts/checks/type_check.sh'), + kibanaPipeline.scriptTask('Check i18n', 'test/scripts/checks/i18n.sh'), + kibanaPipeline.scriptTask('Check File Casing', 'test/scripts/checks/file_casing.sh'), + kibanaPipeline.scriptTask('Check Lockfile Symlinks', 'test/scripts/checks/lock_file_symlinks.sh'), + kibanaPipeline.scriptTask('Check Licenses', 'test/scripts/checks/licenses.sh'), + kibanaPipeline.scriptTask('Verify Dependency Versions', 'test/scripts/checks/verify_dependency_versions.sh'), + kibanaPipeline.scriptTask('Verify NOTICE', 'test/scripts/checks/verify_notice.sh'), + kibanaPipeline.scriptTask('Test Projects', 'test/scripts/checks/test_projects.sh'), + kibanaPipeline.scriptTask('Test Hardening', 'test/scripts/checks/test_hardening.sh'), + ]) +} + +def lint() { + tasks([ + kibanaPipeline.scriptTask('Lint: eslint', 'test/scripts/lint/eslint.sh'), + kibanaPipeline.scriptTask('Lint: sasslint', 'test/scripts/lint/sasslint.sh'), + ]) +} + +def test() { + tasks([ + // These 2 tasks require isolation because of hard-coded, conflicting ports and such, so let's use Docker here + kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh'), + kibanaPipeline.scriptTaskDocker('Mocha Tests', 'test/scripts/test/mocha.sh'), + + kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), + kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), + kibanaPipeline.scriptTask('@elastic/safer-lodash-set Tests', 'test/scripts/test/safer_lodash_set.sh'), + kibanaPipeline.scriptTask('X-Pack SIEM cyclic dependency', 'test/scripts/test/xpack_siem_cyclic_dependency.sh'), + kibanaPipeline.scriptTask('X-Pack List cyclic dependency', 'test/scripts/test/xpack_list_cyclic_dependency.sh'), + kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), + ]) +} + +def functionalOss(Map params = [:]) { + def config = params ?: [ciGroups: true, firefox: true, accessibility: true, pluginFunctional: true, visualRegression: false] + + task { + kibanaPipeline.buildOss(6) + + if (config.ciGroups) { + def ciGroups = 1..12 + tasks(ciGroups.collect { kibanaPipeline.ossCiGroupProcess(it) }) + } + + if (config.firefox) { + task(kibanaPipeline.functionalTestProcess('oss-firefox', './test/scripts/jenkins_firefox_smoke.sh')) + } + + if (config.accessibility) { + task(kibanaPipeline.functionalTestProcess('oss-accessibility', './test/scripts/jenkins_accessibility.sh')) + } + + if (config.pluginFunctional) { + task(kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh')) + } + + if (config.visualRegression) { + task(kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')) + } + } +} + +def functionalXpack(Map params = [:]) { + def config = params ?: [ + ciGroups: true, + firefox: true, + accessibility: true, + pluginFunctional: true, + savedObjectsFieldMetrics:true, + pageLoadMetrics: false, + visualRegression: false, + ] + + task { + kibanaPipeline.buildXpack(10) + + if (config.ciGroups) { + def ciGroups = 1..10 + tasks(ciGroups.collect { kibanaPipeline.xpackCiGroupProcess(it) }) + } + + if (config.firefox) { + task(kibanaPipeline.functionalTestProcess('xpack-firefox', './test/scripts/jenkins_xpack_firefox_smoke.sh')) + } + + if (config.accessibility) { + task(kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh')) + } + + if (config.visualRegression) { + task(kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')) + } + + if (config.savedObjectsFieldMetrics) { + task(kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh')) + } + + whenChanged([ + 'x-pack/plugins/security_solution/', + 'x-pack/test/security_solution_cypress/', + 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', + 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx', + ]) { + task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')) + } + } +} + +return this diff --git a/vars/withTaskQueue.groovy b/vars/withTaskQueue.groovy new file mode 100644 index 0000000000000..8132d6264744f --- /dev/null +++ b/vars/withTaskQueue.groovy @@ -0,0 +1,154 @@ +import groovy.transform.Field + +public static @Field TASK_QUEUES = [:] +public static @Field TASK_QUEUES_COUNTER = 0 + +/** + withTaskQueue creates a queue of "tasks" (just plain closures to execute), and executes them with your desired level of concurrency. + This way, you can define, for example, 40 things that need to execute, then only allow 10 of them to execute at once. + + Each "process" will execute in a separate, unique, empty directory. + If you want each process to have a bootstrapped kibana repo, check out kibanaPipeline.withCiTaskQueue + + Using the queue currently requires an agent/worker. + + Usage: + + withTaskQueue(parallel: 10) { + task { print "This is a task" } + + // This is the same as calling task() multiple times + tasks([ { print "Another task" }, { print "And another task" } ]) + + // Tasks can queue up subsequent tasks + task { + buildThing() + task { print "I depend on buildThing()" } + } + } + + You can also define a setup task that each process should execute one time before executing tasks: + withTaskQueue(parallel: 10, setup: { sh "my-setup-scrupt.sh" }) { + ... + } + +*/ +def call(Map options = [:], Closure closure) { + def config = [ parallel: 10 ] + options + def counter = ++TASK_QUEUES_COUNTER + + // We're basically abusing withEnv() to create a "scope" for all steps inside of a withTaskQueue block + // This way, we could have multiple task queue instances in the same pipeline + withEnv(["TASK_QUEUE_ID=${counter}"]) { + withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID] = [ + tasks: [], + tmpFile: sh(script: 'mktemp', returnStdout: true).trim() + ] + + closure.call() + + def processesExecuting = 0 + def processes = [:] + def iterationId = 0 + + for(def i = 1; i <= config.parallel; i++) { + def j = i + processes["task-queue-process-${j}"] = { + catchErrors { + withEnv([ + "TASK_QUEUE_PROCESS_ID=${j}", + "TASK_QUEUE_ITERATION_ID=${++iterationId}" + ]) { + dir("${WORKSPACE}/parallel/${j}/kibana") { + if (config.setup) { + config.setup.call(j) + } + + def isDone = false + while(!isDone) { // TODO some kind of timeout? + catchErrors { + if (!getTasks().isEmpty()) { + processesExecuting++ + catchErrors { + def task + try { + task = getTasks().pop() + } catch (java.util.NoSuchElementException ex) { + return + } + + task.call() + } + processesExecuting-- + // If a task finishes, and no new tasks were queued up, and nothing else is executing + // Then all of the processes should wake up and exit + if (processesExecuting < 1 && getTasks().isEmpty()) { + taskNotify() + } + return + } + + if (processesExecuting > 0) { + taskSleep() + return + } + + // Queue is empty, no processes are executing + isDone = true + } + } + } + } + } + } + } + parallel(processes) + } +} + +// If we sleep in a loop using Groovy code, Pipeline Steps is flooded with Sleep steps +// So, instead, we just watch a file and `touch` it whenever something happens that could modify the queue +// There's a 20 minute timeout just in case something goes wrong, +// in which case this method will get called again if the process is actually supposed to be waiting. +def taskSleep() { + sh(script: """#!/bin/bash + TIMESTAMP=\$(date '+%s' -d "0 seconds ago") + for (( i=1; i<=240; i++ )) + do + if [ "\$(stat -c %Y '${getTmpFile()}')" -ge "\$TIMESTAMP" ] + then + break + else + sleep 5 + if [[ \$i == 240 ]]; then + echo "Waited for new tasks for 20 minutes, exiting in case something went wrong" + fi + fi + done + """, label: "Waiting for new tasks...") +} + +// Used to let the task queue processes know that either a new task has been queued up, or work is complete +def taskNotify() { + sh "touch '${getTmpFile()}'" +} + +def getTasks() { + return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tasks +} + +def getTmpFile() { + return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tmpFile +} + +def addTask(Closure closure) { + getTasks() << closure + taskNotify() +} + +def addTasks(List closures) { + closures.reverse().each { + getTasks() << it + } + taskNotify() +} diff --git a/vars/workers.groovy b/vars/workers.groovy index f5a28c97c6812..e582e996a78b5 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -13,6 +13,8 @@ def label(size) { return 'docker && tests-l' case 'xl': return 'docker && tests-xl' + case 'xl-highmem': + return 'docker && tests-xl-highmem' case 'xxl': return 'docker && tests-xxl' } @@ -55,6 +57,11 @@ def base(Map params, Closure closure) { } } + sh( + script: "mkdir -p ${env.WORKSPACE}/tmp", + label: "Create custom temp directory" + ) + def checkoutInfo = [:] if (config.scm) { @@ -89,6 +96,7 @@ def base(Map params, Closure closure) { "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", "TEST_BROWSER_HEADLESS=1", "GIT_BRANCH=${checkoutInfo.branch}", + "TMPDIR=${env.WORKSPACE}/tmp", // For Chrome and anything else that respects it ]) { withCredentials([ string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), @@ -169,7 +177,9 @@ def parallelProcesses(Map params) { sleep(delay) } - processClosure(processNumber) + withEnv(["CI_PARALLEL_PROCESS_NUMBER=${processNumber}"]) { + processClosure() + } } } diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index b51a85edaa67b..85ec7baf18c62 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import fs from 'fs'; import { ReactChildren } from 'react'; import path from 'path'; import moment from 'moment'; @@ -94,6 +95,12 @@ jest.mock('../shareable_runtime/components/rendered_element'); // @ts-expect-error RenderedElement.mockImplementation(() => 'RenderedElement'); +// Some of the code requires that this directory exists, but the tests don't actually require any css to be present +const cssDir = path.resolve(__dirname, '../../../../built_assets/css'); +if (!fs.existsSync(cssDir)) { + fs.mkdirSync(cssDir, { recursive: true }); +} + addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots From 0502fee6518b81e39afca0f28df2a39b8397c36a Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 4 Aug 2020 10:19:51 -0700 Subject: [PATCH 097/115] [kbn/optimizer] remove unused modules (#74195) Co-authored-by: spalger --- packages/kbn-optimizer/package.json | 3 --- yarn.lock | 9 ++------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 4fbbc920c4447..e6eb5de31abd8 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -15,12 +15,9 @@ "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", "@types/compression-webpack-plugin": "^2.0.2", - "@types/estree": "^0.0.44", "@types/loader-utils": "^1.1.3", "@types/watchpack": "^1.1.5", "@types/webpack": "^4.41.3", - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index a70f04e030447..0638267afd256 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4527,11 +4527,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^0.0.44": - version "0.0.44" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21" - integrity sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g== - "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -6188,7 +6183,7 @@ acorn-walk@^6.0.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== -acorn-walk@^7.0.0, acorn-walk@^7.1.1: +acorn-walk@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== @@ -6218,7 +6213,7 @@ acorn@^7.0.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe" integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ== -acorn@^7.1.0, acorn@^7.1.1: +acorn@^7.1.0: version "7.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== From 3067184c45caf67c64096fa2a7fbde21a97738fd Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 4 Aug 2020 13:30:09 -0400 Subject: [PATCH 098/115] [Maps] Custom color ramps should show correctly on the map for mvt layers (#74169) --- .../public/classes/fields/es_agg_field.ts | 4 +++ .../maps/public/classes/fields/field.ts | 9 +++++ .../maps/public/classes/fields/mvt_field.ts | 4 +++ .../fields/top_term_percentage_field.ts | 4 +++ .../properties/dynamic_color_property.js | 10 ++---- .../properties/dynamic_color_property.test.js | 36 ++++++++++++++----- .../properties/dynamic_size_property.js | 8 ++--- .../properties/dynamic_style_property.tsx | 16 ++++++--- 8 files changed, 65 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts index 15779d22681c0..7b184819b839b 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts @@ -134,6 +134,10 @@ export class ESAggField implements IESAggField { supportsAutoDomain(): boolean { return true; } + + canReadFromGeoJson(): boolean { + return true; + } } export function esAggFieldsFactory( diff --git a/x-pack/plugins/maps/public/classes/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts index 410b38e79ffe4..2c190d54f0265 100644 --- a/x-pack/plugins/maps/public/classes/fields/field.ts +++ b/x-pack/plugins/maps/public/classes/fields/field.ts @@ -26,7 +26,12 @@ export interface IField { // then styling properties that require the domain to be known cannot use this property. supportsAutoDomain(): boolean; + // Determinse wheter Maps-app can automatically deterime the domain of the field-values + // _without_ having to retrieve the data as GeoJson + // e.g. for ES-sources, this would use the extended_stats API supportsFieldMeta(): boolean; + + canReadFromGeoJson(): boolean; } export class AbstractField implements IField { @@ -90,4 +95,8 @@ export class AbstractField implements IField { supportsAutoDomain(): boolean { return true; } + + canReadFromGeoJson(): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts index eb2bb94b36a69..7c8d08bacdb51 100644 --- a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts @@ -56,4 +56,8 @@ export class MVTField extends AbstractField implements IField { supportsAutoDomain() { return false; } + + canReadFromGeoJson(): boolean { + return false; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts index f4625e42ab5de..fc931b13619ef 100644 --- a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts @@ -79,4 +79,8 @@ export class TopTermPercentageField implements IESAggField { canValueBeFormatted(): boolean { return false; } + + canReadFromGeoJson(): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js index cde4d1f201199..e643abcaf8d54 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js @@ -8,7 +8,7 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { makeMbClampedNumberExpression, dynamicRound } from '../style_util'; import { getOrdinalMbColorRampStops, getColorPalette } from '../../color_palettes'; import React from 'react'; -import { COLOR_MAP_TYPE, MB_LOOKUP_FUNCTION } from '../../../../../common/constants'; +import { COLOR_MAP_TYPE } from '../../../../../common/constants'; import { isCategoricalStopsInvalid, getOtherCategoryLabel, @@ -91,10 +91,6 @@ export class DynamicColorProperty extends DynamicStyleProperty { return colors ? colors.length : 0; } - supportsMbFeatureState() { - return true; - } - _getMbColor() { if (!this._field || !this._field.getName()) { return null; @@ -120,7 +116,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { const lessThanFirstStopValue = firstStopValue - 1; return [ 'step', - ['coalesce', ['feature-state', targetName], lessThanFirstStopValue], + ['coalesce', [this.getMbLookupFunction(), targetName], lessThanFirstStopValue], RGBA_0000, // MB will assign the base value to any features that is below the first stop value ...colorStops, ]; @@ -146,7 +142,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { makeMbClampedNumberExpression({ minValue: rangeFieldMeta.min, maxValue: rangeFieldMeta.max, - lookupFunction: MB_LOOKUP_FUNCTION.FEATURE_STATE, + lookupFunction: this.getMbLookupFunction(), fallback: lessThanFirstStopValue, fieldName: targetName, }), diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js index 2183a298a2841..425954c9af860 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js @@ -343,6 +343,15 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { }); describe('custom color ramp', () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + useCustomColorRamp: true, + customColorRamp: [ + { stop: 10, color: '#f7faff' }, + { stop: 100, color: '#072f6b' }, + ], + }; + test('should return null when customColorRamp is not provided', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.ORDINAL, @@ -362,15 +371,7 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { expect(colorProperty._getMbColor()).toBeNull(); }); - test('should return mapbox expression for custom color ramp', async () => { - const dynamicStyleOptions = { - type: COLOR_MAP_TYPE.ORDINAL, - useCustomColorRamp: true, - customColorRamp: [ - { stop: 10, color: '#f7faff' }, - { stop: 100, color: '#072f6b' }, - ], - }; + test('should use `feature-state` by default', async () => { const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toEqual([ 'step', @@ -382,6 +383,23 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { '#072f6b', ]); }); + + test('should use `get` when source cannot return raw geojson', async () => { + const field = Object.create(mockField); + field.canReadFromGeoJson = function () { + return false; + }; + const colorProperty = makeProperty(dynamicStyleOptions, undefined, field); + expect(colorProperty._getMbColor()).toEqual([ + 'step', + ['coalesce', ['get', 'foobar'], 9], + 'rgba(0,0,0,0)', + 10, + '#f7faff', + 100, + '#072f6b', + ]); + }); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js index 662d1ccf33b95..83bd4b70ba5c3 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js @@ -33,7 +33,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { return false; } - return true; + return super.supportsMbFeatureState(); } syncHaloWidthWithMb(mbLayerId, mbMap) { @@ -109,17 +109,13 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _getMbDataDrivenSize({ targetName, minSize, maxSize, minValue, maxValue }) { - const lookup = this.supportsMbFeatureState() - ? MB_LOOKUP_FUNCTION.FEATURE_STATE - : MB_LOOKUP_FUNCTION.GET; - const stops = minValue === maxValue ? [maxValue, maxSize] : [minValue, minSize, maxValue, maxSize]; return [ 'interpolate', ['linear'], makeMbClampedNumberExpression({ - lookupFunction: lookup, + lookupFunction: this.getMbLookupFunction(), maxValue, minValue, fieldName: targetName, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index f666a9aad7804..18b7faf6283c4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -11,9 +11,10 @@ import { Feature } from 'geojson'; import { AbstractStyleProperty, IStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { - STYLE_TYPE, - SOURCE_META_DATA_REQUEST_ID, FIELD_ORIGIN, + MB_LOOKUP_FUNCTION, + SOURCE_META_DATA_REQUEST_ID, + STYLE_TYPE, VECTOR_STYLES, } from '../../../../../common/constants'; import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover'; @@ -21,8 +22,8 @@ import { CategoricalFieldMetaPopover } from '../components/field_meta/categorica import { CategoryFieldMeta, FieldMetaOptions, - StyleMetaData, RangeFieldMeta, + StyleMetaData, } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; @@ -41,6 +42,7 @@ export interface IDynamicStyleProperty extends IStyleProperty { supportsFieldMeta(): boolean; getFieldMetaRequest(): Promise; supportsMbFeatureState(): boolean; + getMbLookupFunction(): MB_LOOKUP_FUNCTION; pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null; pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null; getValueSuggestions(query: string): Promise; @@ -193,7 +195,13 @@ export class DynamicStyleProperty extends AbstractStyleProperty } supportsMbFeatureState() { - return true; + return !!this._field && this._field.canReadFromGeoJson(); + } + + getMbLookupFunction(): MB_LOOKUP_FUNCTION { + return this.supportsMbFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; } getFieldMetaOptions() { From de22caa0f657812378c0bfbdec728f2b4a0d852a Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 4 Aug 2020 13:32:21 -0400 Subject: [PATCH 099/115] [Monitoring] Ensure setup mode works on cloud but only for alerts (#73127) * Ensure setup mode works on cloud but only for alerts * Update snapshot * Update translations * Restructure tests to understand the failure better * PR feedback * Backwards, whoops * Remove commented out code Co-authored-by: Elastic Machine --- x-pack/plugins/monitoring/common/enums.ts | 5 ++ .../monitoring/public/alerts/badge.tsx | 2 +- .../components/apm/instances/instances.js | 6 ++- .../components/beats/listing/listing.js | 6 ++- .../components/cluster/overview/apm_panel.js | 21 ++++---- .../cluster/overview/beats_panel.js | 21 ++++---- .../cluster/overview/elasticsearch_panel.js | 21 ++++---- .../cluster/overview/kibana_panel.js | 21 ++++---- .../cluster/overview/logstash_panel.js | 21 ++++---- .../components/elasticsearch/nodes/nodes.js | 12 +++-- .../components/kibana/instances/instances.js | 6 ++- .../components/logstash/listing/listing.js | 6 ++- .../__snapshots__/setup_mode.test.js.snap | 2 + .../public/components/renderers/setup_mode.js | 6 ++- .../__snapshots__/enter_button.test.tsx.snap | 1 + .../__snapshots__/tooltip.test.js.snap | 25 +++++++++ .../components/setup_mode/enter_button.tsx | 2 +- .../public/components/setup_mode/tooltip.js | 6 ++- .../public/components/table/eui_table.js | 4 +- .../public/components/table/eui_table_ssp.js | 8 ++- .../public/directives/main/index.js | 9 +++- .../monitoring/public/lib/setup_mode.tsx | 21 +++++--- .../public/views/base_controller.js | 6 +-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apps/monitoring/_get_lifecycle_methods.js | 6 +-- .../test/functional/apps/monitoring/index.js | 2 + .../monitoring/setup/metricbeat_migration.js | 51 +++++++++++++++++++ x-pack/test/functional/services/index.ts | 2 + .../functional/services/monitoring/index.js | 1 + .../services/monitoring/setup_mode.js | 37 ++++++++++++++ 31 files changed, 260 insertions(+), 79 deletions(-) create mode 100644 x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js create mode 100644 x-pack/test/functional/services/monitoring/setup_mode.js diff --git a/x-pack/plugins/monitoring/common/enums.ts b/x-pack/plugins/monitoring/common/enums.ts index 74711b31756be..d4058e9de801e 100644 --- a/x-pack/plugins/monitoring/common/enums.ts +++ b/x-pack/plugins/monitoring/common/enums.ts @@ -26,3 +26,8 @@ export enum AlertParamType { Duration = 'duration', Percentage = 'percentage', } + +export enum SetupModeFeature { + MetricbeatMigration = 'metricbeatMigration', + Alerts = 'alerts', +} diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index 02963e9457ab5..1d67eebb1705c 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -180,7 +180,7 @@ export const AlertsBadge: React.FC = (props: Props) => { } return ( - + {badges.map((badge, index) => ( {badge} diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index 7754af1be8588..6dcfa6dd043aa 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -26,6 +26,8 @@ import { APM_SYSTEM_ID } from '../../../../common/constants'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { SetupModeBadge } from '../../setup_mode/badge'; import { FormattedMessage } from '@kbn/i18n/react'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; function getColumns(setupMode) { return [ @@ -36,7 +38,7 @@ function getColumns(setupMode) { field: 'name', render: (name, apm) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const status = list[apm.uuid] || {}; const instance = { @@ -129,7 +131,7 @@ export function ApmServerInstances({ apms, setupMode }) { const { pagination, sorting, onTableChange, data } = apms; let setupModeCallout = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallout = ( { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const status = list[beat.uuid] || {}; const instance = { @@ -122,7 +124,7 @@ export class Listing extends PureComponent { const { stats, data, sorting, pagination, onTableChange, setupMode } = this.props; let setupModeCallOut = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallOut = ( getSafeForExternalLink('#/apm/instances'); const setupModeData = get(setupMode.data, 'apm'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; return ( - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js index df0070176727e..3591ad178f4cd 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -25,6 +25,8 @@ import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { BEATS_SYSTEM_ID } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; export function BeatsPanel(props) { const { setupMode } = props; @@ -35,14 +37,15 @@ export function BeatsPanel(props) { } const setupModeData = get(setupMode.data, 'beats'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; const beatTypes = props.beats.types.map((beat, index) => { return [ @@ -142,7 +145,7 @@ export function BeatsPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {beatTypes} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index edf4c5d73f837..34e995510cf72 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -44,6 +44,8 @@ import { } from '../../../../common/constants'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { SetupModeFeature } from '../../../../common/enums'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; const calculateShards = (shards) => { const total = get(shards, 'total', 0); @@ -172,14 +174,15 @@ export function ElasticsearchPanel(props) { const { primaries, replicas } = calculateShards(get(props, 'cluster_stats.indices.shards', {})); const setupModeData = get(setupMode.data, 'elasticsearch'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; const showMlJobs = () => { // if license doesn't support ML, then `ml === null` @@ -367,7 +370,7 @@ export function ElasticsearchPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {nodesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index eb1f82eb5550d..6fa533302db48 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -32,6 +32,8 @@ import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../com import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH]; @@ -50,14 +52,15 @@ export function KibanaPanel(props) { const goToInstances = () => getSafeForExternalLink('#/kibana/instances'); const setupModeData = get(setupMode.data, 'kibana'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; let instancesAlertStatus = null; if (shouldShowAlertBadge(alerts, INSTANCES_PANEL_ALERTS)) { @@ -165,7 +168,7 @@ export function KibanaPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {instancesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index 7c9758bc0ddb6..9b4a50271a247 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -37,6 +37,8 @@ import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH]; @@ -56,14 +58,15 @@ export function LogstashPanel(props) { const goToPipelines = () => getSafeForExternalLink('#/logstash/pipelines'); const setupModeData = get(setupMode.data, 'logstash'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; let nodesAlertStatus = null; if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS)) { @@ -162,7 +165,7 @@ export function LogstashPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {nodesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index b7463fe6532b7..43512f8e528f6 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -32,6 +32,8 @@ import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { AlertsStatus } from '../../../alerts/status'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const getNodeTooltip = (node) => { const { nodeTypeLabel, nodeTypeClass } = node; @@ -85,7 +87,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler ); let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = _.get(setupMode, 'data.byUuid', {}); const status = list[node.resolver] || {}; const instance = { @@ -309,7 +311,11 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear // Merge the nodes data with the setup data if enabled const nodes = props.nodes || []; - if (setupMode.enabled && setupMode.data) { + if ( + setupMode && + setupMode.enabled && + isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration) + ) { // We want to create a seamless experience for the user by merging in the setup data // and the node data from monitoring indices in the likely scenario where some nodes // are using MB collection and some are using no collection @@ -332,7 +338,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear } let setupModeCallout = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallout = ( { const columns = [ @@ -39,7 +41,7 @@ const getColumns = (setupMode, alerts) => { field: 'name', render: (name, kibana) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const uuid = get(kibana, 'kibana.uuid'); const status = list[uuid] || {}; @@ -166,7 +168,7 @@ export class KibanaInstances extends PureComponent { let setupModeCallOut = null; // Merge the instances data with the setup data if enabled const instances = this.props.instances || []; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { // We want to create a seamless experience for the user by merging in the setup data // and the node data from monitoring indices in the likely scenario where some instances // are using MB collection and some are using no collection diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js index caa21e5e69292..4a1137079ebb4 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js @@ -25,6 +25,8 @@ import { SetupModeBadge } from '../../setup_mode/badge'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsStatus } from '../../../alerts/status'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; export class Listing extends PureComponent { getColumns() { @@ -40,7 +42,7 @@ export class Listing extends PureComponent { sortable: true, render: (name, node) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const uuid = get(node, 'logstash.uuid'); const status = list[uuid] || {}; @@ -167,7 +169,7 @@ export class Listing extends PureComponent { })); let setupModeCallOut = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallOut = ( - + diff --git a/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap index 2eaa25803c81e..0d9e50d14657b 100644 --- a/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap +++ b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap @@ -3,6 +3,7 @@ exports[`EnterButton should render properly 1`] = `
= ( } return ( -
+
{tooltip}; + return ( + + {tooltip} + + ); } diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table.js b/x-pack/plugins/monitoring/public/components/table/eui_table.js index cc58d7267f6dc..44ee883c135d2 100644 --- a/x-pack/plugins/monitoring/public/components/table/eui_table.js +++ b/x-pack/plugins/monitoring/public/components/table/eui_table.js @@ -8,6 +8,8 @@ import React, { Fragment } from 'react'; import { EuiInMemoryTable, EuiButton, EuiSpacer, EuiSearchBar } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getIdentifier } from '../setup_mode/formatting'; +import { isSetupModeFeatureEnabled } from '../../lib/setup_mode'; +import { SetupModeFeature } from '../../../common/enums'; export function EuiMonitoringTable({ rows: items, @@ -45,7 +47,7 @@ export function EuiMonitoringTable({ }); let footerContent = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { footerContent = ( diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js index 618547398bd96..9b4b086a0208b 100644 --- a/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js +++ b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js @@ -8,6 +8,8 @@ import React, { Fragment } from 'react'; import { EuiBasicTable, EuiSpacer, EuiSearchBar, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getIdentifier } from '../setup_mode/formatting'; +import { isSetupModeFeatureEnabled } from '../../lib/setup_mode'; +import { SetupModeFeature } from '../../../common/enums'; export function EuiMonitoringSSPTable({ rows: items, @@ -46,7 +48,11 @@ export function EuiMonitoringSSPTable({ }); let footerContent = null; - if (setupMode && setupMode.enabled) { + if ( + setupMode && + setupMode.enabled && + isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration) + ) { footerContent = ( diff --git a/x-pack/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js index eda32cd39c0d0..d682e87b7ca95 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.js +++ b/x-pack/plugins/monitoring/public/directives/main/index.js @@ -11,9 +11,14 @@ import { get } from 'lodash'; import template from './index.html'; import { Legacy } from '../../legacy_shims'; import { shortenPipelineHash } from '../../../common/formatting'; -import { getSetupModeState, initSetupModeState } from '../../lib/setup_mode'; +import { + getSetupModeState, + initSetupModeState, + isSetupModeFeatureEnabled, +} from '../../lib/setup_mode'; import { Subscription } from 'rxjs'; import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; +import { SetupModeFeature } from '../../../common/enums'; const setOptions = (controller) => { if ( @@ -179,7 +184,7 @@ export class MonitoringMainController { isDisabledTab(product) { const setupMode = getSetupModeState(); - if (!setupMode.enabled || !setupMode.data) { + if (!isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { return false; } diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index a36b945e82ef7..b99093a3d8ad1 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from '../legacy_shims'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; +import { SetupModeFeature } from '../../common/enums'; function isOnPage(hash: string) { return includes(window.location.hash, hash); @@ -93,16 +94,12 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; const hasPermissions = get(data, '_meta.hasPermissions', false); - if (Legacy.shims.isCloud || !hasPermissions) { + if (!hasPermissions) { let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { defaultMessage: 'You do not have the necessary permissions to do this.', }); - } else { - text = i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', { - defaultMessage: 'This feature is not available on cloud.', - }); } angularState.scope.$evalAsync(() => { @@ -180,7 +177,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const enabled = !globalState.inSetupMode && !Legacy.shims.isCloud; + const enabled = !globalState.inSetupMode; render( , @@ -212,3 +209,15 @@ export const isInSetupMode = () => { const globalState = $injector.get('globalState'); return globalState.inSetupMode; }; + +export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { + if (!setupModeState.enabled) { + return false; + } + if (feature === SetupModeFeature.MetricbeatMigration) { + if (Legacy.shims.isCloud) { + return false; + } + } + return true; +}; diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index 2f88245d88c4a..a41d4ec4bbfa2 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -11,7 +11,8 @@ import { getPageData } from '../lib/get_page_data'; import { PageLoading } from '../components'; import { Legacy } from '../legacy_shims'; import { PromiseWithCancel } from '../../common/cancel_promise'; -import { updateSetupModeData, getSetupModeState } from '../lib/setup_mode'; +import { SetupModeFeature } from '../../common/enums'; +import { updateSetupModeData, isSetupModeFeatureEnabled } from '../lib/setup_mode'; /** * Given a timezone, this function will calculate the offset in milliseconds @@ -150,11 +151,10 @@ export class MonitoringViewBaseController { } const _api = apiUrlFn ? apiUrlFn() : api; const promises = [_getPageData($injector, _api, this.getPaginationRouteOptions())]; - const setupMode = getSetupModeState(); if (alerts.shouldFetch) { promises.push(fetchAlerts()); } - if (setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { promises.push(updateSetupModeData()); } this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 544f6cc6ec342..aa12e2a34075e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14033,7 +14033,6 @@ "xpack.monitoring.setupMode.node": "ノード", "xpack.monitoring.setupMode.nodes": "ノード", "xpack.monitoring.setupMode.noMonitoringDataFound": "{product} {identifier} が検出されませんでした", - "xpack.monitoring.setupMode.notAvailableCloud": "この機能はクラウドで使用できません。", "xpack.monitoring.setupMode.notAvailablePermissions": "これを実行するために必要な権限がありません。", "xpack.monitoring.setupMode.notAvailableTitle": "設定モードは使用できません", "xpack.monitoring.setupMode.server": "サーバー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2ee85fc87cc72..befce4de3d247 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14038,7 +14038,6 @@ "xpack.monitoring.setupMode.node": "节点", "xpack.monitoring.setupMode.nodes": "节点", "xpack.monitoring.setupMode.noMonitoringDataFound": "未检测到 {product} {identifier}", - "xpack.monitoring.setupMode.notAvailableCloud": "此功能在云上不可用。", "xpack.monitoring.setupMode.notAvailablePermissions": "您没有所需的权限来执行此功能。", "xpack.monitoring.setupMode.notAvailableTitle": "设置模式不可用", "xpack.monitoring.setupMode.server": "服务器", diff --git a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js index d01883e9ea549..3564c72cfc348 100644 --- a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js +++ b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js @@ -8,11 +8,10 @@ export const getLifecycleMethods = (getService, getPageObjects) => { const esArchiver = getService('esArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['monitoring', 'timePicker', 'security']); - const noData = getService('monitoringNoData'); let _archive; return { - async setup(archive, { from, to }) { + async setup(archive, { from, to, useSuperUser = false }) { _archive = archive; const kibanaServer = getService('kibanaServer'); @@ -24,8 +23,7 @@ export const getLifecycleMethods = (getService, getPageObjects) => { await esArchiver.load(archive); await kibanaServer.uiSettings.replace({}); - await PageObjects.monitoring.navigateTo(); - await noData.isOnNoDataPage(); + await PageObjects.monitoring.navigateTo(useSuperUser); // pause autorefresh in the time filter because we don't wait any ticks, // and we don't want ES to log a warning when data gets wiped out diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index c383d8593a4fa..6a2037bbc4924 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -39,5 +39,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./time_filter')); loadTestFile(require.resolve('./enable_monitoring')); + + loadTestFile(require.resolve('./setup/metricbeat_migration')); }); } diff --git a/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js new file mode 100644 index 0000000000000..34ac06450f684 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { getLifecycleMethods } from '../_get_lifecycle_methods'; + +export default function ({ getService, getPageObjects }) { + const setupMode = getService('monitoringSetupMode'); + const PageObjects = getPageObjects(['common', 'console']); + + describe('Setup mode metricbeat migration', function () { + describe('setup mode btn', () => { + const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); + + before(async () => { + await setup('monitoring/setup/collection/es_and_kibana_mb', { + from: 'Apr 9, 2019 @ 00:00:00.741', + to: 'Apr 9, 2019 @ 23:59:59.741', + useSuperUser: true, + }); + }); + + after(async () => { + await tearDown(); + }); + + it('should exist', async () => { + expect(await setupMode.doesSetupModeBtnAppear()).to.be(true); + }); + + it('should be clickable and show the bottom bar', async () => { + await setupMode.clickSetupModeBtn(); + await PageObjects.common.sleep(1000); // bottom drawer animation + expect(await setupMode.doesBottomBarAppear()).to.be(true); + }); + + it('should not show metricbeat migration if cloud', async () => { + const isCloud = await PageObjects.common.isCloud(); + expect(await setupMode.doesMetricbeatMigrationTooltipAppear()).to.be(!isCloud); + }); + + // TODO: this does not work because TLS isn't enabled in the test env + // it('should show alerts all the time', async () => { + // expect(await setupMode.doesAlertsTooltipAppear()).to.be(true); + // }); + }); + }); +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 4b6342758be93..6d8eade25d7e6 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -30,6 +30,7 @@ import { MonitoringKibanaInstancesProvider, MonitoringKibanaInstanceProvider, MonitoringKibanaSummaryStatusProvider, + MonitoringSetupModeProvider, // @ts-ignore not ts yet } from './monitoring'; // @ts-ignore not ts yet @@ -85,6 +86,7 @@ export const services = { monitoringKibanaInstances: MonitoringKibanaInstancesProvider, monitoringKibanaInstance: MonitoringKibanaInstanceProvider, monitoringKibanaSummaryStatus: MonitoringKibanaSummaryStatusProvider, + monitoringSetupMode: MonitoringSetupModeProvider, pipelineList: PipelineListProvider, pipelineEditor: PipelineEditorProvider, random: RandomProvider, diff --git a/x-pack/test/functional/services/monitoring/index.js b/x-pack/test/functional/services/monitoring/index.js index 0087cbaae2b08..0187a3f976837 100644 --- a/x-pack/test/functional/services/monitoring/index.js +++ b/x-pack/test/functional/services/monitoring/index.js @@ -25,3 +25,4 @@ export { MonitoringKibanaOverviewProvider } from './kibana_overview'; export { MonitoringKibanaInstancesProvider } from './kibana_instances'; export { MonitoringKibanaInstanceProvider } from './kibana_instance'; export { MonitoringKibanaSummaryStatusProvider } from './kibana_summary_status'; +export { MonitoringSetupModeProvider } from './setup_mode'; diff --git a/x-pack/test/functional/services/monitoring/setup_mode.js b/x-pack/test/functional/services/monitoring/setup_mode.js new file mode 100644 index 0000000000000..a71ad924a852f --- /dev/null +++ b/x-pack/test/functional/services/monitoring/setup_mode.js @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function MonitoringSetupModeProvider({ getService }) { + const testSubjects = getService('testSubjects'); + + const SUBJ_SETUP_MODE_BTN = 'monitoringSetupModeBtn'; + const SUBJ_SETUP_MODE_BOTTOM_BAR = 'monitoringSetupModeBottomBar'; + const SUBJ_SETUP_MODE_METRICBEAT_MIGRATION_TOOLTIP = + 'monitoringSetupModeMetricbeatMigrationTooltip'; + const SUBJ_SETUP_MODE_ALERTS_BADGE = 'monitoringSetupModeAlertBadges'; + + return new (class SetupMode { + async doesSetupModeBtnAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_BTN); + } + + async clickSetupModeBtn() { + return await testSubjects.click(SUBJ_SETUP_MODE_BTN); + } + + async doesBottomBarAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_BOTTOM_BAR); + } + + async doesMetricbeatMigrationTooltipAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_METRICBEAT_MIGRATION_TOOLTIP); + } + + async doesAlertsTooltipAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_ALERTS_BADGE); + } + })(); +} From b7a8a4a54ada7cd16d03d23064c9d378cb292c6a Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 4 Aug 2020 14:19:26 -0400 Subject: [PATCH 100/115] [CI] Fix baseline_capture job by adding parallel process number back --- .ci/Jenkinsfile_baseline_capture | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index b0d3591821642..9a49c19b94df2 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -7,18 +7,22 @@ kibanaPipeline(timeoutMinutes: 120) { githubCommitStatus.trackBuild(params.commit, 'kibana-ci-baseline') { ciStats.trackBuild { catchError { - parallel([ - 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) - } - }, - 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) - } - }, - ]) + withEnv([ + 'CI_PARALLEL_PROCESS_NUMBER=1' + ]) { + parallel([ + 'oss-visualRegression': { + workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { + kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')() + } + }, + 'xpack-visualRegression': { + workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { + kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')() + } + }, + ]) + } } kibanaPipeline.sendMail() From ea4d36f71375bf78a0313ee3adf6b426d3690fc0 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 4 Aug 2020 15:27:04 -0400 Subject: [PATCH 101/115] [CI] Fix xpack kibana build dir in xpack visual regression script --- test/scripts/jenkins_xpack_visual_regression.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index 7fb7d7b71b2e4..55d4a524820c5 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -7,19 +7,19 @@ echo " -> building and extracting default Kibana distributable" cd "$KIBANA_DIR" node scripts/build --debug --no-oss linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" -installDir="$PARENT_DIR/install/kibana" +installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 +mkdir -p "$WORKSPACE/kibana-build-xpack" +cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ + # cd "$KIBANA_DIR" # source "test/scripts/jenkins_xpack_page_load_metrics.sh" cd "$KIBANA_DIR" source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" -cd "$KIBANA_DIR" -source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" - echo " -> running visual regression tests from x-pack directory" cd "$XPACK_DIR" yarn percy exec -t 10000 -- -- \ From 8e0e228dbf281449ff17e56f3d2cc894d73ce2f3 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 4 Aug 2020 20:35:00 +0100 Subject: [PATCH 102/115] [Security Solution] Keep original note creator (#74203) * keep original note creator * unit test * fix types * typo * wording --- .../server/graphql/note/resolvers.ts | 14 ++- .../server/lib/note/saved_object.ts | 12 ++- .../timeline/routes/create_timelines_route.ts | 3 +- .../routes/import_timelines_route.test.ts | 98 ++++++++++++++++++- .../timeline/routes/utils/create_timelines.ts | 60 +++++++++--- .../timeline/routes/utils/failure_cases.ts | 1 + .../timeline/routes/utils/import_timelines.ts | 8 +- 7 files changed, 171 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts index 5f816b9ada54e..10faa362363ad 100644 --- a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts @@ -81,10 +81,16 @@ export const createNoteResolvers = ( return true; }, async persistNote(root, args, { req }) { - return libs.note.persistNote(req, args.noteId || null, args.version || null, { - ...args.note, - timelineId: args.note.timelineId || null, - }); + return libs.note.persistNote( + req, + args.noteId || null, + args.version || null, + { + ...args.note, + timelineId: args.note.timelineId || null, + }, + true + ); }, }, }); diff --git a/x-pack/plugins/security_solution/server/lib/note/saved_object.ts b/x-pack/plugins/security_solution/server/lib/note/saved_object.ts index bf6090f0337f7..0b043d4e2fdd0 100644 --- a/x-pack/plugins/security_solution/server/lib/note/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/note/saved_object.ts @@ -49,7 +49,8 @@ export interface Note { request: FrameworkRequest, noteId: string | null, version: string | null, - note: SavedNote + note: SavedNote, + overrideOwner: boolean ) => Promise; convertSavedObjectToSavedNote: ( savedObject: unknown, @@ -136,7 +137,8 @@ export const persistNote = async ( request: FrameworkRequest, noteId: string | null, version: string | null, - note: SavedNote + note: SavedNote, + overrideOwner: boolean = true ): Promise => { try { const savedObjectsClient = request.context.core.savedObjects.client; @@ -163,14 +165,14 @@ export const persistNote = async ( note: convertSavedObjectToSavedNote( await savedObjectsClient.create( noteSavedObjectType, - pickSavedNote(noteId, note, request.user) + overrideOwner ? pickSavedNote(noteId, note, request.user) : note ), timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined ), }; } - // Update new note + // Update existing note const existingNote = await getSavedNote(request, noteId); return { @@ -180,7 +182,7 @@ export const persistNote = async ( await savedObjectsClient.update( noteSavedObjectType, noteId, - pickSavedNote(noteId, note, request.user), + overrideOwner ? pickSavedNote(noteId, note, request.user) : note, { version: existingNote.version || undefined, } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts index 5bc4bec45dfb2..7abcb390d0221 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts @@ -20,6 +20,7 @@ import { TimelineStatusActions, } from './utils/common'; import { createTimelines } from './utils/create_timelines'; +import { DEFAULT_ERROR } from './utils/failure_cases'; export const createTimelinesRoute = ( router: IRouter, @@ -85,7 +86,7 @@ export const createTimelinesRoute = ( return siemResponse.error( compareTimelinesStatus.checkIsFailureCases(TimelineStatusActions.create) || { statusCode: 405, - body: 'update timeline error', + body: DEFAULT_ERROR, } ); } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts index b817896e901c1..2ad6c5d6fff60 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts @@ -46,6 +46,7 @@ describe('import timelines', () => { let mockPersistTimeline: jest.Mock; let mockPersistPinnedEventOnTimeline: jest.Mock; let mockPersistNote: jest.Mock; + let mockGetNote: jest.Mock; let mockGetTupleDuplicateErrorsAndUniqueTimeline: jest.Mock; beforeEach(() => { @@ -69,6 +70,7 @@ describe('import timelines', () => { mockPersistTimeline = jest.fn(); mockPersistPinnedEventOnTimeline = jest.fn(); mockPersistNote = jest.fn(); + mockGetNote = jest.fn(); mockGetTupleDuplicateErrorsAndUniqueTimeline = jest.fn(); jest.doMock('../create_timelines_stream_from_ndjson', () => { @@ -113,6 +115,37 @@ describe('import timelines', () => { jest.doMock('../../note/saved_object', () => { return { persistNote: mockPersistNote, + getNote: mockGetNote + .mockResolvedValueOnce({ + noteId: 'd2649d40-6bc5-11ea-86f0-5db0048c6086', + version: 'WzExNjQsMV0=', + eventId: undefined, + note: 'original note', + created: '1584830796960', + createdBy: 'original author A', + updated: '1584830796960', + updatedBy: 'original author A', + }) + .mockResolvedValueOnce({ + noteId: '73ac2370-6bc2-11ea-a90b-f5341fb7a189', + version: 'WzExMjgsMV0=', + eventId: 'ZaAi8nAB5OldxqFfdhke', + note: 'original event note', + created: '1584830796960', + createdBy: 'original author B', + updated: '1584830796960', + updatedBy: 'original author B', + }) + .mockResolvedValue({ + noteId: 'f7b71620-6bc2-11ea-a0b6-33c7b2a78885', + version: 'WzExMzUsMV0=', + eventId: 'ZaAi8nAB5OldxqFfdhke', + note: 'event note2', + created: '1584830796960', + createdBy: 'angela', + updated: '1584830796960', + updatedBy: 'angela', + }), }; }); @@ -213,6 +246,14 @@ describe('import timelines', () => { ); }); + test('should Check if note exists', async () => { + const mockRequest = getImportTimelinesRequest(); + await server.inject(mockRequest, context); + expect(mockGetNote.mock.calls[0][1]).toEqual( + mockUniqueParsedObjects[0].globalNotes[0].noteId + ); + }); + test('should Create notes', async () => { const mockRequest = getImportTimelinesRequest(); await server.inject(mockRequest, context); @@ -237,20 +278,67 @@ describe('import timelines', () => { expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTimeline.version); }); - test('should provide new notes when Creating notes for a timeline', async () => { + test('should provide new notes with original author info when Creating notes for a timeline', async () => { + const mockRequest = getImportTimelinesRequest(); + await server.inject(mockRequest, context); + expect(mockPersistNote.mock.calls[0][3]).toEqual({ + eventId: undefined, + note: 'original note', + created: '1584830796960', + createdBy: 'original author A', + updated: '1584830796960', + updatedBy: 'original author A', + timelineId: mockCreatedTimeline.savedObjectId, + }); + expect(mockPersistNote.mock.calls[1][3]).toEqual({ + eventId: mockUniqueParsedObjects[0].eventNotes[0].eventId, + note: 'original event note', + created: '1584830796960', + createdBy: 'original author B', + updated: '1584830796960', + updatedBy: 'original author B', + timelineId: mockCreatedTimeline.savedObjectId, + }); + expect(mockPersistNote.mock.calls[2][3]).toEqual({ + eventId: mockUniqueParsedObjects[0].eventNotes[1].eventId, + note: 'event note2', + created: '1584830796960', + createdBy: 'angela', + updated: '1584830796960', + updatedBy: 'angela', + timelineId: mockCreatedTimeline.savedObjectId, + }); + }); + + test('should keep current author if note does not exist when Creating notes for a timeline', async () => { + mockGetNote.mockReset(); + mockGetNote.mockRejectedValue(new Error()); + const mockRequest = getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ + created: mockUniqueParsedObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedObjects[0].globalNotes[0].updatedBy, eventId: undefined, note: mockUniqueParsedObjects[0].globalNotes[0].note, timelineId: mockCreatedTimeline.savedObjectId, }); expect(mockPersistNote.mock.calls[1][3]).toEqual({ + created: mockUniqueParsedObjects[0].eventNotes[0].created, + createdBy: mockUniqueParsedObjects[0].eventNotes[0].createdBy, + updated: mockUniqueParsedObjects[0].eventNotes[0].updated, + updatedBy: mockUniqueParsedObjects[0].eventNotes[0].updatedBy, eventId: mockUniqueParsedObjects[0].eventNotes[0].eventId, note: mockUniqueParsedObjects[0].eventNotes[0].note, timelineId: mockCreatedTimeline.savedObjectId, }); expect(mockPersistNote.mock.calls[2][3]).toEqual({ + created: mockUniqueParsedObjects[0].eventNotes[1].created, + createdBy: mockUniqueParsedObjects[0].eventNotes[1].createdBy, + updated: mockUniqueParsedObjects[0].eventNotes[1].updated, + updatedBy: mockUniqueParsedObjects[0].eventNotes[1].updatedBy, eventId: mockUniqueParsedObjects[0].eventNotes[1].eventId, note: mockUniqueParsedObjects[0].eventNotes[1].note, timelineId: mockCreatedTimeline.savedObjectId, @@ -573,6 +661,10 @@ describe('import timeline templates', () => { expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note, + created: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updatedBy, timelineId: mockCreatedTemplateTimeline.savedObjectId, }); }); @@ -721,6 +813,10 @@ describe('import timeline templates', () => { expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note, + created: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updatedBy, timelineId: mockCreatedTemplateTimeline.savedObjectId, }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts index cdedffbbd9458..6bdecb5d80ecc 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts @@ -45,26 +45,56 @@ export const savePinnedEvents = ( ) ); -export const saveNotes = ( +const getNewNote = async ( frameworkRequest: FrameworkRequest, + note: NoteResult, timelineSavedObjectId: string, - timelineVersion?: string | null, - existingNoteIds?: string[], - newNotes?: NoteResult[] -) => { - return Promise.all( - newNotes?.map((note) => { - const newNote: SavedNote = { + overrideOwner: boolean +): Promise => { + let savedNote = note; + try { + savedNote = await noteLib.getNote(frameworkRequest, note.noteId); + // eslint-disable-next-line no-empty + } catch (e) {} + return overrideOwner + ? { eventId: note.eventId, note: note.note, timelineId: timelineSavedObjectId, + } + : { + eventId: savedNote.eventId, + note: savedNote.note, + created: savedNote.created, + createdBy: savedNote.createdBy, + updated: savedNote.updated, + updatedBy: savedNote.updatedBy, + timelineId: timelineSavedObjectId, }; +}; +export const saveNotes = async ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + timelineVersion?: string | null, + existingNoteIds?: string[], + newNotes?: NoteResult[], + overrideOwner: boolean = true +) => { + return Promise.all( + newNotes?.map(async (note) => { + const newNote = await getNewNote( + frameworkRequest, + note, + timelineSavedObjectId, + overrideOwner + ); return noteLib.persistNote( frameworkRequest, - existingNoteIds?.find((nId) => nId === note.noteId) ?? null, + overrideOwner ? existingNoteIds?.find((nId) => nId === note.noteId) ?? null : null, timelineVersion ?? null, - newNote + newNote, + overrideOwner ); }) ?? [] ); @@ -75,12 +105,18 @@ interface CreateTimelineProps { timeline: SavedTimeline; timelineSavedObjectId?: string | null; timelineVersion?: string | null; + overrideNotesOwner?: boolean; pinnedEventIds?: string[] | null; notes?: NoteResult[]; existingNoteIds?: string[]; isImmutable?: boolean; } +/** allow overrideNotesOwner means overriding by current username, + * disallow overrideNotesOwner means keep the original username. + * overrideNotesOwner = false only happens when import timeline templates, + * as we want to keep the original creator for notes + **/ export const createTimelines = async ({ frameworkRequest, timeline, @@ -90,6 +126,7 @@ export const createTimelines = async ({ notes = [], existingNoteIds = [], isImmutable, + overrideNotesOwner = true, }: CreateTimelineProps): Promise => { const responseTimeline = await saveTimelines( frameworkRequest, @@ -119,7 +156,8 @@ export const createTimelines = async ({ timelineSavedObjectId ?? newTimelineSavedObjectId, newTimelineVersion, existingNoteIds, - notes + notes, + overrideNotesOwner ), ]; } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts index b926819d66c92..e358ad9dbb57d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts @@ -37,6 +37,7 @@ export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = 'You cannot convert a Timeline template to a timeline, or a timeline to a Timeline template.'; export const UPDAT_TIMELINE_VIA_IMPORT_NOT_ALLOWED_ERROR_MESSAGE = 'You cannot update a timeline via imports. Use the UI to modify existing timelines.'; +export const DEFAULT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; const isUpdatingStatus = ( isHandlingTemplateTimeline: boolean, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts index 1fea11f01bcc5..f62f02cc7bba9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts @@ -26,6 +26,7 @@ import { createPromiseFromStreams } from '../../../../../../../../src/legacy/uti import { getTupleDuplicateErrorsAndUniqueTimeline } from './get_timelines_from_stream'; import { CompareTimelinesStatus } from './compare_timelines_status'; import { TimelineStatusActions } from './common'; +import { DEFAULT_ERROR } from './failure_cases'; export type ImportedTimeline = SavedTimeline & { savedObjectId: string | null; @@ -96,7 +97,6 @@ export const setTimeline = ( }; const CHUNK_PARSED_OBJECT_SIZE = 10; -const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; export const importTimelines = async ( file: Readable, @@ -173,6 +173,7 @@ export const importTimelines = async ( pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], isImmutable, + overrideNotesOwner: false, }); resolve({ @@ -186,7 +187,7 @@ export const importTimelines = async ( const errorMessage = compareTimelinesStatus.checkIsFailureCases( TimelineStatusActions.createViaImport ); - const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; + const message = errorMessage?.body ?? DEFAULT_ERROR; resolve( createBulkErrorObject({ @@ -206,6 +207,7 @@ export const importTimelines = async ( notes: globalNotes, existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, isImmutable, + overrideNotesOwner: false, }); resolve({ @@ -218,7 +220,7 @@ export const importTimelines = async ( TimelineStatusActions.updateViaImport ); - const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; + const message = errorMessage?.body ?? DEFAULT_ERROR; resolve( createBulkErrorObject({ From 54e13ad6f69dfcfa674ab297a4a2fcad7c328e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Tue, 4 Aug 2020 15:50:17 -0400 Subject: [PATCH 103/115] Make the actions plugin support generics (#71439) * Initial attempt at making the actions plugin support generics * Export WebhookMethods * Fix typings for registry * Usage of Record * Apply feedback from Gidi * Cleanup * Fix validate_with_schema * Cleanup pt2 * Fix failing tests * Add generics to ActionType for ActionTypeExecutorResult Co-authored-by: Elastic Machine --- .../server/action_type_registry.test.ts | 10 +- .../actions/server/action_type_registry.ts | 26 +++- .../actions/server/actions_client.test.ts | 2 +- .../plugins/actions/server/actions_client.ts | 2 +- .../server/builtin_action_types/case/utils.ts | 18 ++- .../server/builtin_action_types/email.test.ts | 17 ++- .../server/builtin_action_types/email.ts | 29 +++-- .../builtin_action_types/es_index.test.ts | 22 ++-- .../server/builtin_action_types/es_index.ts | 17 ++- .../builtin_action_types/pagerduty.test.ts | 51 +++++--- .../server/builtin_action_types/pagerduty.ts | 24 +++- .../builtin_action_types/server_log.test.ts | 15 ++- .../server/builtin_action_types/server_log.ts | 15 ++- .../builtin_action_types/servicenow/index.ts | 31 +++-- .../server/builtin_action_types/slack.test.ts | 19 ++- .../server/builtin_action_types/slack.ts | 32 +++-- .../builtin_action_types/webhook.test.ts | 52 +++++--- .../server/builtin_action_types/webhook.ts | 43 ++++--- .../actions/server/lib/action_executor.ts | 6 +- .../actions/server/lib/license_state.test.ts | 8 +- .../actions/server/lib/task_runner_factory.ts | 2 +- .../server/lib/validate_with_schema.test.ts | 2 +- .../server/lib/validate_with_schema.ts | 32 ++++- x-pack/plugins/actions/server/plugin.test.ts | 4 +- x-pack/plugins/actions/server/plugin.ts | 25 +++- .../actions/server/routes/execute.test.ts | 4 +- .../plugins/actions/server/routes/execute.ts | 2 +- x-pack/plugins/actions/server/types.ts | 67 +++++----- .../public/cases/containers/api.ts | 19 ++- .../plugins/alerts/server/action_types.ts | 117 +++++++++++------- 30 files changed, 466 insertions(+), 247 deletions(-) diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index ce5c1fe8500fb..b25e33400df5d 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -41,7 +41,7 @@ beforeEach(() => { }; }); -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; @@ -203,7 +203,9 @@ describe('isActionTypeEnabled', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'basic', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { @@ -258,7 +260,9 @@ describe('ensureActionTypeEnabled', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'basic', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 1f7409fedd2c2..4015381ff9502 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -8,9 +8,15 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; -import { ActionType, PreConfiguredAction } from './types'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; +import { + ActionType, + PreConfiguredAction, + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, +} from './types'; export interface ActionTypeRegistryOpts { taskManager: TaskManagerSetupContract; @@ -77,7 +83,12 @@ export class ActionTypeRegistry { /** * Registers an action type to the action type registry */ - public register(actionType: ActionType) { + public register< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void + >(actionType: ActionType) { if (this.has(actionType.id)) { throw new Error( i18n.translate( @@ -91,7 +102,7 @@ export class ActionTypeRegistry { ) ); } - this.actionTypes.set(actionType.id, { ...actionType }); + this.actionTypes.set(actionType.id, { ...actionType } as ActionType); this.taskManager.registerTaskDefinitions({ [`actions:${actionType.id}`]: { title: actionType.name, @@ -112,7 +123,12 @@ export class ActionTypeRegistry { /** * Returns an action type, throws if not registered */ - public get(id: string): ActionType { + public get< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void + >(id: string): ActionType { if (!this.has(id)) { throw Boom.badRequest( i18n.translate('xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage', { @@ -123,7 +139,7 @@ export class ActionTypeRegistry { }) ); } - return this.actionTypes.get(id)!; + return this.actionTypes.get(id)! as ActionType; } /** diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 90b989ac3b52e..16a5a59882dd6 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -39,7 +39,7 @@ let actionsClient: ActionsClient; let mockedLicenseState: jest.Mocked; let actionTypeRegistry: ActionTypeRegistry; let actionTypeRegistryParams: ActionTypeRegistryOpts; -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 6744a8d111623..d46ad3e2e2423 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -298,7 +298,7 @@ export class ActionsClient { public async execute({ actionId, params, - }: Omit): Promise { + }: Omit): Promise> { await this.authorization.ensureAuthorized('execute'); return this.actionExecutor.execute({ actionId, params, request: this.request }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 676a4776d0055..82dedb09c429e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -10,6 +10,10 @@ import { schema } from '@kbn/config-schema'; import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; import { ExecutorParamsSchema } from './schema'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from './types'; import { CreateExternalServiceArgs, @@ -23,6 +27,7 @@ import { TransformFieldsArgs, Comment, ExecutorSubActionPushParams, + PushToServiceResponse, } from './types'; import { transformers } from './transformers'; @@ -63,14 +68,17 @@ export const createConnectorExecutor = ({ api, createExternalService, }: CreateExternalServiceBasicArgs) => async ( - execOptions: ActionTypeExecutorOptions -): Promise => { + execOptions: ActionTypeExecutorOptions< + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, + ExecutorParams + > +): Promise> => { const { actionId, config, params, secrets } = execOptions; - const { subAction, subActionParams } = params as ExecutorParams; + const { subAction, subActionParams } = params; let data = {}; - const res: Pick & - Pick = { + const res: ActionTypeExecutorResult = { status: 'ok', actionId, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 1a24622e1cabb..195f6db538ae5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -10,7 +10,6 @@ jest.mock('./lib/send_email', () => ({ import { Logger } from '../../../../../src/core/server'; -import { ActionType, ActionTypeExecutorOptions } from '../types'; import { actionsConfigMock } from '../actions_config.mock'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; @@ -21,6 +20,8 @@ import { ActionTypeConfigType, ActionTypeSecretsType, getActionType, + EmailActionType, + EmailActionTypeExecutorOptions, } from './email'; const sendEmailMock = sendEmail as jest.Mock; @@ -29,13 +30,17 @@ const ACTION_TYPE_ID = '.email'; const services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: EmailActionType; let mockedLogger: jest.Mocked; beforeEach(() => { jest.resetAllMocks(); const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); }); describe('actionTypeRegistry.get() works', () => { @@ -242,7 +247,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, @@ -306,7 +311,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, @@ -363,7 +368,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 7ddb123a4d780..a51a0432a01e0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -15,6 +15,18 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +export type EmailActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -30,10 +42,9 @@ const ConfigSchema = schema.object(ConfigSchemaProps); function validateConfig( configurationUtilities: ActionsConfigurationUtilities, - configObject: unknown + configObject: ActionTypeConfigType ): string | void { - // avoids circular reference ... - const config = configObject as ActionTypeConfigType; + const config = configObject; // Make sure service is set, or if not, both host/port must be set. // If service is set, host/port are ignored, when the email is sent. @@ -113,7 +124,7 @@ interface GetActionTypeParams { } // action type definition -export function getActionType(params: GetActionTypeParams): ActionType { +export function getActionType(params: GetActionTypeParams): EmailActionType { const { logger, configurationUtilities } = params; return { id: '.email', @@ -136,12 +147,12 @@ export function getActionType(params: GetActionTypeParams): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: EmailActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const secrets = execOptions.secrets; + const params = execOptions.params; const transport: Transport = {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index be60f4c2f28af..7a0e24521a1c6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -8,21 +8,25 @@ jest.mock('./lib/send_email', () => ({ sendEmail: jest.fn(), })); -import { ActionType, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; -import { ActionParamsType, ActionTypeConfigType } from './es_index'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ActionTypeConfigType, + ESIndexActionType, + ESIndexActionTypeExecutorOptions, +} from './es_index'; const ACTION_TYPE_ID = '.index'; const services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: ESIndexActionType; beforeAll(() => { const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get(ACTION_TYPE_ID); }); beforeEach(() => { @@ -144,12 +148,12 @@ describe('params validation', () => { describe('execute()', () => { test('ensure parameters are as expected', async () => { const secrets = {}; - let config: Partial; + let config: ActionTypeConfigType; let params: ActionParamsType; - let executorOptions: ActionTypeExecutorOptions; + let executorOptions: ESIndexActionTypeExecutorOptions; // minimal params - config = { index: 'index-value', refresh: false }; + config = { index: 'index-value', refresh: false, executionTimeField: null }; params = { documents: [{ jim: 'bob' }], }; @@ -215,7 +219,7 @@ describe('execute()', () => { `); // minimal params - config = { index: 'index-value', executionTimeField: undefined, refresh: false }; + config = { index: 'index-value', executionTimeField: null, refresh: false }; params = { documents: [{ jim: 'bob' }], }; @@ -245,7 +249,7 @@ describe('execute()', () => { `); // multiple documents - config = { index: 'index-value', executionTimeField: undefined, refresh: false }; + config = { index: 'index-value', executionTimeField: null, refresh: false }; params = { documents: [{ a: 1 }, { b: 2 }], }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 899684367d52d..53bf75651b1e5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -11,6 +11,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; +export type ESIndexActionType = ActionType; +export type ESIndexActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + {}, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -33,7 +40,7 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType({ logger }: { logger: Logger }): ActionType { +export function getActionType({ logger }: { logger: Logger }): ESIndexActionType { return { id: '.index', minimumLicenseRequired: 'basic', @@ -52,11 +59,11 @@ export function getActionType({ logger }: { logger: Logger }): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ESIndexActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const params = execOptions.params; const services = execOptions.services; const index = config.index; diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index b1ed3728edfae..c379c05ee88e3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -8,14 +8,21 @@ jest.mock('./lib/post_pagerduty', () => ({ postPagerduty: jest.fn(), })); -import { getActionType } from './pagerduty'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; +import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { postPagerduty } from './lib/post_pagerduty'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ActionTypeConfigType, + ActionTypeSecretsType, + getActionType, + PagerDutyActionType, + PagerDutyActionTypeExecutorOptions, +} from './pagerduty'; const postPagerdutyMock = postPagerduty as jest.Mock; @@ -23,12 +30,16 @@ const ACTION_TYPE_ID = '.pagerduty'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: PagerDutyActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); mockedLogger = logger; }); @@ -167,7 +178,7 @@ describe('execute()', () => { test('should succeed with minimal valid params', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -175,7 +186,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -219,7 +230,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'trigger', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -236,7 +247,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -284,7 +295,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'acknowledge', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -301,7 +312,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -340,7 +351,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'resolve', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -357,7 +368,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -390,7 +401,7 @@ describe('execute()', () => { test('should fail when sendPagerdury throws', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -398,7 +409,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -418,7 +429,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 429', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -426,7 +437,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -446,7 +457,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 501', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -454,7 +465,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -474,7 +485,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 418', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -482,7 +493,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 0c8802060164d..b76e57419bc56 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -16,6 +16,18 @@ import { ActionsConfigurationUtilities } from '../actions_config'; // https://v2.developer.pagerduty.com/docs/events-api-v2 const PAGER_DUTY_API_URL = 'https://events.pagerduty.com/v2/enqueue'; +export type PagerDutyActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type PagerDutyActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -100,7 +112,7 @@ export function getActionType({ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { +}): PagerDutyActionType { return { id: '.pagerduty', minimumLicenseRequired: 'gold', @@ -142,12 +154,12 @@ function getPagerDutyApiUrl(config: ActionTypeConfigType): string { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: PagerDutyActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const secrets = execOptions.secrets; + const params = execOptions.params; const services = execOptions.services; const apiUrl = getPagerDutyApiUrl(config); diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index d5a9c0cc1ccd2..e4828f33765ce 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -4,20 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../types'; import { validateParams } from '../lib'; import { Logger } from '../../../../../src/core/server'; import { createActionTypeRegistry } from './index.test'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ServerLogActionType, + ServerLogActionTypeExecutorOptions, +} from './server_log'; const ACTION_TYPE_ID = '.server-log'; -let actionType: ActionType; +let actionType: ServerLogActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get<{}, {}, ActionParamsType>(ACTION_TYPE_ID); mockedLogger = logger; expect(actionType).toBeTruthy(); }); @@ -88,13 +92,14 @@ describe('validateParams()', () => { describe('execute()', () => { test('calls the executor with proper params', async () => { const actionId = 'some-id'; - await actionType.executor({ + const executorOptions: ServerLogActionTypeExecutorOptions = { actionId, services: actionsMock.createServices(), params: { message: 'message text here', level: 'info' }, config: {}, secrets: {}, - }); + }; + await actionType.executor(executorOptions); expect(mockedLogger.info).toHaveBeenCalledWith('Server log: message text here'); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index bf8a3d8032cc5..490764fb16bfd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -12,6 +12,13 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { withoutControlCharacters } from './lib/string_utils'; +export type ServerLogActionType = ActionType<{}, {}, ActionParamsType>; +export type ServerLogActionTypeExecutorOptions = ActionTypeExecutorOptions< + {}, + {}, + ActionParamsType +>; + // params definition export type ActionParamsType = TypeOf; @@ -32,7 +39,7 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType({ logger }: { logger: Logger }): ActionType { +export function getActionType({ logger }: { logger: Logger }): ServerLogActionType { return { id: '.server-log', minimumLicenseRequired: 'basic', @@ -50,10 +57,10 @@ export function getActionType({ logger }: { logger: Logger }): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ServerLogActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const params = execOptions.params as ActionParamsType; + const params = execOptions.params; const sanitizedMessage = withoutControlCharacters(params.message); try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index e62ca465f30f8..109008b8fc9fb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -17,9 +17,14 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; import { createExternalService } from './service'; import { api } from './api'; -import { ExecutorParams, ExecutorSubActionPushParams } from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; +import { + ExecutorParams, + ExecutorSubActionPushParams, + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, +} from './types'; // TODO: to remove, need to support Case import { buildMap, mapParams } from '../case/utils'; @@ -31,7 +36,14 @@ interface GetActionTypeParams { } // action type definition -export function getActionType(params: GetActionTypeParams): ActionType { +export function getActionType( + params: GetActionTypeParams +): ActionType< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse | {} +> { const { logger, configurationUtilities } = params; return { id: '.servicenow', @@ -54,10 +66,14 @@ export function getActionType(params: GetActionTypeParams): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ActionTypeExecutorOptions< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams + > +): Promise> { const { actionId, config, params, secrets } = execOptions; - const { subAction, subActionParams } = params as ExecutorParams; + const { subAction, subActionParams } = params; let data: PushToServiceResponse | null = null; const externalService = createExternalService({ @@ -81,9 +97,8 @@ async function executor( const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; const { comments, externalId, ...restParams } = pushToServiceParams; - const mapping = config.incidentConfiguration - ? buildMap(config.incidentConfiguration.mapping) - : null; + const incidentConfiguration = config.incidentConfiguration; + const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null; const externalObject = config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index d1a739c2304f2..6d4176067c3ba 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,14 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ActionType, - Services, - ActionTypeExecutorOptions, - ActionTypeExecutorResult, -} from '../types'; +import { Services, ActionTypeExecutorResult } from '../types'; import { validateParams, validateSecrets } from '../lib'; -import { getActionType } from './slack'; +import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; @@ -19,11 +14,13 @@ const ACTION_TYPE_ID = '.slack'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: SlackActionType; beforeAll(() => { actionType = getActionType({ - async executor() {}, + async executor(options) { + return { status: 'ok', actionId: options.actionId }; + }, configurationUtilities: actionsConfigMock.create(), }); }); @@ -119,7 +116,7 @@ describe('validateActionTypeSecrets()', () => { describe('execute()', () => { beforeAll(() => { - async function mockSlackExecutor(options: ActionTypeExecutorOptions) { + async function mockSlackExecutor(options: SlackActionTypeExecutorOptions) { const { params } = options; const { message } = params; if (message == null) throw new Error('message property required in parameter'); @@ -134,7 +131,7 @@ describe('execute()', () => { text: `slack mockExecutor success: ${message}`, actionId: '', status: 'ok', - } as ActionTypeExecutorResult; + } as ActionTypeExecutorResult; } actionType = getActionType({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 55c373f14cd69..209582585256b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -21,6 +21,13 @@ import { } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; +export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions< + {}, + ActionTypeSecretsType, + ActionParamsType +>; + // secrets definition export type ActionTypeSecretsType = TypeOf; @@ -46,8 +53,8 @@ export function getActionType({ executor = slackExecutor, }: { configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; -}): ActionType { + executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; +}): SlackActionType { return { id: '.slack', minimumLicenseRequired: 'gold', @@ -92,11 +99,11 @@ function valdiateActionTypeConfig( // action executor async function slackExecutor( - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: SlackActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const secrets = execOptions.secrets; + const params = execOptions.params; let result: IncomingWebhookResult; const { webhookUrl } = secrets; @@ -156,18 +163,21 @@ async function slackExecutor( return successResult(actionId, result); } -function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } -function errorResult(actionId: string, message: string): ActionTypeExecutorResult { +function errorResult(actionId: string, message: string): ActionTypeExecutorResult { return { status: 'error', message, actionId, }; } -function serviceErrorResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function serviceErrorResult( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.slack.errorPostingErrorMessage', { defaultMessage: 'error posting slack message', }); @@ -179,7 +189,7 @@ function serviceErrorResult(actionId: string, serviceMessage: string): ActionTyp }; } -function retryResult(actionId: string, message: string): ActionTypeExecutorResult { +function retryResult(actionId: string, message: string): ActionTypeExecutorResult { const errMessage = i18n.translate( 'xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage', { @@ -198,7 +208,7 @@ function retryResultSeconds( actionId: string, message: string, retryAfter: number -): ActionTypeExecutorResult { +): ActionTypeExecutorResult { const retryEpoch = Date.now() + retryAfter * 1000; const retry = new Date(retryEpoch); const retryString = retry.toISOString(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 53b17f58d6e18..26dd8a1a1402a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -8,14 +8,21 @@ jest.mock('axios', () => ({ request: jest.fn(), })); -import { getActionType } from './webhook'; -import { ActionType, Services } from '../types'; +import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { actionsConfigMock } from '../actions_config.mock'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import { actionsMock } from '../mocks'; import axios from 'axios'; +import { + ActionParamsType, + ActionTypeConfigType, + ActionTypeSecretsType, + getActionType, + WebhookActionType, + WebhookMethods, +} from './webhook'; const axiosRequestMock = axios.request as jest.Mock; @@ -23,12 +30,16 @@ const ACTION_TYPE_ID = '.webhook'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: WebhookActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); mockedLogger = logger; }); @@ -235,16 +246,17 @@ describe('execute()', () => { }); test('execute with username/password sends request with basic auth', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + }; await actionType.executor({ actionId: 'some-id', services, - config: { - url: 'https://abc.def/my-webhook', - method: 'post', - headers: { - aheader: 'a value', - }, - }, + config, secrets: { user: 'abc', password: '123' }, params: { body: 'some data' }, }); @@ -266,17 +278,19 @@ describe('execute()', () => { }); test('execute without username/password sends request without basic auth', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + }; + const secrets: ActionTypeSecretsType = { user: null, password: null }; await actionType.executor({ actionId: 'some-id', services, - config: { - url: 'https://abc.def/my-webhook', - method: 'post', - headers: { - aheader: 'a value', - }, - }, - secrets: {}, + config, + secrets, params: { body: 'some data' }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 0b8b27b278928..be75742fa882e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -17,11 +17,23 @@ import { ActionsConfigurationUtilities } from '../actions_config'; import { Logger } from '../../../../../src/core/server'; // config definition -enum WebhookMethods { +export enum WebhookMethods { POST = 'post', PUT = 'put', } +export type WebhookActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type WebhookActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + const HeadersSchema = schema.recordOf(schema.string(), schema.string()); const configSchemaProps = { url: schema.string(), @@ -31,7 +43,7 @@ const configSchemaProps = { headers: nullableType(HeadersSchema), }; const ConfigSchema = schema.object(configSchemaProps); -type ActionTypeConfigType = TypeOf; +export type ActionTypeConfigType = TypeOf; // secrets definition export type ActionTypeSecretsType = TypeOf; @@ -51,7 +63,7 @@ const SecretsSchema = schema.object(secretSchemaProps, { }); // params definition -type ActionParamsType = TypeOf; +export type ActionParamsType = TypeOf; const ParamsSchema = schema.object({ body: schema.maybe(schema.string()), }); @@ -63,7 +75,7 @@ export function getActionType({ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { +}): WebhookActionType { return { id: '.webhook', minimumLicenseRequired: 'gold', @@ -112,13 +124,13 @@ function validateActionTypeConfig( // action executor export async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: WebhookActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const { method, url, headers = {} } = execOptions.config as ActionTypeConfigType; - const { body: data } = execOptions.params as ActionParamsType; + const { method, url, headers = {} } = execOptions.config; + const { body: data } = execOptions.params; - const secrets: ActionTypeSecretsType = execOptions.secrets as ActionTypeSecretsType; + const secrets: ActionTypeSecretsType = execOptions.secrets; const basicAuth = isString(secrets.user) && isString(secrets.password) ? { auth: { username: secrets.user, password: secrets.password } } @@ -172,11 +184,14 @@ export async function executor( } // Action Executor Result w/ internationalisation -function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } -function errorResultInvalid(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function errorResultInvalid( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.webhook.invalidResponseErrorMessage', { defaultMessage: 'error calling webhook, invalid response', }); @@ -188,7 +203,7 @@ function errorResultInvalid(actionId: string, serviceMessage: string): ActionTyp }; } -function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { +function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.webhook.unreachableErrorMessage', { defaultMessage: 'error calling webhook, unexpected error', }); @@ -199,7 +214,7 @@ function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult }; } -function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { const errMessage = i18n.translate( 'xpack.actions.builtin.webhook.invalidResponseRetryLaterErrorMessage', { @@ -220,7 +235,7 @@ function retryResultSeconds( serviceMessage: string, retryAfter: number -): ActionTypeExecutorResult { +): ActionTypeExecutorResult { const retryEpoch = Date.now() + retryAfter * 1000; const retry = new Date(retryEpoch); const retryString = retry.toISOString(); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 0e63cc8f5956e..bce06c829b1bc 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -59,7 +59,7 @@ export class ActionExecutor { actionId, params, request, - }: ExecuteOptions): Promise { + }: ExecuteOptions): Promise> { if (!this.isInitialized) { throw new Error('ActionExecutor not initialized'); } @@ -125,7 +125,7 @@ export class ActionExecutor { }; eventLogger.startTiming(event); - let rawResult: ActionTypeExecutorResult | null | undefined | void; + let rawResult: ActionTypeExecutorResult; try { rawResult = await actionType.executor({ actionId, @@ -173,7 +173,7 @@ export class ActionExecutor { } } -function actionErrorToMessage(result: ActionTypeExecutorResult): string { +function actionErrorToMessage(result: ActionTypeExecutorResult): string { let message = result.message || 'unknown error running action'; if (result.serviceMessage) { diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index 0a474ec3ae3ea..32c3c54faf007 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -59,7 +59,9 @@ describe('isLicenseValidForActionType', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'gold', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { @@ -120,7 +122,9 @@ describe('ensureLicenseForActionType()', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'gold', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index 9204c41b9288c..10a8501e856d2 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -94,7 +94,7 @@ export class TaskRunnerFactory { }, } as unknown) as KibanaRequest; - let executorResult: ActionTypeExecutorResult; + let executorResult: ActionTypeExecutorResult; try { executorResult = await actionExecutor.execute({ params, diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts index 03ae7a9b35a81..10c688c075eab 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { ActionType, ExecutorType } from '../types'; -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.ts index 021c460f4c815..50231f1c9a3a1 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.ts @@ -5,24 +5,44 @@ */ import Boom from 'boom'; -import { ActionType } from '../types'; +import { ActionType, ActionTypeConfig, ActionTypeSecrets, ActionTypeParams } from '../types'; -export function validateParams(actionType: ActionType, value: unknown) { +export function validateParams< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'params', value); } -export function validateConfig(actionType: ActionType, value: unknown) { +export function validateConfig< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'config', value); } -export function validateSecrets(actionType: ActionType, value: unknown) { +export function validateSecrets< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'secrets', value); } type ValidKeys = 'params' | 'config' | 'secrets'; -function validateWithSchema( - actionType: ActionType, +function validateWithSchema< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>( + actionType: ActionType, key: ValidKeys, value: unknown ): Record { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index ac4b332e7fd7a..ca93e88d01203 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -125,7 +125,9 @@ describe('Actions Plugin', () => { id: 'test', name: 'test', minimumLicenseRequired: 'basic', - async executor() {}, + async executor(options) { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(async () => { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 5b8b25d02658b..54d137cc0f617 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -33,13 +33,20 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { SecurityPluginSetup } from '../../security/server'; import { ActionsConfig } from './config'; -import { Services, ActionType, PreConfiguredAction } from './types'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; import { createExecutionEnqueuerFunction } from './create_execute_function'; import { registerBuiltInActionTypes } from './builtin_action_types'; import { registerActionsUsageCollector } from './usage'; +import { + Services, + ActionType, + PreConfiguredAction, + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, +} from './types'; import { getActionsConfigurationUtilities } from './actions_config'; @@ -70,7 +77,13 @@ export const EVENT_LOG_ACTIONS = { }; export interface PluginSetupContract { - registerType: (actionType: ActionType) => void; + registerType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams + >( + actionType: ActionType + ): void; } export interface PluginStartContract { @@ -219,7 +232,13 @@ export class ActionsPlugin implements Plugin, Plugi executeActionRoute(router, this.licenseState); return { - registerType: (actionType: ActionType) => { + registerType: < + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams + >( + actionType: ActionType + ) => { if (!(actionType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${actionType.minimumLicenseRequired}" is not a valid license type`); } diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 38fca656bef5a..b668e3460828a 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -71,7 +71,9 @@ describe('executeActionRoute', () => { const router = httpServiceMock.createRouter(); const actionsClient = actionsClientMock.create(); - actionsClient.execute.mockResolvedValueOnce((null as unknown) as ActionTypeExecutorResult); + actionsClient.execute.mockResolvedValueOnce( + (null as unknown) as ActionTypeExecutorResult + ); const [context, req, res] = mockHandlerArguments( { actionsClient }, diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 0d49d9a3a256e..f15a117106210 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -48,7 +48,7 @@ export const executeActionRoute = (router: IRouter, licenseState: ILicenseState) const { params } = req.body; const { id } = req.params; try { - const body: ActionTypeExecutorResult = await actionsClient.execute({ + const body: ActionTypeExecutorResult = await actionsClient.execute({ params, actionId: id, }); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index a8e19e3ff2e79..ecec45ade0460 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -21,6 +21,9 @@ export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; +export type ActionTypeConfig = Record; +export type ActionTypeSecrets = Record; +export type ActionTypeParams = Record; export interface Services { callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; @@ -49,32 +52,27 @@ export interface ActionsConfigType { } // the parameters passed to an action type executor function -export interface ActionTypeExecutorOptions { +export interface ActionTypeExecutorOptions { actionId: string; services: Services; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params: Record; + config: Config; + secrets: Secrets; + params: Params; } -export interface ActionResult { +export interface ActionResult { id: string; actionTypeId: string; name: string; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config?: Record; + config?: Config; isPreconfigured: boolean; } -export interface PreConfiguredAction extends ActionResult { - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: Record; +export interface PreConfiguredAction< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets +> extends ActionResult { + secrets: Secrets; } export interface FindActionResult extends ActionResult { @@ -82,38 +80,45 @@ export interface FindActionResult extends ActionResult { } // the result returned from an action type executor function -export interface ActionTypeExecutorResult { +export interface ActionTypeExecutorResult { actionId: string; status: 'ok' | 'error'; message?: string; serviceMessage?: string; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: any; + data?: Data; retry?: null | boolean | Date; } // signature of the action type executor function -export type ExecutorType = ( - options: ActionTypeExecutorOptions -) => Promise; +export type ExecutorType = ( + options: ActionTypeExecutorOptions +) => Promise>; + +interface ValidatorType { + validate(value: unknown): Type; +} -interface ValidatorType { - validate(value: unknown): Record; +export interface ActionValidationService { + isWhitelistedHostname(hostname: string): boolean; + isWhitelistedUri(uri: string): boolean; } -export type ActionTypeCreator = (config?: ActionsConfigType) => ActionType; -export interface ActionType { +export interface ActionType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +> { id: string; name: string; maxAttempts?: number; minimumLicenseRequired: LicenseType; validate?: { - params?: ValidatorType; - config?: ValidatorType; - secrets?: ValidatorType; + params?: ValidatorType; + config?: ValidatorType; + secrets?: ValidatorType; }; - executor: ExecutorType; + executor: ExecutorType; } export interface RawAction extends SavedObjectAttributes { diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 4f7ef290370cc..35422fa3c5fef 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -241,16 +241,15 @@ export const pushToService = async ( casePushParams: ServiceConnectorCaseParams, signal: AbortSignal ): Promise => { - const response = await KibanaServices.get().http.fetch( - `${ACTION_URL}/action/${connectorId}/_execute`, - { - method: 'POST', - body: JSON.stringify({ - params: { subAction: 'pushToService', subActionParams: casePushParams }, - }), - signal, - } - ); + const response = await KibanaServices.get().http.fetch< + ActionTypeExecutorResult> + >(`${ACTION_URL}/action/${connectorId}/_execute`, { + method: 'POST', + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), + signal, + }); if (response.status === 'error') { throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts index fd0d03dc18410..7c43ac0bbe56f 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts @@ -5,16 +5,14 @@ */ import { CoreSetup } from 'src/core/server'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; -import { ActionType, ActionTypeExecutorOptions } from '../../../../../../../plugins/actions/server'; +import { ActionType } from '../../../../../../../plugins/actions/server'; export function defineActionTypes( core: CoreSetup, { actions }: Pick ) { - const clusterClient = core.elasticsearch.legacy.client; - // Action types const noopActionType: ActionType = { id: 'test.noop', @@ -32,24 +30,39 @@ export function defineActionTypes( throw new Error('this action is intended to fail'); }, }; - const indexRecordActionType: ActionType = { + actions.registerType(noopActionType); + actions.registerType(throwActionType); + actions.registerType(getIndexRecordActionType()); + actions.registerType(getFailingActionType()); + actions.registerType(getRateLimitedActionType()); + actions.registerType(getAuthorizationActionType(core)); +} + +function getIndexRecordActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + message: schema.string(), + }); + type ParamsType = TypeOf; + const configSchema = schema.object({ + unencrypted: schema.string(), + }); + type ConfigType = TypeOf; + const secretsSchema = schema.object({ + encrypted: schema.string(), + }); + type SecretsType = TypeOf; + const result: ActionType = { id: 'test.index-record', name: 'Test: Index Record', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - message: schema.string(), - }), - config: schema.object({ - unencrypted: schema.string(), - }), - secrets: schema.object({ - encrypted: schema.string(), - }), + params: paramsSchema, + config: configSchema, + secrets: secretsSchema, }, - async executor({ config, secrets, params, services, actionId }: ActionTypeExecutorOptions) { + async executor({ config, secrets, params, services, actionId }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -64,17 +77,23 @@ export function defineActionTypes( return { status: 'ok', actionId }; }, }; - const failingActionType: ActionType = { + return result; +} + +function getFailingActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.failing', name: 'Test: Failing', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - }), + params: paramsSchema, }, - async executor({ config, secrets, params, services }: ActionTypeExecutorOptions) { + async executor({ config, secrets, params, services }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -89,19 +108,25 @@ export function defineActionTypes( throw new Error(`expected failure for ${params.index} ${params.reference}`); }, }; - const rateLimitedActionType: ActionType = { + return result; +} + +function getRateLimitedActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + retryAt: schema.number(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.rate-limit', name: 'Test: Rate Limit', minimumLicenseRequired: 'gold', maxAttempts: 2, validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - retryAt: schema.number(), - }), + params: paramsSchema, }, - async executor({ config, params, services }: ActionTypeExecutorOptions) { + async executor({ config, params, services }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -119,20 +144,27 @@ export function defineActionTypes( }; }, }; - const authorizationActionType: ActionType = { + return result; +} + +function getAuthorizationActionType(core: CoreSetup) { + const clusterClient = core.elasticsearch.legacy.client; + const paramsSchema = schema.object({ + callClusterAuthorizationIndex: schema.string(), + savedObjectsClientType: schema.string(), + savedObjectsClientId: schema.string(), + index: schema.string(), + reference: schema.string(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.authorization', name: 'Test: Authorization', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - callClusterAuthorizationIndex: schema.string(), - savedObjectsClientType: schema.string(), - savedObjectsClientId: schema.string(), - index: schema.string(), - reference: schema.string(), - }), + params: paramsSchema, }, - async executor({ params, services, actionId }: ActionTypeExecutorOptions) { + async executor({ params, services, actionId }) { // Call cluster let callClusterSuccess = false; let callClusterError; @@ -200,10 +232,5 @@ export function defineActionTypes( }; }, }; - actions.registerType(noopActionType); - actions.registerType(throwActionType); - actions.registerType(indexRecordActionType); - actions.registerType(failingActionType); - actions.registerType(rateLimitedActionType); - actions.registerType(authorizationActionType); + return result; } From f506279a5556a0a9f41ce750f2d5cec0aa4d524f Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 4 Aug 2020 14:04:45 -0600 Subject: [PATCH 104/115] [Security Solution][Tech Debt] cleans up ts-ignore issues and some smaller linter issues (#74268) ## Summary * Removes ts-ignore where it is not being used * Replaces ts-ignore with the better alternative which is the ts-expect-error --- .../check_circular_deps/run_check_circular_deps_cli.ts | 2 +- .../security_solution/cypress/support/commands.js | 1 - .../security_solution/cypress/tasks/date_picker.ts | 2 -- .../components/event_details/event_fields_browser.tsx | 2 +- .../public/common/components/events_viewer/index.tsx | 1 - .../common/components/import_data_modal/index.tsx | 1 - .../public/common/components/loader/index.tsx | 2 +- .../common/components/matrix_histogram/index.tsx | 1 - .../components/ml/tables/anomalies_host_table.tsx | 4 ++-- .../components/ml/tables/anomalies_network_table.tsx | 4 ++-- .../jobs_table/filters/jobs_table_filters.tsx | 1 - .../public/common/containers/errors/index.test.tsx | 3 +-- .../public/common/lib/compose/helpers.test.ts | 6 ++---- .../components/alerts_table/actions.test.tsx | 6 ++---- .../components/alerts_table/default_config.tsx | 2 +- .../endpoint_hosts/store/mock_host_result_list.ts | 2 +- .../pages/policy/store/policy_details/reducer.ts | 4 ++-- .../components/embeddables/embedded_map_helpers.tsx | 2 +- .../timelines/components/open_timeline/helpers.ts | 1 - .../components/open_timeline/search_row/index.tsx | 1 - .../row_renderers_browser/row_renderers_browser.tsx | 2 +- .../signals/filter_events_with_list.test.ts | 10 +++++----- .../lib/ip_details/elasticsearch_adapter.test.ts | 2 +- .../timeline/convert_saved_object_to_savedtimeline.ts | 1 - .../lib/timeline/routes/update_timelines_route.ts | 1 - .../uncommon_processes/elasticsearch_adapter.test.ts | 2 +- 26 files changed, 25 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts index 430e4983882cb..f9ef5b8fde5b5 100644 --- a/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts +++ b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts @@ -6,7 +6,7 @@ import { resolve } from 'path'; -// @ts-ignore +// @ts-expect-error import madge from 'madge'; import { createFailError, run } from '@kbn/dev-utils'; diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index 789759643e319..4f382f13bcd5d 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -32,7 +32,6 @@ Cypress.Commands.add('stubSecurityApi', function (dataFileName) { cy.on('window:before:load', (win) => { - // @ts-ignore no null, this is a temp hack see issue above win.fetch = null; }); cy.server(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts index 809498d25c5d8..2e1d3379dc202 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts @@ -38,7 +38,6 @@ export const setTimelineEndDate = (date: string) => { cy.get(DATE_PICKER_ABSOLUTE_INPUT).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_INPUT).then(($el) => { - // @ts-ignore if (Cypress.dom.isAttached($el)) { cy.wrap($el).click({ force: true }); } @@ -55,7 +54,6 @@ export const setTimelineStartDate = (date: string) => { cy.get(DATE_PICKER_ABSOLUTE_INPUT).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_INPUT).then(($el) => { - // @ts-ignore if (Cypress.dom.isAttached($el)) { cy.wrap($el).click({ force: true }); } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index af40d4ff18d5a..00a4e581320bb 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -55,7 +55,7 @@ export const EventFieldsBrowser = React.memo( return (
, column `render` callbacks expect complete BrowserField + // @ts-expect-error items going in match Partial, column `render` callbacks expect complete BrowserField items={items} columns={columns} pagination={false} diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 1563eab6039a6..e4520dab4626a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -220,7 +220,6 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, - // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && deepEqual(prevProps.columns, nextProps.columns) && diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx index d5d670b4c03ff..038c116c9fc8c 100644 --- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx @@ -8,7 +8,6 @@ import { EuiButton, EuiButtonEmpty, EuiCheckbox, - // @ts-ignore no-exported-member EuiFilePicker, EuiModal, EuiModalBody, diff --git a/x-pack/plugins/security_solution/public/common/components/loader/index.tsx b/x-pack/plugins/security_solution/public/common/components/loader/index.tsx index e78f148418588..cd660ae611610 100644 --- a/x-pack/plugins/security_solution/public/common/components/loader/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/loader/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, - // @ts-ignore + // @ts-expect-error EuiLoadingSpinnerSize, EuiText, } from '@elastic/eui'; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index fa512ad1ed80b..e93ade7191f52 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -64,7 +64,6 @@ const HeaderChildrenFlexItem = styled(EuiFlexItem)` margin-left: 24px; `; -// @ts-ignore - the EUI type definitions for Panel do no play nice with styled-components const HistogramPanel = styled(Panel)<{ height?: number }>` display: flex; flex-direction: column; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index 78cd23e6647c0..9bfae686b1a59 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -77,9 +77,9 @@ const AnomaliesHostTableComponent: React.FC = ({ /> type is not as specific as EUI's... + // @ts-expect-error the Columns type is not as specific as EUI's... columns={columns} - // @ts-ignore ...which leads to `networks` not "matching" the columns + // @ts-expect-error ...which leads to `networks` not "matching" the columns items={hosts} pagination={pagination} sorting={sorting} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index 73fe7b1ea5f6e..af27d411b990d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -67,9 +67,9 @@ const AnomaliesNetworkTableComponent: React.FC = ({ /> type is not as specific as EUI's... + // @ts-expect-error the Columns type is not as specific as EUI's... columns={columns} - // @ts-ignore ...which leads to `networks` not "matching" the columns + // @ts-expect-error ...which leads to `networks` not "matching" the columns items={networks} pagination={pagination} sorting={sorting} diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx index 8cb35fc689185..4cfb7f8ad2b5b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx @@ -11,7 +11,6 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, - // @ts-ignore no-exported-member EuiSearchBar, } from '@elastic/eui'; import { EuiSearchBarQuery } from '../../../../../timelines/components/open_timeline/types'; diff --git a/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx index e1b192df104d7..1bcbebd12b9bc 100644 --- a/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx @@ -14,8 +14,7 @@ import { onError } from 'apollo-link-error'; const mockDispatch = jest.fn(); jest.mock('apollo-link-error'); jest.mock('../../store'); -// @ts-ignore -store.getStore.mockReturnValue({ dispatch: mockDispatch }); +(store.getStore as jest.Mock).mockReturnValue({ dispatch: mockDispatch }); describe('errorLinkHandler', () => { const mockGraphQLErrors: GraphQLError = { diff --git a/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts b/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts index 4a3d734d0a6d4..c34027648c896 100644 --- a/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts @@ -18,10 +18,8 @@ jest.mock('../../containers/errors'); const mockWithClientState = 'mockWithClientState'; const mockHttpLink = { mockHttpLink: 'mockHttpLink' }; -// @ts-ignore -withClientState.mockReturnValue(mockWithClientState); -// @ts-ignore -apolloLinkHttp.createHttpLink.mockImplementation(() => mockHttpLink); +(withClientState as jest.Mock).mockReturnValue(mockWithClientState); +(apolloLinkHttp.createHttpLink as jest.Mock).mockImplementation(() => mockHttpLink); describe('getLinks helper', () => { test('It should return links in correct order', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 16d1a1481bc96..c2b51e29c230d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -255,8 +255,7 @@ describe('alert actions', () => { nonEcsData: [], updateTimelineIsLoading, }); - // @ts-ignore - const createTimelineArg = createTimeline.mock.calls[0][0]; + const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0]; expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); @@ -285,8 +284,7 @@ describe('alert actions', () => { nonEcsData: [], updateTimelineIsLoading, }); - // @ts-ignore - const createTimelineArg = createTimeline.mock.calls[0][0]; + const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0]; expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimelineArg.timeline.kqlQuery.filterQueryDraft.kind).toEqual('kuery'); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index f38a9107afca9..be9725dac5ff3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -95,7 +95,7 @@ export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): key: 'signal.rule.building_block_type', value: 'exists', }, - // @ts-ignore TODO: Rework parent typings to support ExistsFilter[] + // @ts-expect-error TODO: Rework parent typings to support ExistsFilter[] exists: { field: 'signal.rule.building_block_type' }, }, ]), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts index 355c2bb5c19fc..b69e5c5cd0e65 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts @@ -140,7 +140,7 @@ const hostListApiPathHandlerMocks = ({ // Build a GET route handler for each host details based on the list of Hosts passed on input if (hostsResults) { hostsResults.forEach((host) => { - // @ts-ignore + // @ts-expect-error apiHandlers[`/api/endpoint/metadata/${host.metadata.host.id}`] = () => host; }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index e7aa2c8893f8e..43a6ad2c585b4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -109,14 +109,14 @@ export const policyDetailsReducer: ImmutableReducer { /** * this is not safe because `action.payload.policyConfig` may have excess keys */ - // @ts-ignore + // @ts-expect-error newPolicy[section as keyof UIPolicyConfig] = { ...newPolicy[section as keyof UIPolicyConfig], ...newSettings, diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index c58e53d07acba..25928197590ea 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -114,7 +114,7 @@ export const createEmbeddable = async ( if (!isErrorEmbeddable(embeddableObject)) { embeddableObject.setRenderTooltipContent(renderTooltipContent); - // @ts-ignore + // @ts-expect-error await embeddableObject.setLayerList(getLayerList(indexPatterns)); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 27920fa297a48..5b5c2c9eeafc0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -238,7 +238,6 @@ export const getTimelineStatus = ( return duplicate ? TimelineStatus.active : timeline.status; }; -// eslint-disable-next-line complexity export const defaultTimelineToTimelineModel = ( timeline: TimelineResult, duplicate: boolean, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx index 5b927db3c37a9..69f79fb7aece3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx @@ -9,7 +9,6 @@ import { EuiFilterButton, EuiFlexGroup, EuiFlexItem, - // @ts-ignore EuiSearchBar, } from '@elastic/eui'; import React, { useMemo } from 'react'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index d2b0ad788fdb5..7baa7c42fb45e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -87,7 +87,7 @@ const RowRenderersBrowserComponent = React.forwardRef( const handleNameClick = useCallback( (item: RowRendererOption) => () => { const newSelection = xor([item], notExcludedRowRenderers); - // @ts-ignore + // @ts-expect-error ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions }, [notExcludedRowRenderers, ref] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 8c39a254e4261..3334cc17b9050 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -126,7 +126,7 @@ describe('filterEventsAgainstList', () => { ); expect(res.hits.hits.length).toEqual(2); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(['3.3.3.3', '7.7.7.7']).toEqual(ipVals); }); @@ -188,7 +188,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(6); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(['1.1.1.1', '3.3.3.3', '5.5.5.5', '7.7.7.7', '8.8.8.8', '9.9.9.9']).toEqual(ipVals); }); @@ -247,7 +247,7 @@ describe('filterEventsAgainstList', () => { buildRuleMessage, }); expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(res.hits.hits.length).toEqual(7); @@ -324,7 +324,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(8); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect([ '1.1.1.1', @@ -386,7 +386,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(9); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect([ '1.1.1.1', diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts index 6493a3e05bfc9..6249e60d9a2be 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts @@ -45,7 +45,7 @@ describe('elasticsearch_adapter', () => { describe('#getUsers', () => { test('will format edges correctly', () => { - // @ts-ignore Re-work `DatabaseSearchResponse` types as mock ES Response won't match + // @ts-expect-error Re-work `DatabaseSearchResponse` types as mock ES Response won't match const edges = getUsersEdges(mockUsersData); expect(edges).toEqual(mockFormattedUsersEdges); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index b54d12d7efce3..f888675b60410 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -37,7 +37,6 @@ const getTimelineTypeAndStatus = ( status: TimelineStatus | null = TimelineStatus.active ) => { // TODO: Added to support legacy TimelineType.draft, can be removed in 7.10 - // @ts-ignore if (timelineType === 'draft') { return { timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts index a622ee9b15706..07ce9a7336d4d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts @@ -34,7 +34,6 @@ export const updateTimelinesRoute = ( tags: ['access:securitySolution'], }, }, - // eslint-disable-next-line complexity async (context, request, response) => { const siemResponse = buildSiemResponse(response); diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts index 90839f5ac01c4..2a15f1fe074f8 100644 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts @@ -131,7 +131,7 @@ describe('elasticsearch_adapter', () => { _id: 'id-9', _score: 0, _source: { - // @ts-ignore ts doesn't like seeing the object written this way, but sometimes this is the data we get! + // @ts-expect-error ts doesn't like seeing the object written this way, but sometimes this is the data we get! 'host.id': ['host-id-9'], 'host.name': ['host-9'], }, From 2eb67130f3259573fe2441a4d0a1526ca5e3297f Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 4 Aug 2020 21:19:05 +0100 Subject: [PATCH 105/115] [Security Solution] styling for notes' panel (#74274) * styling for notes' pannel * styling * remove additional const * styling --- .../timelines/components/notes/index.tsx | 23 ++++--------------- .../timeline/properties/notes_size.ts | 1 - 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx index 957b37a0bd1c2..7d083735e6c71 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx @@ -9,7 +9,6 @@ import { EuiInMemoryTableProps, EuiModalBody, EuiModalHeader, - EuiPanel, EuiSpacer, } from '@elastic/eui'; import React, { useState } from 'react'; @@ -20,7 +19,6 @@ import { Note } from '../../../common/lib/note'; import { AddNote } from './add_note'; import { columns } from './columns'; import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers'; -import { NOTES_PANEL_WIDTH, NOTES_PANEL_HEIGHT } from '../timeline/properties/notes_size'; import { TimelineStatusLiteral, TimelineStatus } from '../../../../common/types/timeline'; interface Props { @@ -32,23 +30,12 @@ interface Props { updateNote: UpdateNote; } -const NotesPanel = styled(EuiPanel)` - height: ${NOTES_PANEL_HEIGHT}px; - width: ${NOTES_PANEL_WIDTH}px; - - & thead { - display: none; - } -`; - -NotesPanel.displayName = 'NotesPanel'; - const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( EuiInMemoryTable as React.ComponentType> )` - overflow-x: hidden; - overflow-y: auto; - height: 220px; + & thead { + display: none; + } ` as any; // eslint-disable-line @typescript-eslint/no-explicit-any InMemoryTable.displayName = 'InMemoryTable'; @@ -60,7 +47,7 @@ export const Notes = React.memo( const isImmutable = status === TimelineStatus.immutable; return ( - + <> @@ -84,7 +71,7 @@ export const Notes = React.memo( sorting={true} /> - + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts index 3a01df8f48a3f..cc979816f0141 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts @@ -5,4 +5,3 @@ */ export const NOTES_PANEL_WIDTH = 1024; -export const NOTES_PANEL_HEIGHT = 750; From c123cfead76a7bfe02d831df920bd9ce54fea9ff Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Tue, 4 Aug 2020 15:15:57 -0600 Subject: [PATCH 106/115] Fix TMS not loaded in legacy maps (#73570) Co-authored-by: Elastic Machine --- .../public/map/service_settings.js | 18 ++++++-------- .../public/map/service_settings.test.js | 24 ++++--------------- .../__tests__/region_map_visualization.js | 8 ++----- src/plugins/tile_map/config.ts | 9 ------- .../coordinate_maps_visualization.js | 8 ++----- src/plugins/tile_map/server/index.ts | 1 - 6 files changed, 16 insertions(+), 52 deletions(-) diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index ae40b2c92d40e..833304378402a 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -30,6 +30,7 @@ export class ServiceSettings { constructor(mapConfig, tilemapsConfig) { this._mapConfig = mapConfig; this._tilemapsConfig = tilemapsConfig; + this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; this._showZoomMessage = true; this._emsClient = new EMSClient({ @@ -53,13 +54,10 @@ export class ServiceSettings { linkify: true, }); - // TMS attribution - const attributionFromConfig = _.escape( - markdownIt.render(this._tilemapsConfig.deprecated.config.options.attribution || '') - ); // TMS Options - this.tmsOptionsFromConfig = _.assign({}, this._tilemapsConfig.deprecated.config.options, { - attribution: attributionFromConfig, + this.tmsOptionsFromConfig = _.assign({}, this._tilemapsConfig.options, { + attribution: _.escape(markdownIt.render(this._tilemapsConfig.options.attribution || '')), + url: this._tilemapsConfig.url, }); } @@ -122,7 +120,7 @@ export class ServiceSettings { */ async getTMSServices() { let allServices = []; - if (this._tilemapsConfig.deprecated.isOverridden) { + if (this._hasTmsConfigured) { //use tilemap.* settings from yml const tmsService = _.cloneDeep(this.tmsOptionsFromConfig); tmsService.id = TMS_IN_YML_ID; @@ -210,14 +208,12 @@ export class ServiceSettings { if (tmsServiceConfig.origin === ORIGIN.EMS) { return this._getAttributesForEMSTMSLayer(isDesaturated, isDarkMode); } else if (tmsServiceConfig.origin === ORIGIN.KIBANA_YML) { - const config = this._tilemapsConfig.deprecated.config; - const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); + const attrs = _.pick(this._tilemapsConfig, ['url', 'minzoom', 'maxzoom', 'attribution']); return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; } else { //this is an older config. need to resolve this dynamically. if (tmsServiceConfig.id === TMS_IN_YML_ID) { - const config = this._tilemapsConfig.deprecated.config; - const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); + const attrs = _.pick(this._tilemapsConfig, ['url', 'minzoom', 'maxzoom', 'attribution']); return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; } else { //assume ems diff --git a/src/plugins/maps_legacy/public/map/service_settings.test.js b/src/plugins/maps_legacy/public/map/service_settings.test.js index 6e416f7fd5c84..b924c3bb53056 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.test.js +++ b/src/plugins/maps_legacy/public/map/service_settings.test.js @@ -49,11 +49,7 @@ describe('service_settings (FKA tile_map test)', function () { }; const defaultTilemapConfig = { - deprecated: { - config: { - options: {}, - }, - }, + options: {}, }; function makeServiceSettings(mapConfigOptions = {}, tilemapOptions = {}) { @@ -160,13 +156,8 @@ describe('service_settings (FKA tile_map test)', function () { serviceSettings = makeServiceSettings( {}, { - deprecated: { - isOverridden: true, - config: { - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { minZoom: 0, maxZoom: 20 }, - }, - }, + url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { minZoom: 0, maxZoom: 20 }, } ); @@ -251,13 +242,8 @@ describe('service_settings (FKA tile_map test)', function () { includeElasticMapsService: false, }, { - deprecated: { - isOverridden: true, - config: { - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { minZoom: 0, maxZoom: 20 }, - }, - }, + url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { minZoom: 0, maxZoom: 20 }, } ); const tilemapServices = await serviceSettings.getTMSServices(); diff --git a/src/plugins/region_map/public/__tests__/region_map_visualization.js b/src/plugins/region_map/public/__tests__/region_map_visualization.js index 0a2a18c7cef4f..648193e8e2490 100644 --- a/src/plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/plugins/region_map/public/__tests__/region_map_visualization.js @@ -111,12 +111,8 @@ describe('RegionMapsVisualizationTests', function () { emsLandingPageUrl: '', }; const tilemapsConfig = { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, + options: { + attribution: '123', }, }; const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); diff --git a/src/plugins/tile_map/config.ts b/src/plugins/tile_map/config.ts index 435e52103d156..e754c84291116 100644 --- a/src/plugins/tile_map/config.ts +++ b/src/plugins/tile_map/config.ts @@ -21,15 +21,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ url: schema.maybe(schema.string()), - deprecated: schema.any({ - defaultValue: { - config: { - options: { - attribution: '', - }, - }, - }, - }), options: schema.object({ attribution: schema.string({ defaultValue: '' }), minZoom: schema.number({ defaultValue: 0, min: 0 }), diff --git a/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 9ff25ce674d3d..f2830e58e0eea 100644 --- a/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -98,12 +98,8 @@ describe('CoordinateMapsVisualizationTest', function () { emsLandingPageUrl: '', }; const tilemapsConfig = { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, + options: { + attribution: '123', }, }; diff --git a/src/plugins/tile_map/server/index.ts b/src/plugins/tile_map/server/index.ts index 3381553fe9364..4bf8c98c99d2c 100644 --- a/src/plugins/tile_map/server/index.ts +++ b/src/plugins/tile_map/server/index.ts @@ -23,7 +23,6 @@ import { configSchema, ConfigSchema } from '../config'; export const config: PluginConfigDescriptor = { exposeToBrowser: { url: true, - deprecated: true, options: true, }, schema: configSchema, From 372d8817a7d93e0d3c28815136555ac7e2cdf376 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 4 Aug 2020 14:27:44 -0700 Subject: [PATCH 107/115] [Reporting] Shorten asset path to help CLI FS Watcher (#74185) --- src/dev/precommit_hook/casing_check_config.js | 12 ++++++------ .../assets/fonts/noto/LICENSE_OFL.txt | 0 .../assets/fonts/noto/NotoSansCJKtc-Medium.ttf | Bin .../assets/fonts/noto/NotoSansCJKtc-Regular.ttf | Bin .../lib/pdf => common}/assets/fonts/noto/index.js | 0 .../pdf => common}/assets/fonts/roboto/LICENSE.txt | 0 .../assets/fonts/roboto/Roboto-Italic.ttf | Bin .../assets/fonts/roboto/Roboto-Medium.ttf | Bin .../assets/fonts/roboto/Roboto-Regular.ttf | Bin .../lib/pdf => common}/assets/img/logo-grey.png | Bin .../export_types/printable_pdf/lib/pdf/index.js | 2 +- 11 files changed, 7 insertions(+), 7 deletions(-) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/noto/LICENSE_OFL.txt (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/noto/NotoSansCJKtc-Medium.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/noto/NotoSansCJKtc-Regular.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/noto/index.js (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/roboto/LICENSE.txt (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/roboto/Roboto-Italic.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/roboto/Roboto-Medium.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/roboto/Roboto-Regular.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/img/logo-grey.png (100%) diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 404ad67174681..36d0ff8f51d88 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -177,12 +177,12 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/plugins/monitoring/public/icons/health-green.svg', 'x-pack/plugins/monitoring/public/icons/health-red.svg', 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/img/logo-grey.png', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/data.json.gz', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json', 'x-pack/test/functional/es_archives/monitoring/logstash-pipelines/data.json.gz', diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/LICENSE_OFL.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/LICENSE_OFL.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/index.js similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/index.js diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/LICENSE.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/LICENSE.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png b/x-pack/plugins/reporting/server/export_types/common/assets/img/logo-grey.png similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png rename to x-pack/plugins/reporting/server/export_types/common/assets/img/logo-grey.png diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js index f9a9d9d85bfd3..1042fd66abad7 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js @@ -11,7 +11,7 @@ import Printer from 'pdfmake'; import xRegExp from 'xregexp'; import { i18n } from '@kbn/i18n'; -const assetPath = path.resolve(__dirname, 'assets'); +const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); const tableBorderWidth = 1; From 409779d1ed00b799694174af372b91eb26970959 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 4 Aug 2020 15:34:01 -0700 Subject: [PATCH 108/115] [src/dev/build] build Kibana Platform bundles from source (#73591) Co-authored-by: spalger --- .../serializers/absolute_path_serializer.ts | 7 +++-- .../kbn-optimizer/src/common/bundle_cache.ts | 14 +++++++++ .../src/optimizer/get_plugin_bundles.test.ts | 31 ++++++++++--------- .../src/optimizer/get_plugin_bundles.ts | 12 +++++-- .../src/optimizer/optimizer_config.test.ts | 31 +++++++++++++++---- .../src/optimizer/optimizer_config.ts | 20 ++++++++++-- .../tasks/build_kibana_platform_plugins.ts | 18 +++++------ src/dev/build/tasks/copy_source_task.ts | 2 +- vars/kibanaPipeline.groovy | 1 + 9 files changed, 99 insertions(+), 37 deletions(-) diff --git a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts index 884614c8b9551..4008cf852c3a8 100644 --- a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts +++ b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts @@ -19,9 +19,12 @@ import { REPO_ROOT } from '../repo_root'; -export function createAbsolutePathSerializer(rootPath: string = REPO_ROOT) { +export function createAbsolutePathSerializer( + rootPath: string = REPO_ROOT, + replacement = '' +) { return { test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), - serialize: (value: string) => value.replace(rootPath, '').replace(/\\/g, '/'), + serialize: (value: string) => value.replace(rootPath, replacement).replace(/\\/g, '/'), }; } diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts index 7607e270b5b4f..578108fce51fa 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -104,4 +104,18 @@ export class BundleCache { public getOptimizerCacheKey() { return this.get().optimizerCacheKey; } + + public clear() { + this.state = undefined; + + if (this.path) { + try { + Fs.unlinkSync(this.path); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + } + } } diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index a823f66cf767b..702ad16144e7b 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -21,7 +21,9 @@ import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { getPluginBundles } from './get_plugin_bundles'; -expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo', '')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/output', '')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/outside/of/repo', '')); it('returns a bundle for core and each plugin', () => { expect( @@ -56,46 +58,47 @@ it('returns a bundle for core and each plugin', () => { manifestPath: '/repo/x-pack/plugins/box/kibana.json', }, ], - '/repo' + '/repo', + '/output' ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ Object { "banner": undefined, - "contextDir": /plugins/foo, + "contextDir": /plugins/foo, "id": "foo", - "manifestPath": /plugins/foo/kibana.json, - "outputDir": /plugins/foo/target/public, + "manifestPath": /plugins/foo/kibana.json, + "outputDir": /plugins/foo/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, Object { "banner": undefined, - "contextDir": "/outside/of/repo/plugins/baz", + "contextDir": /plugins/baz, "id": "baz", - "manifestPath": "/outside/of/repo/plugins/baz/kibana.json", - "outputDir": "/outside/of/repo/plugins/baz/target/public", + "manifestPath": /plugins/baz/kibana.json, + "outputDir": /plugins/baz/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, Object { "banner": "/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ ", - "contextDir": /x-pack/plugins/box, + "contextDir": /x-pack/plugins/box, "id": "box", - "manifestPath": /x-pack/plugins/box/kibana.json, - "outputDir": /x-pack/plugins/box/target/public, + "manifestPath": /x-pack/plugins/box/kibana.json, + "outputDir": /x-pack/plugins/box/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, ] diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 9350b9464242a..d2d19dcd87cca 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -23,7 +23,11 @@ import { Bundle } from '../common'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; -export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { +export function getPluginBundles( + plugins: KibanaPlatformPlugin[], + repoRoot: string, + outputRoot: string +) { const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; return plugins @@ -36,7 +40,11 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri publicDirNames: ['public', ...p.extraPublicDirs], sourceRoot: repoRoot, contextDir: p.directory, - outputDir: Path.resolve(p.directory, 'target/public'), + outputDir: Path.resolve( + outputRoot, + Path.relative(repoRoot, p.directory), + 'target/public' + ), manifestPath: p.manifestPath, banner: p.directory.startsWith(xpackDirSlash) ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements.\n` + diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index f97646e2bbbd3..afc2dc8952c87 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -23,16 +23,20 @@ jest.mock('./get_plugin_bundles.ts'); jest.mock('../common/theme_tags.ts'); jest.mock('./filter_by_id.ts'); -import Path from 'path'; -import Os from 'os'; +jest.mock('os', () => { + const realOs = jest.requireActual('os'); + jest.spyOn(realOs, 'cpus').mockImplementation(() => { + return ['foo'] as any; + }); + return realOs; +}); +import Path from 'path'; import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; -import { OptimizerConfig } from './optimizer_config'; +import { OptimizerConfig, ParsedOptions } from './optimizer_config'; import { parseThemeTags } from '../common'; -jest.spyOn(Os, 'cpus').mockReturnValue(['foo'] as any); - expect.addSnapshotSerializer(createAbsolutePathSerializer()); beforeEach(() => { @@ -118,6 +122,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -145,6 +150,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -172,6 +178,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -201,6 +208,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -227,6 +235,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /x/y/z, @@ -253,6 +262,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -276,6 +286,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -299,6 +310,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -323,6 +335,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -347,6 +360,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -384,18 +398,22 @@ describe('OptimizerConfig::create()', () => { getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]); filterById.mockReturnValue(Symbol('filtered bundles')); - jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({ + jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): { + [key in keyof ParsedOptions]: any; + } => ({ cache: Symbol('parsed cache'), dist: Symbol('parsed dist'), maxWorkerCount: Symbol('parsed max worker count'), pluginPaths: Symbol('parsed plugin paths'), pluginScanDirs: Symbol('parsed plugin scan dirs'), repoRoot: Symbol('parsed repo root'), + outputRoot: Symbol('parsed output root'), watch: Symbol('parsed watch'), themeTags: Symbol('theme tags'), inspectWorkers: Symbol('parsed inspect workers'), profileWebpack: Symbol('parsed profile webpack'), filters: [], + includeCoreBundle: false, })); }); @@ -474,6 +492,7 @@ describe('OptimizerConfig::create()', () => { Array [ Symbol(new platform plugins), Symbol(parsed repo root), + Symbol(parsed output root), ], ], "instances": Array [ diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 0e588ab36238b..45598ff8831b0 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -55,6 +55,13 @@ function omit(obj: T, keys: K[]): Omit { interface Options { /** absolute path to root of the repo/build */ repoRoot: string; + /** + * absolute path to the root directory where output should be written to. This + * defaults to the repoRoot but can be customized to write output somewhere else. + * + * This is how we write output to the build directory in the Kibana build tasks. + */ + outputRoot?: string; /** enable to run the optimizer in watch mode */ watch?: boolean; /** the maximum number of workers that will be created */ @@ -107,8 +114,9 @@ interface Options { themes?: ThemeTag | '*' | ThemeTag[]; } -interface ParsedOptions { +export interface ParsedOptions { repoRoot: string; + outputRoot: string; watch: boolean; maxWorkerCount: number; profileWebpack: boolean; @@ -139,6 +147,11 @@ export class OptimizerConfig { throw new TypeError('repoRoot must be an absolute path'); } + const outputRoot = options.outputRoot ?? repoRoot; + if (!Path.isAbsolute(outputRoot)) { + throw new TypeError('outputRoot must be an absolute path'); + } + /** * BEWARE: this needs to stay roughly synchronized with * `src/core/server/config/env.ts` which determines which paths @@ -182,6 +195,7 @@ export class OptimizerConfig { watch, dist, repoRoot, + outputRoot, maxWorkerCount, profileWebpack, cache, @@ -206,11 +220,11 @@ export class OptimizerConfig { publicDirNames: ['public', 'public/utils'], sourceRoot: options.repoRoot, contextDir: Path.resolve(options.repoRoot, 'src/core'), - outputDir: Path.resolve(options.repoRoot, 'src/core/target/public'), + outputDir: Path.resolve(options.outputRoot, 'src/core/target/public'), }), ] : []), - ...getPluginBundles(plugins, options.repoRoot), + ...getPluginBundles(plugins, options.repoRoot, options.outputRoot), ]; return new OptimizerConfig( diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index beb5ad40229df..48625078e9bd1 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -17,7 +17,7 @@ * under the License. */ -import { CiStatsReporter } from '@kbn/dev-utils'; +import { CiStatsReporter, REPO_ROOT } from '@kbn/dev-utils'; import { runOptimizer, OptimizerConfig, @@ -29,9 +29,10 @@ import { Task } from '../lib'; export const BuildKibanaPlatformPlugins: Task = { description: 'Building distributable versions of Kibana platform plugins', - async run(config, log, build) { - const optimizerConfig = OptimizerConfig.create({ - repoRoot: build.resolvePath(), + async run(_, log, build) { + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + outputRoot: build.resolvePath(), cache: false, oss: build.isOss(), examples: false, @@ -42,11 +43,10 @@ export const BuildKibanaPlatformPlugins: Task = { const reporter = CiStatsReporter.fromEnv(log); - await runOptimizer(optimizerConfig) - .pipe( - reportOptimizerStats(reporter, optimizerConfig, log), - logOptimizerState(log, optimizerConfig) - ) + await runOptimizer(config) + .pipe(reportOptimizerStats(reporter, config, log), logOptimizerState(log, config)) .toPromise(); + + await Promise.all(config.bundles.map((b) => b.cache.clear())); }, }; diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index c8489673b83af..79279997671e5 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -30,7 +30,7 @@ export const CopySource: Task = { 'src/**', '!src/**/*.{test,test.mocks,mock}.{js,ts,tsx}', '!src/**/mocks.ts', // special file who imports .mock files - '!src/**/{__tests__,__snapshots__,__mocks__}/**', + '!src/**/{target,__tests__,__snapshots__,__mocks__}/**', '!src/test_utils/**', '!src/fixtures/**', '!src/legacy/core_plugins/console/public/tests/**', diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 5f3e8d1a0660d..173c5b7e11764 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -170,6 +170,7 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ + '**/target/public/.kbn-optimizer-cache', 'target/kibana-*', 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', From 36f25e66e85242fb16119915821646744711fff4 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 4 Aug 2020 16:00:56 -0700 Subject: [PATCH 109/115] Fixed Alert details does not update page title and breadcrumb (#74214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed Alert details does not update page title and breadcrumb * Added alert name to details breadcrumb as a dynamic title * Update x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts Co-authored-by: Mike Côté Co-authored-by: Mike Côté --- .../public/application/home.tsx | 4 ++-- .../public/application/lib/breadcrumb.test.ts | 21 ++++++++++++++----- .../public/application/lib/breadcrumb.ts | 14 +++++++++++-- .../components/alert_details.tsx | 16 +++++++++++++- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 15099242b6e17..eb6b1ada3ba93 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -22,7 +22,7 @@ import { import { i18n } from '@kbn/i18n'; import { Section, routeToConnectors, routeToAlerts } from './constants'; -import { getCurrentBreadcrumb } from './lib/breadcrumb'; +import { getAlertingSectionBreadcrumb } from './lib/breadcrumb'; import { getCurrentDocTitle } from './lib/doc_title'; import { useAppDependencies } from './app_context'; import { hasShowActionsCapability } from './lib/capabilities'; @@ -75,7 +75,7 @@ export const TriggersActionsUIHome: React.FunctionComponent { - setBreadcrumbs([getCurrentBreadcrumb(section || 'home')]); + setBreadcrumbs([getAlertingSectionBreadcrumb(section || 'home')]); chrome.docTitle.change(getCurrentDocTitle(section || 'home')); }, [section, chrome, setBreadcrumbs]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts index 8ba909beff2a8..f5578aa5271be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts @@ -3,25 +3,25 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getCurrentBreadcrumb } from './breadcrumb'; +import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from './breadcrumb'; import { i18n } from '@kbn/i18n'; import { routeToConnectors, routeToAlerts, routeToHome } from '../constants'; -describe('getCurrentBreadcrumb', () => { +describe('getAlertingSectionBreadcrumb', () => { test('if change calls return proper breadcrumb title ', async () => { - expect(getCurrentBreadcrumb('connectors')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('connectors')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.connectors.breadcrumbTitle', { defaultMessage: 'Connectors', }), href: `${routeToConnectors}`, }); - expect(getCurrentBreadcrumb('alerts')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('alerts')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.alerts.breadcrumbTitle', { defaultMessage: 'Alerts', }), href: `${routeToAlerts}`, }); - expect(getCurrentBreadcrumb('home')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('home')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', { defaultMessage: 'Alerts and Actions', }), @@ -29,3 +29,14 @@ describe('getCurrentBreadcrumb', () => { }); }); }); + +describe('getAlertDetailsBreadcrumb', () => { + test('if select an alert should return proper breadcrumb title with alert name ', async () => { + expect(getAlertDetailsBreadcrumb('testId', 'testName')).toMatchObject({ + text: i18n.translate('xpack.triggersActionsUI.alertDetails.breadcrumbTitle', { + defaultMessage: 'testName', + }), + href: '/alert/testId', + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts index 3735942ff97af..db624688f9c76 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { routeToHome, routeToConnectors, routeToAlerts } from '../constants'; +import { routeToHome, routeToConnectors, routeToAlerts, routeToAlertDetails } from '../constants'; -export const getCurrentBreadcrumb = (type: string): { text: string; href: string } => { +export const getAlertingSectionBreadcrumb = (type: string): { text: string; href: string } => { // Home and sections switch (type) { case 'connectors': @@ -33,3 +33,13 @@ export const getCurrentBreadcrumb = (type: string): { text: string; href: string }; } }; + +export const getAlertDetailsBreadcrumb = ( + id: string, + name: string +): { text: string; href: string } => { + return { + text: name, + href: `${routeToAlertDetails.replace(':alertId', id)}`, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index b1dd78ff59f34..6ee7915e2be71 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useEffect } from 'react'; import { keyBy } from 'lodash'; import { useHistory } from 'react-router-dom'; import { @@ -29,6 +29,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb'; +import { getCurrentDocTitle } from '../../../lib/doc_title'; import { Alert, AlertType, ActionType } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -69,8 +71,20 @@ export const AlertDetails: React.FunctionComponent = ({ docLinks, charts, dataPlugin, + setBreadcrumbs, + chrome, } = useAppDependencies(); + // Set breadcrumb and page title + useEffect(() => { + setBreadcrumbs([ + getAlertingSectionBreadcrumb('alerts'), + getAlertDetailsBreadcrumb(alert.id, alert.name), + ]); + chrome.docTitle.change(getCurrentDocTitle('alerts')); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const canExecuteActions = hasExecuteActionsCapability(capabilities); const canSaveAlert = hasAllPrivilege(alert, alertType) && From 6a77bec233c81410413e539dba84ddce12072f5c Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 4 Aug 2020 20:06:30 -0400 Subject: [PATCH 110/115] [Security Solution][Exceptions] - Fixes exceptions builder nested deletion issue and adds unit tests (#74250) ### Summary - Updates logic for deleting exception item entries in the builder - found that there was a bug in deleting nested entries - Adds more unit tests --- .../exceptions/add_exception_modal/index.tsx | 4 +- .../exceptions/builder/and_badge.test.tsx | 48 ++ .../exceptions/builder/and_badge.tsx | 52 +++ .../builder/entry_delete_button.test.tsx | 136 ++++++ .../builder/entry_delete_button.tsx | 68 +++ ...ntry_item.test.tsx => entry_item.test.tsx} | 17 +- ...{builder_entry_item.tsx => entry_item.tsx} | 3 - ..._item.test.tsx => exception_item.test.tsx} | 55 +-- ..._exception_item.tsx => exception_item.tsx} | 100 +--- .../exceptions/builder/helpers.test.tsx | 6 +- .../components/exceptions/builder/helpers.tsx | 18 +- .../exceptions/builder/index.test.tsx | 422 +++++++++++++++++ .../components/exceptions/builder/index.tsx | 15 +- ....stories.tsx => logic_buttons.stories.tsx} | 17 +- ...ptions.test.tsx => logic_buttons.test.tsx} | 22 +- ...r_button_options.tsx => logic_buttons.tsx} | 4 +- .../exceptions/builder/reducer.test.ts | 441 ++++++++++++++++++ .../exceptions/edit_exception_modal/index.tsx | 4 +- 18 files changed, 1260 insertions(+), 172 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_entry_item.test.tsx => entry_item.test.tsx} (97%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_entry_item.tsx => entry_item.tsx} (99%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_exception_item.test.tsx => exception_item.test.tsx} (84%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_exception_item.tsx => exception_item.tsx} (57%) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_button_options.stories.tsx => logic_buttons.stories.tsx} (90%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_button_options.test.tsx => logic_buttons.test.tsx} (94%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_button_options.tsx => logic_buttons.tsx} (94%) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index e6eaa4947e404..7526c52d16fde 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -31,7 +31,7 @@ import * as i18n from './translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { ExceptionBuilder } from '../builder'; +import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; @@ -317,7 +317,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ {i18n.EXCEPTION_BUILDER_INFO} - { + test('it renders exceptionItemEntryFirstRowAndBadge for very first exception item in builder', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it renders exceptionItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryInvisibleAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it renders regular "and" badge if exception item is not the first one and includes more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx new file mode 100644 index 0000000000000..3ce2f704b3643 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { AndOrBadge } from '../../and_or_badge'; + +const MyInvisibleAndBadge = styled(EuiFlexItem)` + visibility: hidden; +`; + +const MyFirstRowContainer = styled(EuiFlexItem)` + padding-top: 20px; +`; + +interface BuilderAndBadgeProps { + entriesLength: number; + exceptionItemIndex: number; +} + +export const BuilderAndBadgeComponent = React.memo( + ({ entriesLength, exceptionItemIndex }) => { + const badge = ; + + if (entriesLength > 1 && exceptionItemIndex === 0) { + return ( + + {badge} + + ); + } else if (entriesLength <= 1) { + return ( + + {badge} + + ); + } else { + return ( + + {badge} + + ); + } + } +); + +BuilderAndBadgeComponent.displayName = 'BuilderAndBadge'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx new file mode 100644 index 0000000000000..b766a0536d237 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; + +import { BuilderEntryDeleteButtonComponent } from './entry_delete_button'; + +describe('BuilderEntryDeleteButtonComponent', () => { + test('it renders firstRowBuilderDeleteButton for very first entry in builder', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if entryIndex is not 0', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if exceptionItemIndex is not 0', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if nestedParentIndex is not null', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it invokes "onDelete" when button is clicked', () => { + const onDelete = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="builderDeleteButton"] button').simulate('click'); + + expect(onDelete).toHaveBeenCalledTimes(1); + expect(onDelete).toHaveBeenCalledWith(0, null); + }); + + test('it disables button if it is the only entry left and no field has been selected', () => { + const exceptionItem = { + ...getExceptionListItemSchemaMock(), + entries: [{ ...getEntryMatchMock(), field: '' }], + }; + const wrapper = mount( + + ); + + const button = wrapper.find('[data-test-subj="builderDeleteButton"] button').at(0); + + expect(button.prop('disabled')).toBeTruthy(); + }); + + test('it does not disable button if it is the only entry left and field has been selected', () => { + const wrapper = mount( + + ); + + const button = wrapper.find('[data-test-subj="builderDeleteButton"] button').at(0); + + expect(button.prop('disabled')).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx new file mode 100644 index 0000000000000..e63f95064cba0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { BuilderEntry } from '../types'; + +const MyFirstRowContainer = styled(EuiFlexItem)` + padding-top: 20px; +`; + +interface BuilderEntryDeleteButtonProps { + entries: BuilderEntry[]; + isOnlyItem: boolean; + entryIndex: number; + exceptionItemIndex: number; + nestedParentIndex: number | null; + onDelete: (item: number, parent: number | null) => void; +} + +export const BuilderEntryDeleteButtonComponent = React.memo( + ({ entries, nestedParentIndex, isOnlyItem, entryIndex, exceptionItemIndex, onDelete }) => { + const isDisabled: boolean = + isOnlyItem && + entries.length === 1 && + exceptionItemIndex === 0 && + (entries[0].field == null || entries[0].field === ''); + + const handleDelete = useCallback((): void => { + onDelete(entryIndex, nestedParentIndex); + }, [onDelete, entryIndex, nestedParentIndex]); + + const button = ( + + ); + + if (entryIndex === 0 && exceptionItemIndex === 0 && nestedParentIndex == null) { + // This logic was added to work around it including the field + // labels in centering the delete icon for the first row + return ( + + {button} + + ); + } else { + return ( + + {button} + + ); + } + } +); + +BuilderEntryDeleteButtonComponent.displayName = 'BuilderEntryDeleteButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx index 0f54ec29cc540..2a116c4cd8acf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { BuilderEntryItem } from './builder_entry_item'; +import { BuilderEntryItem } from './entry_item'; import { isOperator, isNotOperator, @@ -64,7 +64,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -91,7 +90,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -122,7 +120,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -155,7 +152,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -188,7 +184,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -221,7 +216,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -254,7 +248,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -287,7 +280,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -323,7 +315,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -377,7 +368,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -416,7 +406,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -451,7 +440,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -486,7 +474,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -521,7 +508,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -556,7 +542,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx index 3883a2fad2cf2..3044f6d01b745 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx @@ -34,7 +34,6 @@ interface EntryItemProps { indexPattern: IIndexPattern; showLabel: boolean; listType: ExceptionListType; - addNested: boolean; onChange: (arg: BuilderEntry, i: number) => void; onlyShowListOperators?: boolean; } @@ -43,7 +42,6 @@ export const BuilderEntryItem: React.FC = ({ entry, indexPattern, listType, - addNested, showLabel, onChange, onlyShowListOperators = false, @@ -51,7 +49,6 @@ export const BuilderEntryItem: React.FC = ({ const handleFieldChange = useCallback( ([newField]: IFieldType[]): void => { const { updatedEntry, index } = getEntryOnFieldChange(entry, newField); - onChange(updatedEntry, index); }, [onChange, entry] diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx similarity index 84% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx index 7624ce147abd9..e90639a2c0285 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx @@ -15,11 +15,11 @@ import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/s import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock'; -import { ExceptionListItemComponent } from './builder_exception_item'; +import { BuilderExceptionListItemComponent } from './exception_item'; jest.mock('../../../../common/lib/kibana'); -describe('ExceptionListItemComponent', () => { +describe('BuilderExceptionListItemComponent', () => { const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); beforeAll(() => { @@ -46,7 +46,7 @@ describe('ExceptionListItemComponent', () => { }; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -71,11 +70,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders "and" badge when more than one exception item entry exists and it is not the first exception item', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -98,11 +96,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders indented "and" badge when "andLogicIncluded" is "true" and only one entry exists', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -127,11 +124,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders no "and" badge when "andLogicIncluded" is "false"', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={false} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -167,7 +163,7 @@ describe('ExceptionListItemComponent', () => { entries: [{ ...getEntryMatchMock(), field: '' }], }; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeTruthy(); }); test('it does not render delete button disabled when it is not the only entry left in builder', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeFalsy(); }); test('it does not render delete button disabled when "exceptionItemIndex" is not "0"', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - { // this to be true, but done for testing purposes isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeFalsy(); }); test('it does not render delete button disabled when more than one entry exists', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').at(0).props() + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').at(0).props() .disabled ).toBeFalsy(); }); test('it invokes "onChangeExceptionItem" when delete button clicked', () => { const mockOnDeleteExceptionItem = jest.fn(); - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchAnyMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={mockOnDeleteExceptionItem} onChangeExceptionItem={jest.fn()} /> ); wrapper - .find('[data-test-subj="exceptionItemEntryDeleteButton"] button') + .find('[data-test-subj="builderItemEntryDeleteButton"] button') .at(0) .simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx index 50a6150833796..cd8b66acd223a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx @@ -5,23 +5,16 @@ */ import React, { useMemo, useCallback } from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { AndOrBadge } from '../../and_or_badge'; -import { BuilderEntryItem } from './builder_entry_item'; import { getFormattedBuilderEntries, getUpdatedEntriesOnDelete } from './helpers'; import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types'; import { ExceptionListType } from '../../../../../public/lists_plugin_deps'; - -const MyInvisibleAndBadge = styled(EuiFlexItem)` - visibility: hidden; -`; - -const MyFirstRowContainer = styled(EuiFlexItem)` - padding-top: 20px; -`; +import { BuilderEntryItem } from './entry_item'; +import { BuilderEntryDeleteButtonComponent } from './entry_delete_button'; +import { BuilderAndBadgeComponent } from './and_badge'; const MyBeautifulLine = styled(EuiFlexItem)` &:after { @@ -33,7 +26,7 @@ const MyBeautifulLine = styled(EuiFlexItem)` } `; -interface ExceptionListItemProps { +interface BuilderExceptionListItemProps { exceptionItem: ExceptionsBuilderExceptionItem; exceptionId: string; exceptionItemIndex: number; @@ -41,13 +34,12 @@ interface ExceptionListItemProps { andLogicIncluded: boolean; isOnlyItem: boolean; listType: ExceptionListType; - addNested: boolean; onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onlyShowListOperators?: boolean; } -export const ExceptionListItemComponent = React.memo( +export const BuilderExceptionListItemComponent = React.memo( ({ exceptionItem, exceptionId, @@ -55,7 +47,6 @@ export const ExceptionListItemComponent = React.memo( indexPattern, isOnlyItem, listType, - addNested, andLogicIncluded, onDeleteExceptionItem, onChangeExceptionItem, @@ -81,8 +72,8 @@ export const ExceptionListItemComponent = React.memo( (entryIndex: number, parentIndex: number | null): void => { const updatedExceptionItem = getUpdatedEntriesOnDelete( exceptionItem, - parentIndex ? parentIndex : entryIndex, - parentIndex ? entryIndex : null + entryIndex, + parentIndex ); onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex); @@ -98,63 +89,15 @@ export const ExceptionListItemComponent = React.memo( [exceptionItem.entries, indexPattern] ); - const getAndBadge = useCallback((): JSX.Element => { - const badge = ; - - if (andLogicIncluded && exceptionItem.entries.length > 1 && exceptionItemIndex === 0) { - return ( - - {badge} - - ); - } else if (andLogicIncluded && exceptionItem.entries.length <= 1) { - return ( - - {badge} - - ); - } else if (andLogicIncluded && exceptionItem.entries.length > 1) { - return ( - - {badge} - - ); - } else { - return <>; - } - }, [exceptionItem.entries.length, exceptionItemIndex, andLogicIncluded]); - - const getDeleteButton = useCallback( - (entryIndex: number, parentIndex: number | null): JSX.Element => { - const button = ( - handleDeleteEntry(entryIndex, parentIndex)} - isDisabled={ - isOnlyItem && - exceptionItem.entries.length === 1 && - exceptionItemIndex === 0 && - (exceptionItem.entries[0].field == null || exceptionItem.entries[0].field === '') - } - aria-label="entryDeleteButton" - className="exceptionItemEntryDeleteButton" - data-test-subj="exceptionItemEntryDeleteButton" - /> - ); - if (entryIndex === 0 && exceptionItemIndex === 0 && parentIndex == null) { - return {button}; - } else { - return {button}; - } - }, - [exceptionItemIndex, exceptionItem.entries, handleDeleteEntry, isOnlyItem] - ); - return ( - {getAndBadge()} + {andLogicIncluded && ( + + )} {entries.map((item, index) => ( @@ -166,7 +109,6 @@ export const ExceptionListItemComponent = React.memo( entry={item} indexPattern={indexPattern} listType={listType} - addNested={addNested} showLabel={ exceptionItemIndex === 0 && index === 0 && item.nested !== 'child' } @@ -174,10 +116,14 @@ export const ExceptionListItemComponent = React.memo( onlyShowListOperators={onlyShowListOperators} /> - {getDeleteButton( - item.entryIndex, - item.parent != null ? item.parent.parentIndex : null - )} + ))} @@ -189,4 +135,4 @@ export const ExceptionListItemComponent = React.memo( } ); -ExceptionListItemComponent.displayName = 'ExceptionListItem'; +BuilderExceptionListItemComponent.displayName = 'BuilderExceptionListItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index 224c99756eb5c..a3c5d09a0fb64 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -654,7 +654,7 @@ describe('Exception builder helpers', () => { expect(output).toEqual(expected); }); - test('it removes entry corresponding to "nestedEntryIndex"', () => { + test('it removes nested entry of "entryIndex" with corresponding parent index', () => { const payloadItem: ExceptionsBuilderExceptionItem = { ...getExceptionListItemSchemaMock(), entries: [ @@ -664,10 +664,10 @@ describe('Exception builder helpers', () => { }, ], }; - const output = getUpdatedEntriesOnDelete(payloadItem, 0, 1); + const output = getUpdatedEntriesOnDelete(payloadItem, 0, 0); const expected: ExceptionsBuilderExceptionItem = { ...getExceptionListItemSchemaMock(), - entries: [{ ...getEntryNestedMock(), entries: [{ ...getEntryExistsMock() }] }], + entries: [{ ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] }], }; expect(output).toEqual(expected); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx index 8585f58504e31..f6b703b7e622e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx @@ -248,22 +248,22 @@ export const getFormattedBuilderEntries = ( export const getUpdatedEntriesOnDelete = ( exceptionItem: ExceptionsBuilderExceptionItem, entryIndex: number, - nestedEntryIndex: number | null + nestedParentIndex: number | null ): ExceptionsBuilderExceptionItem => { - const itemOfInterest: BuilderEntry = exceptionItem.entries[entryIndex]; + const itemOfInterest: BuilderEntry = exceptionItem.entries[nestedParentIndex ?? entryIndex]; - if (nestedEntryIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) { + if (nestedParentIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) { const updatedEntryEntries: Array = [ - ...itemOfInterest.entries.slice(0, nestedEntryIndex), - ...itemOfInterest.entries.slice(nestedEntryIndex + 1), + ...itemOfInterest.entries.slice(0, entryIndex), + ...itemOfInterest.entries.slice(entryIndex + 1), ]; if (updatedEntryEntries.length === 0) { return { ...exceptionItem, entries: [ - ...exceptionItem.entries.slice(0, entryIndex), - ...exceptionItem.entries.slice(entryIndex + 1), + ...exceptionItem.entries.slice(0, nestedParentIndex), + ...exceptionItem.entries.slice(nestedParentIndex + 1), ], }; } else { @@ -277,9 +277,9 @@ export const getUpdatedEntriesOnDelete = ( return { ...exceptionItem, entries: [ - ...exceptionItem.entries.slice(0, entryIndex), + ...exceptionItem.entries.slice(0, nestedParentIndex), updatedItemOfInterest, - ...exceptionItem.entries.slice(entryIndex + 1), + ...exceptionItem.entries.slice(nestedParentIndex + 1), ], }; } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx new file mode 100644 index 0000000000000..3fa0e59f9acb0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx @@ -0,0 +1,422 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { wait as waitFor } from '@testing-library/react'; + +import { + fields, + getField, +} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { getEmptyValue } from '../../empty_value'; + +import { ExceptionBuilderComponent } from './'; + +jest.mock('../../../../common/lib/kibana'); + +describe('ExceptionBuilderComponent', () => { + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + autocomplete: { + getValueSuggestions: getValueSuggestionsMock, + }, + }, + }, + }); + }); + + afterEach(() => { + getValueSuggestionsMock.mockClear(); + }); + + test('it displays empty entry if no "exceptionListItems" are passed in', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('Search'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual( + 'Search field value...' + ); + }); + + test('it displays "exceptionListItems" that are passed in', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is one of' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual( + 'some ip' + ); + + wrapper.unmount(); + }); + + test('it displays "or", "and" and "add nested button" enabled', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionsAndButton"] button').prop('disabled') + ).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="exceptionsOrButton"] button').prop('disabled') + ).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="exceptionsNestedButton"] button').prop('disabled') + ).toBeFalsy(); + }); + + test('it adds an entry when "and" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + + wrapper.find('[data-test-subj="exceptionsAndButton"] button').simulate('click'); + + await waitFor(() => { + expect( + wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]') + ).toHaveLength(2); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text() + ).toEqual('Search field value...'); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').at(1).text()).toEqual( + 'Search' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(1).text()).toEqual( + 'is' + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(1).text() + ).toEqual('Search field value...'); + }); + }); + + test('it adds an exception item when "or" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]')).toHaveLength( + 1 + ); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + await waitFor(() => { + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]')).toHaveLength( + 2 + ); + + const item1 = wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]').at(0); + const item2 = wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]').at(1); + + expect(item1.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(item1.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect(item1.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text()).toEqual( + 'Search field value...' + ); + + expect(item2.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(item2.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect(item2.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text()).toEqual( + 'Search field value...' + ); + }); + }); + + test('it displays empty entry if user deletes last remaining entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is one of' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual( + 'some ip' + ); + + wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"] button').simulate('click'); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('Search'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual( + 'Search field value...' + ); + + wrapper.unmount(); + }); + + test('it displays "and" badge if at least one exception item includes more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + + wrapper.find('[data-test-subj="exceptionsAndButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it does not display "and" badge if none of the exception items include more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + }); + + describe('nested entry', () => { + test('it adds a nested entry when "add nested entry" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsNestedButton"] button').simulate('click'); + + await waitFor(() => { + const entry2 = wrapper + .find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]') + .at(1); + expect(entry2.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search nested field' + ); + expect( + entry2.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text() + ).toEqual('is'); + expect( + entry2.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').at(0).text() + ).toEqual(getEmptyValue()); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index b82607a541aaa..165f3314c2f15 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; -import { ExceptionListItemComponent } from './builder_exception_item'; +import { BuilderExceptionListItemComponent } from './exception_item'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { ExceptionListItemSchema, @@ -20,7 +20,7 @@ import { entriesNested, } from '../../../../../public/lists_plugin_deps'; import { AndOrBadge } from '../../and_or_badge'; -import { BuilderButtonOptions } from './builder_button_options'; +import { BuilderLogicButtons } from './logic_buttons'; import { getNewExceptionItem, filterExceptionItems } from '../helpers'; import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; import { State, exceptionsBuilderReducer } from './reducer'; @@ -72,7 +72,7 @@ interface ExceptionBuilderProps { onChange: (arg: OnChangeProps) => void; } -export const ExceptionBuilder = ({ +export const ExceptionBuilderComponent = ({ exceptionListItems, listType, listId, @@ -310,6 +310,8 @@ export const ExceptionBuilder = ({ onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete }); }, [onChange, exceptionsToDelete, exceptions]); + // Defaults builder to never be sans entry, instead + // always falls back to an empty entry if user deletes all useEffect(() => { if ( exceptions.length === 0 || @@ -351,13 +353,12 @@ export const ExceptionBuilder = ({ ))} - )} - ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} )); -storiesOf('Components|Exceptions|BuilderButtonOptions', module) +storiesOf('Exceptions|BuilderLogicButtons', module) .add('and/or buttons', () => { return ( - { return ( - { return ( - { return ( - { return ( - { return ( - { +describe('BuilderLogicButtons', () => { test('it renders "and" and "or" buttons', () => { const wrapper = mount( - { const onOrClicked = jest.fn(); const wrapper = mount( - { const onAndClicked = jest.fn(); const wrapper = mount( - { const onAddClickWhenNested = jest.fn(); const wrapper = mount( - { test('it disables "and" button if "isAndDisabled" is true', () => { const wrapper = mount( - { test('it disables "or" button if "isOrDisabled" is "true"', () => { const wrapper = mount( - { test('it disables "add nested" button if "isNestedDisabled" is "true"', () => { const wrapper = mount( - { const onNestedClicked = jest.fn(); const wrapper = mount( - { const onAndClicked = jest.fn(); const wrapper = mount( - void; } -export const BuilderButtonOptions: React.FC = ({ +export const BuilderLogicButtons: React.FC = ({ isOrDisabled = false, isAndDisabled = false, showNestedButton = false, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts new file mode 100644 index 0000000000000..ee5bd1329f35b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts @@ -0,0 +1,441 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; +import { getEntryNestedMock } from '../../../../../../lists/common/schemas/types/entry_nested.mock'; +import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock'; + +import { ExceptionsBuilderExceptionItem } from '../types'; +import { Action, State, exceptionsBuilderReducer } from './reducer'; +import { getDefaultEmptyEntry } from './helpers'; + +const initialState: State = { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: false, + addNested: false, + exceptions: [], + exceptionsToDelete: [], +}; + +describe('exceptionsBuilderReducer', () => { + let reducer: (state: State, action: Action) => State; + + beforeEach(() => { + reducer = exceptionsBuilderReducer(); + }); + + describe('#setExceptions', () => { + test('should return "andLogicIncluded" ', () => { + const update = reducer(initialState, { + type: 'setExceptions', + exceptions: [], + }); + + expect(update).toEqual({ + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: false, + addNested: false, + exceptions: [], + exceptionsToDelete: [], + }); + }); + + test('should set "andLogicIncluded" to true if any of the exceptions include entries with length greater than 1 ', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryMatchMock()], + }, + ]; + const { andLogicIncluded } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(andLogicIncluded).toBeTruthy(); + }); + + test('should set "andLogicIncluded" to false if any of the exceptions include entries with length greater than 1 ', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { andLogicIncluded } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(andLogicIncluded).toBeFalsy(); + }); + + test('should set "addNested" to true if last exception entry is type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { addNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(addNested).toBeTruthy(); + }); + + test('should set "addNested" to false if last exception item entry is not type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { addNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(addNested).toBeFalsy(); + }); + + test('should set "disableOr" to true if last exception entry is type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { disableOr } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableOr).toBeTruthy(); + }); + + test('should set "disableOr" to false if last exception item entry is not type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableOr } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableOr).toBeFalsy(); + }); + + test('should set "disableNested" to true if an exception item includes an entry of type list', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { disableNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableNested).toBeTruthy(); + }); + + test('should set "disableNested" to false if an exception item does not include an entry of type list', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableNested).toBeFalsy(); + }); + + // What does that even mean?! :) Just checking if a user has selected + // to add a nested entry but has not yet selected the nested field + test('should set "disableAnd" to true if last exception item is a nested entry with no entries itself', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], + }, + ]; + const { disableAnd } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableAnd).toBeTruthy(); + }); + + test('should set "disableAnd" to false if last exception item is a nested entry with no entries itself', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableAnd } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableAnd).toBeFalsy(); + }); + }); + + describe('#setDefault', () => { + test('should restore initial state and add default empty entry to item" ', () => { + const update = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDefault', + initialState, + lastException: { + ...getExceptionListItemSchemaMock(), + entries: [], + }, + } + ); + + expect(update).toEqual({ + ...initialState, + exceptions: [ + { + ...getExceptionListItemSchemaMock(), + entries: [getDefaultEmptyEntry()], + }, + ], + }); + }); + }); + + describe('#setExceptionsToDelete', () => { + test('should add passed in exception item to "exceptionsToDelete"', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + id: '2', + entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], + }, + ]; + const { exceptionsToDelete } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions, + exceptionsToDelete: [], + }, + { + type: 'setExceptionsToDelete', + exceptions: [ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + ], + } + ); + + expect(exceptionsToDelete).toEqual([ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + ]); + }); + }); + + describe('#setDisableAnd', () => { + test('should set "disableAnd" to false if "action.shouldDisable" is false', () => { + const { disableAnd } = reducer( + { + disableAnd: true, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableAnd', + shouldDisable: false, + } + ); + + expect(disableAnd).toBeFalsy(); + }); + + test('should set "disableAnd" to true if "action.shouldDisable" is true', () => { + const { disableAnd } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableAnd', + shouldDisable: true, + } + ); + + expect(disableAnd).toBeTruthy(); + }); + }); + + describe('#setDisableOr', () => { + test('should set "disableOr" to false if "action.shouldDisable" is false', () => { + const { disableOr } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: true, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableOr', + shouldDisable: false, + } + ); + + expect(disableOr).toBeFalsy(); + }); + + test('should set "disableOr" to true if "action.shouldDisable" is true', () => { + const { disableOr } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableOr', + shouldDisable: true, + } + ); + + expect(disableOr).toBeTruthy(); + }); + }); + + describe('#setAddNested', () => { + test('should set "addNested" to false if "action.addNested" is false', () => { + const { addNested } = reducer( + { + disableAnd: false, + disableNested: true, + disableOr: false, + andLogicIncluded: true, + addNested: true, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setAddNested', + addNested: false, + } + ); + + expect(addNested).toBeFalsy(); + }); + + test('should set "disableOr" to true if "action.addNested" is true', () => { + const { addNested } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setAddNested', + addNested: true, + } + ); + + expect(addNested).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 6109b85f2da5a..e1352ac38dc49 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -31,7 +31,7 @@ import { import * as i18n from './translations'; import { useKibana } from '../../../lib/kibana'; import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { ExceptionBuilder } from '../builder'; +import { ExceptionBuilderComponent } from '../builder'; import { useAddOrUpdateException } from '../use_add_exception'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -232,7 +232,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {i18n.EXCEPTION_BUILDER_INFO} - Date: Tue, 4 Aug 2020 19:36:34 -0600 Subject: [PATCH 111/115] [Security Solution][Detections] Fixes Severity Override not matching for Elastic Endpoint Security rule (#74317) ## Summary Fixes an issue where the `Severity Override` would not match for the `Elastic Endpoint Security` rule. This is a temporary fix until we can provide more robust comparisons between the user provided `severityMapping.value` (string) and the `severityMapping.field`'s type. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../signals/__mocks__/es_results.ts | 18 ++++++++ .../build_severity_from_mapping.test.ts | 44 ++++++++++++++++++- .../mappings/build_severity_from_mapping.ts | 29 ++++++++++-- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 513d6a93d1b5b..95ec753c21fd8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -109,6 +109,24 @@ export const sampleDocNoSortId = ( sort: [], }); +export const sampleDocSeverity = ( + severity?: Array | string | number | null +): SignalSourceHit => ({ + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _version: 1, + _id: sampleIdGuid, + _source: { + someKey: 'someValue', + '@timestamp': '2020-04-20T21:27:45+0000', + event: { + severity: severity ?? 100, + }, + }, + sort: [], +}); + export const sampleEmptyDocSearchResults = (): SignalSearchResponse => ({ took: 10, timed_out: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts index 80950335934f4..fb1d51364ab39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { sampleDocNoSortId, sampleDocSeverity } from '../__mocks__/es_results'; import { buildSeverityFromMapping } from './build_severity_from_mapping'; describe('buildSeverityFromMapping', () => { @@ -12,7 +12,7 @@ describe('buildSeverityFromMapping', () => { jest.clearAllMocks(); }); - test('severity defaults to provided if mapping is incomplete', () => { + test('severity defaults to provided if mapping is undefined', () => { const severity = buildSeverityFromMapping({ doc: sampleDocNoSortId(), severity: 'low', @@ -22,5 +22,45 @@ describe('buildSeverityFromMapping', () => { expect(severity).toEqual({ severity: 'low', severityMeta: {} }); }); + test('severity is overridden to highest matched mapping', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocSeverity(23), + severity: 'low', + severityMapping: [ + { field: 'event.severity', operator: 'equals', value: '23', severity: 'critical' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'low' }, + { field: 'event.severity', operator: 'equals', value: '11', severity: 'critical' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, + ], + }); + + expect(severity).toEqual({ + severity: 'critical', + severityMeta: { + severityOverrideField: 'event.severity', + }, + }); + }); + + test('severity is overridden when field is event.severity and source value is number', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocSeverity(23), + severity: 'low', + severityMapping: [ + { field: 'event.severity', operator: 'equals', value: '13', severity: 'low' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, + { field: 'event.severity', operator: 'equals', value: '33', severity: 'high' }, + { field: 'event.severity', operator: 'equals', value: '43', severity: 'critical' }, + ], + }); + + expect(severity).toEqual({ + severity: 'medium', + severityMeta: { + severityOverrideField: 'event.severity', + }, + }); + }); + // TODO: Enhance... }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts index a3c4f47b491be..c0a62a2cc887d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -24,6 +24,13 @@ interface BuildSeverityFromMappingReturn { severityMeta: Meta; // TODO: Stricter types } +const severitySortMapping = { + low: 0, + medium: 1, + high: 2, + critical: 3, +}; + export const buildSeverityFromMapping = ({ doc, severity, @@ -31,10 +38,24 @@ export const buildSeverityFromMapping = ({ }: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { if (severityMapping != null && severityMapping.length > 0) { let severityMatch: SeverityMappingItem | undefined; - severityMapping.forEach((mapping) => { - // TODO: Expand by verifying fieldType from index via doc._index - const mappedValue = get(mapping.field, doc._source); - if (mapping.value === mappedValue) { + + // Sort the SeverityMapping from low to high, so last match (highest severity) is used + const severityMappingSorted = severityMapping.sort( + (a, b) => severitySortMapping[a.severity] - severitySortMapping[b.severity] + ); + + severityMappingSorted.forEach((mapping) => { + const docValue = get(mapping.field, doc._source); + // TODO: Expand by verifying fieldType from index via doc._index + // Till then, explicit parsing of event.severity (long) to number. If not ECS, this could be + // another datatype, but until we can lookup datatype we must assume number for the Elastic + // Endpoint Security rule to function correctly + let parsedMappingValue: string | number = mapping.value; + if (mapping.field === 'event.severity') { + parsedMappingValue = Math.floor(Number(parsedMappingValue)); + } + + if (parsedMappingValue === docValue) { severityMatch = { ...mapping }; } }); From 7c60b09baae541ee7c4bbfe2a6cf06ffb0df736a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 5 Aug 2020 00:05:27 -0700 Subject: [PATCH 112/115] skip flaky suite (#74327) --- .../functional/apps/monitoring/setup/metricbeat_migration.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js index 34ac06450f684..95bd866d386b1 100644 --- a/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js +++ b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js @@ -11,7 +11,8 @@ export default function ({ getService, getPageObjects }) { const setupMode = getService('monitoringSetupMode'); const PageObjects = getPageObjects(['common', 'console']); - describe('Setup mode metricbeat migration', function () { + // FLAKY: https://github.com/elastic/kibana/issues/74327 + describe.skip('Setup mode metricbeat migration', function () { describe('setup mode btn', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); From f97641c84ac24438f8b5d509597ab4d905888123 Mon Sep 17 00:00:00 2001 From: EamonnTP Date: Wed, 5 Aug 2020 09:28:20 +0100 Subject: [PATCH 113/115] [DOCS] Add Observability topic (#73041) * Add observability content * Remove xpack from file name * Updates following review * Review edits Co-authored-by: Elastic Machine --- .../images/observability-overview.png | Bin 0 -> 470892 bytes docs/observability/index.asciidoc | 24 ++++++++++++++++++ docs/user/index.asciidoc | 2 ++ 3 files changed, 26 insertions(+) create mode 100644 docs/observability/images/observability-overview.png create mode 100644 docs/observability/index.asciidoc diff --git a/docs/observability/images/observability-overview.png b/docs/observability/images/observability-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d3d09139a891db9a3bccccf8fcf4869e21bc6e GIT binary patch literal 470892 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%0D5-lJ!>b}CC<&pEP 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

0|nJ0(E7ETveds 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%0Dv-qdX3LWII(vp;f9)ckyr6H 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%0Ax|uScf$5<` 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 M${L^|MU%k)52z)LLjV8( literal 0 HcmV?d00001 diff --git a/docs/observability/index.asciidoc b/docs/observability/index.asciidoc new file mode 100644 index 0000000000000..d63402e8df2fb --- /dev/null +++ b/docs/observability/index.asciidoc @@ -0,0 +1,24 @@ +[chapter] +[role="xpack"] +[[observability]] += Observability + +Observability enables you to add and monitor your logs, system +metrics, uptime data, and application traces, as a single stack. + +With *Observability*, you have: + +* A central place to add and configure your data sources. +* A variety of charts displaying analytics relating to each data source. +* *View in app* options to drill down and analyze data in the Logs, Metrics, Uptime, and APM apps. +* An alerts chart to keep you informed of any issues that you may need to resolve quickly. + +[role="screenshot"] +image::observability/images/observability-overview.png[Observability Overview in {kib}] + +[float] +== Get started + +{kib} provides step-by-step instructions to help you add and configure your data +sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information +and instructions. diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index 01be8c2e264c5..abbdbeb68d9cb 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -27,6 +27,8 @@ include::graph/index.asciidoc[] include::visualize.asciidoc[] +include::{kib-repo-dir}/observability/index.asciidoc[] + include::{kib-repo-dir}/logs/index.asciidoc[] include::{kib-repo-dir}/infrastructure/index.asciidoc[] From c24b17f334fed777f65d34ddae104c80be3ec9c9 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 5 Aug 2020 10:49:36 +0200 Subject: [PATCH 114/115] [Discover] Inline noWhiteSpace function (#74331) * Inline noWhiteSpace function * Fix TypeScript * Remove unused function file --- .../kibana/common/utils/no_white_space.js | 37 ------------------- .../angular/doc_table/components/table_row.ts | 21 +++++++---- 2 files changed, 14 insertions(+), 44 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/common/utils/no_white_space.js diff --git a/src/legacy/core_plugins/kibana/common/utils/no_white_space.js b/src/legacy/core_plugins/kibana/common/utils/no_white_space.js deleted file mode 100644 index 580418eb3423f..0000000000000 --- a/src/legacy/core_plugins/kibana/common/utils/no_white_space.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const TAGS_WITH_WS = />\s+<'); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index 7b862ec518a04..e7fafde2e68d0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -17,13 +17,10 @@ * under the License. */ -import _ from 'lodash'; +import { find, template } from 'lodash'; import $ from 'jquery'; -// @ts-ignore import rison from 'rison-node'; import '../../doc_viewer'; -// @ts-ignore -import { noWhiteSpace } from '../../../../../../../legacy/core_plugins/kibana/common/utils/no_white_space'; import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; @@ -35,6 +32,16 @@ import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_he import { esFilters } from '../../../../../../data/public'; import { getServices } from '../../../../kibana_services'; +const TAGS_WITH_WS = />\s+<'); +} + // guesstimate at the minimum number of chars wide cells in the table should be const MIN_LINE_LENGTH = 20; @@ -43,8 +50,8 @@ interface LazyScope extends ng.IScope { } export function createTableRowDirective($compile: ng.ICompileService, $httpParamSerializer: any) { - const cellTemplate = _.template(noWhiteSpace(cellTemplateHtml)); - const truncateByHeightTemplate = _.template(noWhiteSpace(truncateByHeightTemplateHtml)); + const cellTemplate = template(noWhiteSpace(cellTemplateHtml)); + const truncateByHeightTemplate = template(noWhiteSpace(truncateByHeightTemplateHtml)); return { restrict: 'A', @@ -169,7 +176,7 @@ export function createTableRowDirective($compile: ng.ICompileService, $httpParam const $cell = $cells.eq(i); if ($cell.data('discover:html') === html) return; - const reuse = _.find($cells.slice(i + 1), function (cell: any) { + const reuse = find($cells.slice(i + 1), function (cell: any) { return $.data(cell, 'discover:html') === html; }); From 22d6f09d317b54cf2832ec4cac51df42db17c28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Wed, 5 Aug 2020 11:19:41 +0200 Subject: [PATCH 115/115] [Logs UI] Correct trial period duration in anomaly splash screen (#74249) --- .../logging/log_analysis_setup/subscription_splash_content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx index e0e293b1cc3e7..81f52f986cab8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx @@ -61,7 +61,7 @@ export const SubscriptionSplashContent: React.FC = () => { description = ( );