From ba37975fbc751e310bc921309acb97547fcd1f54 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 26 Jan 2021 08:41:23 -0600 Subject: [PATCH 001/163] Show legend on error rate chart (#89234) This probably was hidden for reasons related to implementing (or deferring the implementation of) comparisons. --- .../shared/charts/transaction_error_rate_chart/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 90877a895b05b..d712fa27c75ac 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -77,7 +77,6 @@ export function TransactionErrorRateChart({ data: errorRates, type: 'linemark', color: theme.eui.euiColorVis7, - hideLegend: true, title: i18n.translate('xpack.apm.errorRate.chart.errorRate', { defaultMessage: 'Error rate (avg.)', }), From cb9afddb9163283682b797d5920f28c6c6dfb0a2 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 26 Jan 2021 08:35:23 -0700 Subject: [PATCH 002/163] [Maps] add video to maps docs (#89039) * [Maps] add video to maps docs * review feedback, revert some changes * move video to be below intro, clean up geojson upload section * Update docs/maps/index.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/maps/index.asciidoc | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index 59b592ba1ec59..8f55697249fb2 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -9,28 +9,40 @@ [partintro] -- -Maps enables you to parse through your geographical data at scale, with speed, and in real time. With features like multiple layers and indices in a map, plotting of raw documents, dynamic client-side styling, and global search across multiple layers, you can understand and monitor your data with ease. +Create beautiful maps from your geographical data. With **Maps**, you can: -With Maps, you can: - -* Create maps with multiple layers and indices. -* Upload GeoJSON files into Elasticsearch. +* Build maps with multiple layers and indices. +* Upload GeoJSON. * Embed your map in dashboards. * Symbolize features using data values. -* Focus in on just the data you want. - -*Ready to get started?* Start your tour of Maps with the <>. +* Focus on only the data that’s important to you. + +*Ready to get started?* Watch the https://videos.elastic.co/watch/BYzRDtH4u7RSD8wKhuEW1b[video], and then start your tour of **Maps** with the <>. + +++++ + + +
+++++ [float] -=== Create maps with multiple layers and indices -You can use multiple layers and indices to show all your data in a single map. This enables your map to show how data sits relative to physical features like weather patterns, human-made features like international borders, and business-specific features like sales regions. You can plot individual documents or use aggregations to plot any data set, no matter how large. +=== Build maps with multiple layers and indices +Use multiple layers and indices to show all your data in a single map. Show how data sits relative to physical features like weather patterns, human-made features like international borders, and business-specific features like sales regions. Plot individual documents or use aggregations to plot any data set, no matter how large. [role="screenshot"] image::maps/images/sample_data_ecommerce.png[] [float] -=== Upload GeoJSON files into Elasticsearch -Maps makes it easy to import geospatial data into the Elastic Stack. Using the GeoJSON Upload feature, you can drag and drop your point and shape data files directly into Elasticsearch, and then use them as layers in the map. +=== Upload GeoJSON +Use **Maps** to drag and drop your GeoJSON points, lines, and polygons into Elasticsearch, and then use them as layers in your map. [float] === Embed your map in dashboards @@ -43,11 +55,11 @@ image::maps/images/embed_in_dashboard.jpeg[] [float] === Symbolize features using data values -You can customize each layer to highlight meaningful dimensions in your data. For example, you can use dark colors to symbolize areas with more web log traffic, and lighter colors to symbolize areas with less traffic. +Customize each layer to highlight meaningful dimensions in your data. For example, use dark colors to symbolize areas with more web log traffic, and lighter colors to symbolize areas with less traffic. [float] -=== Focus in on just the data you want -You can search across your Elasticsearch layers to focus in on just the data you want. Draw a polygon on the map or use the shape from features to create spatial filters to narrow search results to documents that either intersect with, are within, or do not intersect with the specified geometry. Filter individual layers to compares facets. +=== Focus on only the data that’s important to you +Search across your Elasticsearch layers to focus in on just the data you want. Combine free text search with field-based search using the <>. Set the time filter to restrict layers by time. Draw a polygon on the map or use the shape from features to create spatial filters. Filter individual layers to compares facets. -- From a4c884b92bdc081242e3f520f051222f35b9b6b9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 26 Jan 2021 16:48:47 +0100 Subject: [PATCH 003/163] tsconfig file for lens (#89135) --- x-pack/plugins/lens/kibana.json | 1 - .../suffix_formatter.ts | 8 +++- x-pack/plugins/lens/tsconfig.json | 41 +++++++++++++++++++ x-pack/test/tsconfig.json | 1 + x-pack/tsconfig.json | 2 + x-pack/tsconfig.refs.json | 1 + 6 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/lens/tsconfig.json diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 4ecc7f0128591..dc0a92ac702d0 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -12,7 +12,6 @@ "urlForwarding", "visualizations", "dashboard", - "charts", "uiActions", "embeddable", "share" diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts index 3d9f3be01a11b..26541c9f890b9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts @@ -5,7 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { FieldFormat, KBN_FIELD_TYPES } from '../../../../../src/plugins/data/public'; +import { + FieldFormat, + FieldFormatInstanceType, + KBN_FIELD_TYPES, +} from '../../../../../src/plugins/data/public'; import { FormatFactory } from '../types'; import { TimeScaleUnit } from './time_scale'; @@ -23,7 +27,7 @@ export const unitSuffixesLong: Record = { d: i18n.translate('xpack.lens.fieldFormats.longSuffix.d', { defaultMessage: 'per day' }), }; -export function getSuffixFormatter(formatFactory: FormatFactory) { +export function getSuffixFormatter(formatFactory: FormatFactory): FieldFormatInstanceType { return class SuffixFormatter extends FieldFormat { static id = 'suffix'; static title = i18n.translate('xpack.lens.fieldFormats.suffix.title', { diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json new file mode 100644 index 0000000000000..7ac5a2980d0ba --- /dev/null +++ b/x-pack/plugins/lens/tsconfig.json @@ -0,0 +1,41 @@ + +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "*.ts", + "common/**/*", + "public/**/*", + "server/**/*", + "../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../global_search/tsconfig.json"}, + { "path": "../saved_objects_tagging/tsconfig.json"}, + { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/charts/tsconfig.json"}, + { "path": "../../../src/plugins/expressions/tsconfig.json"}, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json"}, + { "path": "../../../src/plugins/lens_oss/tsconfig.json"}, + { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, + ] + } \ No newline at end of file diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 699ff64af3f88..6368751fedf75 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -46,6 +46,7 @@ { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, { "path": "../plugins/event_log/tsconfig.json" }, { "path": "../plugins/licensing/tsconfig.json" }, + { "path": "../plugins/lens/tsconfig.json" }, { "path": "../plugins/task_manager/tsconfig.json" }, { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index ae12773023663..a6eb098b5d678 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -18,6 +18,7 @@ "plugins/embeddable_enhanced/**/*", "plugins/event_log/**/*", "plugins/licensing/**/*", + "plugins/lens/**/*", "plugins/searchprofiler/**/*", "plugins/security_solution/cypress/**/*", "plugins/task_manager/**/*", @@ -84,6 +85,7 @@ { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, { "path": "./plugins/event_log/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, + { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/searchprofiler/tsconfig.json" }, { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 02623b11ce314..6a9e54e2e7adf 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -5,6 +5,7 @@ { "path": "./plugins/alerts/tsconfig.json"}, { "path": "./plugins/dashboard_enhanced/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, + { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/console_extensions/tsconfig.json" }, { "path": "./plugins/discover_enhanced/tsconfig.json" }, { "path": "./plugins/data_enhanced/tsconfig.json" }, From 110e880fbf653cb6388eaf4c750874a9c24b48ed Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 26 Jan 2021 10:06:38 -0600 Subject: [PATCH 004/163] [Workplace Search] Add source logic and sources logic unit tests (#89247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move shared data to mock * Change name of mock Everywhere else we don’t use the “mock” prefix for mocked data so I’m changing here to match. Also added missing “size” prop from mock. * Remove unused actions These were missed on the migration to the new add_source_logic file. All of that logic lives there now * Add tests for source logic * REmove resetFlashMessages This is no longer used as Kibana resets its own. This was removed from the component already. * Export items for use in tests * Remove unnecessary condition It’s literally not possible for this function to receive an empty contentSources parameter. Not sure why this was added. Even if the server sends response with no privateContentSources, the reducer falls back to an empty array. * Add tests for sources logic * Fix typo --- .../__mocks__/content_sources.mock.ts | 11 + .../workplace_search/__mocks__/meta.mock.ts | 3 +- .../components/source_content.test.tsx | 17 +- .../content_sources/source_logic.test.ts | 444 ++++++++++++++++++ .../views/content_sources/source_logic.ts | 7 - .../content_sources/sources_logic.test.ts | 319 +++++++++++++ .../views/content_sources/sources_logic.ts | 16 +- .../views/groups/groups.test.tsx | 4 +- 8 files changed, 785 insertions(+), 36 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 0e0d1fa864033..efae95f83034e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -323,3 +323,14 @@ export const mostRecentIndexJob = { activeReindexJobId: '123', numDocumentsWithErrors: 1, }; + +export const contentItems = [ + { + id: '1234', + last_updated: '2021-01-21', + }, + { + id: '1235', + last_updated: '2021-01-20', + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/meta.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/meta.mock.ts index e596ea5d7e948..acfbad1400c66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/meta.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/meta.mock.ts @@ -6,10 +6,11 @@ import { DEFAULT_META } from '../../shared/constants'; -export const mockMeta = { +export const meta = { ...DEFAULT_META, page: { current: 1, + size: 5, total_results: 50, total_pages: 5, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx index c445a7aec04f6..a404ae508c130 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx @@ -20,8 +20,8 @@ import { EuiLink, } from '@elastic/eui'; -import { mockMeta } from '../../../__mocks__/meta.mock'; -import { fullContentSources } from '../../../__mocks__/content_sources.mock'; +import { meta } from '../../../__mocks__/meta.mock'; +import { fullContentSources, contentItems } from '../../../__mocks__/content_sources.mock'; import { DEFAULT_META } from '../../../../shared/constants'; import { ComponentLoader } from '../../../components/shared/component_loader'; @@ -38,17 +38,8 @@ describe('SourceContent', () => { const mockValues = { contentSource: fullContentSources[0], - contentMeta: mockMeta, - contentItems: [ - { - id: '1234', - last_updated: '2021-01-21', - }, - { - id: '1235', - last_updated: '2021-01-20', - }, - ], + contentMeta: meta, + contentItems, contentFilterValue: '', dataLoading: false, sectionLoading: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts new file mode 100644 index 0000000000000..a0efbfe4aca1d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -0,0 +1,444 @@ +/* + * 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 { + LogicMounter, + mockFlashMessageHelpers, + mockHttpValues, + mockKibanaValues, + expectedAsyncError, +} from '../../../__mocks__'; + +import { AppLogic } from '../../app_logic'; +jest.mock('../../app_logic', () => ({ + AppLogic: { values: { isOrganization: true } }, +})); + +import { + fullContentSources, + sourceConfigData, + contentItems, +} from '../../__mocks__/content_sources.mock'; +import { meta } from '../../__mocks__/meta.mock'; + +import { DEFAULT_META } from '../../../shared/constants'; +import { NOT_FOUND_PATH } from '../../routes'; + +import { SourceLogic } from './source_logic'; + +describe('SourceLogic', () => { + const { http } = mockHttpValues; + const { + clearFlashMessages, + flashAPIErrors, + setSuccessMessage, + setQueuedSuccessMessage, + } = mockFlashMessageHelpers; + const { navigateToUrl } = mockKibanaValues; + const { mount, getListeners } = new LogicMounter(SourceLogic); + + const contentSource = fullContentSources[0]; + + const defaultValues = { + contentSource: {}, + contentItems: [], + sourceConfigData: {}, + dataLoading: true, + sectionLoading: true, + buttonLoading: false, + contentMeta: DEFAULT_META, + contentFilterValue: '', + }; + + const searchServerResponse = { + results: contentItems, + meta, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(SourceLogic.values).toEqual(defaultValues); + }); + + describe('actions', () => { + it('onInitializeSource', () => { + SourceLogic.actions.onInitializeSource(contentSource); + + expect(SourceLogic.values.contentSource).toEqual(contentSource); + expect(SourceLogic.values.dataLoading).toEqual(false); + }); + + it('onUpdateSourceName', () => { + const NAME = 'foo'; + SourceLogic.actions.onInitializeSource(contentSource); + SourceLogic.actions.onUpdateSourceName(NAME); + + expect(SourceLogic.values.contentSource).toEqual({ + ...contentSource, + name: NAME, + }); + expect(setSuccessMessage).toHaveBeenCalled(); + }); + + it('setSourceConfigData', () => { + SourceLogic.actions.setSourceConfigData(sourceConfigData); + + expect(SourceLogic.values.sourceConfigData).toEqual(sourceConfigData); + expect(SourceLogic.values.dataLoading).toEqual(false); + }); + + it('setSearchResults', () => { + SourceLogic.actions.setSearchResults(searchServerResponse); + + expect(SourceLogic.values.contentItems).toEqual(contentItems); + expect(SourceLogic.values.contentMeta).toEqual(meta); + expect(SourceLogic.values.sectionLoading).toEqual(false); + }); + + it('setContentFilterValue', () => { + const VALUE = 'bar'; + SourceLogic.actions.setSearchResults(searchServerResponse); + SourceLogic.actions.onInitializeSource(contentSource); + SourceLogic.actions.setContentFilterValue(VALUE); + + expect(SourceLogic.values.contentMeta).toEqual({ + ...meta, + page: { + ...meta.page, + current: DEFAULT_META.page.current, + }, + }); + expect(SourceLogic.values.contentFilterValue).toEqual(VALUE); + }); + + it('setActivePage', () => { + const PAGE = 2; + SourceLogic.actions.setSearchResults(searchServerResponse); + SourceLogic.actions.setActivePage(PAGE); + + expect(SourceLogic.values.contentMeta).toEqual({ + ...meta, + page: { + ...meta.page, + current: PAGE, + }, + }); + }); + + it('setButtonNotLoading', () => { + // Set button state to loading + SourceLogic.actions.removeContentSource(contentSource.id); + SourceLogic.actions.setButtonNotLoading(); + + expect(SourceLogic.values.buttonLoading).toEqual(false); + }); + }); + + describe('listeners', () => { + describe('initializeSource', () => { + it('calls API and sets values (org)', async () => { + const onInitializeSourceSpy = jest.spyOn(SourceLogic.actions, 'onInitializeSource'); + const promise = Promise.resolve(contentSource); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/sources/123'); + await promise; + expect(onInitializeSourceSpy).toHaveBeenCalledWith(contentSource); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + + const onInitializeSourceSpy = jest.spyOn(SourceLogic.actions, 'onInitializeSource'); + const promise = Promise.resolve(contentSource); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/account/sources/123'); + await promise; + expect(onInitializeSourceSpy).toHaveBeenCalledWith(contentSource); + }); + + it('handles federated source', async () => { + AppLogic.values.isOrganization = false; + + const initializeFederatedSummarySpy = jest.spyOn( + SourceLogic.actions, + 'initializeFederatedSummary' + ); + const promise = Promise.resolve({ + ...contentSource, + isFederatedSource: true, + }); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/account/sources/123'); + await promise; + expect(initializeFederatedSummarySpy).toHaveBeenCalledWith(contentSource.id); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + + it('handles not found state', async () => { + const error = { + response: { + error: 'this is an error', + status: 404, + }, + }; + const promise = Promise.reject(error); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + await expectedAsyncError(promise); + + expect(navigateToUrl).toHaveBeenCalledWith(NOT_FOUND_PATH); + }); + }); + + describe('initializeFederatedSummary', () => { + it('calls API and sets values', async () => { + const onUpdateSummarySpy = jest.spyOn(SourceLogic.actions, 'onUpdateSummary'); + const promise = Promise.resolve(contentSource); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeFederatedSummary(contentSource.id); + + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/org/sources/123/federated_summary' + ); + await promise; + expect(onUpdateSummarySpy).toHaveBeenCalledWith(contentSource.summary); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeFederatedSummary(contentSource.id); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + }); + + describe('searchContentSourceDocuments', () => { + const mockBreakpoint = jest.fn(); + const values = { contentMeta: meta, contentFilterValue: '' }; + const actions = { setSearchResults: jest.fn() }; + const { searchContentSourceDocuments } = getListeners({ + values, + actions, + }); + + it('calls API and sets values (org)', async () => { + AppLogic.values.isOrganization = true; + const promise = Promise.resolve(searchServerResponse); + http.post.mockReturnValue(promise); + + await searchContentSourceDocuments({ sourceId: contentSource.id }, mockBreakpoint); + expect(http.post).toHaveBeenCalledWith('/api/workplace_search/org/sources/123/documents', { + body: JSON.stringify({ query: '', page: meta.page }), + }); + + await promise; + expect(actions.setSearchResults).toHaveBeenCalledWith(searchServerResponse); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + const promise = Promise.resolve(searchServerResponse); + http.post.mockReturnValue(promise); + + SourceLogic.actions.searchContentSourceDocuments(contentSource.id); + await searchContentSourceDocuments({ sourceId: contentSource.id }, mockBreakpoint); + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/123/documents', + { + body: JSON.stringify({ query: '', page: meta.page }), + } + ); + + await promise; + expect(actions.setSearchResults).toHaveBeenCalledWith(searchServerResponse); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.post.mockReturnValue(promise); + + await searchContentSourceDocuments({ sourceId: contentSource.id }, mockBreakpoint); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + }); + + describe('updateContentSource', () => { + it('calls API and sets values (org)', async () => { + AppLogic.values.isOrganization = true; + + const onUpdateSourceNameSpy = jest.spyOn(SourceLogic.actions, 'onUpdateSourceName'); + const promise = Promise.resolve(contentSource); + http.patch.mockReturnValue(promise); + SourceLogic.actions.updateContentSource(contentSource.id, contentSource); + + expect(http.patch).toHaveBeenCalledWith('/api/workplace_search/org/sources/123/settings', { + body: JSON.stringify({ content_source: contentSource }), + }); + await promise; + expect(onUpdateSourceNameSpy).toHaveBeenCalledWith(contentSource.name); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + + const onUpdateSourceNameSpy = jest.spyOn(SourceLogic.actions, 'onUpdateSourceName'); + const promise = Promise.resolve(contentSource); + http.patch.mockReturnValue(promise); + SourceLogic.actions.updateContentSource(contentSource.id, contentSource); + + expect(http.patch).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/123/settings', + { + body: JSON.stringify({ content_source: contentSource }), + } + ); + await promise; + expect(onUpdateSourceNameSpy).toHaveBeenCalledWith(contentSource.name); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.patch.mockReturnValue(promise); + SourceLogic.actions.updateContentSource(contentSource.id, contentSource); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + }); + + describe('removeContentSource', () => { + it('calls API and sets values (org)', async () => { + AppLogic.values.isOrganization = true; + + const setButtonNotLoadingSpy = jest.spyOn(SourceLogic.actions, 'setButtonNotLoading'); + const promise = Promise.resolve(contentSource); + http.delete.mockReturnValue(promise); + SourceLogic.actions.removeContentSource(contentSource.id); + + expect(clearFlashMessages).toHaveBeenCalled(); + expect(http.delete).toHaveBeenCalledWith('/api/workplace_search/org/sources/123'); + await promise; + expect(setQueuedSuccessMessage).toHaveBeenCalled(); + expect(setButtonNotLoadingSpy).toHaveBeenCalled(); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + + const setButtonNotLoadingSpy = jest.spyOn(SourceLogic.actions, 'setButtonNotLoading'); + const promise = Promise.resolve(contentSource); + http.delete.mockReturnValue(promise); + SourceLogic.actions.removeContentSource(contentSource.id); + + expect(clearFlashMessages).toHaveBeenCalled(); + expect(http.delete).toHaveBeenCalledWith('/api/workplace_search/account/sources/123'); + await promise; + expect(setButtonNotLoadingSpy).toHaveBeenCalled(); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.delete.mockReturnValue(promise); + SourceLogic.actions.removeContentSource(contentSource.id); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + }); + + describe('getSourceConfigData', () => { + const serviceType = 'github'; + + it('calls API and sets values', async () => { + AppLogic.values.isOrganization = true; + + const setSourceConfigDataSpy = jest.spyOn(SourceLogic.actions, 'setSourceConfigData'); + const promise = Promise.resolve(contentSource); + http.get.mockReturnValue(promise); + SourceLogic.actions.getSourceConfigData(serviceType); + + expect(http.get).toHaveBeenCalledWith( + `/api/workplace_search/org/settings/connectors/${serviceType}` + ); + await promise; + expect(setSourceConfigDataSpy).toHaveBeenCalled(); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.get.mockReturnValue(promise); + SourceLogic.actions.getSourceConfigData(serviceType); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + }); + + it('resetSourceState', () => { + SourceLogic.actions.resetSourceState(); + + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 2de70009c56a2..ba5c29c190f95 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -126,29 +126,22 @@ export const SourceLogic = kea>({ onInitializeSource: () => false, setSourceConfigData: () => false, resetSourceState: () => false, - setPreContentSourceConfigData: () => false, }, ], buttonLoading: [ false, { setButtonNotLoading: () => false, - setSourceConnectData: () => false, setSourceConfigData: () => false, resetSourceState: () => false, removeContentSource: () => true, - saveSourceConfig: () => true, - getSourceConnectData: () => true, - createContentSource: () => true, }, ], sectionLoading: [ true, { searchContentSourceDocuments: () => true, - getPreContentSourceConfigData: () => true, setSearchResults: () => false, - setPreContentSourceConfigData: () => false, }, ], contentItems: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts new file mode 100644 index 0000000000000..11e3a52081637 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts @@ -0,0 +1,319 @@ +/* + * 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 { + LogicMounter, + mockFlashMessageHelpers, + mockHttpValues, + expectedAsyncError, +} from '../../../__mocks__'; + +import { AppLogic } from '../../app_logic'; +jest.mock('../../app_logic', () => ({ + AppLogic: { values: { isOrganization: true } }, +})); + +import { configuredSources, contentSources } from '../../__mocks__/content_sources.mock'; + +import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_logic'; + +describe('SourcesLogic', () => { + const { http } = mockHttpValues; + const { flashAPIErrors, setQueuedSuccessMessage } = mockFlashMessageHelpers; + const { mount, unmount } = new LogicMounter(SourcesLogic); + + const contentSource = contentSources[0]; + + const defaultValues = { + contentSources: [], + privateContentSources: [], + sourceData: [], + availableSources: [], + configuredSources: [], + serviceTypes: [], + permissionsModal: null, + dataLoading: true, + serverStatuses: null, + }; + + const serverStatuses = [ + { + id: '123', + name: 'my source', + service_type: 'github', + status: { + status: 'this is a thing', + synced_at: '2021-01-25', + error_reason: 1, + }, + }, + ]; + + const serverResponse = { + contentSources, + privateContentSources: contentSources, + serviceTypes: configuredSources, + }; + + beforeEach(() => { + jest.useFakeTimers(); + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(SourcesLogic.values).toEqual(defaultValues); + }); + + it('handles unmounting', async () => { + unmount(); + expect(clearInterval).toHaveBeenCalled(); + }); + + describe('actions', () => { + describe('onInitializeSources', () => { + it('sets values', () => { + SourcesLogic.actions.onInitializeSources(serverResponse); + + expect(SourcesLogic.values.contentSources).toEqual(contentSources); + expect(SourcesLogic.values.privateContentSources).toEqual(contentSources); + expect(SourcesLogic.values.serviceTypes).toEqual(configuredSources); + expect(SourcesLogic.values.dataLoading).toEqual(false); + }); + + it('fallbacks', () => { + SourcesLogic.actions.onInitializeSources({ + contentSources, + serviceTypes: undefined as any, + }); + + expect(SourcesLogic.values.serviceTypes).toEqual([]); + expect(SourcesLogic.values.privateContentSources).toEqual([]); + }); + }); + + it('setServerSourceStatuses', () => { + SourcesLogic.actions.setServerSourceStatuses(serverStatuses); + const source = serverStatuses[0]; + + expect(SourcesLogic.values.serverStatuses).toEqual({ + [source.id]: source.status.status, + }); + }); + + it('onSetSearchability', () => { + const id = contentSources[0].id; + const updatedSources = [...contentSources]; + updatedSources[0].searchable = false; + SourcesLogic.actions.onInitializeSources(serverResponse); + SourcesLogic.actions.onSetSearchability(id, false); + + expect(SourcesLogic.values.contentSources).toEqual(updatedSources); + expect(SourcesLogic.values.privateContentSources).toEqual(updatedSources); + }); + + describe('setAddedSource', () => { + it('configured', () => { + const name = contentSources[0].name; + SourcesLogic.actions.setAddedSource(name, false, 'custom'); + + expect(SourcesLogic.values.permissionsModal).toEqual({ + addedSourceName: name, + additionalConfiguration: false, + serviceType: 'custom', + }); + expect(setQueuedSuccessMessage).toHaveBeenCalledWith('Successfully connected source. '); + }); + + it('unconfigured', () => { + const name = contentSources[0].name; + SourcesLogic.actions.setAddedSource(name, true, 'custom'); + + expect(SourcesLogic.values.permissionsModal).toEqual({ + addedSourceName: name, + additionalConfiguration: true, + serviceType: 'custom', + }); + expect(setQueuedSuccessMessage).toHaveBeenCalledWith( + 'Successfully connected source. This source requires additional configuration.' + ); + }); + }); + + it('resetPermissionsModal', () => { + SourcesLogic.actions.resetPermissionsModal(); + + expect(SourcesLogic.values.permissionsModal).toEqual(null); + }); + }); + + describe('listeners', () => { + describe('initializeSources', () => { + it('calls API and sets values (org)', async () => { + AppLogic.values.isOrganization = true; + const pollForSourceStatusChangesSpy = jest.spyOn( + SourcesLogic.actions, + 'pollForSourceStatusChanges' + ); + const onInitializeSourcesSpy = jest.spyOn(SourcesLogic.actions, 'onInitializeSources'); + const promise = Promise.resolve(contentSources); + http.get.mockReturnValue(promise); + SourcesLogic.actions.initializeSources(); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/sources'); + await promise; + expect(pollForSourceStatusChangesSpy).toHaveBeenCalled(); + expect(onInitializeSourcesSpy).toHaveBeenCalledWith(contentSources); + }); + + it('calls API (account)', async () => { + AppLogic.values.isOrganization = false; + const promise = Promise.resolve(contentSource); + http.get.mockReturnValue(promise); + SourcesLogic.actions.initializeSources(); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/account/sources'); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.get.mockReturnValue(promise); + SourcesLogic.actions.initializeSources(); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + }); + + describe('setSourceSearchability', () => { + const id = contentSources[0].id; + + it('calls API and sets values (org)', async () => { + AppLogic.values.isOrganization = true; + const onSetSearchability = jest.spyOn(SourcesLogic.actions, 'onSetSearchability'); + const promise = Promise.resolve(contentSources); + http.put.mockReturnValue(promise); + SourcesLogic.actions.setSourceSearchability(id, true); + + expect(http.put).toHaveBeenCalledWith('/api/workplace_search/org/sources/123/searchable', { + body: JSON.stringify({ searchable: true }), + }); + await promise; + expect(onSetSearchability).toHaveBeenCalledWith(id, true); + }); + + it('calls API (account)', async () => { + AppLogic.values.isOrganization = false; + const promise = Promise.resolve(contentSource); + http.put.mockReturnValue(promise); + SourcesLogic.actions.setSourceSearchability(id, true); + + expect(http.put).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/123/searchable', + { + body: JSON.stringify({ searchable: true }), + } + ); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.put.mockReturnValue(promise); + SourcesLogic.actions.setSourceSearchability(id, true); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + }); + + describe('pollForSourceStatusChanges', () => { + it('calls API and sets values', async () => { + AppLogic.values.isOrganization = true; + SourcesLogic.actions.setServerSourceStatuses(serverStatuses); + + const setServerSourceStatusesSpy = jest.spyOn( + SourcesLogic.actions, + 'setServerSourceStatuses' + ); + const promise = Promise.resolve(contentSources); + http.get.mockReturnValue(promise); + SourcesLogic.actions.pollForSourceStatusChanges(); + + jest.advanceTimersByTime(POLLING_INTERVAL); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/sources/status'); + await promise; + expect(setServerSourceStatusesSpy).toHaveBeenCalledWith(contentSources); + }); + }); + + it('resetSourcesState', () => { + SourcesLogic.actions.resetSourcesState(); + + expect(clearInterval).toHaveBeenCalled(); + }); + }); + + describe('selectors', () => { + it('availableSources & configuredSources have correct length', () => { + SourcesLogic.actions.onInitializeSources(serverResponse); + + expect(SourcesLogic.values.availableSources).toHaveLength(1); + expect(SourcesLogic.values.configuredSources).toHaveLength(5); + }); + }); + + describe('fetchSourceStatuses', () => { + it('calls API and sets values (org)', async () => { + const setServerSourceStatusesSpy = jest.spyOn( + SourcesLogic.actions, + 'setServerSourceStatuses' + ); + const promise = Promise.resolve(contentSources); + http.get.mockReturnValue(promise); + fetchSourceStatuses(true); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/sources/status'); + await promise; + expect(setServerSourceStatusesSpy).toHaveBeenCalledWith(contentSources); + }); + + it('calls API (account)', async () => { + const promise = Promise.resolve(contentSource); + http.get.mockReturnValue(promise); + fetchSourceStatuses(false); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/account/sources/status'); + }); + + it('handles error', async () => { + const error = { + response: { + error: 'this is an error', + status: 400, + }, + }; + const promise = Promise.reject(error); + http.get.mockReturnValue(promise); + fetchSourceStatuses(true); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index 0a3d047796f49..57e1a97e7bdf6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -12,11 +12,7 @@ import { i18n } from '@kbn/i18n'; import { HttpLogic } from '../../../shared/http'; -import { - flashAPIErrors, - setQueuedSuccessMessage, - clearFlashMessages, -} from '../../../shared/flash_messages'; +import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types'; @@ -40,7 +36,6 @@ export interface ISourcesActions { additionalConfiguration: boolean, serviceType: string ): { addedSourceName: string; additionalConfiguration: boolean; serviceType: string }; - resetFlashMessages(): void; resetPermissionsModal(): void; resetSourcesState(): void; initializeSources(): void; @@ -78,7 +73,7 @@ interface ISourcesServerResponse { } let pollingInterval: number; -const POLLING_INTERVAL = 10000; +export const POLLING_INTERVAL = 10000; export const SourcesLogic = kea>({ path: ['enterprise_search', 'workplace_search', 'sources_logic'], @@ -91,7 +86,6 @@ export const SourcesLogic = kea>( additionalConfiguration: boolean, serviceType: string ) => ({ addedSourceName, additionalConfiguration, serviceType }), - resetFlashMessages: () => true, resetPermissionsModal: () => true, resetSourcesState: () => true, initializeSources: () => true, @@ -238,9 +232,6 @@ export const SourcesLogic = kea>( ].join(' ') ); }, - resetFlashMessages: () => { - clearFlashMessages(); - }, resetSourcesState: () => { clearInterval(pollingInterval); }, @@ -252,7 +243,7 @@ export const SourcesLogic = kea>( }), }); -const fetchSourceStatuses = async (isOrganization: boolean) => { +export const fetchSourceStatuses = async (isOrganization: boolean) => { const route = isOrganization ? '/api/workplace_search/org/sources/status' : '/api/workplace_search/account/sources/status'; @@ -273,7 +264,6 @@ const updateSourcesOnToggle = ( sourceId: string, searchable: boolean ): ContentSourceDetails[] => { - if (!contentSources) return []; const sources = cloneDeep(contentSources) as ContentSourceDetails[]; const index = findIndex(sources, ({ id }) => id === sourceId); const updatedSource = sources[index]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx index 7c746f75ffc94..30f345d8e017e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -7,7 +7,7 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../__mocks__'; import { groups } from '../../__mocks__/groups.mock'; -import { mockMeta } from '../../__mocks__/meta.mock'; +import { meta } from '../../__mocks__/meta.mock'; import React from 'react'; import { shallow } from 'enzyme'; @@ -46,7 +46,7 @@ const mockValues = { newGroup: null, groupListLoading: false, hasFiltersSet: false, - groupsMeta: mockMeta, + groupsMeta: meta, filteredSources: [], filteredUsers: [], filterValue: '', From b35a4e645d2ae2a4f44a8e127bd75d4b53e2ab50 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 26 Jan 2021 17:36:44 +0100 Subject: [PATCH 005/163] [ML] Fix swim lane time selection with a single time point and the Watcher URL (#89125) * [ML] fix swim lane selected times with only start boundaries * [ML] unit test * [ML] update url variables * [ML] selectedLanes to an array type * [ML] handle legacy query params --- .../ml/common/types/ml_url_generator.ts | 13 ++++++- .../explorer/hooks/use_selected_cells.test.ts | 34 +++++++++++++++++ .../explorer/hooks/use_selected_cells.ts | 37 +++++++++++++------ .../components/create_watch_flyout/email.html | 2 +- 4 files changed, 73 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index 3ff57fc622da4..d7fded8299952 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -93,7 +93,18 @@ export interface ExplorerAppState { mlExplorerSwimlane: { selectedType?: 'overall' | 'viewBy'; selectedLanes?: string[]; - selectedTimes?: [number, number]; + /** + * @deprecated legacy query param variable, use `selectedLanes` + */ + selectedLane?: string[] | string; + /** + * It's possible to have only "from" time boundaries, e.g. in the Watcher URL + */ + selectedTimes?: [number, number] | number; + /** + * @deprecated legacy query param variable, use `selectedTimes` + */ + selectedTime?: [number, number] | number; showTopFieldValues?: boolean; viewByFieldName?: string; viewByPerPage?: number; diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.test.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.test.ts index 08c8d11987f19..2308d4ae4f15c 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.test.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.test.ts @@ -130,4 +130,38 @@ describe('useSelectedCells', () => { }, }); }); + + test('should extend single time point selection with a bucket interval value', () => { + (useTimefilter() as jest.Mocked).getBounds.mockReturnValue({ + min: moment(1498824778 * 1000), + max: moment(1502366798 * 1000), + }); + + const urlState = { + mlExplorerSwimlane: { + selectedType: 'overall', + selectedLanes: ['Overall'], + selectedTimes: 1498780800, + showTopFieldValues: true, + viewByFieldName: 'apache2.access.remote_ip', + viewByFromPage: 1, + viewByPerPage: 10, + }, + mlExplorerFilter: {}, + } as ExplorerAppState; + + const setUrlState = jest.fn(); + + const bucketInterval = 86400; + + const { result } = renderHook(() => useSelectedCells(urlState, setUrlState, bucketInterval)); + + expect(result.current[0]).toEqual({ + lanes: ['Overall'], + showTopFieldValues: true, + times: [1498780800, 1498867200], + type: 'overall', + viewByFieldName: 'apache2.access.remote_ip', + }); + }); }); diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts index 4ce828b0b7633..becc6197af888 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts @@ -19,18 +19,33 @@ export const useSelectedCells = ( const timeBounds = timeFilter.getBounds(); // keep swimlane selection, restore selectedCells from AppState - const selectedCells = useMemo(() => { - return appState?.mlExplorerSwimlane?.selectedType !== undefined - ? { - type: appState.mlExplorerSwimlane.selectedType, - lanes: appState.mlExplorerSwimlane.selectedLanes!, - times: appState.mlExplorerSwimlane.selectedTimes!, - showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues, - viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName, - } - : undefined; + const selectedCells: AppStateSelectedCells | undefined = useMemo(() => { + if (!appState?.mlExplorerSwimlane?.selectedType) { + return; + } + + let times = + appState.mlExplorerSwimlane.selectedTimes ?? appState.mlExplorerSwimlane.selectedTime!; + if (typeof times === 'number' && bucketIntervalInSeconds) { + times = [times, times + bucketIntervalInSeconds]; + } + + let lanes = + appState.mlExplorerSwimlane.selectedLanes ?? appState.mlExplorerSwimlane.selectedLane!; + + if (typeof lanes === 'string') { + lanes = [lanes]; + } + + return { + type: appState.mlExplorerSwimlane.selectedType, + lanes, + times, + showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues, + viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName, + } as AppStateSelectedCells; // TODO fix appState to use memoization - }, [JSON.stringify(appState?.mlExplorerSwimlane)]); + }, [JSON.stringify(appState?.mlExplorerSwimlane), bucketIntervalInSeconds]); const setSelectedCells = useCallback( (swimlaneSelectedCells?: AppStateSelectedCells) => { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html index 2e93c7eefcf1e..713a68ba0c036 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html @@ -22,7 +22,7 @@

- + <%= openInAnomalyExplorerLinkText %>
From 1f644e44c97ecd6e3a522bcbc4cb25b3ec42c3aa Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 26 Jan 2021 17:42:41 +0100 Subject: [PATCH 006/163] Update babel to v7.12 (#89006) * bump babel version * build kbn-pm * fix integration test * remove cicular dependency between files which crashes Kibana in rutime Co-authored-by: spalger --- package.json | 48 +- packages/kbn-pm/dist/index.js | 926 ++++++++--------- .../reporting/server/lib/layouts/index.ts | 4 +- .../reporting/server/lib/layouts/layout.ts | 4 +- .../server/lib/layouts/preserve_layout.ts | 3 +- .../server/lib/layouts/print_layout.ts | 3 +- yarn.lock | 971 +++++++++++++++++- 7 files changed, 1371 insertions(+), 588 deletions(-) diff --git a/package.json b/package.json index dac83dacf6fbf..d14c5b0a7dc5f 100644 --- a/package.json +++ b/package.json @@ -93,8 +93,8 @@ "yarn": "^1.21.1" }, "dependencies": { - "@babel/core": "^7.11.6", - "@babel/runtime": "^7.11.2", + "@babel/core": "^7.12.10", + "@babel/runtime": "^7.12.5", "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", "@elastic/ems-client": "7.11.0", @@ -330,22 +330,22 @@ "yauzl": "^2.10.0" }, "devDependencies": { - "@babel/cli": "^7.10.5", - "@babel/parser": "^7.11.2", - "@babel/plugin-proposal-class-properties": "^7.10.4", - "@babel/plugin-proposal-export-namespace-from": "^7.10.4", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", - "@babel/plugin-proposal-object-rest-spread": "^7.11.0", - "@babel/plugin-proposal-optional-chaining": "^7.11.0", - "@babel/plugin-proposal-private-methods": "^7.10.4", - "@babel/plugin-transform-modules-commonjs": "^7.10.4", - "@babel/plugin-transform-runtime": "^7.11.0", - "@babel/preset-env": "^7.11.0", - "@babel/preset-react": "^7.10.4", - "@babel/preset-typescript": "^7.10.4", - "@babel/register": "^7.10.5", - "@babel/traverse": "^7.11.5", - "@babel/types": "^7.11.0", + "@babel/cli": "^7.12.10", + "@babel/parser": "^7.12.11", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-export-namespace-from": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", + "@babel/plugin-proposal-private-methods": "^7.12.1", + "@babel/plugin-transform-modules-commonjs": "^7.12.1", + "@babel/plugin-transform-runtime": "^7.12.10", + "@babel/preset-env": "^7.12.11", + "@babel/preset-react": "^7.12.10", + "@babel/preset-typescript": "^7.12.7", + "@babel/register": "^7.12.10", + "@babel/traverse": "^7.12.12", + "@babel/types": "^7.12.12", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.5.0", "@elastic/apm-rum": "^5.6.1", @@ -407,7 +407,7 @@ "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", "@types/archiver": "^5.1.0", - "@types/babel__core": "^7.1.10", + "@types/babel__core": "^7.1.12", "@types/base64-js": "^1.2.5", "@types/bluebird": "^3.1.1", "@types/chance": "^1.0.0", @@ -580,10 +580,10 @@ "argsplit": "^1.0.5", "autoprefixer": "^9.7.4", "axe-core": "^4.0.2", - "babel-eslint": "^10.0.3", - "babel-jest": "^26.3.0", - "babel-loader": "^8.0.6", - "babel-plugin-add-module-exports": "^1.0.2", + "babel-eslint": "^10.1.0", + "babel-jest": "^26.6.3", + "babel-loader": "^8.2.2", + "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-istanbul": "^6.0.0", "babel-plugin-require-context-hook": "^1.0.0", "babel-plugin-styled-components": "^1.10.7", @@ -629,7 +629,7 @@ "eslint-import-resolver-node": "0.3.2", "eslint-import-resolver-webpack": "0.11.1", "eslint-module-utils": "2.5.0", - "eslint-plugin-babel": "^5.3.0", + "eslint-plugin-babel": "^5.3.1", "eslint-plugin-ban": "^1.4.0", "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-eslint-comments": "^3.2.0", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 0c74315d0f3fb..09995a9be30a6 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -59381,11 +59381,11 @@ const os = __webpack_require__(121); const pMap = __webpack_require__(513); const arrify = __webpack_require__(508); const globby = __webpack_require__(514); -const hasGlob = __webpack_require__(714); -const cpFile = __webpack_require__(716); -const junk = __webpack_require__(726); -const pFilter = __webpack_require__(727); -const CpyError = __webpack_require__(729); +const hasGlob = __webpack_require__(710); +const cpFile = __webpack_require__(712); +const junk = __webpack_require__(722); +const pFilter = __webpack_require__(723); +const CpyError = __webpack_require__(725); const defaultOptions = { ignoreJunk: true @@ -59633,8 +59633,8 @@ const fs = __webpack_require__(134); const arrayUnion = __webpack_require__(515); const glob = __webpack_require__(147); const fastGlob = __webpack_require__(517); -const dirGlob = __webpack_require__(707); -const gitignore = __webpack_require__(710); +const dirGlob = __webpack_require__(703); +const gitignore = __webpack_require__(706); const DEFAULT_FILTER = () => false; @@ -59885,11 +59885,11 @@ module.exports.generateTasks = pkg.generateTasks; Object.defineProperty(exports, "__esModule", { value: true }); var optionsManager = __webpack_require__(519); var taskManager = __webpack_require__(520); -var reader_async_1 = __webpack_require__(678); -var reader_stream_1 = __webpack_require__(702); -var reader_sync_1 = __webpack_require__(703); -var arrayUtils = __webpack_require__(705); -var streamUtils = __webpack_require__(706); +var reader_async_1 = __webpack_require__(674); +var reader_stream_1 = __webpack_require__(698); +var reader_sync_1 = __webpack_require__(699); +var arrayUtils = __webpack_require__(701); +var streamUtils = __webpack_require__(702); /** * Synchronous API. */ @@ -60470,16 +60470,16 @@ module.exports.win32 = win32; var util = __webpack_require__(112); var braces = __webpack_require__(526); var toRegex = __webpack_require__(527); -var extend = __webpack_require__(644); +var extend = __webpack_require__(640); /** * Local dependencies */ -var compilers = __webpack_require__(646); -var parsers = __webpack_require__(673); -var cache = __webpack_require__(674); -var utils = __webpack_require__(675); +var compilers = __webpack_require__(642); +var parsers = __webpack_require__(669); +var cache = __webpack_require__(670); +var utils = __webpack_require__(671); var MAX_LENGTH = 1024 * 64; /** @@ -61360,8 +61360,8 @@ var extend = __webpack_require__(550); */ var compilers = __webpack_require__(552); -var parsers = __webpack_require__(567); -var Braces = __webpack_require__(571); +var parsers = __webpack_require__(565); +var Braces = __webpack_require__(569); var utils = __webpack_require__(553); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -63801,7 +63801,7 @@ utils.extend = __webpack_require__(550); utils.flatten = __webpack_require__(557); utils.isObject = __webpack_require__(535); utils.fillRange = __webpack_require__(558); -utils.repeat = __webpack_require__(566); +utils.repeat = __webpack_require__(564); utils.unique = __webpack_require__(549); utils.define = function(obj, key, val) { @@ -64444,9 +64444,9 @@ function flat(arr, res) { var util = __webpack_require__(112); var isNumber = __webpack_require__(559); -var extend = __webpack_require__(562); -var repeat = __webpack_require__(564); -var toRegex = __webpack_require__(565); +var extend = __webpack_require__(550); +var repeat = __webpack_require__(562); +var toRegex = __webpack_require__(563); /** * Return a range of numbers or letters. @@ -64825,66 +64825,6 @@ function isSlowBuffer (obj) { /* 562 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; - - -var isObject = __webpack_require__(563); - -module.exports = function extend(o/*, objects*/) { - if (!isObject(o)) { o = {}; } - - var len = arguments.length; - for (var i = 1; i < len; i++) { - var obj = arguments[i]; - - if (isObject(obj)) { - assign(o, obj); - } - } - return o; -}; - -function assign(a, b) { - for (var key in b) { - if (hasOwn(b, key)) { - a[key] = b[key]; - } - } -} - -/** - * Returns true if the given `key` is an own property of `obj`. - */ - -function hasOwn(obj, key) { - return Object.prototype.hasOwnProperty.call(obj, key); -} - - -/***/ }), -/* 563 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * is-extendable - * - * Copyright (c) 2015, Jon Schlinkert. - * Licensed under the MIT License. - */ - - - -module.exports = function isExtendable(val) { - return typeof val !== 'undefined' && val !== null - && (typeof val === 'object' || typeof val === 'function'); -}; - - -/***/ }), -/* 564 */ -/***/ (function(module, exports, __webpack_require__) { - "use strict"; /*! * repeat-string @@ -64959,7 +64899,7 @@ function repeat(str, num) { /***/ }), -/* 565 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64972,7 +64912,7 @@ function repeat(str, num) { -var repeat = __webpack_require__(564); +var repeat = __webpack_require__(562); var isNumber = __webpack_require__(559); var cache = {}; @@ -65260,7 +65200,7 @@ module.exports = toRegexRange; /***/ }), -/* 566 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65285,13 +65225,13 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 567 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(568); +var Node = __webpack_require__(566); var utils = __webpack_require__(553); /** @@ -65652,15 +65592,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 568 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var isObject = __webpack_require__(535); -var define = __webpack_require__(569); -var utils = __webpack_require__(570); +var define = __webpack_require__(567); +var utils = __webpack_require__(568); var ownNames; /** @@ -66151,7 +66091,7 @@ exports = module.exports = Node; /***/ }), -/* 569 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66189,7 +66129,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 570 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67215,16 +67155,16 @@ function assert(val, message) { /***/ }), -/* 571 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var extend = __webpack_require__(550); -var Snapdragon = __webpack_require__(572); +var Snapdragon = __webpack_require__(570); var compilers = __webpack_require__(552); -var parsers = __webpack_require__(567); +var parsers = __webpack_require__(565); var utils = __webpack_require__(553); /** @@ -67326,17 +67266,17 @@ module.exports = Braces; /***/ }), -/* 572 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(573); -var define = __webpack_require__(602); -var Compiler = __webpack_require__(612); -var Parser = __webpack_require__(641); -var utils = __webpack_require__(621); +var Base = __webpack_require__(571); +var define = __webpack_require__(598); +var Compiler = __webpack_require__(608); +var Parser = __webpack_require__(637); +var utils = __webpack_require__(617); var regexCache = {}; var cache = {}; @@ -67507,20 +67447,20 @@ module.exports.Parser = Parser; /***/ }), -/* 573 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(574); -var CacheBase = __webpack_require__(575); -var Emitter = __webpack_require__(576); +var define = __webpack_require__(572); +var CacheBase = __webpack_require__(573); +var Emitter = __webpack_require__(574); var isObject = __webpack_require__(535); -var merge = __webpack_require__(596); -var pascal = __webpack_require__(599); -var cu = __webpack_require__(600); +var merge = __webpack_require__(592); +var pascal = __webpack_require__(595); +var cu = __webpack_require__(596); /** * Optionally define a custom `cache` namespace to use. @@ -67949,7 +67889,7 @@ module.exports.namespace = namespace; /***/ }), -/* 574 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67987,21 +67927,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 575 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var isObject = __webpack_require__(535); -var Emitter = __webpack_require__(576); -var visit = __webpack_require__(577); -var toPath = __webpack_require__(580); -var union = __webpack_require__(581); -var del = __webpack_require__(587); -var get = __webpack_require__(584); -var has = __webpack_require__(592); -var set = __webpack_require__(595); +var Emitter = __webpack_require__(574); +var visit = __webpack_require__(575); +var toPath = __webpack_require__(578); +var union = __webpack_require__(579); +var del = __webpack_require__(583); +var get = __webpack_require__(581); +var has = __webpack_require__(588); +var set = __webpack_require__(591); /** * Create a `Cache` constructor that when instantiated will @@ -68255,7 +68195,7 @@ module.exports.namespace = namespace; /***/ }), -/* 576 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { @@ -68424,7 +68364,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 577 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68437,8 +68377,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(578); -var mapVisit = __webpack_require__(579); +var visit = __webpack_require__(576); +var mapVisit = __webpack_require__(577); module.exports = function(collection, method, val) { var result; @@ -68461,7 +68401,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 578 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68501,14 +68441,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 579 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(578); +var visit = __webpack_require__(576); /** * Map `visit` over an array of objects. @@ -68545,7 +68485,7 @@ function isObject(val) { /***/ }), -/* 580 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68585,16 +68525,16 @@ function filter(arr) { /***/ }), -/* 581 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(582); -var union = __webpack_require__(583); -var get = __webpack_require__(584); -var set = __webpack_require__(585); +var isObject = __webpack_require__(551); +var union = __webpack_require__(580); +var get = __webpack_require__(581); +var set = __webpack_require__(582); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -68622,27 +68562,7 @@ function arrayify(val) { /***/ }), -/* 582 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * is-extendable - * - * Copyright (c) 2015, Jon Schlinkert. - * Licensed under the MIT License. - */ - - - -module.exports = function isExtendable(val) { - return typeof val !== 'undefined' && val !== null - && (typeof val === 'object' || typeof val === 'function'); -}; - - -/***/ }), -/* 583 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68678,7 +68598,7 @@ module.exports = function union(init) { /***/ }), -/* 584 */ +/* 581 */ /***/ (function(module, exports) { /*! @@ -68734,7 +68654,7 @@ function toString(val) { /***/ }), -/* 585 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68748,9 +68668,9 @@ function toString(val) { var split = __webpack_require__(554); -var extend = __webpack_require__(586); +var extend = __webpack_require__(550); var isPlainObject = __webpack_require__(544); -var isObject = __webpack_require__(582); +var isObject = __webpack_require__(551); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -68796,47 +68716,7 @@ function isValidKey(key) { /***/ }), -/* 586 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -var isObject = __webpack_require__(582); - -module.exports = function extend(o/*, objects*/) { - if (!isObject(o)) { o = {}; } - - var len = arguments.length; - for (var i = 1; i < len; i++) { - var obj = arguments[i]; - - if (isObject(obj)) { - assign(o, obj); - } - } - return o; -}; - -function assign(a, b) { - for (var key in b) { - if (hasOwn(b, key)) { - a[key] = b[key]; - } - } -} - -/** - * Returns true if the given `key` is an own property of `obj`. - */ - -function hasOwn(obj, key) { - return Object.prototype.hasOwnProperty.call(obj, key); -} - - -/***/ }), -/* 587 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68850,7 +68730,7 @@ function hasOwn(obj, key) { var isObject = __webpack_require__(535); -var has = __webpack_require__(588); +var has = __webpack_require__(584); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -68875,7 +68755,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 588 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68888,9 +68768,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(589); -var hasValues = __webpack_require__(591); -var get = __webpack_require__(584); +var isObject = __webpack_require__(585); +var hasValues = __webpack_require__(587); +var get = __webpack_require__(581); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -68901,7 +68781,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 589 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68914,7 +68794,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(590); +var isArray = __webpack_require__(586); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -68922,7 +68802,7 @@ module.exports = function isObject(val) { /***/ }), -/* 590 */ +/* 586 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -68933,7 +68813,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 591 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68976,7 +68856,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 592 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68990,8 +68870,8 @@ module.exports = function hasValue(o, noZero) { var isObject = __webpack_require__(535); -var hasValues = __webpack_require__(593); -var get = __webpack_require__(584); +var hasValues = __webpack_require__(589); +var get = __webpack_require__(581); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -68999,7 +68879,7 @@ module.exports = function(val, prop) { /***/ }), -/* 593 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69012,7 +68892,7 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(594); +var typeOf = __webpack_require__(590); var isNumber = __webpack_require__(559); module.exports = function hasValue(val) { @@ -69066,7 +68946,7 @@ module.exports = function hasValue(val) { /***/ }), -/* 594 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(561); @@ -69191,7 +69071,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 595 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69205,9 +69085,9 @@ module.exports = function kindOf(val) { var split = __webpack_require__(554); -var extend = __webpack_require__(586); +var extend = __webpack_require__(550); var isPlainObject = __webpack_require__(544); -var isObject = __webpack_require__(582); +var isObject = __webpack_require__(551); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69253,14 +69133,14 @@ function isValidKey(key) { /***/ }), -/* 596 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(597); -var forIn = __webpack_require__(598); +var isExtendable = __webpack_require__(593); +var forIn = __webpack_require__(594); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69324,7 +69204,7 @@ module.exports = mixinDeep; /***/ }), -/* 597 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69345,7 +69225,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 598 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69368,7 +69248,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 599 */ +/* 595 */ /***/ (function(module, exports) { /*! @@ -69395,14 +69275,14 @@ module.exports = pascalcase; /***/ }), -/* 600 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(601); +var utils = __webpack_require__(597); /** * Expose class utils @@ -69767,7 +69647,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 601 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69781,10 +69661,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(583); -utils.define = __webpack_require__(602); +utils.union = __webpack_require__(580); +utils.define = __webpack_require__(598); utils.isObj = __webpack_require__(535); -utils.staticExtend = __webpack_require__(609); +utils.staticExtend = __webpack_require__(605); /** @@ -69795,7 +69675,7 @@ module.exports = utils; /***/ }), -/* 602 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69808,7 +69688,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(603); +var isDescriptor = __webpack_require__(599); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -69833,7 +69713,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 603 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69846,9 +69726,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(604); -var isAccessor = __webpack_require__(605); -var isData = __webpack_require__(607); +var typeOf = __webpack_require__(600); +var isAccessor = __webpack_require__(601); +var isData = __webpack_require__(603); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -69862,7 +69742,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 604 */ +/* 600 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -70015,7 +69895,7 @@ function isBuffer(val) { /***/ }), -/* 605 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70028,7 +69908,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(606); +var typeOf = __webpack_require__(602); // accessor descriptor properties var accessor = { @@ -70091,7 +69971,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 606 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(561); @@ -70213,7 +70093,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 607 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70226,7 +70106,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(608); +var typeOf = __webpack_require__(604); // data descriptor properties var data = { @@ -70275,7 +70155,7 @@ module.exports = isDataDescriptor; /***/ }), -/* 608 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(561); @@ -70397,7 +70277,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 609 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70410,8 +70290,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(610); -var define = __webpack_require__(602); +var copy = __webpack_require__(606); +var define = __webpack_require__(598); var util = __webpack_require__(112); /** @@ -70494,15 +70374,15 @@ module.exports = extend; /***/ }), -/* 610 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var typeOf = __webpack_require__(560); -var copyDescriptor = __webpack_require__(611); -var define = __webpack_require__(602); +var copyDescriptor = __webpack_require__(607); +var define = __webpack_require__(598); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -70675,7 +70555,7 @@ module.exports.has = has; /***/ }), -/* 611 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70763,16 +70643,16 @@ function isObject(val) { /***/ }), -/* 612 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(613); -var define = __webpack_require__(602); -var debug = __webpack_require__(615)('snapdragon:compiler'); -var utils = __webpack_require__(621); +var use = __webpack_require__(609); +var define = __webpack_require__(598); +var debug = __webpack_require__(611)('snapdragon:compiler'); +var utils = __webpack_require__(617); /** * Create a new `Compiler` with the given `options`. @@ -70926,7 +70806,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(640); + var sourcemaps = __webpack_require__(636); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -70947,7 +70827,7 @@ module.exports = Compiler; /***/ }), -/* 613 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70960,7 +70840,7 @@ module.exports = Compiler; -var utils = __webpack_require__(614); +var utils = __webpack_require__(610); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -71075,7 +70955,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 614 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71089,7 +70969,7 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(602); +utils.define = __webpack_require__(598); utils.isObject = __webpack_require__(535); @@ -71105,7 +70985,7 @@ module.exports = utils; /***/ }), -/* 615 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71114,14 +70994,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(616); + module.exports = __webpack_require__(612); } else { - module.exports = __webpack_require__(619); + module.exports = __webpack_require__(615); } /***/ }), -/* 616 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71130,7 +71010,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(617); +exports = module.exports = __webpack_require__(613); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -71312,7 +71192,7 @@ function localstorage() { /***/ }), -/* 617 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { @@ -71328,7 +71208,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(618); +exports.humanize = __webpack_require__(614); /** * The currently active debug mode names, and names to skip. @@ -71520,7 +71400,7 @@ function coerce(val) { /***/ }), -/* 618 */ +/* 614 */ /***/ (function(module, exports) { /** @@ -71678,7 +71558,7 @@ function plural(ms, n, name) { /***/ }), -/* 619 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71694,7 +71574,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(617); +exports = module.exports = __webpack_require__(613); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -71873,7 +71753,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(620); + var net = __webpack_require__(616); stream = new net.Socket({ fd: fd, readable: false, @@ -71932,13 +71812,13 @@ exports.enable(load()); /***/ }), -/* 620 */ +/* 616 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 621 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71948,9 +71828,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(586); -exports.SourceMap = __webpack_require__(622); -exports.sourceMapResolve = __webpack_require__(633); +exports.extend = __webpack_require__(550); +exports.SourceMap = __webpack_require__(618); +exports.sourceMapResolve = __webpack_require__(629); /** * Convert backslash in the given string to forward slashes @@ -71993,7 +71873,7 @@ exports.last = function(arr, n) { /***/ }), -/* 622 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -72001,13 +71881,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(623).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(629).SourceMapConsumer; -exports.SourceNode = __webpack_require__(632).SourceNode; +exports.SourceMapGenerator = __webpack_require__(619).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(625).SourceMapConsumer; +exports.SourceNode = __webpack_require__(628).SourceNode; /***/ }), -/* 623 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72017,10 +71897,10 @@ exports.SourceNode = __webpack_require__(632).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(624); -var util = __webpack_require__(626); -var ArraySet = __webpack_require__(627).ArraySet; -var MappingList = __webpack_require__(628).MappingList; +var base64VLQ = __webpack_require__(620); +var util = __webpack_require__(622); +var ArraySet = __webpack_require__(623).ArraySet; +var MappingList = __webpack_require__(624).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -72429,7 +72309,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 624 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72469,7 +72349,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(625); +var base64 = __webpack_require__(621); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -72575,7 +72455,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 625 */ +/* 621 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72648,7 +72528,7 @@ exports.decode = function (charCode) { /***/ }), -/* 626 */ +/* 622 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73071,7 +72951,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 627 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73081,7 +72961,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(626); +var util = __webpack_require__(622); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73198,7 +73078,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 628 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73208,7 +73088,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(626); +var util = __webpack_require__(622); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73283,7 +73163,7 @@ exports.MappingList = MappingList; /***/ }), -/* 629 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73293,11 +73173,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(626); -var binarySearch = __webpack_require__(630); -var ArraySet = __webpack_require__(627).ArraySet; -var base64VLQ = __webpack_require__(624); -var quickSort = __webpack_require__(631).quickSort; +var util = __webpack_require__(622); +var binarySearch = __webpack_require__(626); +var ArraySet = __webpack_require__(623).ArraySet; +var base64VLQ = __webpack_require__(620); +var quickSort = __webpack_require__(627).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -74371,7 +74251,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 630 */ +/* 626 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74488,7 +74368,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 631 */ +/* 627 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74608,7 +74488,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 632 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74618,8 +74498,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(623).SourceMapGenerator; -var util = __webpack_require__(626); +var SourceMapGenerator = __webpack_require__(619).SourceMapGenerator; +var util = __webpack_require__(622); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -75027,17 +74907,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 633 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(634) -var resolveUrl = __webpack_require__(635) -var decodeUriComponent = __webpack_require__(636) -var urix = __webpack_require__(638) -var atob = __webpack_require__(639) +var sourceMappingURL = __webpack_require__(630) +var resolveUrl = __webpack_require__(631) +var decodeUriComponent = __webpack_require__(632) +var urix = __webpack_require__(634) +var atob = __webpack_require__(635) @@ -75335,7 +75215,7 @@ module.exports = { /***/ }), -/* 634 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -75398,7 +75278,7 @@ void (function(root, factory) { /***/ }), -/* 635 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75416,13 +75296,13 @@ module.exports = resolveUrl /***/ }), -/* 636 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(637) +var decodeUriComponent = __webpack_require__(633) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -75433,7 +75313,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 637 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75534,7 +75414,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 638 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75557,7 +75437,7 @@ module.exports = urix /***/ }), -/* 639 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75571,7 +75451,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 640 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75579,8 +75459,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(602); -var utils = __webpack_require__(621); +var define = __webpack_require__(598); +var utils = __webpack_require__(617); /** * Expose `mixin()`. @@ -75723,19 +75603,19 @@ exports.comment = function(node) { /***/ }), -/* 641 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(613); +var use = __webpack_require__(609); var util = __webpack_require__(112); -var Cache = __webpack_require__(642); -var define = __webpack_require__(602); -var debug = __webpack_require__(615)('snapdragon:parser'); -var Position = __webpack_require__(643); -var utils = __webpack_require__(621); +var Cache = __webpack_require__(638); +var define = __webpack_require__(598); +var debug = __webpack_require__(611)('snapdragon:parser'); +var Position = __webpack_require__(639); +var utils = __webpack_require__(617); /** * Create a new `Parser` with the given `input` and `options`. @@ -76263,7 +76143,7 @@ module.exports = Parser; /***/ }), -/* 642 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76370,13 +76250,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 643 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(602); +var define = __webpack_require__(598); /** * Store position for a node @@ -76391,13 +76271,13 @@ module.exports = function Position(start, parser) { /***/ }), -/* 644 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(645); +var isExtendable = __webpack_require__(641); var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { @@ -76458,7 +76338,7 @@ function isEnum(obj, key) { /***/ }), -/* 645 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76479,14 +76359,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 646 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(647); -var extglob = __webpack_require__(662); +var nanomatch = __webpack_require__(643); +var extglob = __webpack_require__(658); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -76563,7 +76443,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 647 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76575,16 +76455,16 @@ function escapeExtglobs(compiler) { var util = __webpack_require__(112); var toRegex = __webpack_require__(527); -var extend = __webpack_require__(648); +var extend = __webpack_require__(644); /** * Local dependencies */ -var compilers = __webpack_require__(650); -var parsers = __webpack_require__(651); -var cache = __webpack_require__(654); -var utils = __webpack_require__(656); +var compilers = __webpack_require__(646); +var parsers = __webpack_require__(647); +var cache = __webpack_require__(650); +var utils = __webpack_require__(652); var MAX_LENGTH = 1024 * 64; /** @@ -77408,13 +77288,13 @@ module.exports = nanomatch; /***/ }), -/* 648 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(649); +var isExtendable = __webpack_require__(645); var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { @@ -77475,7 +77355,7 @@ function isEnum(obj, key) { /***/ }), -/* 649 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77496,7 +77376,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 650 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77842,7 +77722,7 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 651 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77850,7 +77730,7 @@ module.exports = function(nanomatch, options) { var regexNot = __webpack_require__(546); var toRegex = __webpack_require__(527); -var isOdd = __webpack_require__(652); +var isOdd = __webpack_require__(648); /** * Characters to use in negation regex (we want to "not" match @@ -78236,7 +78116,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 652 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78249,7 +78129,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(653); +var isNumber = __webpack_require__(649); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78263,7 +78143,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 653 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78291,14 +78171,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 654 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(655))(); +module.exports = new (__webpack_require__(651))(); /***/ }), -/* 655 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78311,7 +78191,7 @@ module.exports = new (__webpack_require__(655))(); -var MapCache = __webpack_require__(642); +var MapCache = __webpack_require__(638); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78433,7 +78313,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 656 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78446,13 +78326,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(657)(); -var Snapdragon = __webpack_require__(572); -utils.define = __webpack_require__(658); -utils.diff = __webpack_require__(659); -utils.extend = __webpack_require__(648); -utils.pick = __webpack_require__(660); -utils.typeOf = __webpack_require__(661); +var isWindows = __webpack_require__(653)(); +var Snapdragon = __webpack_require__(570); +utils.define = __webpack_require__(654); +utils.diff = __webpack_require__(655); +utils.extend = __webpack_require__(644); +utils.pick = __webpack_require__(656); +utils.typeOf = __webpack_require__(657); utils.unique = __webpack_require__(549); /** @@ -78819,7 +78699,7 @@ utils.unixify = function(options) { /***/ }), -/* 657 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -78847,7 +78727,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 658 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78892,7 +78772,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 659 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78946,7 +78826,7 @@ function diffArray(one, two) { /***/ }), -/* 660 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78988,7 +78868,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 661 */ +/* 657 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -79123,7 +79003,7 @@ function isBuffer(val) { /***/ }), -/* 662 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79133,7 +79013,7 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(586); +var extend = __webpack_require__(550); var unique = __webpack_require__(549); var toRegex = __webpack_require__(527); @@ -79141,10 +79021,10 @@ var toRegex = __webpack_require__(527); * Local dependencies */ -var compilers = __webpack_require__(663); -var parsers = __webpack_require__(669); -var Extglob = __webpack_require__(672); -var utils = __webpack_require__(671); +var compilers = __webpack_require__(659); +var parsers = __webpack_require__(665); +var Extglob = __webpack_require__(668); +var utils = __webpack_require__(667); var MAX_LENGTH = 1024 * 64; /** @@ -79461,13 +79341,13 @@ module.exports = extglob; /***/ }), -/* 663 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(664); +var brackets = __webpack_require__(660); /** * Extglob compilers @@ -79637,7 +79517,7 @@ module.exports = function(extglob) { /***/ }), -/* 664 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79647,16 +79527,16 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(665); -var parsers = __webpack_require__(667); +var compilers = __webpack_require__(661); +var parsers = __webpack_require__(663); /** * Module dependencies */ -var debug = __webpack_require__(615)('expand-brackets'); -var extend = __webpack_require__(586); -var Snapdragon = __webpack_require__(572); +var debug = __webpack_require__(611)('expand-brackets'); +var extend = __webpack_require__(550); +var Snapdragon = __webpack_require__(570); var toRegex = __webpack_require__(527); /** @@ -79855,13 +79735,13 @@ module.exports = brackets; /***/ }), -/* 665 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(666); +var posix = __webpack_require__(662); module.exports = function(brackets) { brackets.compiler @@ -79949,7 +79829,7 @@ module.exports = function(brackets) { /***/ }), -/* 666 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79978,14 +79858,14 @@ module.exports = { /***/ }), -/* 667 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(668); -var define = __webpack_require__(602); +var utils = __webpack_require__(664); +var define = __webpack_require__(598); /** * Text regex @@ -80204,7 +80084,7 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 668 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80245,15 +80125,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 669 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(664); -var define = __webpack_require__(670); -var utils = __webpack_require__(671); +var brackets = __webpack_require__(660); +var define = __webpack_require__(666); +var utils = __webpack_require__(667); /** * Characters to use in text regex (we want to "not" match @@ -80408,7 +80288,7 @@ module.exports = parsers; /***/ }), -/* 670 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80446,14 +80326,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 671 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var regex = __webpack_require__(546); -var Cache = __webpack_require__(655); +var Cache = __webpack_require__(651); /** * Utils @@ -80522,7 +80402,7 @@ utils.createRegex = function(str) { /***/ }), -/* 672 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80532,16 +80412,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(572); -var define = __webpack_require__(670); -var extend = __webpack_require__(586); +var Snapdragon = __webpack_require__(570); +var define = __webpack_require__(666); +var extend = __webpack_require__(550); /** * Local dependencies */ -var compilers = __webpack_require__(663); -var parsers = __webpack_require__(669); +var compilers = __webpack_require__(659); +var parsers = __webpack_require__(665); /** * Customize Snapdragon parser and renderer @@ -80607,14 +80487,14 @@ module.exports = Extglob; /***/ }), -/* 673 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(662); -var nanomatch = __webpack_require__(647); +var extglob = __webpack_require__(658); +var nanomatch = __webpack_require__(643); var regexNot = __webpack_require__(546); var toRegex = __webpack_require__(527); var not; @@ -80697,14 +80577,14 @@ function textRegex(pattern) { /***/ }), -/* 674 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(655))(); +module.exports = new (__webpack_require__(651))(); /***/ }), -/* 675 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80717,12 +80597,12 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(572); -utils.define = __webpack_require__(676); -utils.diff = __webpack_require__(659); -utils.extend = __webpack_require__(644); -utils.pick = __webpack_require__(660); -utils.typeOf = __webpack_require__(677); +var Snapdragon = __webpack_require__(570); +utils.define = __webpack_require__(672); +utils.diff = __webpack_require__(655); +utils.extend = __webpack_require__(640); +utils.pick = __webpack_require__(656); +utils.typeOf = __webpack_require__(673); utils.unique = __webpack_require__(549); /** @@ -81020,7 +80900,7 @@ utils.unixify = function(options) { /***/ }), -/* 676 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81065,7 +80945,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 677 */ +/* 673 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81200,7 +81080,7 @@ function isBuffer(val) { /***/ }), -/* 678 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81219,9 +81099,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(679); -var reader_1 = __webpack_require__(692); -var fs_stream_1 = __webpack_require__(696); +var readdir = __webpack_require__(675); +var reader_1 = __webpack_require__(688); +var fs_stream_1 = __webpack_require__(692); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81282,15 +81162,15 @@ exports.default = ReaderAsync; /***/ }), -/* 679 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(680); -const readdirAsync = __webpack_require__(688); -const readdirStream = __webpack_require__(691); +const readdirSync = __webpack_require__(676); +const readdirAsync = __webpack_require__(684); +const readdirStream = __webpack_require__(687); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81374,7 +81254,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 680 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81382,11 +81262,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(681); +const DirectoryReader = __webpack_require__(677); let syncFacade = { - fs: __webpack_require__(686), - forEach: __webpack_require__(687), + fs: __webpack_require__(682), + forEach: __webpack_require__(683), sync: true }; @@ -81415,7 +81295,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 681 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81424,9 +81304,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(682); -const stat = __webpack_require__(684); -const call = __webpack_require__(685); +const normalizeOptions = __webpack_require__(678); +const stat = __webpack_require__(680); +const call = __webpack_require__(681); /** * Asynchronously reads the contents of a directory and streams the results @@ -81802,14 +81682,14 @@ module.exports = DirectoryReader; /***/ }), -/* 682 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(683); +const globToRegExp = __webpack_require__(679); module.exports = normalizeOptions; @@ -81986,7 +81866,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 683 */ +/* 679 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82123,13 +82003,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 684 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(685); +const call = __webpack_require__(681); module.exports = stat; @@ -82204,7 +82084,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 685 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82265,14 +82145,14 @@ function callOnce (fn) { /***/ }), -/* 686 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(685); +const call = __webpack_require__(681); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82336,7 +82216,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 687 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82365,7 +82245,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 688 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82373,12 +82253,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(689); -const DirectoryReader = __webpack_require__(681); +const maybe = __webpack_require__(685); +const DirectoryReader = __webpack_require__(677); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(690), + forEach: __webpack_require__(686), async: true }; @@ -82420,7 +82300,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 689 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82447,7 +82327,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 690 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82483,7 +82363,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 691 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82491,11 +82371,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(681); +const DirectoryReader = __webpack_require__(677); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(690), + forEach: __webpack_require__(686), async: true }; @@ -82515,16 +82395,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 692 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(693); -var entry_1 = __webpack_require__(695); -var pathUtil = __webpack_require__(694); +var deep_1 = __webpack_require__(689); +var entry_1 = __webpack_require__(691); +var pathUtil = __webpack_require__(690); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -82590,13 +82470,13 @@ exports.default = Reader; /***/ }), -/* 693 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(694); +var pathUtils = __webpack_require__(690); var patternUtils = __webpack_require__(521); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { @@ -82680,7 +82560,7 @@ exports.default = DeepFilter; /***/ }), -/* 694 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82711,13 +82591,13 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 695 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(694); +var pathUtils = __webpack_require__(690); var patternUtils = __webpack_require__(521); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { @@ -82803,7 +82683,7 @@ exports.default = EntryFilter; /***/ }), -/* 696 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82823,8 +82703,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(697); -var fs_1 = __webpack_require__(701); +var fsStat = __webpack_require__(693); +var fs_1 = __webpack_require__(697); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -82874,14 +82754,14 @@ exports.default = FileSystemStream; /***/ }), -/* 697 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(698); -const statProvider = __webpack_require__(700); +const optionsManager = __webpack_require__(694); +const statProvider = __webpack_require__(696); /** * Asynchronous API. */ @@ -82912,13 +82792,13 @@ exports.statSync = statSync; /***/ }), -/* 698 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(699); +const fsAdapter = __webpack_require__(695); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -82931,7 +82811,7 @@ exports.prepare = prepare; /***/ }), -/* 699 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82954,7 +82834,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 700 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83006,7 +82886,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 701 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83037,7 +82917,7 @@ exports.default = FileSystem; /***/ }), -/* 702 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83057,9 +82937,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(679); -var reader_1 = __webpack_require__(692); -var fs_stream_1 = __webpack_require__(696); +var readdir = __webpack_require__(675); +var reader_1 = __webpack_require__(688); +var fs_stream_1 = __webpack_require__(692); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83127,7 +83007,7 @@ exports.default = ReaderStream; /***/ }), -/* 703 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83146,9 +83026,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(679); -var reader_1 = __webpack_require__(692); -var fs_sync_1 = __webpack_require__(704); +var readdir = __webpack_require__(675); +var reader_1 = __webpack_require__(688); +var fs_sync_1 = __webpack_require__(700); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83208,7 +83088,7 @@ exports.default = ReaderSync; /***/ }), -/* 704 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83227,8 +83107,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(697); -var fs_1 = __webpack_require__(701); +var fsStat = __webpack_require__(693); +var fs_1 = __webpack_require__(697); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83274,7 +83154,7 @@ exports.default = FileSystemSync; /***/ }), -/* 705 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83290,7 +83170,7 @@ exports.flatten = flatten; /***/ }), -/* 706 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83311,13 +83191,13 @@ exports.merge = merge; /***/ }), -/* 707 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(708); +const pathType = __webpack_require__(704); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83383,13 +83263,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 708 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(709); +const pify = __webpack_require__(705); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83432,7 +83312,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 709 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83523,7 +83403,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 710 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83531,9 +83411,9 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const fastGlob = __webpack_require__(517); -const gitIgnore = __webpack_require__(711); -const pify = __webpack_require__(712); -const slash = __webpack_require__(713); +const gitIgnore = __webpack_require__(707); +const pify = __webpack_require__(708); +const slash = __webpack_require__(709); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -83631,7 +83511,7 @@ module.exports.sync = options => { /***/ }), -/* 711 */ +/* 707 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84100,7 +83980,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 712 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84175,7 +84055,7 @@ module.exports = (input, options) => { /***/ }), -/* 713 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84193,7 +84073,7 @@ module.exports = input => { /***/ }), -/* 714 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84206,7 +84086,7 @@ module.exports = input => { -var isGlob = __webpack_require__(715); +var isGlob = __webpack_require__(711); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84226,7 +84106,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 715 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84257,17 +84137,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 716 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(717); -const CpFileError = __webpack_require__(720); -const fs = __webpack_require__(722); -const ProgressEmitter = __webpack_require__(725); +const pEvent = __webpack_require__(713); +const CpFileError = __webpack_require__(716); +const fs = __webpack_require__(718); +const ProgressEmitter = __webpack_require__(721); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84381,12 +84261,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 717 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(718); +const pTimeout = __webpack_require__(714); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -84677,12 +84557,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 718 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(719); +const pFinally = __webpack_require__(715); class TimeoutError extends Error { constructor(message) { @@ -84728,7 +84608,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 719 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84750,12 +84630,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 720 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(721); +const NestedError = __webpack_require__(717); class CpFileError extends NestedError { constructor(message, nested) { @@ -84769,7 +84649,7 @@ module.exports = CpFileError; /***/ }), -/* 721 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -84825,16 +84705,16 @@ module.exports = NestedError; /***/ }), -/* 722 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(723); -const pEvent = __webpack_require__(717); -const CpFileError = __webpack_require__(720); +const makeDir = __webpack_require__(719); +const pEvent = __webpack_require__(713); +const CpFileError = __webpack_require__(716); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -84931,7 +84811,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 723 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84939,7 +84819,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(724); +const semver = __webpack_require__(720); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85094,7 +84974,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 724 */ +/* 720 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -86696,7 +86576,7 @@ function coerce (version, options) { /***/ }), -/* 725 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86737,7 +86617,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 726 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86783,12 +86663,12 @@ exports.default = module.exports; /***/ }), -/* 727 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(728); +const pMap = __webpack_require__(724); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -86805,7 +86685,7 @@ module.exports.default = pFilter; /***/ }), -/* 728 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86884,12 +86764,12 @@ module.exports.default = pMap; /***/ }), -/* 729 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(721); +const NestedError = __webpack_require__(717); class CpyError extends NestedError { constructor(message, nested) { diff --git a/x-pack/plugins/reporting/server/lib/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts index e0b5b3f095443..b6804b6ac8c8a 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -7,7 +7,7 @@ import { LevelLogger } from '../'; import { LayoutSelectorDictionary, Size } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; -import { Layout } from './layout'; +import type { Layout } from './layout'; export { LayoutParams, @@ -17,7 +17,7 @@ export { Size, } from '../../../common/types'; export { createLayout } from './create_layout'; -export { Layout } from './layout'; +export type { Layout } from './layout'; export { PreserveLayout } from './preserve_layout'; export { CanvasLayout } from './canvas_layout'; export { PrintLayout } from './print_layout'; diff --git a/x-pack/plugins/reporting/server/lib/layouts/layout.ts b/x-pack/plugins/reporting/server/lib/layouts/layout.ts index cb068632063a5..027404f4c33e2 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/layout.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CustomPageSize, PredefinedPageSize } from 'pdfmake/interfaces'; -import { PageSizeParams, PdfImageSize, Size } from './'; +import type { CustomPageSize, PredefinedPageSize } from 'pdfmake/interfaces'; +import type { PageSizeParams, PdfImageSize, Size } from '../../../common/types'; export interface ViewZoomWidthHeight { zoom: number; diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index ccd08e01fec19..e7c84f2088319 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -9,7 +9,8 @@ import { CustomPageSize } from 'pdfmake/interfaces'; import { getDefaultLayoutSelectors } from '../../../common'; import { LAYOUT_TYPES } from '../../../common/constants'; import { LayoutSelectorDictionary, PageSizeParams, Size } from '../../../common/types'; -import { Layout, LayoutInstance } from './'; +import type { LayoutInstance } from './'; +import { Layout } from './layout'; // We use a zoom of two to bump up the resolution of the screenshot a bit. const ZOOM: number = 2; diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index 8db1fa7ff6347..c51582aa404d9 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -13,7 +13,8 @@ import { LAYOUT_TYPES } from '../../../common/constants'; import { LayoutSelectorDictionary, Size } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { Layout, LayoutInstance } from './'; +import type { LayoutInstance } from './'; +import { Layout } from './layout'; export class PrintLayout extends Layout implements LayoutInstance { public readonly selectors: LayoutSelectorDictionary = { diff --git a/yarn.lock b/yarn.lock index 7625510d3b915..174f1284a3a6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@babel/cli@^7.10.5": - version "7.11.6" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.11.6.tgz#1fcbe61c2a6900c3539c06ee58901141f3558482" - integrity sha512-+w7BZCvkewSmaRM6H4L2QM3RL90teqEIHDIFXAmrW33+0jhlymnDAEdqVeCZATvxhQuio1ifoGVlJJbIiH9Ffg== +"@babel/cli@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.12.10.tgz#67a1015b1cd505bde1696196febf910c4c339a48" + integrity sha512-+y4ZnePpvWs1fc/LhZRTHkTesbXkyBYuOB+5CyodZqrEuETXi3zOVfpAQIdgC3lXbHLTDG9dQosxR9BhvLKDLQ== dependencies: commander "^4.0.1" convert-source-map "^1.1.0" @@ -16,7 +16,8 @@ slash "^2.0.0" source-map "^0.5.0" optionalDependencies: - chokidar "^2.1.8" + "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents" + chokidar "^3.4.0" "@babel/code-frame@7.8.3": version "7.8.3" @@ -32,6 +33,13 @@ dependencies: "@babel/highlight" "^7.10.4" +"@babel/code-frame@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" @@ -41,6 +49,16 @@ invariant "^2.2.4" semver "^5.5.0" +"@babel/compat-data@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.5.tgz#f56db0c4bb1bbbf221b4e81345aab4141e7cb0e9" + integrity sha512-DTsS7cxrsH3by8nqQSpFSyjSfSYl57D6Cf4q8dW3LK83tBKBDCkfcay1nYkXq1nIHXnpX8WMMb/O25HOy3h1zg== + +"@babel/compat-data@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41" + integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw== + "@babel/core@7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330" @@ -83,7 +101,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@>=7.9.0", "@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.7.5", "@babel/core@^7.9.0": +"@babel/core@>=7.9.0", "@babel/core@^7.1.0", "@babel/core@^7.7.5", "@babel/core@^7.9.0": version "7.11.6" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651" integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg== @@ -105,6 +123,27 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd" + integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.10" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.5" + "@babel/parser" "^7.12.10" + "@babel/template" "^7.12.7" + "@babel/traverse" "^7.12.10" + "@babel/types" "^7.12.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.10.5", "@babel/generator@^7.11.5", "@babel/generator@^7.11.6", "@babel/generator@^7.4.4", "@babel/generator@^7.9.6": version "7.11.6" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" @@ -114,6 +153,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.10", "@babel/generator@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" + integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== + dependencies: + "@babel/types" "^7.12.11" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -121,6 +169,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-annotate-as-pure@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d" + integrity sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ== + dependencies: + "@babel/types" "^7.12.10" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" @@ -157,6 +212,16 @@ levenary "^1.1.1" semver "^5.5.0" +"@babel/helper-compilation-targets@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831" + integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw== + dependencies: + "@babel/compat-data" "^7.12.5" + "@babel/helper-validator-option" "^7.12.1" + browserslist "^4.14.5" + semver "^5.5.0" + "@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5", "@babel/helper-create-class-features-plugin@^7.3.0": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" @@ -169,6 +234,17 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" +"@babel/helper-create-class-features-plugin@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e" + integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -178,6 +254,15 @@ "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" +"@babel/helper-create-regexp-features-plugin@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz#18b1302d4677f9dc4740fe8c9ed96680e29d37e8" + integrity sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-regex" "^7.10.4" + regexpu-core "^4.7.1" + "@babel/helper-define-map@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" @@ -204,6 +289,15 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-function-name@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" + integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== + dependencies: + "@babel/helper-get-function-arity" "^7.12.10" + "@babel/template" "^7.12.7" + "@babel/types" "^7.12.11" + "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -211,6 +305,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-get-function-arity@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" + integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== + dependencies: + "@babel/types" "^7.12.10" + "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" @@ -225,6 +326,13 @@ dependencies: "@babel/types" "^7.11.0" +"@babel/helper-member-expression-to-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" + integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" @@ -232,6 +340,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" + integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== + dependencies: + "@babel/types" "^7.12.5" + "@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" @@ -245,6 +360,21 @@ "@babel/types" "^7.11.0" lodash "^4.17.19" +"@babel/helper-module-transforms@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" + integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== + dependencies: + "@babel/helper-module-imports" "^7.12.1" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-simple-access" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + lodash "^4.17.19" + "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -275,6 +405,15 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-remap-async-to-generator@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" + integrity sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-wrap-function" "^7.10.4" + "@babel/types" "^7.12.1" + "@babel/helper-replace-supers@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" @@ -285,6 +424,16 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-replace-supers@^7.12.1": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9" + integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" + "@babel/helper-simple-access@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" @@ -293,6 +442,13 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-simple-access@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" + integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-skip-transparent-expression-wrappers@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" @@ -300,6 +456,13 @@ dependencies: "@babel/types" "^7.11.0" +"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" + integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" @@ -307,11 +470,33 @@ dependencies: "@babel/types" "^7.11.0" +"@babel/helper-split-export-declaration@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" + integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== + dependencies: + "@babel/types" "^7.12.11" + "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-identifier@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== + +"@babel/helper-validator-option@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz#175567380c3e77d60ff98a54bb015fe78f2178d9" + integrity sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A== + +"@babel/helper-validator-option@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f" + integrity sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw== + "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -331,6 +516,15 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helpers@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" + integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" + "@babel/highlight@^7.10.4", "@babel/highlight@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" @@ -340,11 +534,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.11.2", "@babel/parser@^7.11.5", "@babel/parser@^7.2.0", "@babel/parser@^7.4.5", "@babel/parser@^7.9.6": +"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.11.5", "@babel/parser@^7.2.0", "@babel/parser@^7.4.5", "@babel/parser@^7.9.6": version "7.11.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== +"@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" + integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== + "@babel/plugin-proposal-async-generator-functions@^7.10.4", "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" @@ -354,6 +553,15 @@ "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" +"@babel/plugin-proposal-async-generator-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e" + integrity sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.12.1" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-proposal-class-properties@7.3.0": version "7.3.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.0.tgz#272636bc0fa19a0bc46e601ec78136a173ea36cd" @@ -370,6 +578,14 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-proposal-class-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de" + integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-decorators@^7.8.3": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4" @@ -387,6 +603,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" +"@babel/plugin-proposal-dynamic-import@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc" + integrity sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-proposal-export-default-from@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.10.4.tgz#08f66eef0067cbf6a7bc036977dcdccecaf0c6c5" @@ -403,6 +627,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" +"@babel/plugin-proposal-export-namespace-from@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4" + integrity sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-proposal-json-strings@^7.10.4", "@babel/plugin-proposal-json-strings@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" @@ -411,6 +643,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" +"@babel/plugin-proposal-json-strings@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c" + integrity sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-proposal-logical-assignment-operators@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" @@ -419,6 +659,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" +"@babel/plugin-proposal-logical-assignment-operators@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751" + integrity sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1", "@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" @@ -427,6 +675,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" +"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c" + integrity sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-proposal-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" @@ -435,6 +691,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" +"@babel/plugin-proposal-numeric-separator@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b" + integrity sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.4.tgz#50129ac216b9a6a55b3853fdd923e74bf553a4c0" @@ -461,6 +725,15 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.10.4" +"@babel/plugin-proposal-object-rest-spread@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069" + integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-transform-parameters" "^7.12.1" + "@babel/plugin-proposal-optional-catch-binding@^7.10.4", "@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" @@ -469,6 +742,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" +"@babel/plugin-proposal-optional-catch-binding@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942" + integrity sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-proposal-optional-chaining@^7.10.1", "@babel/plugin-proposal-optional-chaining@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" @@ -478,6 +759,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" +"@babel/plugin-proposal-optional-chaining@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c" + integrity sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-proposal-private-methods@^7.10.4", "@babel/plugin-proposal-private-methods@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" @@ -486,6 +776,14 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-proposal-private-methods@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389" + integrity sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" @@ -494,6 +792,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-proposal-unicode-property-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072" + integrity sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-async-generators@^7.2.0", "@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -515,6 +821,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-class-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978" + integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-decorators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz#6853085b2c429f9d322d02f5a635018cdeb2360c" @@ -571,6 +884,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-jsx@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926" + integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -613,7 +933,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.10.4", "@babel/plugin-syntax-top-level-await@^7.8.3": +"@babel/plugin-syntax-top-level-await@^7.10.4", "@babel/plugin-syntax-top-level-await@^7.12.1", "@babel/plugin-syntax-top-level-await@^7.8.3": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== @@ -627,6 +947,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-typescript@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz#460ba9d77077653803c3dd2e673f76d66b4029e5" + integrity sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-arrow-functions@^7.10.4", "@babel/plugin-transform-arrow-functions@^7.2.0", "@babel/plugin-transform-arrow-functions@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" @@ -634,6 +961,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-arrow-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3" + integrity sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-async-to-generator@^7.10.4", "@babel/plugin-transform-async-to-generator@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" @@ -643,6 +977,15 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-remap-async-to-generator" "^7.10.4" +"@babel/plugin-transform-async-to-generator@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1" + integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A== + dependencies: + "@babel/helper-module-imports" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.12.1" + "@babel/plugin-transform-block-scoped-functions@^7.10.4", "@babel/plugin-transform-block-scoped-functions@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" @@ -650,6 +993,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-block-scoped-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9" + integrity sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-block-scoping@^7.10.4", "@babel/plugin-transform-block-scoping@^7.4.4", "@babel/plugin-transform-block-scoping@^7.8.3": version "7.11.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" @@ -657,6 +1007,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-block-scoping@^7.12.11": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz#d93a567a152c22aea3b1929bb118d1d0a175cdca" + integrity sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-classes@^7.10.4", "@babel/plugin-transform-classes@^7.4.4", "@babel/plugin-transform-classes@^7.9.5": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" @@ -671,6 +1028,20 @@ "@babel/helper-split-export-declaration" "^7.10.4" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6" + integrity sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-define-map" "^7.10.4" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.10.4" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.10.4", "@babel/plugin-transform-computed-properties@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" @@ -678,6 +1049,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-computed-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852" + integrity sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-destructuring@^7.10.4", "@babel/plugin-transform-destructuring@^7.4.4", "@babel/plugin-transform-destructuring@^7.9.5": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" @@ -685,6 +1063,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-destructuring@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847" + integrity sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" @@ -693,6 +1078,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-dotall-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975" + integrity sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-duplicate-keys@^7.10.4", "@babel/plugin-transform-duplicate-keys@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" @@ -700,6 +1093,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-duplicate-keys@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228" + integrity sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator@^7.10.4", "@babel/plugin-transform-exponentiation-operator@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" @@ -708,6 +1108,14 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-exponentiation-operator@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0" + integrity sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-flow-strip-types@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.10.4.tgz#c497957f09e86e3df7296271e9eb642876bf7788" @@ -723,6 +1131,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-for-of@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa" + integrity sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-function-name@^7.10.4", "@babel/plugin-transform-function-name@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" @@ -731,6 +1146,14 @@ "@babel/helper-function-name" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-function-name@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667" + integrity sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-literals@^7.10.4", "@babel/plugin-transform-literals@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" @@ -738,6 +1161,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57" + integrity sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-member-expression-literals@^7.10.4", "@babel/plugin-transform-member-expression-literals@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" @@ -745,6 +1175,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-member-expression-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad" + integrity sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-modules-amd@^7.10.4", "@babel/plugin-transform-modules-amd@^7.2.0": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" @@ -754,6 +1191,15 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-amd@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9" + integrity sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ== + dependencies: + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" @@ -764,6 +1210,16 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-commonjs@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648" + integrity sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag== + dependencies: + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-simple-access" "^7.12.1" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-systemjs@^7.10.4", "@babel/plugin-transform-modules-systemjs@^7.4.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" @@ -774,6 +1230,17 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-systemjs@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086" + integrity sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q== + dependencies: + "@babel/helper-hoist-variables" "^7.10.4" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-identifier" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-umd@^7.10.4", "@babel/plugin-transform-modules-umd@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" @@ -782,6 +1249,14 @@ "@babel/helper-module-transforms" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-modules-umd@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902" + integrity sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q== + dependencies: + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex@^7.10.4", "@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" @@ -789,6 +1264,13 @@ dependencies: "@babel/helper-create-regexp-features-plugin" "^7.10.4" +"@babel/plugin-transform-named-capturing-groups-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753" + integrity sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.12.1" + "@babel/plugin-transform-new-target@^7.10.4", "@babel/plugin-transform-new-target@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" @@ -796,6 +1278,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-new-target@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0" + integrity sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-object-super@^7.10.4", "@babel/plugin-transform-object-super@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" @@ -804,6 +1293,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" +"@babel/plugin-transform-object-super@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e" + integrity sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.4.4", "@babel/plugin-transform-parameters@^7.9.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" @@ -812,6 +1309,13 @@ "@babel/helper-get-function-arity" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-parameters@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d" + integrity sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-property-literals@^7.10.4", "@babel/plugin-transform-property-literals@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" @@ -819,6 +1323,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-property-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd" + integrity sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-react-constant-elements@^7.9.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.4.tgz#0f485260bf1c29012bb973e7e404749eaac12c9e" @@ -833,6 +1344,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-react-display-name@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d" + integrity sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-react-jsx-development@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz#6ec90f244394604623880e15ebc3c34c356258ba" @@ -842,6 +1360,13 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" +"@babel/plugin-transform-react-jsx-development@^7.12.7": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz#bccca33108fe99d95d7f9e82046bfe762e71f4e7" + integrity sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.12.12" + "@babel/plugin-transform-react-jsx-self@^7.0.0", "@babel/plugin-transform-react-jsx-self@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369" @@ -868,6 +1393,17 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" +"@babel/plugin-transform-react-jsx@^7.12.10", "@babel/plugin-transform-react-jsx@^7.12.12": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz#b0da51ffe5f34b9a900e9f1f5fb814f9e512d25e" + integrity sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.12.10" + "@babel/helper-module-imports" "^7.12.5" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.12.1" + "@babel/types" "^7.12.12" + "@babel/plugin-transform-react-pure-annotations@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501" @@ -876,6 +1412,14 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-react-pure-annotations@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42" + integrity sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-regenerator@^7.10.4", "@babel/plugin-transform-regenerator@^7.4.5": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" @@ -883,6 +1427,13 @@ dependencies: regenerator-transform "^0.14.2" +"@babel/plugin-transform-regenerator@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753" + integrity sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng== + dependencies: + regenerator-transform "^0.14.2" + "@babel/plugin-transform-reserved-words@^7.10.4", "@babel/plugin-transform-reserved-words@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" @@ -890,6 +1441,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-reserved-words@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8" + integrity sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-runtime@7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.2.0.tgz#566bc43f7d0aedc880eaddbd29168d0f248966ea" @@ -900,14 +1458,13 @@ resolve "^1.8.1" semver "^5.5.1" -"@babel/plugin-transform-runtime@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz#e27f78eb36f19448636e05c33c90fd9ad9b8bccf" - integrity sha512-LFEsP+t3wkYBlis8w6/kmnd6Kb1dxTd+wGJ8MlxTGzQo//ehtqlVL4S9DNUa53+dtPSQobN2CXx4d81FqC58cw== +"@babel/plugin-transform-runtime@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.10.tgz#af0fded4e846c4b37078e8e5d06deac6cd848562" + integrity sha512-xOrUfzPxw7+WDm9igMgQCbO3cJKymX7dFdsgRr1eu9n3KjjyU4pptIXbXPseQDquw+W+RuJEJMHKHNsPNNm3CA== dependencies: - "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-module-imports" "^7.12.5" "@babel/helper-plugin-utils" "^7.10.4" - resolve "^1.8.1" semver "^5.5.1" "@babel/plugin-transform-shorthand-properties@^7.10.4", "@babel/plugin-transform-shorthand-properties@^7.2.0", "@babel/plugin-transform-shorthand-properties@^7.8.3": @@ -917,6 +1474,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-shorthand-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3" + integrity sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-spread@^7.11.0", "@babel/plugin-transform-spread@^7.2.0", "@babel/plugin-transform-spread@^7.8.3": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" @@ -925,6 +1489,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" +"@babel/plugin-transform-spread@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e" + integrity sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" + "@babel/plugin-transform-sticky-regex@^7.10.4", "@babel/plugin-transform-sticky-regex@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" @@ -933,6 +1505,13 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-regex" "^7.10.4" +"@babel/plugin-transform-sticky-regex@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad" + integrity sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-template-literals@^7.10.4", "@babel/plugin-transform-template-literals@^7.4.4", "@babel/plugin-transform-template-literals@^7.8.3": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" @@ -941,6 +1520,13 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-template-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843" + integrity sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-typeof-symbol@^7.10.4", "@babel/plugin-transform-typeof-symbol@^7.2.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" @@ -948,6 +1534,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-typeof-symbol@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz#de01c4c8f96580bd00f183072b0d0ecdcf0dec4b" + integrity sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-typescript@^7.10.4": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.11.0.tgz#2b4879676af37342ebb278216dd090ac67f13abb" @@ -957,6 +1550,15 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-typescript" "^7.10.4" +"@babel/plugin-transform-typescript@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz#d92cc0af504d510e26a754a7dbc2e5c8cd9c7ab4" + integrity sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript" "^7.12.1" + "@babel/plugin-transform-unicode-escapes@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" @@ -964,6 +1566,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-unicode-escapes@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709" + integrity sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-unicode-regex@^7.10.4", "@babel/plugin-transform-unicode-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" @@ -972,6 +1581,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-unicode-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb" + integrity sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/preset-env@7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58" @@ -1026,7 +1643,79 @@ js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.9.5", "@babel/preset-env@^7.9.6": +"@babel/preset-env@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.11.tgz#55d5f7981487365c93dbbc84507b1c7215e857f9" + integrity sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw== + dependencies: + "@babel/compat-data" "^7.12.7" + "@babel/helper-compilation-targets" "^7.12.5" + "@babel/helper-module-imports" "^7.12.5" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-option" "^7.12.11" + "@babel/plugin-proposal-async-generator-functions" "^7.12.1" + "@babel/plugin-proposal-class-properties" "^7.12.1" + "@babel/plugin-proposal-dynamic-import" "^7.12.1" + "@babel/plugin-proposal-export-namespace-from" "^7.12.1" + "@babel/plugin-proposal-json-strings" "^7.12.1" + "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1" + "@babel/plugin-proposal-numeric-separator" "^7.12.7" + "@babel/plugin-proposal-object-rest-spread" "^7.12.1" + "@babel/plugin-proposal-optional-catch-binding" "^7.12.1" + "@babel/plugin-proposal-optional-chaining" "^7.12.7" + "@babel/plugin-proposal-private-methods" "^7.12.1" + "@babel/plugin-proposal-unicode-property-regex" "^7.12.1" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.12.1" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-syntax-top-level-await" "^7.12.1" + "@babel/plugin-transform-arrow-functions" "^7.12.1" + "@babel/plugin-transform-async-to-generator" "^7.12.1" + "@babel/plugin-transform-block-scoped-functions" "^7.12.1" + "@babel/plugin-transform-block-scoping" "^7.12.11" + "@babel/plugin-transform-classes" "^7.12.1" + "@babel/plugin-transform-computed-properties" "^7.12.1" + "@babel/plugin-transform-destructuring" "^7.12.1" + "@babel/plugin-transform-dotall-regex" "^7.12.1" + "@babel/plugin-transform-duplicate-keys" "^7.12.1" + "@babel/plugin-transform-exponentiation-operator" "^7.12.1" + "@babel/plugin-transform-for-of" "^7.12.1" + "@babel/plugin-transform-function-name" "^7.12.1" + "@babel/plugin-transform-literals" "^7.12.1" + "@babel/plugin-transform-member-expression-literals" "^7.12.1" + "@babel/plugin-transform-modules-amd" "^7.12.1" + "@babel/plugin-transform-modules-commonjs" "^7.12.1" + "@babel/plugin-transform-modules-systemjs" "^7.12.1" + "@babel/plugin-transform-modules-umd" "^7.12.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.1" + "@babel/plugin-transform-new-target" "^7.12.1" + "@babel/plugin-transform-object-super" "^7.12.1" + "@babel/plugin-transform-parameters" "^7.12.1" + "@babel/plugin-transform-property-literals" "^7.12.1" + "@babel/plugin-transform-regenerator" "^7.12.1" + "@babel/plugin-transform-reserved-words" "^7.12.1" + "@babel/plugin-transform-shorthand-properties" "^7.12.1" + "@babel/plugin-transform-spread" "^7.12.1" + "@babel/plugin-transform-sticky-regex" "^7.12.7" + "@babel/plugin-transform-template-literals" "^7.12.1" + "@babel/plugin-transform-typeof-symbol" "^7.12.10" + "@babel/plugin-transform-unicode-escapes" "^7.12.1" + "@babel/plugin-transform-unicode-regex" "^7.12.1" + "@babel/preset-modules" "^0.1.3" + "@babel/types" "^7.12.11" + core-js-compat "^3.8.0" + semver "^5.5.0" + +"@babel/preset-env@^7.9.5", "@babel/preset-env@^7.9.6": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== @@ -1130,7 +1819,7 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" -"@babel/preset-react@^7.0.0", "@babel/preset-react@^7.10.4", "@babel/preset-react@^7.8.3", "@babel/preset-react@^7.9.4": +"@babel/preset-react@^7.0.0", "@babel/preset-react@^7.8.3", "@babel/preset-react@^7.9.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf" integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw== @@ -1143,7 +1832,27 @@ "@babel/plugin-transform-react-jsx-source" "^7.10.4" "@babel/plugin-transform-react-pure-annotations" "^7.10.4" -"@babel/preset-typescript@^7.10.4", "@babel/preset-typescript@^7.9.0": +"@babel/preset-react@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.10.tgz#4fed65f296cbb0f5fb09de6be8cddc85cc909be9" + integrity sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-react-display-name" "^7.12.1" + "@babel/plugin-transform-react-jsx" "^7.12.10" + "@babel/plugin-transform-react-jsx-development" "^7.12.7" + "@babel/plugin-transform-react-pure-annotations" "^7.12.1" + +"@babel/preset-typescript@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.12.7.tgz#fc7df8199d6aae747896f1e6c61fc872056632a3" + integrity sha512-nOoIqIqBmHBSEgBXWR4Dv/XBehtIFcw9PqZw6rFYuKrzsZmOQm3PR5siLBnKZFEsDb03IegG8nSjU/iXXXYRmw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-option" "^7.12.1" + "@babel/plugin-transform-typescript" "^7.12.1" + +"@babel/preset-typescript@^7.9.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.10.4.tgz#7d5d052e52a682480d6e2cc5aa31be61c8c25e36" integrity sha512-SdYnvGPv+bLlwkF2VkJnaX/ni1sMNetcGI1+nThF1gyv6Ph8Qucc4ZZAjM5yZcE/AKRXIOTZz7eSRDWOEjPyRQ== @@ -1162,6 +1871,17 @@ pirates "^4.0.0" source-map-support "^0.5.16" +"@babel/register@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.12.10.tgz#19b87143f17128af4dbe7af54c735663b3999f60" + integrity sha512-EvX/BvMMJRAA3jZgILWgbsrHwBQvllC5T8B29McyME8DvkdOxk4ujESfrMvME8IHSDvWXrmMXxPvA/lx2gqPLQ== + dependencies: + find-cache-dir "^2.0.0" + lodash "^4.17.19" + make-dir "^2.1.0" + pirates "^4.0.0" + source-map-support "^0.5.16" + "@babel/runtime-corejs2@^7.2.0": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.11.2.tgz#700a03945ebad0d31ba6690fc8a6bcc9040faa47" @@ -1199,6 +1919,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" @@ -1208,7 +1935,16 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5", "@babel/traverse@^7.11.5", "@babel/traverse@^7.4.5": +"@babel/template@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" + integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.12.7" + "@babel/types" "^7.12.7" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5", "@babel/traverse@^7.11.5", "@babel/traverse@^7.4.5": version "7.11.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== @@ -1223,6 +1959,21 @@ globals "^11.1.0" lodash "^4.17.19" +"@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.12", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.0": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" + integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== + dependencies: + "@babel/code-frame" "^7.12.11" + "@babel/generator" "^7.12.11" + "@babel/helper-function-name" "^7.12.11" + "@babel/helper-split-export-declaration" "^7.12.11" + "@babel/parser" "^7.12.11" + "@babel/types" "^7.12.12" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.9.5": version "7.11.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" @@ -1232,6 +1983,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.7.0": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" + integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz#860ce718b0b73f4009e153541faff2cb6b85d047" @@ -3061,6 +3821,23 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": + version "2.1.8-no-fsevents" + resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz#da7c3996b8e6e19ebd14d82eaced2313e7769f9b" + integrity sha512-+nb9vWloHNNMFHjGofEam3wopE3m1yuambrrd/fnPc+lFOMB9ROTqQlche9ByFWNkdNqfSgR/kkQtQ8DzEWt2w== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -4555,7 +5332,7 @@ resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.4.tgz#bfd5b0d0d1ba13e351dff65b6e52783b816826c8" integrity sha512-WiZhq3SVJHFRgRYLXvpf65XnV6ipVHhnNaNvE8yCimejrGglkg38kEj0JcizqwSHxmPSjcTlig/6JouxLGEhGw== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.10", "@types/babel__core@^7.1.7": +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.10" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.10.tgz#ca58fc195dd9734e77e57c6f2df565623636ab40" integrity sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw== @@ -4566,6 +5343,17 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" +"@types/babel__core@^7.1.12": + version "7.1.12" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" + integrity sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__generator@*": version "7.0.2" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc" @@ -7492,6 +8280,11 @@ async-done@^1.2.0, async-done@^1.2.2: process-nextick-args "^2.0.0" stream-exhaust "^1.0.1" +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" @@ -7675,15 +8468,15 @@ babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-eslint@^10.0.3: - version "10.0.3" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" - integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA== +babel-eslint@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" + integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.0.0" - "@babel/traverse" "^7.0.0" - "@babel/types" "^7.0.0" + "@babel/parser" "^7.7.0" + "@babel/traverse" "^7.7.0" + "@babel/types" "^7.7.0" eslint-visitor-keys "^1.0.0" resolve "^1.12.0" @@ -7736,7 +8529,7 @@ babel-helper-to-multiple-sequence-expressions@^0.5.0: resolved "https://registry.yarnpkg.com/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz#a3f924e3561882d42fcf48907aa98f7979a4588d" integrity sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA== -babel-jest@^26.3.0, babel-jest@^26.6.3: +babel-jest@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== @@ -7760,6 +8553,16 @@ babel-loader@^8.0.6: mkdirp "^0.5.1" pify "^4.0.1" +babel-loader@^8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81" + integrity sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^1.4.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" @@ -7767,13 +8570,18 @@ babel-messages@^6.23.0: dependencies: babel-runtime "^6.22.0" -babel-plugin-add-module-exports@1.0.2, babel-plugin-add-module-exports@^1.0.2: +babel-plugin-add-module-exports@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-1.0.2.tgz#96cd610d089af664f016467fc4567c099cce2d9c" integrity sha512-4paN7RivvU3Rzju1vGSHWPjO8Y0rI6droWvSFKI6dvEQ4mvoV0zGojnlzVRfI6N8zISo6VERXt3coIuVmzuvNg== optionalDependencies: chokidar "^2.0.4" +babel-plugin-add-module-exports@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-1.0.4.tgz#6caa4ddbe1f578c6a5264d4d3e6c8a2720a7ca2b" + integrity sha512-g+8yxHUZ60RcyaUpfNzy56OtWW+x9cyEe9j+CranqLiqbju2yf/Cy6ZtYK40EZxtrdHllzlVZgLmcOUCTlJ7Jg== + babel-plugin-add-react-displayname@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz#339d4cddb7b65fd62d1df9db9fe04de134122bd5" @@ -8304,6 +9112,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + binary-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" @@ -8450,7 +9263,7 @@ brace@0.11.1, brace@^0.11.0, brace@^0.11.1: resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg= -braces@^2.3.1: +braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== @@ -8746,6 +9559,28 @@ browserslist@^4.12.0, browserslist@^4.8.3: node-releases "^1.1.53" pkg-up "^2.0.0" +browserslist@^4.14.5: + version "4.14.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.7.tgz#c071c1b3622c1c2e790799a37bb09473a4351cb6" + integrity sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ== + dependencies: + caniuse-lite "^1.0.30001157" + colorette "^1.2.1" + electron-to-chromium "^1.3.591" + escalade "^3.1.1" + node-releases "^1.1.66" + +browserslist@^4.16.1: + version "4.16.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.1.tgz#bf757a2da376b3447b800a16f0f1c96358138766" + integrity sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA== + dependencies: + caniuse-lite "^1.0.30001173" + colorette "^1.2.1" + electron-to-chromium "^1.3.634" + escalade "^3.1.1" + node-releases "^1.1.69" + browserslist@^4.6.0, browserslist@^4.8.5: version "4.14.5" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" @@ -9100,11 +9935,16 @@ camelize@^1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= -caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: +caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135, caniuse-lite@^1.0.30001173: version "1.0.30001179" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001179.tgz" integrity sha512-blMmO0QQujuUWZKyVrD1msR4WNDAqb/UPO1Sw2WWsQ7deoM5bJiicKnWJ1Y0NS/aGINSnKPIWBMw5luX+NDUCA== +caniuse-lite@^1.0.30001157: + version "1.0.30001164" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001164.tgz#5bbfd64ca605d43132f13cc7fdabb17c3036bfdc" + integrity sha512-G+A/tkf4bu0dSp9+duNiXc7bGds35DioCyC6vgK2m/rjA4Krpy5WeZgZyfH2f0wj2kI6yAWWucyap6oOwmY1mg== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -9343,7 +10183,7 @@ cheerio@^1.0.0-rc.3: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@2.1.2, chokidar@3.3.0, chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.1, chokidar@^2.1.2, chokidar@^2.1.8, chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.4.1, chokidar@^3.4.3: +chokidar@2.1.2, chokidar@3.3.0, chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.1, chokidar@^2.1.2, chokidar@^2.1.8, chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.4.0, chokidar@^3.4.1, chokidar@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== @@ -10226,6 +11066,14 @@ core-js-compat@^3.6.2: browserslist "^4.8.3" semver "7.0.0" +core-js-compat@^3.8.0: + version "3.8.3" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.3.tgz#9123fb6b9cad30f0651332dc77deba48ef9b0b3f" + integrity sha512-1sCb0wBXnBIL16pfFG1Gkvei6UzvKyTNYpiC41yrdjEv0UoJoq9E/abTMzyYJ6JpTkAj15dLjbqifIzEBDVvog== + dependencies: + browserslist "^4.16.1" + semver "7.0.0" + core-js-pure@^3.0.0, core-js-pure@^3.0.1: version "3.6.5" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" @@ -12191,6 +13039,16 @@ electron-to-chromium@^1.3.571: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.577.tgz#9885f3f72c6e3367010b461ff6f2d9624a929720" integrity sha512-dSb64JQSFif/pD8mpVAgSFkbVi6YHbK6JeEziwNNmXlr/Ne2rZtseFK5SM7JoWSLf6gP0gVvRGi4/2ZRhSX/rA== +electron-to-chromium@^1.3.591: + version "1.3.598" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.598.tgz#8f757018902ab6190323a8c5f6124d854893a35b" + integrity sha512-G5Ztk23/ubLYVPxPXnB1uu105uzIPd4xB/D8ld8x1GaSC9+vU9NZL16nYZya8H77/7CCKKN7dArzJL3pBs8N7A== + +electron-to-chromium@^1.3.634: + version "1.3.642" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.642.tgz#8b884f50296c2ae2a9997f024d0e3e57facc2b94" + integrity sha512-cev+jOrz/Zm1i+Yh334Hed6lQVOkkemk2wRozfMF4MtTR7pxf3r3L5Rbd7uX1zMcEqVJ7alJBnJL7+JffkC6FQ== + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -12740,10 +13598,10 @@ eslint-module-utils@2.5.0, eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-babel@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.0.tgz#2e7f251ccc249326da760c1a4c948a91c32d0023" - integrity sha512-HPuNzSPE75O+SnxHIafbW5QB45r2w78fxqwK3HmjqIUoPfPzVrq6rD+CINU3yzoDSzEhUkX07VUphbF73Lth/w== +eslint-plugin-babel@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz#75a2413ffbf17e7be57458301c60291f2cfbf560" + integrity sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g== dependencies: eslint-rule-composer "^0.3.0" @@ -16486,6 +17344,13 @@ is-bigint@^1.0.0: resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -20656,6 +21521,16 @@ node-releases@^1.1.61: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== +node-releases@^1.1.66: + version "1.1.67" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" + integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== + +node-releases@^1.1.69: + version "1.1.70" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.70.tgz#66e0ed0273aa65666d7fe78febe7634875426a08" + integrity sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw== + node-sass@^4.14.1: version "4.14.1" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5" @@ -23847,6 +24722,15 @@ readdir-scoped-modules@^1.0.0: graceful-fs "^4.1.2" once "^1.3.0" +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -24102,6 +24986,18 @@ regexpu-core@^4.7.0: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.2.0" +regexpu-core@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.2.0" + regjsgen "^0.5.1" + regjsparser "^0.6.4" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.2.0" + registry-auth-token@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" @@ -28175,6 +29071,11 @@ unzip-response@^1.0.0: resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" integrity sha1-uYTwh3/AqJwsdzzB73tbIytbBv4= +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + update-notifier@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc" From 0076384a0f9de9d35711008eaa8b244a2d6a487c Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 26 Jan 2021 12:32:12 -0500 Subject: [PATCH 007/163] [Maps] Implement fields and bounds retrieval on GeoJsonFileSource (#88294) --- .../source_descriptor_types.ts | 6 + .../classes/fields/geojson_file_field.ts | 43 +++++++ .../layers/file_upload_wizard/wizard.tsx | 7 +- .../geojson_file_source/geojson_file.test.ts | 105 ++++++++++++++++++ .../geojson_file_source.ts | 88 ++++++++++++--- .../sources/geojson_file_source/index.ts | 2 +- .../maps/public/selectors/map_selectors.ts | 12 +- 7 files changed, 240 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/fields/geojson_file_field.ts create mode 100644 x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file.test.ts diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index 603e1d767e1c6..b849b42429cf6 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -156,7 +156,13 @@ export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & tooltipProperties: string[]; }; +export type GeoJsonFileFieldDescriptor = { + name: string; + type: 'string' | 'number'; +}; + export type GeojsonFileSourceDescriptor = { + __fields?: GeoJsonFileFieldDescriptor[]; __featureCollection: FeatureCollection; name: string; type: string; diff --git a/x-pack/plugins/maps/public/classes/fields/geojson_file_field.ts b/x-pack/plugins/maps/public/classes/fields/geojson_file_field.ts new file mode 100644 index 0000000000000..ae42b09d491c5 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/geojson_file_field.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 { FIELD_ORIGIN } from '../../../common/constants'; +import { IField, AbstractField } from './field'; +import { IVectorSource } from '../sources/vector_source'; +import { GeoJsonFileSource } from '../sources/geojson_file_source'; + +export class GeoJsonFileField extends AbstractField implements IField { + private readonly _source: GeoJsonFileSource; + private readonly _dataType: string; + + constructor({ + fieldName, + source, + origin, + dataType, + }: { + fieldName: string; + source: GeoJsonFileSource; + origin: FIELD_ORIGIN; + dataType: string; + }) { + super({ fieldName, origin }); + this._source = source; + this._dataType = dataType; + } + + getSource(): IVectorSource { + return this._source; + } + + async getLabel(): Promise { + return this.getName(); + } + + async getDataType(): Promise { + return this._dataType; + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index a0029c5c64e0b..68fd25ce9e7ae 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -14,7 +14,7 @@ import { SCALING_TYPES, } from '../../../../common/constants'; import { getFileUploadComponent } from '../../../kibana_services'; -import { GeojsonFileSource } from '../../sources/geojson_file_source'; +import { GeoJsonFileSource } from '../../sources/geojson_file_source'; import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; @@ -79,7 +79,10 @@ export class ClientFileCreateSourceEditor extends Component { + describe('getName', () => { + it('should get default display name', async () => { + const geojsonFileSource = new GeoJsonFileSource({}); + expect(await geojsonFileSource.getDisplayName()).toBe('Features'); + }); + }); + describe('getBounds', () => { + it('should get null bounds', async () => { + const geojsonFileSource = new GeoJsonFileSource({}); + expect( + await geojsonFileSource.getBoundsForFilters(({} as unknown) as BoundsFilters, () => {}) + ).toEqual(null); + }); + + it('should get bounds from feature collection', async () => { + const geojsonFileSource = new GeoJsonFileSource({ + __featureCollection: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [0, 1], + }, + properties: {}, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [2, 3], + }, + properties: {}, + }, + ], + }, + }); + + expect(geojsonFileSource.isBoundsAware()).toBe(true); + expect( + await geojsonFileSource.getBoundsForFilters(({} as unknown) as BoundsFilters, () => {}) + ).toEqual({ + maxLat: 3, + maxLon: 2, + minLat: 1, + minLon: 0, + }); + }); + }); + + describe('getFields', () => { + it('should get fields from config', async () => { + const geojsonFileSource = new GeoJsonFileSource({ + __fields: [ + { + type: 'string', + name: 'foo', + }, + { + type: 'number', + name: 'bar', + }, + ], + }); + + const fields = await geojsonFileSource.getFields(); + + const actualFields = fields.map(async (field) => { + return { + dataType: await field.getDataType(), + origin: field.getOrigin(), + name: field.getName(), + source: field.getSource(), + }; + }); + + expect(await Promise.all(actualFields)).toEqual([ + { + dataType: 'string', + origin: FIELD_ORIGIN.SOURCE, + source: geojsonFileSource, + name: 'foo', + }, + { + dataType: 'number', + origin: FIELD_ORIGIN.SOURCE, + source: geojsonFileSource, + name: 'bar', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts index 6172405152739..69d84dc65d382 100644 --- a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts @@ -5,13 +5,22 @@ */ import { Feature, FeatureCollection } from 'geojson'; -import { AbstractVectorSource, GeoJsonWithMeta } from '../vector_source'; -import { EMPTY_FEATURE_COLLECTION, SOURCE_TYPES } from '../../../../common/constants'; -import { GeojsonFileSourceDescriptor } from '../../../../common/descriptor_types'; +import { AbstractVectorSource, BoundsFilters, GeoJsonWithMeta } from '../vector_source'; +import { EMPTY_FEATURE_COLLECTION, FIELD_ORIGIN, SOURCE_TYPES } from '../../../../common/constants'; +import { + GeoJsonFileFieldDescriptor, + GeojsonFileSourceDescriptor, + MapExtent, +} from '../../../../common/descriptor_types'; import { registerSource } from '../source_registry'; import { IField } from '../../fields/field'; +import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds'; +import { GeoJsonFileField } from '../../fields/geojson_file_field'; +import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; -function getFeatureCollection(geoJson: Feature | FeatureCollection | null): FeatureCollection { +function getFeatureCollection( + geoJson: Feature | FeatureCollection | null | undefined +): FeatureCollection { if (!geoJson) { return EMPTY_FEATURE_COLLECTION; } @@ -30,18 +39,73 @@ function getFeatureCollection(geoJson: Feature | FeatureCollection | null): Feat return EMPTY_FEATURE_COLLECTION; } -export class GeojsonFileSource extends AbstractVectorSource { +export class GeoJsonFileSource extends AbstractVectorSource { static createDescriptor( - geoJson: Feature | FeatureCollection | null, - name: string + descriptor: Partial ): GeojsonFileSourceDescriptor { return { type: SOURCE_TYPES.GEOJSON_FILE, - __featureCollection: getFeatureCollection(geoJson), - name, + __featureCollection: getFeatureCollection(descriptor.__featureCollection), + __fields: descriptor.__fields || [], + name: descriptor.name || 'Features', }; } + constructor(descriptor: Partial, inspectorAdapters?: Adapters) { + const normalizedDescriptor = GeoJsonFileSource.createDescriptor(descriptor); + super(normalizedDescriptor, inspectorAdapters); + } + + _getFields(): GeoJsonFileFieldDescriptor[] { + const fields = (this._descriptor as GeojsonFileSourceDescriptor).__fields; + return fields ? fields : []; + } + + createField({ fieldName }: { fieldName: string }): IField { + const fields = this._getFields(); + const descriptor: GeoJsonFileFieldDescriptor | undefined = fields.find((field) => { + return field.name === fieldName; + }); + + if (!descriptor) { + throw new Error( + `Cannot find corresponding field ${fieldName} in __fields array ${JSON.stringify( + this._getFields() + )} ` + ); + } + return new GeoJsonFileField({ + fieldName: descriptor.name, + source: this, + origin: FIELD_ORIGIN.SOURCE, + dataType: descriptor.type, + }); + } + + async getFields(): Promise { + const fields = this._getFields(); + return fields.map((field: GeoJsonFileFieldDescriptor) => { + return new GeoJsonFileField({ + fieldName: field.name, + source: this, + origin: FIELD_ORIGIN.SOURCE, + dataType: field.type, + }); + }); + } + + isBoundsAware(): boolean { + return true; + } + + async getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (callback: () => void) => void + ): Promise { + const featureCollection = (this._descriptor as GeojsonFileSourceDescriptor).__featureCollection; + return getFeatureCollectionBounds(featureCollection, false); + } + async getGeoJsonWithMeta(): Promise { return { data: (this._descriptor as GeojsonFileSourceDescriptor).__featureCollection, @@ -49,10 +113,6 @@ export class GeojsonFileSource extends AbstractVectorSource { }; } - createField({ fieldName }: { fieldName: string }): IField { - throw new Error('Not implemented'); - } - async getDisplayName() { return (this._descriptor as GeojsonFileSourceDescriptor).name; } @@ -63,6 +123,6 @@ export class GeojsonFileSource extends AbstractVectorSource { } registerSource({ - ConstructorFunction: GeojsonFileSource, + ConstructorFunction: GeoJsonFileSource, type: SOURCE_TYPES.GEOJSON_FILE, }); diff --git a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/index.ts b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/index.ts index cf0d15dcb747a..ef6806e0aa151 100644 --- a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/index.ts +++ b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { GeojsonFileSource } from './geojson_file_source'; +export { GeoJsonFileSource } from './geojson_file_source'; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 7b72a3c979abe..8876b9536ce92 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -22,7 +22,7 @@ import { TiledVectorLayer } from '../classes/layers/tiled_vector_layer/tiled_vec import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; import { InnerJoin } from '../classes/joins/inner_join'; import { getSourceByType } from '../classes/sources/source_registry'; -import { GeojsonFileSource } from '../classes/sources/geojson_file_source'; +import { GeoJsonFileSource } from '../classes/sources/geojson_file_source'; import { SOURCE_DATA_REQUEST_ID, STYLE_TYPE, @@ -241,10 +241,10 @@ export const getSpatialFiltersLayer = createSelector( type: 'FeatureCollection', features: extractFeaturesFromFilters(filters), }; - const geoJsonSourceDescriptor = GeojsonFileSource.createDescriptor( - featureCollection, - 'spatialFilters' - ); + const geoJsonSourceDescriptor = GeoJsonFileSource.createDescriptor({ + __featureCollection: featureCollection, + name: 'spatialFilters', + }); return new VectorLayer({ layerDescriptor: VectorLayer.createDescriptor({ @@ -272,7 +272,7 @@ export const getSpatialFiltersLayer = createSelector( }, }), }), - source: new GeojsonFileSource(geoJsonSourceDescriptor), + source: new GeoJsonFileSource(geoJsonSourceDescriptor), }); } ); From 3938fa67ad5b404da32b165f0cd8a40ec23765bf Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 26 Jan 2021 10:35:57 -0700 Subject: [PATCH 008/163] [Maps] allow saving maps to dashboards (#88759) * [Maps] allow saving maps to dashboards * update saveMap functional test method * update tags functional test * review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/kibana.json | 2 +- .../routes/map_page/saved_map/saved_map.ts | 15 ++- .../public/routes/map_page/top_nav_config.tsx | 108 ++++++++++-------- .../apps/maps/embeddable/save_and_return.js | 5 +- .../test/functional/page_objects/gis_page.ts | 18 ++- .../functional/tests/maps_integration.ts | 10 +- 6 files changed, 93 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index e47968b027cc3..42adf6f2a950b 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -23,5 +23,5 @@ "ui": true, "server": true, "extraPublicDirs": ["common/constants"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "mapsOss"] + "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "mapsOss", "presentationUtil"] } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index 27fd78980710f..df424124be3f2 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -281,10 +281,12 @@ export class SavedMap { returnToOrigin, newTags, saveByReference, + dashboardId, }: OnSaveProps & { - returnToOrigin: boolean; + returnToOrigin?: boolean; newTags?: string[]; saveByReference: boolean; + dashboardId?: string | null; }) { if (!this._attributes) { throw new Error('Invalid usage, must await whenReady before calling save'); @@ -337,7 +339,7 @@ export class SavedMap { }); return; } - this._getStateTransfer().navigateToWithEmbeddablePackage(this._originatingApp, { + await this._getStateTransfer().navigateToWithEmbeddablePackage(this._originatingApp, { state: { embeddableId: newCopyOnSave ? undefined : this._embeddableId, type: MAP_SAVED_OBJECT_TYPE, @@ -345,6 +347,15 @@ export class SavedMap { }, }); return; + } else if (dashboardId) { + await this._getStateTransfer().navigateToWithEmbeddablePackage('dashboards', { + state: { + type: MAP_SAVED_OBJECT_TYPE, + input: updatedMapEmbeddableInput, + }, + path: dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`, + }); + return; } this._mapEmbeddableInput = updatedMapEmbeddableInput; diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 43a74a9c73012..7010c281d24c6 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -10,6 +10,7 @@ import { Adapters } from 'src/plugins/inspector/public'; import { getCoreChrome, getMapsCapabilities, + getIsAllowByValueEmbeddables, getInspector, getCoreI18n, getSavedObjectsClient, @@ -25,6 +26,7 @@ import { import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { SavedMap } from './saved_map'; import { getMapEmbeddableDisplayName } from '../../../common/i18n_getters'; +import { SavedObjectSaveModalDashboard } from '../../../../../../src/plugins/presentation_util/public'; export function getTopNavConfig({ savedMap, @@ -139,53 +141,67 @@ export function getTopNavConfig({ /> ) : undefined; - const saveModal = ( - { - try { - await checkForDuplicateTitle( - { - id: props.newCopyOnSave ? undefined : savedMap.getSavedObjectId(), - title: props.newTitle, - copyOnSave: props.newCopyOnSave, - lastSavedTitle: savedMap.getSavedObjectId() ? savedMap.getTitle() : '', - getEsType: () => MAP_SAVED_OBJECT_TYPE, - getDisplayName: getMapEmbeddableDisplayName, - }, - props.isTitleDuplicateConfirmed, - props.onTitleDuplicate, - { - savedObjectsClient: getSavedObjectsClient(), - overlays: getCoreOverlays(), - } - ); - } catch (e) { - // ignore duplicate title failure, user notified in save modal - return {}; - } + const saveModalProps = { + onSave: async ( + props: OnSaveProps & { returnToOrigin?: boolean; dashboardId?: string | null } + ) => { + try { + await checkForDuplicateTitle( + { + id: props.newCopyOnSave ? undefined : savedMap.getSavedObjectId(), + title: props.newTitle, + copyOnSave: props.newCopyOnSave, + lastSavedTitle: savedMap.getSavedObjectId() ? savedMap.getTitle() : '', + getEsType: () => MAP_SAVED_OBJECT_TYPE, + getDisplayName: getMapEmbeddableDisplayName, + }, + props.isTitleDuplicateConfirmed, + props.onTitleDuplicate, + { + savedObjectsClient: getSavedObjectsClient(), + overlays: getCoreOverlays(), + } + ); + } catch (e) { + // ignore duplicate title failure, user notified in save modal + return {}; + } + + await savedMap.save({ + ...props, + newTags: selectedTags, + saveByReference: !props.dashboardId, + }); + // showSaveModal wrapper requires onSave to return an object with an id to close the modal after successful save + return { id: 'id' }; + }, + onClose: () => {}, + documentInfo: { + description: mapDescription, + id: savedMap.getSavedObjectId(), + title: savedMap.getTitle(), + }, + objectType: i18n.translate('xpack.maps.topNav.saveModalType', { + defaultMessage: 'map', + }), + }; + + const saveModal = + savedMap.getOriginatingApp() || !getIsAllowByValueEmbeddables() ? ( + + ) : ( + + ); - await savedMap.save({ - ...props, - newTags: selectedTags, - saveByReference: true, - }); - // showSaveModal wrapper requires onSave to return an object with an id to close the modal after successful save - return { id: 'id' }; - }} - onClose={() => {}} - documentInfo={{ - description: mapDescription, - id: savedMap.getSavedObjectId(), - title: savedMap.getTitle(), - }} - objectType={i18n.translate('xpack.maps.topNav.saveModalType', { - defaultMessage: 'map', - })} - options={tagSelector} - /> - ); showSaveModal(saveModal, getCoreI18n().Context); }, }); diff --git a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js index 40af8ddb9d44b..13aa08cbbeb38 100644 --- a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js +++ b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js @@ -31,6 +31,7 @@ export default function ({ getPageObjects, getService }) { after(async () => { await security.testUser.restoreDefaults(); }); + describe('new map', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); @@ -55,7 +56,7 @@ export default function ({ getPageObjects, getService }) { it('should cut the originator and stay in maps application', async () => { await PageObjects.maps.saveMap( 'map created from dashboard save and return with originator app cut', - true + false ); await PageObjects.maps.waitForLayersToLoad(); await testSubjects.missingOrFail('mapSaveAndReturnButton'); @@ -94,7 +95,7 @@ export default function ({ getPageObjects, getService }) { describe('save as and uncheck return to origin switch', () => { it('should cut the originator and stay in maps application', async () => { - await PageObjects.maps.saveMap('Clone 2 of map embeddable example', true); + await PageObjects.maps.saveMap('Clone 2 of map embeddable example', false); await PageObjects.maps.waitForLayersToLoad(); await testSubjects.missingOrFail('mapSaveAndReturnButton'); await testSubjects.existOrFail('mapSaveButton'); diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 7e22acf785d36..7767499cb6bd3 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -9,7 +9,7 @@ import { APP_ID } from '../../../plugins/maps/common/constants'; import { FtrProviderContext } from '../ftr_provider_context'; export function GisPageProvider({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'header', 'timePicker']); + const PageObjects = getPageObjects(['common', 'header', 'timePicker', 'visualize']); const log = getService('log'); const testSubjects = getService('testSubjects'); @@ -148,18 +148,14 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await renderable.waitForRender(); } - async saveMap(name: string, uncheckReturnToOriginModeSwitch = false, tags?: string[]) { + async saveMap(name: string, redirectToOrigin = true, tags?: string[]) { await testSubjects.click('mapSaveButton'); await testSubjects.setValue('savedObjectTitle', name); - if (uncheckReturnToOriginModeSwitch) { - const redirectToOriginCheckboxExists = await testSubjects.exists( - 'returnToOriginModeSwitch' - ); - if (!redirectToOriginCheckboxExists) { - throw new Error('Unable to uncheck "returnToOriginModeSwitch", it does not exist.'); - } - await testSubjects.setEuiSwitch('returnToOriginModeSwitch', 'uncheck'); - } + await PageObjects.visualize.setSaveModalValues(name, { + addToDashboard: false, + redirectToOrigin, + saveAsNew: true, + }); if (tags) { await testSubjects.click('savedObjectTagSelector'); for (const tagName of tags) { diff --git a/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts index 4e44659b4fc67..32b9cc378db45 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts @@ -13,7 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); const find = getService('find'); - const PageObjects = getPageObjects(['maps', 'tagManagement', 'common']); + const PageObjects = getPageObjects(['maps', 'tagManagement', 'common', 'visualize']); /** * Select tags in the searchbar's tag filter. @@ -78,7 +78,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('allows to select tags for a new map', async () => { - await PageObjects.maps.saveMap('my-new-map', false, ['tag-1', 'tag-3']); + await PageObjects.maps.saveMap('my-new-map', true, ['tag-1', 'tag-3']); await PageObjects.maps.gotoMapListingPage(); await selectFilterTags('tag-1'); @@ -91,6 +91,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.click('mapSaveButton'); await testSubjects.setValue('savedObjectTitle', 'map-with-new-tag'); + await PageObjects.visualize.setSaveModalValues('map-with-new-tag', { + addToDashboard: false, + saveAsNew: true, + }); await testSubjects.click('savedObjectTagSelector'); await testSubjects.click(`tagSelectorOption-action__create`); @@ -127,7 +131,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allows to select tags for an existing map', async () => { await listingTable.clickItemLink('map', 'map 4 (tag-1)'); - await PageObjects.maps.saveMap('map 4 (tag-1)', false, ['tag-3']); + await PageObjects.maps.saveMap('map 4 (tag-1)', true, ['tag-3']); await PageObjects.maps.gotoMapListingPage(); await selectFilterTags('tag-3'); From eaf6831022991d17629c274aa2d11d8e0a1594db Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Tue, 26 Jan 2021 12:48:22 -0500 Subject: [PATCH 009/163] Adding better aria-labels for global search and field search in Lens (#89215) --- .../public/components/__snapshots__/search_bar.test.tsx.snap | 2 +- .../global_search_bar/public/components/search_bar.tsx | 3 +++ .../lens/public/indexpattern_datasource/datapanel.tsx | 5 +++-- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap index f5e7a030d59e3..8433d98c232d6 100644 --- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap +++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap @@ -33,7 +33,7 @@ exports[`SearchBar supports keyboard shortcuts 1`] = ` { setLocalState({ ...localState, nameFilter: e.target.value }); }} - aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', { - defaultMessage: 'Search fields', + aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', { + defaultMessage: 'Search field names', + description: 'Search the list of fields in the index pattern for the provided text', })} aria-describedby={fieldSearchDescriptionId} /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 93267c950c10e..e28b41e0ce5e3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11431,7 +11431,6 @@ "xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去", "xpack.lens.indexPatterns.fieldFiltersLabel": "フィールドフィルター", "xpack.lens.indexPatterns.fieldSearchLiveRegion": "{availableFields}使用可能な{availableFields, plural, other {フィールド}}。{emptyFields}空の{emptyFields, plural, other {フィールド}}。 {metaFields}メタ{metaFields, plural, other {フィールド}}。", - "xpack.lens.indexPatterns.filterByNameAriaLabel": "検索フィールド", "xpack.lens.indexPatterns.filterByNameLabel": "検索フィールド名", "xpack.lens.indexPatterns.noAvailableDataLabel": "データを含むフィールドはありません。", "xpack.lens.indexPatterns.noDataLabel": "フィールドがありません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 41bdf97333dd7..49fd9ce095f66 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11460,7 +11460,6 @@ "xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选", "xpack.lens.indexPatterns.fieldFiltersLabel": "字段筛选", "xpack.lens.indexPatterns.fieldSearchLiveRegion": "{availableFields} 个可用{availableFields, plural, other {字段}}。{emptyFields} 个空{emptyFields, plural, other {字段}}。{metaFields} 个元{metaFields, plural,other {字段}}。", - "xpack.lens.indexPatterns.filterByNameAriaLabel": "搜索字段", "xpack.lens.indexPatterns.filterByNameLabel": "搜索字段名称", "xpack.lens.indexPatterns.noAvailableDataLabel": "没有包含数据的可用字段。", "xpack.lens.indexPatterns.noDataLabel": "无字段。", From ecf512a048393a80a31f7085f0e7fc74a55b731f Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 26 Jan 2021 19:06:40 +0100 Subject: [PATCH 010/163] [Lens] Make Lens visualization load faster on Dashboard (#88953) * :rocket: Load indexPatternRefs only on edit mode * :white_mark_check: Fix test with new editor init flag * :bug: Avoid to save to localStorage undefined indexPattern * :white_mark_check: Adapted tests to new conditional ref loading Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../editor_frame/editor_frame.test.tsx | 8 ++++-- .../editor_frame/editor_frame.tsx | 3 +- .../editor_frame/state_helpers.ts | 15 ++++++++-- .../indexpattern_datasource/indexpattern.tsx | 5 +++- .../indexpattern_datasource/loader.test.ts | 28 +++++++++++++++++++ .../public/indexpattern_datasource/loader.ts | 17 +++++++---- x-pack/plugins/lens/public/types.ts | 7 ++++- 7 files changed, 70 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 76394c2901aaa..c0728bd030a0a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -188,8 +188,12 @@ describe('editor_frame', () => { /> ); }); - expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, [], undefined); - expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, [], undefined); + expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, [], undefined, { + isFullEditor: true, + }); + expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, [], undefined, { + isFullEditor: true, + }); expect(mockDatasource3.initialize).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index f908d16afe470..b6df0caa07577 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -81,7 +81,8 @@ export function EditorFrame(props: EditorFrameProps) { props.datasourceMap, state.datasourceStates, props.doc?.references, - visualizeTriggerFieldContext + visualizeTriggerFieldContext, + { isFullEditor: true } ) .then((result) => { if (!isUnmounted) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 9c5eafc300abc..de747fde2e92c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -10,6 +10,7 @@ import { Datasource, DatasourcePublicAPI, FramePublicAPI, + InitializationOptions, Visualization, VisualizationDimensionGroupConfig, } from '../../types'; @@ -21,14 +22,20 @@ export async function initializeDatasources( datasourceMap: Record, datasourceStates: Record, references?: SavedObjectReference[], - initialContext?: VisualizeFieldContext + initialContext?: VisualizeFieldContext, + options?: InitializationOptions ) { const states: Record = {}; await Promise.all( Object.entries(datasourceMap).map(([datasourceId, datasource]) => { if (datasourceStates[datasourceId]) { return datasource - .initialize(datasourceStates[datasourceId].state || undefined, references, initialContext) + .initialize( + datasourceStates[datasourceId].state || undefined, + references, + initialContext, + options + ) .then((datasourceState) => { states[datasourceId] = { isLoading: false, state: datasourceState }; }); @@ -82,7 +89,9 @@ export async function persistedStateToExpression( { isLoading: false, state }, ]) ), - references + references, + undefined, + { isFullEditor: false } ); const datasourceLayers = createDatasourceLayers(datasources, datasourceStates); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 48592c44aa543..22c0054cb33e0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -18,6 +18,7 @@ import { Operation, DatasourceLayerPanelProps, PublicAPIProps, + InitializationOptions, } from '../types'; import { loadInitialState, @@ -104,7 +105,8 @@ export function getIndexPatternDatasource({ async initialize( persistedState?: IndexPatternPersistedState, references?: SavedObjectReference[], - initialContext?: VisualizeFieldContext + initialContext?: VisualizeFieldContext, + options?: InitializationOptions ) { return loadInitialState({ persistedState, @@ -114,6 +116,7 @@ export function getIndexPatternDatasource({ storage, indexPatternsService, initialContext, + options, }); }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 29786d9bc68f3..801496d1a5701 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -346,6 +346,7 @@ describe('loader', () => { savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, + options: { isFullEditor: true }, }); expect(state).toMatchObject({ @@ -364,12 +365,35 @@ describe('loader', () => { }); }); + it('should load a default state without loading the indexPatterns when embedded', async () => { + const storage = createMockStorage(); + const savedObjectsClient = mockClient(); + const state = await loadInitialState({ + savedObjectsClient, + indexPatternsService: mockIndexPatternsService(), + storage, + options: { isFullEditor: false }, + }); + + expect(state).toMatchObject({ + currentIndexPatternId: undefined, + indexPatternRefs: [], + indexPatterns: {}, + layers: {}, + }); + + expect(storage.set).not.toHaveBeenCalled(); + + expect(savedObjectsClient.find).not.toHaveBeenCalled(); + }); + it('should load a default state when lastUsedIndexPatternId is not found in indexPatternRefs', async () => { const storage = createMockStorage({ indexPatternId: 'c' }); const state = await loadInitialState({ savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, + options: { isFullEditor: true }, }); expect(state).toMatchObject({ @@ -393,6 +417,7 @@ describe('loader', () => { savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage: createMockStorage({ indexPatternId: '2' }), + options: { isFullEditor: true }, }); expect(state).toMatchObject({ @@ -415,6 +440,7 @@ describe('loader', () => { savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, + options: { isFullEditor: true }, }); expect(state).toMatchObject({ @@ -443,6 +469,7 @@ describe('loader', () => { indexPatternId: '1', fieldName: '', }, + options: { isFullEditor: true }, }); expect(state).toMatchObject({ @@ -499,6 +526,7 @@ describe('loader', () => { savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, + options: { isFullEditor: true }, }); expect(state).toMatchObject({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 64c4122245ce0..52505b12be4c3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { SavedObjectsClientContract, HttpSetup, SavedObjectReference } from 'kibana/public'; -import { StateSetter } from '../types'; +import { InitializationOptions, StateSetter } from '../types'; import { IndexPattern, IndexPatternRef, @@ -190,6 +190,7 @@ export async function loadInitialState({ storage, indexPatternsService, initialContext, + options, }: { persistedState?: IndexPatternPersistedState; references?: SavedObjectReference[]; @@ -198,8 +199,10 @@ export async function loadInitialState({ storage: IStorageWrapper; indexPatternsService: IndexPatternsService; initialContext?: VisualizeFieldContext; + options?: InitializationOptions; }): Promise { - const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient); + const { isFullEditor } = options ?? {}; + const indexPatternRefs = await (isFullEditor ? loadIndexPatternRefs(savedObjectsClient) : []); const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); const state = @@ -210,11 +213,15 @@ export async function loadInitialState({ ? Object.values(state.layers) .map((l) => l.indexPatternId) .concat(state.currentIndexPatternId) - : [lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0].id] - ); + : [lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id] + ) + // take out the undefined from the list + .filter(Boolean); const currentIndexPatternId = initialContext?.indexPatternId ?? requiredPatterns[0]; - setLastUsedIndexPatternId(storage, currentIndexPatternId); + if (currentIndexPatternId) { + setLastUsedIndexPatternId(storage, currentIndexPatternId); + } const indexPatterns = await loadIndexPatterns({ indexPatternsService, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index bba601f942380..9feed918635b3 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -139,6 +139,10 @@ export interface DatasourceSuggestion { export type StateSetter = (newState: T | ((prevState: T) => T)) => void; +export interface InitializationOptions { + isFullEditor?: boolean; +} + /** * Interface for the datasource registry */ @@ -151,7 +155,8 @@ export interface Datasource { initialize: ( state?: P, savedObjectReferences?: SavedObjectReference[], - initialContext?: VisualizeFieldContext + initialContext?: VisualizeFieldContext, + options?: InitializationOptions ) => Promise; // Given the current state, which parts should be saved? From 7bb8d3a7b244d3d3f8127a757f480ab6a9d7a312 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 26 Jan 2021 13:09:53 -0600 Subject: [PATCH 011/163] [Workplace Search] Fix Private Dashboard routes (#88985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add index route for personal dashboard * Fix links to personal source flow In ent-search, the base route was /sources so the getSourcesPath helper was not needed. In Kibana, we use the ‘/p’ route to differentiate personal from org so the helper is needed and we pass false as the isOrganization flag * Remove legacy sidebar text When I first migrated this, I left the sidebar copy in so that it was not aboandoned before the design pass. After talking with John we decided to just use the copy to the right of the sidebar so this drops that legacy copy. * Remove constants * Remove legacy sidebar link * Revert "Remove legacy sidebar text" This reverts commit 8c8a3fb63c08154fbbdbe44533a8dd8ec6b265da. * Revert "Remove constants" This reverts commit a88723ec90d10e7b03214f176a2cab133e9d3ae5. * Revert "Remove legacy sidebar link" This reverts commit 5d08a12a7d53690805727f181967bd3d82b24235. * Update TODO Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/applications/workplace_search/index.tsx | 6 ++++++ .../views/content_sources/private_sources.tsx | 11 ++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 65a2c7a4a44dd..d10de7a770171 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -78,6 +78,12 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { {errorConnecting ? : } + + {/* TODO: replace Layout with PrivateSourcesLayout (needs to be created) */} + } restrictWidth readOnlyMode={readOnlyMode}> + + + } />} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx index a1a76c678866c..c11cdaa5ec36f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx @@ -12,7 +12,7 @@ import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui'; import { LicensingLogic } from '../../../../applications/shared/licensing'; -import { ADD_SOURCE_PATH } from '../../routes'; +import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import noSharedSourcesIcon from '../../assets/share_circle.svg'; @@ -74,12 +74,17 @@ export const PrivateSources: React.FC = () => { sidebarLinks.push({ title: PRIVATE_LINK_TITLE, iconType: 'plusInCircle', - path: ADD_SOURCE_PATH, + path: getSourcesPath(ADD_SOURCE_PATH, false), }); } const headerAction = ( - + {PRIVATE_LINK_TITLE} ); From 0c2c451830d7f8b8ecacd8b2500e44f1cf5d7741 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 26 Jan 2021 21:56:31 +0200 Subject: [PATCH 012/163] [Security Solution][Case] Add button to go to case view after adding an alert to a case (#89214) --- .../add_to_case_action.test.tsx | 64 ++++++++++++++++--- .../timeline_actions/add_to_case_action.tsx | 23 ++++++- .../{helpers.test.ts => helpers.test.tsx} | 22 ++----- .../{helpers.ts => helpers.tsx} | 22 ++++--- .../timeline_actions/toaster_content.test.tsx | 45 +++++++++++++ .../timeline_actions/toaster_content.tsx | 46 +++++++++++++ .../timeline_actions/translations.ts | 7 ++ 7 files changed, 195 insertions(+), 34 deletions(-) rename x-pack/plugins/security_solution/public/cases/components/timeline_actions/{helpers.test.ts => helpers.test.tsx} (55%) rename x-pack/plugins/security_solution/public/cases/components/timeline_actions/{helpers.ts => helpers.tsx} (59%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/timeline_actions/toaster_content.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/timeline_actions/toaster_content.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index 0c156e247a5e0..71d7387d8d392 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -6,18 +6,24 @@ /* eslint-disable react/display-name */ import React, { ReactNode } from 'react'; - import { mount } from 'enzyme'; +import { EuiGlobalToastList } from '@elastic/eui'; + +import { useKibana } from '../../../common/lib/kibana'; +import { useStateToaster } from '../../../common/components/toasters'; import { TestProviders } from '../../../common/mock'; import { usePostComment } from '../../containers/use_post_comment'; +import { Case } from '../../containers/types'; import { AddToCaseAction } from './add_to_case_action'; jest.mock('../../containers/use_post_comment'); -jest.mock('../../../common/lib/kibana', () => { - const originalModule = jest.requireActual('../../../common/lib/kibana'); +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../../common/components/toasters', () => { + const actual = jest.requireActual('../../../common/components/toasters'); return { - ...originalModule, - useGetUserSavedObjectPermissions: jest.fn(), + ...actual, + useStateToaster: jest.fn(), }; }); @@ -44,14 +50,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: (theCase: Partial) => void; }) => { return ( <> @@ -95,9 +103,16 @@ describe('AddToCaseAction', () => { disabled: false, }; + const mockDispatchToaster = jest.fn(); + const mockNavigateToApp = jest.fn(); + beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); usePostCommentMock.mockImplementation(() => defaultPostComment); + (useStateToaster as jest.Mock).mockReturnValue([jest.fn(), mockDispatchToaster]); + (useKibana as jest.Mock).mockReturnValue({ + services: { application: { navigateToApp: mockNavigateToApp } }, + }); }); it('it renders', async () => { @@ -187,4 +202,37 @@ describe('AddToCaseAction', () => { type: 'alert', }); }); + + it('navigates to case view', async () => { + usePostCommentMock.mockImplementation(() => { + return { + ...defaultPostComment, + postComment: jest.fn().mockImplementation((caseId, data, updateCase) => updateCase()), + }; + }); + + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click'); + + expect(mockDispatchToaster).toHaveBeenCalled(); + const toast = mockDispatchToaster.mock.calls[0][0].toast; + + const toastWrapper = mount( + {}} /> + ); + + toastWrapper + .find('[data-test-subj="toaster-content-case-view-link"]') + .first() + .simulate('click'); + + expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/new-case' }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 3ebc0654fc019..eed4f2092fd58 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -20,6 +20,10 @@ import { ActionIconItem } from '../../../timelines/components/timeline/body/acti import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { useStateToaster } from '../../../common/components/toasters'; +import { APP_ID } from '../../../../common/constants'; +import { useKibana } from '../../../common/lib/kibana'; +import { getCaseDetailsUrl } from '../../../common/components/link_to'; +import { SecurityPageName } from '../../../app/types'; import { useCreateCaseModal } from '../use_create_case_modal'; import { useAllCasesModal } from '../use_all_cases_modal'; import { createUpdateSuccessToaster } from './helpers'; @@ -39,12 +43,23 @@ const AddToCaseActionComponent: React.FC = ({ const eventId = ecsRowData._id; const eventIndex = ecsRowData._index; + const { navigateToApp } = useKibana().services.application; const [, dispatchToaster] = useStateToaster(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const openPopover = useCallback(() => setIsPopoverOpen(true), []); const closePopover = useCallback(() => setIsPopoverOpen(false), []); const { postComment } = usePostComment(); + + const onViewCaseClick = useCallback( + (id) => { + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCaseDetailsUrl({ id }), + }); + }, + [navigateToApp] + ); + const attachAlertToCase = useCallback( (theCase: Case) => { postComment( @@ -54,10 +69,14 @@ const AddToCaseActionComponent: React.FC = ({ alertId: eventId, index: eventIndex ?? '', }, - () => dispatchToaster({ type: 'addToaster', toast: createUpdateSuccessToaster(theCase) }) + () => + dispatchToaster({ + type: 'addToaster', + toast: createUpdateSuccessToaster(theCase, onViewCaseClick), + }) ); }, - [postComment, eventId, eventIndex, dispatchToaster] + [postComment, eventId, eventIndex, dispatchToaster, onViewCaseClick] ); const { modal: createCaseModal, openModal: openCreateCaseModal } = useCreateCaseModal({ diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.ts b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.tsx similarity index 55% rename from x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.ts rename to x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.tsx index 58c9c4baf82eb..b05dc4134cb10 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.tsx @@ -8,6 +8,7 @@ import { createUpdateSuccessToaster } from './helpers'; import { Case } from '../../containers/types'; const theCase = { + id: 'case-id', title: 'My case', settings: { syncAlerts: true, @@ -15,24 +16,13 @@ const theCase = { } as Case; describe('helpers', () => { + const onViewCaseClick = jest.fn(); + describe('createUpdateSuccessToaster', () => { it('creates the correct toast when the sync alerts is on', () => { - // We remove the id as is randomly generated - const { id, ...toast } = createUpdateSuccessToaster(theCase); - expect(toast).toEqual({ - color: 'success', - iconType: 'check', - text: 'Alerts in this case have their status synched with the case status', - title: 'An alert has been added to "My case"', - }); - }); - - it('creates the correct toast when the sync alerts is off', () => { - // We remove the id as is randomly generated - const { id, ...toast } = createUpdateSuccessToaster({ - ...theCase, - settings: { syncAlerts: false }, - }); + // We remove the id as is randomly generated and the text as it is a React component + // which is being test on toaster_content.test.tsx + const { id, text, ...toast } = createUpdateSuccessToaster(theCase, onViewCaseClick); expect(toast).toEqual({ color: 'success', iconType: 'check', diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.ts rename to x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.tsx index abafa55c28903..b1bae8df0a0b1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.ts +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.tsx @@ -4,22 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import uuid from 'uuid'; import { AppToast } from '../../../common/components/toasters'; import { Case } from '../../containers/types'; +import { ToasterContent } from './toaster_content'; import * as i18n from './translations'; -export const createUpdateSuccessToaster = (theCase: Case): AppToast => { - const toast: AppToast = { +export const createUpdateSuccessToaster = ( + theCase: Case, + onViewCaseClick: (id: string) => void +): AppToast => { + return { id: uuid.v4(), color: 'success', iconType: 'check', title: i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title), + text: ( + + ), }; - - if (theCase.settings.syncAlerts) { - return { ...toast, text: i18n.CASE_CREATED_SUCCESS_TOAST_TEXT }; - } - - return toast; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/toaster_content.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/toaster_content.test.tsx new file mode 100644 index 0000000000000..b04ebb8903aea --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/toaster_content.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { ToasterContent } from './toaster_content'; + +describe('ToasterContent', () => { + const onViewCaseClick = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with syncAlerts=true', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="toaster-content-case-view-link"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="toaster-content-sync-text"]').exists()).toBeTruthy(); + }); + + it('renders with syncAlerts=false', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="toaster-content-case-view-link"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="toaster-content-sync-text"]').exists()).toBeFalsy(); + }); + + it('calls onViewCaseClick', () => { + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="toaster-content-case-view-link"]').first().simulate('click'); + expect(onViewCaseClick).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/toaster_content.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/toaster_content.tsx new file mode 100644 index 0000000000000..871db464d8576 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/toaster_content.tsx @@ -0,0 +1,46 @@ +/* + * 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, { memo, useCallback } from 'react'; +import { EuiButtonEmpty, EuiText } from '@elastic/eui'; +import styled from 'styled-components'; + +import * as i18n from './translations'; + +const EuiTextStyled = styled(EuiText)` + ${({ theme }) => ` + margin-bottom: ${theme.eui?.paddingSizes?.s ?? 8}px; + `} +`; + +interface Props { + caseId: string; + syncAlerts: boolean; + onViewCaseClick: (id: string) => void; +} + +const ToasterContentComponent: React.FC = ({ caseId, syncAlerts, onViewCaseClick }) => { + const onClick = useCallback(() => onViewCaseClick(caseId), [caseId, onViewCaseClick]); + return ( + <> + {syncAlerts && ( + + {i18n.CASE_CREATED_SUCCESS_TOAST_TEXT} + + )} + + {i18n.VIEW_CASE} + + + ); +}; + +export const ToasterContent = memo(ToasterContentComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts index 479323ed1301c..dd3d6f0b50f19 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts @@ -53,3 +53,10 @@ export const CASE_CREATED_SUCCESS_TOAST_TEXT = i18n.translate( defaultMessage: 'Alerts in this case have their status synched with the case status', } ); + +export const VIEW_CASE = i18n.translate( + 'xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToastViewCaseLink', + { + defaultMessage: 'View Case', + } +); From 87992d01da5787c0dd21f1186e8e8926cac97f07 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Tue, 26 Jan 2021 12:49:44 -0800 Subject: [PATCH 013/163] Watcher -functional xpack test using test_user with specific permissions. (#89068) * fixes https://github.com/elastic/kibana/issues/74449 * watcher test with specific permissions * adding the false parameter Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/functional/apps/watcher/watcher_test.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/watcher/watcher_test.js b/x-pack/test/functional/apps/watcher/watcher_test.js index 1dd3fb6bbcc3d..2525f4d8f9621 100644 --- a/x-pack/test/functional/apps/watcher/watcher_test.js +++ b/x-pack/test/functional/apps/watcher/watcher_test.js @@ -15,16 +15,15 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); const log = getService('log'); + const security = getService('security'); const esSupertest = getService('esSupertest'); const PageObjects = getPageObjects(['security', 'common', 'header', 'settings', 'watcher']); - // Still flaky test :c - // https://github.com/elastic/kibana/pull/56361 - // https://github.com/elastic/kibana/pull/56304 - describe.skip('watcher_test', function () { + describe('watcher_test', function () { before('initialize tests', async () => { // There may be system watches if monitoring was previously enabled // These cannot be deleted via the UI, so we need to delete via the API + await security.testUser.setRoles(['kibana_admin', 'watcher_admin'], false); const watches = await esSupertest.get('/.watches/_search'); if (watches.status === 200) { @@ -56,6 +55,10 @@ export default function ({ getService, getPageObjects }) { }); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('create and save a new watch', async () => { await PageObjects.watcher.createWatch(watchID, watchName); const watch = await PageObjects.watcher.getWatch(watchID); From 0b6184769622e1e13c4890c3f7a9711b196e403c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 26 Jan 2021 14:32:09 -0700 Subject: [PATCH 014/163] [Maps] rename file_upload to maps_file_upload (#89225) * migrate file_upload plugin to maps_file_upload * update plugins list * results of running node scripts/build_plugin_list_docs * fix build problems * remove fileUpload from limits.yml Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 8 ++++---- packages/kbn-optimizer/limits.yml | 2 +- x-pack/.i18nrc.json | 2 +- x-pack/plugins/file_upload/README.md | 3 --- x-pack/plugins/maps/kibana.json | 2 +- .../public/classes/layers/file_upload_wizard/wizard.tsx | 2 +- x-pack/plugins/maps/public/kibana_services.ts | 2 +- x-pack/plugins/maps/public/plugin.ts | 4 ++-- x-pack/plugins/maps_file_upload/README.md | 3 +++ .../common/constants/file_import.ts | 0 .../{file_upload => maps_file_upload}/jest.config.js | 2 +- .../plugins/{file_upload => maps_file_upload}/kibana.json | 2 +- .../plugins/{file_upload => maps_file_upload}/mappings.ts | 0 .../public/components/index_settings.js | 0 .../public/components/json_import_progress.js | 0 .../public/components/json_index_file_picker.js | 0 .../public/components/json_upload_and_parse.js | 0 .../public/get_file_upload_component.ts | 0 .../{file_upload => maps_file_upload}/public/index.ts | 0 .../public/kibana_services.js | 0 .../{file_upload => maps_file_upload}/public/plugin.ts | 0 .../public/util/file_parser.js | 0 .../public/util/file_parser.test.js | 0 .../public/util/geo_json_clean_and_validate.js | 0 .../public/util/geo_json_clean_and_validate.test.js | 0 .../public/util/geo_processing.js | 0 .../public/util/geo_processing.test.js | 0 .../public/util/http_service.js | 0 .../public/util/indexing_service.js | 0 .../public/util/indexing_service.test.js | 0 .../public/util/size_limited_chunking.js | 0 .../public/util/size_limited_chunking.test.js | 0 .../server/client/errors.js | 0 .../{file_upload => maps_file_upload}/server/index.js | 0 .../server/kibana_server_services.js | 0 .../server/models/import_data/import_data.js | 0 .../server/models/import_data/index.js | 0 .../{file_upload => maps_file_upload}/server/plugin.js | 0 .../server/routes/file_upload.js | 0 .../server/routes/file_upload.test.js | 0 .../server/telemetry/file_upload_usage_collector.ts | 0 .../server/telemetry/index.ts | 0 .../server/telemetry/mappings.ts | 0 .../server/telemetry/telemetry.test.ts | 0 .../server/telemetry/telemetry.ts | 0 45 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 x-pack/plugins/file_upload/README.md create mode 100644 x-pack/plugins/maps_file_upload/README.md rename x-pack/plugins/{file_upload => maps_file_upload}/common/constants/file_import.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/jest.config.js (84%) rename x-pack/plugins/{file_upload => maps_file_upload}/kibana.json (83%) rename x-pack/plugins/{file_upload => maps_file_upload}/mappings.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/components/index_settings.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/components/json_import_progress.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/components/json_index_file_picker.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/components/json_upload_and_parse.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/get_file_upload_component.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/index.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/kibana_services.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/plugin.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/file_parser.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/file_parser.test.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/geo_json_clean_and_validate.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/geo_json_clean_and_validate.test.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/geo_processing.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/geo_processing.test.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/http_service.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/indexing_service.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/indexing_service.test.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/size_limited_chunking.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/public/util/size_limited_chunking.test.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/client/errors.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/index.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/kibana_server_services.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/models/import_data/import_data.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/models/import_data/index.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/plugin.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/routes/file_upload.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/routes/file_upload.test.js (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/telemetry/file_upload_usage_collector.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/telemetry/index.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/telemetry/mappings.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/telemetry/telemetry.test.ts (100%) rename x-pack/plugins/{file_upload => maps_file_upload}/server/telemetry/telemetry.ts (100%) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index ef3492a545b6a..7ce4896a8bce4 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -380,10 +380,6 @@ and actions. |The features plugin enhance Kibana with a per-feature privilege system. -|{kib-repo}blob/{branch}/x-pack/plugins/file_upload/README.md[fileUpload] -|Backend and core front-end react-components for GeoJson file upload. Only supports the Maps plugin. - - |{kib-repo}blob/{branch}/x-pack/plugins/fleet/README.md[fleet] |Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.fleet.agents.tlsCheckDisabled=false) @@ -453,6 +449,10 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. +|{kib-repo}blob/{branch}/x-pack/plugins/maps_file_upload/README.md[mapsFileUpload] +|Deprecated - plugin targeted for removal and will get merged into file_upload plugin + + |{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] |This plugin provides access to the detailed tile map services from Elastic. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 44cc4fdabb25e..ef672d6cbeb2e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -25,7 +25,6 @@ pageLoadAssetSize: esUiShared: 326654 expressions: 224136 features: 31211 - fileUpload: 24717 globalSearch: 43548 globalSearchBar: 62888 globalSearchProviders: 25554 @@ -106,3 +105,4 @@ pageLoadAssetSize: stackAlerts: 29684 presentationUtil: 28545 spacesOss: 18817 + mapsFileUpload: 23775 diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6937862d20536..bfac437f3500a 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -20,7 +20,7 @@ "xpack.endpoint": "plugins/endpoint", "xpack.enterpriseSearch": "plugins/enterprise_search", "xpack.features": "plugins/features", - "xpack.fileUpload": "plugins/file_upload", + "xpack.fileUpload": "plugins/maps_file_upload", "xpack.globalSearch": ["plugins/global_search"], "xpack.globalSearchBar": ["plugins/global_search_bar"], "xpack.graph": ["plugins/graph"], diff --git a/x-pack/plugins/file_upload/README.md b/x-pack/plugins/file_upload/README.md deleted file mode 100644 index 0d4b4da61ccf6..0000000000000 --- a/x-pack/plugins/file_upload/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# File upload - -Backend and core front-end react-components for GeoJson file upload. Only supports the Maps plugin. \ No newline at end of file diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 42adf6f2a950b..5f6f5224e32a3 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -8,7 +8,7 @@ "features", "inspector", "data", - "fileUpload", + "mapsFileUpload", "uiActions", "navigation", "visualizations", diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 68fd25ce9e7ae..54e58c876a839 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -18,7 +18,7 @@ import { GeoJsonFileSource } from '../../sources/geojson_file_source'; import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { FileUploadComponentProps } from '../../../../../file_upload/public'; +import { FileUploadComponentProps } from '../../../../../maps_file_upload/public'; export const INDEX_SETUP_STEP_ID = 'INDEX_SETUP_STEP_ID'; export const INDEXING_STEP_ID = 'INDEXING_STEP_ID'; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 02b875257a5ac..99c9311a2a454 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -25,7 +25,7 @@ export const getIndexPatternService = () => pluginsStart.data.indexPatterns; export const getAutocompleteService = () => pluginsStart.data.autocomplete; export const getInspector = () => pluginsStart.inspector; export const getFileUploadComponent = async () => { - return await pluginsStart.fileUpload.getFileUploadComponent(); + return await pluginsStart.mapsFileUpload.getFileUploadComponent(); }; export const getUiSettings = () => coreStart.uiSettings; export const getIsDarkMode = () => getUiSettings().get('theme:darkMode', false); diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 690002a771601..dd256126fae62 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -54,7 +54,7 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; -import { StartContract as FileUploadStartContract } from '../../file_upload/public'; +import { StartContract as FileUploadStartContract } from '../../maps_file_upload/public'; import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; import { getIsEnterprisePlus, @@ -77,7 +77,7 @@ export interface MapsPluginSetupDependencies { export interface MapsPluginStartDependencies { data: DataPublicPluginStart; embeddable: EmbeddableStart; - fileUpload: FileUploadStartContract; + mapsFileUpload: FileUploadStartContract; inspector: InspectorStartContract; licensing: LicensingPluginStart; navigation: NavigationPublicPluginStart; diff --git a/x-pack/plugins/maps_file_upload/README.md b/x-pack/plugins/maps_file_upload/README.md new file mode 100644 index 0000000000000..1e3343664afb8 --- /dev/null +++ b/x-pack/plugins/maps_file_upload/README.md @@ -0,0 +1,3 @@ +# Maps File upload + +Deprecated - plugin targeted for removal and will get merged into file_upload plugin diff --git a/x-pack/plugins/file_upload/common/constants/file_import.ts b/x-pack/plugins/maps_file_upload/common/constants/file_import.ts similarity index 100% rename from x-pack/plugins/file_upload/common/constants/file_import.ts rename to x-pack/plugins/maps_file_upload/common/constants/file_import.ts diff --git a/x-pack/plugins/file_upload/jest.config.js b/x-pack/plugins/maps_file_upload/jest.config.js similarity index 84% rename from x-pack/plugins/file_upload/jest.config.js rename to x-pack/plugins/maps_file_upload/jest.config.js index 6a042a4cc5c1e..2893da079141c 100644 --- a/x-pack/plugins/file_upload/jest.config.js +++ b/x-pack/plugins/maps_file_upload/jest.config.js @@ -7,5 +7,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', - roots: ['/x-pack/plugins/file_upload'], + roots: ['/x-pack/plugins/maps_file_upload'], }; diff --git a/x-pack/plugins/file_upload/kibana.json b/x-pack/plugins/maps_file_upload/kibana.json similarity index 83% rename from x-pack/plugins/file_upload/kibana.json rename to x-pack/plugins/maps_file_upload/kibana.json index 7676a01d0b0f9..f544c56cba517 100644 --- a/x-pack/plugins/file_upload/kibana.json +++ b/x-pack/plugins/maps_file_upload/kibana.json @@ -1,5 +1,5 @@ { - "id": "fileUpload", + "id": "mapsFileUpload", "version": "8.0.0", "kibanaVersion": "kibana", "server": true, diff --git a/x-pack/plugins/file_upload/mappings.ts b/x-pack/plugins/maps_file_upload/mappings.ts similarity index 100% rename from x-pack/plugins/file_upload/mappings.ts rename to x-pack/plugins/maps_file_upload/mappings.ts diff --git a/x-pack/plugins/file_upload/public/components/index_settings.js b/x-pack/plugins/maps_file_upload/public/components/index_settings.js similarity index 100% rename from x-pack/plugins/file_upload/public/components/index_settings.js rename to x-pack/plugins/maps_file_upload/public/components/index_settings.js diff --git a/x-pack/plugins/file_upload/public/components/json_import_progress.js b/x-pack/plugins/maps_file_upload/public/components/json_import_progress.js similarity index 100% rename from x-pack/plugins/file_upload/public/components/json_import_progress.js rename to x-pack/plugins/maps_file_upload/public/components/json_import_progress.js diff --git a/x-pack/plugins/file_upload/public/components/json_index_file_picker.js b/x-pack/plugins/maps_file_upload/public/components/json_index_file_picker.js similarity index 100% rename from x-pack/plugins/file_upload/public/components/json_index_file_picker.js rename to x-pack/plugins/maps_file_upload/public/components/json_index_file_picker.js diff --git a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.js b/x-pack/plugins/maps_file_upload/public/components/json_upload_and_parse.js similarity index 100% rename from x-pack/plugins/file_upload/public/components/json_upload_and_parse.js rename to x-pack/plugins/maps_file_upload/public/components/json_upload_and_parse.js diff --git a/x-pack/plugins/file_upload/public/get_file_upload_component.ts b/x-pack/plugins/maps_file_upload/public/get_file_upload_component.ts similarity index 100% rename from x-pack/plugins/file_upload/public/get_file_upload_component.ts rename to x-pack/plugins/maps_file_upload/public/get_file_upload_component.ts diff --git a/x-pack/plugins/file_upload/public/index.ts b/x-pack/plugins/maps_file_upload/public/index.ts similarity index 100% rename from x-pack/plugins/file_upload/public/index.ts rename to x-pack/plugins/maps_file_upload/public/index.ts diff --git a/x-pack/plugins/file_upload/public/kibana_services.js b/x-pack/plugins/maps_file_upload/public/kibana_services.js similarity index 100% rename from x-pack/plugins/file_upload/public/kibana_services.js rename to x-pack/plugins/maps_file_upload/public/kibana_services.js diff --git a/x-pack/plugins/file_upload/public/plugin.ts b/x-pack/plugins/maps_file_upload/public/plugin.ts similarity index 100% rename from x-pack/plugins/file_upload/public/plugin.ts rename to x-pack/plugins/maps_file_upload/public/plugin.ts diff --git a/x-pack/plugins/file_upload/public/util/file_parser.js b/x-pack/plugins/maps_file_upload/public/util/file_parser.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/file_parser.js rename to x-pack/plugins/maps_file_upload/public/util/file_parser.js diff --git a/x-pack/plugins/file_upload/public/util/file_parser.test.js b/x-pack/plugins/maps_file_upload/public/util/file_parser.test.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/file_parser.test.js rename to x-pack/plugins/maps_file_upload/public/util/file_parser.test.js diff --git a/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js b/x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js rename to x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.js diff --git a/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js b/x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.test.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js rename to x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.test.js diff --git a/x-pack/plugins/file_upload/public/util/geo_processing.js b/x-pack/plugins/maps_file_upload/public/util/geo_processing.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/geo_processing.js rename to x-pack/plugins/maps_file_upload/public/util/geo_processing.js diff --git a/x-pack/plugins/file_upload/public/util/geo_processing.test.js b/x-pack/plugins/maps_file_upload/public/util/geo_processing.test.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/geo_processing.test.js rename to x-pack/plugins/maps_file_upload/public/util/geo_processing.test.js diff --git a/x-pack/plugins/file_upload/public/util/http_service.js b/x-pack/plugins/maps_file_upload/public/util/http_service.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/http_service.js rename to x-pack/plugins/maps_file_upload/public/util/http_service.js diff --git a/x-pack/plugins/file_upload/public/util/indexing_service.js b/x-pack/plugins/maps_file_upload/public/util/indexing_service.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/indexing_service.js rename to x-pack/plugins/maps_file_upload/public/util/indexing_service.js diff --git a/x-pack/plugins/file_upload/public/util/indexing_service.test.js b/x-pack/plugins/maps_file_upload/public/util/indexing_service.test.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/indexing_service.test.js rename to x-pack/plugins/maps_file_upload/public/util/indexing_service.test.js diff --git a/x-pack/plugins/file_upload/public/util/size_limited_chunking.js b/x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/size_limited_chunking.js rename to x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.js diff --git a/x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js b/x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.test.js similarity index 100% rename from x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js rename to x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.test.js diff --git a/x-pack/plugins/file_upload/server/client/errors.js b/x-pack/plugins/maps_file_upload/server/client/errors.js similarity index 100% rename from x-pack/plugins/file_upload/server/client/errors.js rename to x-pack/plugins/maps_file_upload/server/client/errors.js diff --git a/x-pack/plugins/file_upload/server/index.js b/x-pack/plugins/maps_file_upload/server/index.js similarity index 100% rename from x-pack/plugins/file_upload/server/index.js rename to x-pack/plugins/maps_file_upload/server/index.js diff --git a/x-pack/plugins/file_upload/server/kibana_server_services.js b/x-pack/plugins/maps_file_upload/server/kibana_server_services.js similarity index 100% rename from x-pack/plugins/file_upload/server/kibana_server_services.js rename to x-pack/plugins/maps_file_upload/server/kibana_server_services.js diff --git a/x-pack/plugins/file_upload/server/models/import_data/import_data.js b/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js similarity index 100% rename from x-pack/plugins/file_upload/server/models/import_data/import_data.js rename to x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js diff --git a/x-pack/plugins/file_upload/server/models/import_data/index.js b/x-pack/plugins/maps_file_upload/server/models/import_data/index.js similarity index 100% rename from x-pack/plugins/file_upload/server/models/import_data/index.js rename to x-pack/plugins/maps_file_upload/server/models/import_data/index.js diff --git a/x-pack/plugins/file_upload/server/plugin.js b/x-pack/plugins/maps_file_upload/server/plugin.js similarity index 100% rename from x-pack/plugins/file_upload/server/plugin.js rename to x-pack/plugins/maps_file_upload/server/plugin.js diff --git a/x-pack/plugins/file_upload/server/routes/file_upload.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.js similarity index 100% rename from x-pack/plugins/file_upload/server/routes/file_upload.js rename to x-pack/plugins/maps_file_upload/server/routes/file_upload.js diff --git a/x-pack/plugins/file_upload/server/routes/file_upload.test.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js similarity index 100% rename from x-pack/plugins/file_upload/server/routes/file_upload.test.js rename to x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js diff --git a/x-pack/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts similarity index 100% rename from x-pack/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts rename to x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts diff --git a/x-pack/plugins/file_upload/server/telemetry/index.ts b/x-pack/plugins/maps_file_upload/server/telemetry/index.ts similarity index 100% rename from x-pack/plugins/file_upload/server/telemetry/index.ts rename to x-pack/plugins/maps_file_upload/server/telemetry/index.ts diff --git a/x-pack/plugins/file_upload/server/telemetry/mappings.ts b/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts similarity index 100% rename from x-pack/plugins/file_upload/server/telemetry/mappings.ts rename to x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts diff --git a/x-pack/plugins/file_upload/server/telemetry/telemetry.test.ts b/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts similarity index 100% rename from x-pack/plugins/file_upload/server/telemetry/telemetry.test.ts rename to x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts diff --git a/x-pack/plugins/file_upload/server/telemetry/telemetry.ts b/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts similarity index 100% rename from x-pack/plugins/file_upload/server/telemetry/telemetry.ts rename to x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts From 46285b2257c5ad11add7412e4145d842e3f91b83 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 26 Jan 2021 22:34:43 +0100 Subject: [PATCH 015/163] [ILM] Timeline component (#88024) * cleaning up unused types and legacy logic * added new relative age logic with unit tests * initial implementation of timeline * added custom infinity icon to timeline component * added comment * move timeline color bar comment * fix nanoseconds and microsecnds bug * added policy timeline heading, removed "at least" copy for now * a few minor changes - fix up copy - fix up responsive/mobile first view of timeline - adjust minimum size of a color bar * minor refactor to css classnames and make trash can for delete more prominent * added delete icon tooltip with rough first copy * added smoke test for timeline and how it interacts with different policy states * update test and copy * convert string svg to react svg component and use euiIcon class and refactor scss * update delete icon tooltip copy and add aria-label to svg * mock EuiIcon component in jest tests because it causes issues with custom react-svg component * added comment to mock * remove setting of classname * fix typo and update delete icon tooltip content * refinements to the delete icon at the end of the timline and support dark mode Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../client_integration/app/app.test.ts | 9 + .../edit_policy/edit_policy.helpers.tsx | 7 + .../edit_policy/edit_policy.test.ts | 46 ++++ .../__jest__/components/edit_policy.test.tsx | 9 + .../common/types/policies.ts | 2 + .../sections/edit_policy/components/index.ts | 1 + .../edit_policy/components/timeline/index.ts | 6 + .../components/timeline/infinity_icon.svg.tsx | 26 +++ .../components/timeline/timeline.scss | 87 ++++++++ .../components/timeline/timeline.tsx | 208 ++++++++++++++++++ .../sections/edit_policy/edit_policy.tsx | 7 +- 11 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/infinity_icon.svg.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts index 9052cf2847baa..1ffbae39d3705 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts @@ -23,6 +23,15 @@ const PERCENT_SIGN_25_SEQUENCE = 'test%25'; window.scrollTo = jest.fn(); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. + }; +}); + describe('', () => { let testBed: AppTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 7206fbfd547d4..72a0372628a22 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -41,6 +41,7 @@ jest.mock('@elastic/eui', () => { }} /> ), + EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. }; }); @@ -236,6 +237,12 @@ export const setup = async (arg?: { appServicesContext: Partial exists('ilmTimelineHotPhase'), + hasWarmPhase: () => exists('ilmTimelineWarmPhase'), + hasColdPhase: () => exists('ilmTimelineColdPhase'), + hasDeletePhase: () => exists('ilmTimelineDeletePhase'), + }, hot: { setMaxSize, setMaxDocs, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index ff070a7f08bb1..e42e503fb853a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -824,4 +824,50 @@ describe('', () => { expect(actions.cold.searchableSnapshotDisabledDueToRollover()).toBeTruthy(); }); }); + + describe('policy timeline', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('showing all phases on the timeline', async () => { + const { actions } = testBed; + // This is how the default policy should look + expect(actions.timeline.hasHotPhase()).toBe(true); + expect(actions.timeline.hasWarmPhase()).toBe(false); + expect(actions.timeline.hasColdPhase()).toBe(false); + expect(actions.timeline.hasDeletePhase()).toBe(false); + + await actions.warm.enable(true); + expect(actions.timeline.hasHotPhase()).toBe(true); + expect(actions.timeline.hasWarmPhase()).toBe(true); + expect(actions.timeline.hasColdPhase()).toBe(false); + expect(actions.timeline.hasDeletePhase()).toBe(false); + + await actions.cold.enable(true); + expect(actions.timeline.hasHotPhase()).toBe(true); + expect(actions.timeline.hasWarmPhase()).toBe(true); + expect(actions.timeline.hasColdPhase()).toBe(true); + expect(actions.timeline.hasDeletePhase()).toBe(false); + + await actions.delete.enable(true); + expect(actions.timeline.hasHotPhase()).toBe(true); + expect(actions.timeline.hasWarmPhase()).toBe(true); + expect(actions.timeline.hasColdPhase()).toBe(true); + expect(actions.timeline.hasDeletePhase()).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index c54ccb9f85edf..6aa6c3177ca5d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -81,6 +81,15 @@ for (let i = 0; i < 105; i++) { } window.scrollTo = jest.fn(); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. + }; +}); + let component: ReactElement; const activatePhase = async (rendered: ReactWrapper, phase: string) => { const testSubject = `enablePhaseSwitch-${phase}`; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 1f4b06e80c49f..a084f16226fda 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -20,6 +20,8 @@ export interface Phases { delete?: SerializedDeletePhase; } +export type PhasesExceptDelete = keyof Omit; + export interface PolicyFromES { modified_date: string; name: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index fa550214db477..d22206d7ae4de 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -11,5 +11,6 @@ export { OptionalLabel } from './optional_label'; export { PolicyJsonFlyout } from './policy_json_flyout'; export { DescribedFormRow, ToggleFieldWithDescribedFormRow } from './described_form_row'; export { FieldLoadingError } from './field_loading_error'; +export { Timeline } from './timeline'; export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts new file mode 100644 index 0000000000000..4664429db37d7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { Timeline } from './timeline'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/infinity_icon.svg.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/infinity_icon.svg.tsx new file mode 100644 index 0000000000000..b3b1b3cc56b3d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/infinity_icon.svg.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 React, { FunctionComponent } from 'react'; + +export const InfinityIconSvg: FunctionComponent = (props) => { + return ( + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss new file mode 100644 index 0000000000000..452221a29a991 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss @@ -0,0 +1,87 @@ +$ilmTimelineBarHeight: $euiSizeS; + +/* +* For theming we need to shade or tint to get the right color from the base EUI color +*/ +$ilmDeletePhaseBackgroundColor: tintOrShade($euiColorVis5_behindText, 80%,80%); +$ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%); + +.ilmTimeline { + overflow: hidden; + width: 100%; + + &__phasesContainer { + /* + * Let the delete icon sit on the same line as the phase color bars + */ + display: inline-block; + width: 100%; + + &__phase:first-child { + padding-left: 0; + padding-right: $euiSizeS; + } + + &__phase:last-child { + padding-left: $euiSizeS; + padding-right: 0; + } + + &__phase:only-child { + padding-left: 0; + padding-right: 0; + } + + &__phase { + /* + * Let the phase color bars sit horizontally + */ + display: inline-block; + + padding-left: $euiSizeS; + padding-right: $euiSizeS; + } + } + + &__deleteIconContainer { + /* + * Create a bit of space between the timeline and the delete icon + */ + padding: $euiSizeM; + margin-left: $euiSizeM; + background-color: $ilmDeletePhaseBackgroundColor; + color: $ilmDeletePhaseColor; + border-radius: calc(#{$euiSizeS} / 2); + } + + &__colorBar { + display: inline-block; + height: $ilmTimelineBarHeight; + border-radius: calc(#{$ilmTimelineBarHeight} / 2); + width: 100%; + } + + &__hotPhase { + width: var(--ilm-timeline-hot-phase-width); + + &__colorBar { + background-color: $euiColorVis9; + } + } + + &__warmPhase { + width: var(--ilm-timeline-warm-phase-width); + + &__colorBar { + background-color: $euiColorVis5; + } + } + + &__coldPhase { + width: var(--ilm-timeline-cold-phase-width); + + &__colorBar { + background-color: $euiColorVis1; + } + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx new file mode 100644 index 0000000000000..40bab9c676de2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -0,0 +1,208 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent, useMemo } from 'react'; +import { + EuiText, + EuiIcon, + EuiIconProps, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiIconTip, +} from '@elastic/eui'; + +import { PhasesExceptDelete } from '../../../../../../common/types'; +import { useFormData } from '../../../../../shared_imports'; + +import { FormInternal } from '../../types'; + +import { + calculateRelativeTimingMs, + normalizeTimingsToHumanReadable, + PhaseAgeInMilliseconds, +} from '../../lib'; + +import './timeline.scss'; +import { InfinityIconSvg } from './infinity_icon.svg'; + +const InfinityIcon: FunctionComponent> = (props) => ( + +); + +const toPercent = (n: number, total: number) => (n / total) * 100; + +const msTimeToOverallPercent = (ms: number, totalMs: number) => { + if (!isFinite(ms)) { + return 100; + } + if (totalMs === 0) { + return 100; + } + return toPercent(ms, totalMs); +}; + +/** + * Each phase, if active, should have a minimum width it occupies. The higher this + * base amount, the smaller the variance in phase size in the timeline. This functions + * as a min-width constraint. + */ +const SCORE_BUFFER_AMOUNT = 50; + +const i18nTexts = { + hotPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle', { + defaultMessage: 'Hot phase', + }), + warmPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle', { + defaultMessage: 'Warm phase', + }), + coldPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.coldPhaseSectionTitle', { + defaultMessage: 'Cold phase', + }), + deleteIcon: { + toolTipContent: i18n.translate('xpack.indexLifecycleMgmt.timeline.deleteIconToolTipContent', { + defaultMessage: 'Policy deletes the index after lifecycle phases complete.', + }), + }, +}; + +const calculateWidths = (inputs: PhaseAgeInMilliseconds) => { + const hotScore = msTimeToOverallPercent(inputs.phases.hot, inputs.total) + SCORE_BUFFER_AMOUNT; + const warmScore = + inputs.phases.warm != null + ? msTimeToOverallPercent(inputs.phases.warm, inputs.total) + SCORE_BUFFER_AMOUNT + : 0; + const coldScore = + inputs.phases.cold != null + ? msTimeToOverallPercent(inputs.phases.cold, inputs.total) + SCORE_BUFFER_AMOUNT + : 0; + + const totalScore = hotScore + warmScore + coldScore; + return { + hot: `${toPercent(hotScore, totalScore)}%`, + warm: `${toPercent(warmScore, totalScore)}%`, + cold: `${toPercent(coldScore, totalScore)}%`, + }; +}; + +const TimelinePhaseText: FunctionComponent<{ + phaseName: string; + durationInPhase?: React.ReactNode | string; +}> = ({ phaseName, durationInPhase }) => ( + + + + {phaseName} + + + + {typeof durationInPhase === 'string' ? ( + {durationInPhase} + ) : ( + durationInPhase + )} + + +); + +export const Timeline: FunctionComponent = () => { + const [formData] = useFormData(); + + const phaseTimingInMs = useMemo(() => { + return calculateRelativeTimingMs(formData); + }, [formData]); + + const humanReadableTimings = useMemo(() => normalizeTimingsToHumanReadable(phaseTimingInMs), [ + phaseTimingInMs, + ]); + + const widths = calculateWidths(phaseTimingInMs); + + const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode => + phaseTimingInMs.phases[phase] === Infinity ? ( + + ) : ( + humanReadableTimings[phase] + ); + + return ( + + + +

+ {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', { + defaultMessage: 'Policy Timeline', + })} +

+
+
+ +
{ + if (el) { + el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot); + el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null); + el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null); + } + }} + > + + +
+ {/* These are the actual color bars for the timeline */} +
+
+ +
+ {formData._meta?.warm.enabled && ( +
+
+ +
+ )} + {formData._meta?.cold.enabled && ( +
+
+ +
+ )} +
+ + {formData._meta?.delete.enabled && ( + +
+ +
+
+ )} + +
+ + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index d945ae8bb3e4e..228a0f9fdb942 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -43,6 +43,7 @@ import { DeletePhase, HotPhase, WarmPhase, + Timeline, } from './components'; import { schema, deserializer, createSerializer, createPolicyNameValidations, Form } from './form'; @@ -256,7 +257,11 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { ) : null} - + + + + + From 1e7b6f0ec25974641f3d8e96f5257ba8c518d515 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 26 Jan 2021 13:42:27 -0800 Subject: [PATCH 016/163] skip flaky suite (#88826) --- test/functional/apps/home/_navigation.ts | 80 +++++++++++++----------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index c90398fa84afc..90d13b4b5e417 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -15,42 +15,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const esArchiver = getService('esArchiver'); - describe('Kibana browser back navigation should work', function describeIndexTests() { - before(async () => { - await esArchiver.loadIfNeeded('discover'); - await esArchiver.loadIfNeeded('logstash_functional'); - }); - - it('detect navigate back issues', async () => { - let currUrl; - // Detects bug described in issue #31238 - where back navigation would get stuck to URL encoding handling in Angular. - // Navigate to home app - await PageObjects.common.navigateToApp('home'); - const homeUrl = await browser.getCurrentUrl(); - - // Navigate to discover app - await appsMenu.clickLink('Discover'); - const discoverUrl = await browser.getCurrentUrl(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - const modifiedTimeDiscoverUrl = await browser.getCurrentUrl(); - - // Navigate to dashboard app - await appsMenu.clickLink('Dashboard'); - - // Navigating back to discover - await browser.goBack(); - currUrl = await browser.getCurrentUrl(); - expect(currUrl).to.be(modifiedTimeDiscoverUrl); - - // Navigating back from time settings - await browser.goBack(); // undo time settings - currUrl = await browser.getCurrentUrl(); - expect(currUrl.startsWith(discoverUrl)).to.be(true); - - // Navigate back home - await browser.goBack(); - currUrl = await browser.getCurrentUrl(); - expect(currUrl).to.be(homeUrl); - }); - }); + // Failing: See https://github.com/elastic/kibana/issues/88826 + describe.skip( + 'Kibana browser back navigation should work', + function describeIndexTests() { + before(async () => { + await esArchiver.loadIfNeeded('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + it('detect navigate back issues', async () => { + let currUrl; + // Detects bug described in issue #31238 - where back navigation would get stuck to URL encoding handling in Angular. + // Navigate to home app + await PageObjects.common.navigateToApp('home'); + const homeUrl = await browser.getCurrentUrl(); + + // Navigate to discover app + await appsMenu.clickLink('Discover'); + const discoverUrl = await browser.getCurrentUrl(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + const modifiedTimeDiscoverUrl = await browser.getCurrentUrl(); + + // Navigate to dashboard app + await appsMenu.clickLink('Dashboard'); + + // Navigating back to discover + await browser.goBack(); + currUrl = await browser.getCurrentUrl(); + expect(currUrl).to.be(modifiedTimeDiscoverUrl); + + // Navigating back from time settings + await browser.goBack(); // undo time settings + currUrl = await browser.getCurrentUrl(); + expect(currUrl.startsWith(discoverUrl)).to.be(true); + + // Navigate back home + await browser.goBack(); + currUrl = await browser.getCurrentUrl(); + expect(currUrl).to.be(homeUrl); + }); + } + ); } From 2a9a9305ee4cf6b79b917ff6c8d76575af739aff Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 26 Jan 2021 14:14:08 -0800 Subject: [PATCH 017/163] Fixes linting caused by skipping flaky suite Signed-off-by: Tyler Smalley --- test/functional/apps/home/_navigation.ts | 79 ++++++++++++------------ 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index 90d13b4b5e417..12f97a5349419 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -16,45 +16,42 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); // Failing: See https://github.com/elastic/kibana/issues/88826 - describe.skip( - 'Kibana browser back navigation should work', - function describeIndexTests() { - before(async () => { - await esArchiver.loadIfNeeded('discover'); - await esArchiver.loadIfNeeded('logstash_functional'); - }); - - it('detect navigate back issues', async () => { - let currUrl; - // Detects bug described in issue #31238 - where back navigation would get stuck to URL encoding handling in Angular. - // Navigate to home app - await PageObjects.common.navigateToApp('home'); - const homeUrl = await browser.getCurrentUrl(); - - // Navigate to discover app - await appsMenu.clickLink('Discover'); - const discoverUrl = await browser.getCurrentUrl(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - const modifiedTimeDiscoverUrl = await browser.getCurrentUrl(); - - // Navigate to dashboard app - await appsMenu.clickLink('Dashboard'); - - // Navigating back to discover - await browser.goBack(); - currUrl = await browser.getCurrentUrl(); - expect(currUrl).to.be(modifiedTimeDiscoverUrl); - - // Navigating back from time settings - await browser.goBack(); // undo time settings - currUrl = await browser.getCurrentUrl(); - expect(currUrl.startsWith(discoverUrl)).to.be(true); - - // Navigate back home - await browser.goBack(); - currUrl = await browser.getCurrentUrl(); - expect(currUrl).to.be(homeUrl); - }); - } - ); + describe.skip('Kibana browser back navigation should work', function describeIndexTests() { + before(async () => { + await esArchiver.loadIfNeeded('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + it('detect navigate back issues', async () => { + let currUrl; + // Detects bug described in issue #31238 - where back navigation would get stuck to URL encoding handling in Angular. + // Navigate to home app + await PageObjects.common.navigateToApp('home'); + const homeUrl = await browser.getCurrentUrl(); + + // Navigate to discover app + await appsMenu.clickLink('Discover'); + const discoverUrl = await browser.getCurrentUrl(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + const modifiedTimeDiscoverUrl = await browser.getCurrentUrl(); + + // Navigate to dashboard app + await appsMenu.clickLink('Dashboard'); + + // Navigating back to discover + await browser.goBack(); + currUrl = await browser.getCurrentUrl(); + expect(currUrl).to.be(modifiedTimeDiscoverUrl); + + // Navigating back from time settings + await browser.goBack(); // undo time settings + currUrl = await browser.getCurrentUrl(); + expect(currUrl.startsWith(discoverUrl)).to.be(true); + + // Navigate back home + await browser.goBack(); + currUrl = await browser.getCurrentUrl(); + expect(currUrl).to.be(homeUrl); + }); + }); } From 20f32d506fd72f567eda678c90a67a4ff9e75f2c Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Tue, 26 Jan 2021 14:20:42 -0800 Subject: [PATCH 018/163] JSON Body payload for the webhook connector in Alerts & Actions (#89253) * fixes https://github.com/elastic/kibana/issues/74449 Co-authored-by: Patrick Mueller --- docs/user/alerting/action-types/webhook.asciidoc | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc index 659c3afad6bd1..fff6814325ea4 100644 --- a/docs/user/alerting/action-types/webhook.asciidoc +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -72,4 +72,17 @@ Password:: An optional password. If set, HTTP basic authentication is used. Cur Webhook actions have the following properties: -Body:: A json payload sent to the request URL. +Body:: A JSON payload sent to the request URL. For example: ++ +[source,text] +-- +{ + "short_description": "{{context.rule.name}}", + "description": "{{context.rule.description}}", + ... +} +-- + +Mustache template variables (the text enclosed in double braces, for example, `context.rule.name`) have +their values escaped, so that the final JSON will be valid (escaping double quote characters). +For more information on Mustache template variables, refer to <>. From b5b9cee1584997a480d6a2a2b5edafcf3fcf9976 Mon Sep 17 00:00:00 2001 From: Justin Ibarra Date: Tue, 26 Jan 2021 13:43:05 -0900 Subject: [PATCH 019/163] Update security solution generic timeline templates (#89239) * Update security solution generic timeline templates --- .../rules/prepackaged_timelines/endpoint.json | 2 +- .../rules/prepackaged_timelines/index.ndjson | 6 +++--- .../rules/prepackaged_timelines/network.json | 2 +- .../rules/prepackaged_timelines/process.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/endpoint.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/endpoint.json index 711050e1f136a..acc5f69358798 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/endpoint.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/endpoint.json @@ -1 +1 @@ -{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":"{agent.type}","field":"agent.type","displayField":"agent.type","value":"{agent.type}","operator":":*"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"","queryMatch":{"displayValue":"endpoint","field":"agent.type","displayField":"agent.type","value":"endpoint","operator":":"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"default","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Endpoint Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"db366523-f1c6-4c1f-8731-6ce5ed9e5717","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735857110,"createdBy":"Elastic","updated":1594736314036,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"","queryMatch":{"displayValue":"endpoint","field":"agent.type","displayField":"agent.type","value":"endpoint","operator":":"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"default","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Endpoint Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"db366523-f1c6-4c1f-8731-6ce5ed9e5717","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735857110,"createdBy":"Elastic","updated":1611609999115,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson index 7c074242c39d1..522430205c25a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson @@ -7,6 +7,6 @@ // Auto generated file from scripts/regen_prepackage_timelines_index.sh // Do not hand edit. Run that script to regenerate package information instead -{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":"{agent.type}","field":"agent.type","displayField":"agent.type","value":"{agent.type}","operator":":*"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"","queryMatch":{"displayValue":"endpoint","field":"agent.type","displayField":"agent.type","value":"endpoint","operator":":"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"default","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Endpoint Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"db366523-f1c6-4c1f-8731-6ce5ed9e5717","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735857110,"createdBy":"Elastic","updated":1594736314036,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} -{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","searchable":null,"example":"user-password-change"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the source (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Port of the source.","columnHeaderType":"not-filtered","id":"source.port","category":"source","type":"number","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the destination (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"destination.port","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"host.name","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{source.ip}","queryMatch":{"displayValue":null,"field":"source.ip","displayField":null,"value":"{source.ip}","operator":":*"},"id":"timeline-1-ec778f01-1802-40f0-9dfb-ed8de1f656cb","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{destination.ip}","queryMatch":{"displayValue":null,"field":"destination.ip","displayField":null,"value":"{destination.ip}","operator":":*"},"id":"timeline-1-e37e37c5-a6e7-4338-af30-47bfbc3c0e1e","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Network Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"91832785-286d-4ebe-b884-1a208d111a70","dateRange":{"start":1588255858373,"end":1588256218373},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735573866,"createdBy":"Elastic","updated":1594736099397,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} -{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":null,"field":"process.name","displayField":null,"value":"{process.name}","operator":":"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{event.type}","queryMatch":{"displayValue":null,"field":"event.type","displayField":null,"value":"{event.type}","operator":":*"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Process Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"76e52245-7519-4251-91ab-262fb1a1728c","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735629389,"createdBy":"Elastic","updated":1594736083598,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"","queryMatch":{"displayValue":"endpoint","field":"agent.type","displayField":"agent.type","value":"endpoint","operator":":"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"default","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Endpoint Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"db366523-f1c6-4c1f-8731-6ce5ed9e5717","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735857110,"createdBy":"Elastic","updated":1611609999115,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","searchable":null,"example":"user-password-change"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the source (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Port of the source.","columnHeaderType":"not-filtered","id":"source.port","category":"source","type":"number","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the destination (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"destination.port","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"host.name","searchable":null}],"dataProviders":[{"and":[{"enabled":true,"excluded":false,"id":"timeline-1-e37e37c5-a6e7-4338-af30-47bfbc3c0e1e","kqlQuery":"","name":"{destination.ip}","queryMatch":{"displayField":"destination.ip","displayValue":"{destination.ip}","field":"destination.ip","operator":":","value":"{destination.ip}"},"type":"template"}],"enabled":true,"excluded":false,"id":"timeline-1-ec778f01-1802-40f0-9dfb-ed8de1f656cb","kqlQuery":"","name":"{source.ip}","queryMatch":{"displayField":"source.ip","displayValue":"{source.ip}","field":"source.ip","operator":":","value":"{source.ip}"},"type":"template"}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Network Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"91832785-286d-4ebe-b884-1a208d111a70","dateRange":{"start":1588255858373,"end":1588256218373},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735573866,"createdBy":"Elastic","updated":1611609960850,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":null,"field":"process.name","displayField":null,"value":"{process.name}","operator":":"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Process Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"76e52245-7519-4251-91ab-262fb1a1728c","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735629389,"createdBy":"Elastic","updated":1611609848602,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network.json index 1634476b4e99d..6e93387579d22 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network.json @@ -1 +1 @@ -{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","searchable":null,"example":"user-password-change"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the source (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Port of the source.","columnHeaderType":"not-filtered","id":"source.port","category":"source","type":"number","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the destination (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"destination.port","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"host.name","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{source.ip}","queryMatch":{"displayValue":null,"field":"source.ip","displayField":null,"value":"{source.ip}","operator":":*"},"id":"timeline-1-ec778f01-1802-40f0-9dfb-ed8de1f656cb","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{destination.ip}","queryMatch":{"displayValue":null,"field":"destination.ip","displayField":null,"value":"{destination.ip}","operator":":*"},"id":"timeline-1-e37e37c5-a6e7-4338-af30-47bfbc3c0e1e","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Network Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"91832785-286d-4ebe-b884-1a208d111a70","dateRange":{"start":1588255858373,"end":1588256218373},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735573866,"createdBy":"Elastic","updated":1594736099397,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","searchable":null,"example":"user-password-change"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the source (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Port of the source.","columnHeaderType":"not-filtered","id":"source.port","category":"source","type":"number","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the destination (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"destination.port","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"host.name","searchable":null}],"dataProviders":[{"and":[{"enabled":true,"excluded":false,"id":"timeline-1-e37e37c5-a6e7-4338-af30-47bfbc3c0e1e","kqlQuery":"","name":"{destination.ip}","queryMatch":{"displayField":"destination.ip","displayValue":"{destination.ip}","field":"destination.ip","operator":":","value":"{destination.ip}"},"type":"template"}],"enabled":true,"excluded":false,"id":"timeline-1-ec778f01-1802-40f0-9dfb-ed8de1f656cb","kqlQuery":"","name":"{source.ip}","queryMatch":{"displayField":"source.ip","displayValue":"{source.ip}","field":"source.ip","operator":":","value":"{source.ip}"},"type":"template"}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Network Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"91832785-286d-4ebe-b884-1a208d111a70","dateRange":{"start":1588255858373,"end":1588256218373},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735573866,"createdBy":"Elastic","updated":1611609960850,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process.json index 767f38133f263..c25873746a9e9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process.json @@ -1 +1 @@ -{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":null,"field":"process.name","displayField":null,"value":"{process.name}","operator":":"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{event.type}","queryMatch":{"displayValue":null,"field":"event.type","displayField":null,"value":"{event.type}","operator":":*"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Process Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"76e52245-7519-4251-91ab-262fb1a1728c","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735629389,"createdBy":"Elastic","updated":1594736083598,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":null,"field":"process.name","displayField":null,"value":"{process.name}","operator":":"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Process Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"76e52245-7519-4251-91ab-262fb1a1728c","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735629389,"createdBy":"Elastic","updated":1611609848602,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} From 0fe7b9e080c67c43aefdb7ea25d5e90a80cb4ade Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 26 Jan 2021 15:27:48 -0800 Subject: [PATCH 020/163] skip flaky suite (#89031) --- test/functional/apps/management/_test_huge_fields.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index ca95af8cb4205..2ab619276d2b9 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }) { const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); - describe('test large number of fields', function () { + // Failing: See https://github.com/elastic/kibana/issues/89031 + describe.skip('test large number of fields', function () { this.tags(['skipCloud']); const EXPECTED_FIELD_COUNT = '10006'; From 2c648acffda7c05cac2949eb5994c0f2066aa12b Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 26 Jan 2021 16:30:52 -0800 Subject: [PATCH 021/163] skip flaky suite (#89379) --- test/functional/apps/home/_sample_data.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index a9fe2026112b6..438dd6f8adce2 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardExpect = getService('dashboardExpect'); const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']); - describe('sample data', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/89379 + describe.skip('sample data', function describeIndexTests() { before(async () => { await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { From 8250b078b4b47415435ee72fb9b04908f99c6ed5 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 27 Jan 2021 00:59:24 +0000 Subject: [PATCH 022/163] chore(NA): improve ts build refs performance on kbn bootstrap (#89333) * chore(NA): improve ts build refs performance on kbn bootstrap * chore(NA): use skipLibCheck=false by default on typechecking * docs(NA): commit core docs changes * fix(NA): eui typings --- ...lugin-core-server.kibanaresponsefactory.md | 8 +- ...ns-data-public.aggconfigs._constructor_.md | 6 +- ...na-plugin-plugins-data-public.searchbar.md | 4 +- ...plugin-plugins-data-server.plugin.start.md | 4 +- package.json | 10 +- src/core/server/server.api.md | 8 +- src/dev/typescript/run_type_check_cli.ts | 6 +- src/plugins/data/public/public.api.md | 8 +- src/plugins/data/server/server.api.md | 2 +- tsconfig.base.json | 2 + typings/@elastic/eui/index.d.ts | 10 +- typings/@elastic/eui/lib/format.d.ts | 9 -- typings/@elastic/eui/lib/services.d.ts | 9 -- yarn.lock | 119 +++++++++--------- 14 files changed, 99 insertions(+), 106 deletions(-) delete mode 100644 typings/@elastic/eui/lib/format.d.ts delete mode 100644 typings/@elastic/eui/lib/services.d.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md index b488baacaff25..d7eafdce017e4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -10,7 +10,7 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom ```typescript kibanaResponseFactory: { - custom: | { + custom: | Buffer | Error | Stream | { message: string | Error; attributes?: Record | undefined; } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; @@ -21,9 +21,9 @@ kibanaResponseFactory: { conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; - redirected: (options: RedirectResponseOptions) => KibanaResponse>; - ok: (options?: HttpResponseOptions) => KibanaResponse>; - accepted: (options?: HttpResponseOptions) => KibanaResponse>; + redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; + ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + accepted: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; noContent: (options?: HttpResponseOptions) => KibanaResponse; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md index c9e08b9712480..6ca7a1a88b30e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md @@ -15,11 +15,11 @@ constructor(indexPattern: IndexPattern, configStates: Pick & Pick<{ + }, "schema" | "enabled" | "id" | "params"> & Pick<{ type: string | IAggType; }, "type"> & Pick<{ type: string | IAggType; - }, never>, "enabled" | "type" | "schema" | "id" | "params">[] | undefined, opts: AggConfigsOptions); + }, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined, opts: AggConfigsOptions); ``` ## Parameters @@ -27,6 +27,6 @@ constructor(indexPattern: IndexPattern, configStates: PickIndexPattern | | -| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("./agg_config").SerializableState | undefined;
schema?: string | undefined;
}, "enabled" | "schema" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "enabled" | "type" | "schema" | "id" | "params">[] | undefined | | +| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("./agg_config").SerializableState | undefined;
schema?: string | undefined;
}, "schema" | "enabled" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined | | | opts | AggConfigsOptions | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index 2fd84730957b6..83fbc00860ca5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "indexPatterns" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index af7abb076d7ef..ea3ba28a52def 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/package.json b/package.json index d14c5b0a7dc5f..42949a7014131 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", "**/request": "^2.88.2", "**/trim": "0.0.3", - "**/typescript": "4.1.2" + "**/typescript": "4.1.3" }, "engines": { "node": "14.15.4", @@ -561,8 +561,8 @@ "@types/xml2js": "^0.4.5", "@types/yauzl": "^2.9.1", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", + "@typescript-eslint/eslint-plugin": "^4.14.1", + "@typescript-eslint/parser": "^4.14.1", "@welldone-software/why-did-you-render": "^5.0.0", "@yarnpkg/lockfile": "^1.1.0", "abab": "^2.0.4", @@ -820,9 +820,9 @@ "topojson-client": "3.0.0", "ts-loader": "^7.0.5", "tsd": "^0.13.1", - "typescript": "4.1.2", + "typescript": "4.1.3", "typescript-fsa": "^3.0.0", - "typescript-fsa-reducers": "^1.2.1", + "typescript-fsa-reducers": "^1.2.2", "unlazy-loader": "^0.1.3", "unstated": "^2.1.1", "url-loader": "^2.2.0", diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ceab69a6cdb18..0b058011267eb 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1256,7 +1256,7 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory; // @public export const kibanaResponseFactory: { - custom: | { + custom: | Buffer | Error | Stream | { message: string | Error; attributes?: Record | undefined; } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; @@ -1267,9 +1267,9 @@ export const kibanaResponseFactory: { conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; - redirected: (options: RedirectResponseOptions) => KibanaResponse>; - ok: (options?: HttpResponseOptions) => KibanaResponse>; - accepted: (options?: HttpResponseOptions) => KibanaResponse>; + redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; + ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + accepted: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; noContent: (options?: HttpResponseOptions) => KibanaResponse; }; diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index c1a25a8e4bdee..6e7ea6fcf2405 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -61,7 +61,7 @@ export async function runTypeCheckCli() { Options: --project [path] {dim Path to a tsconfig.json file determines the project to check} - --skip-lib-check {dim Skip type checking of all declaration files (*.d.ts)} + --skip-lib-check {dim Skip type checking of all declaration files (*.d.ts). Default is false} --help {dim Show this message} `) ); @@ -77,7 +77,9 @@ export async function runTypeCheckCli() { ...['--emitDeclarationOnly', 'false'], '--noEmit', '--pretty', - ...(opts['skip-lib-check'] ? ['--skipLibCheck'] : []), + ...(opts['skip-lib-check'] + ? ['--skipLibCheck', opts['skip-lib-check']] + : ['--skipLibCheck', 'false']), ]; const projects = filterProjectsByFlag(opts.project).filter((p) => !p.disableTypeCheck); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 28997de4517e7..002f033365790 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -218,11 +218,11 @@ export class AggConfigs { id?: string | undefined; params?: {} | import("./agg_config").SerializableState | undefined; schema?: string | undefined; - }, "enabled" | "schema" | "id" | "params"> & Pick<{ + }, "schema" | "enabled" | "id" | "params"> & Pick<{ type: string | IAggType; }, "type"> & Pick<{ type: string | IAggType; - }, never>, "enabled" | "type" | "schema" | "id" | "params">[] | undefined, opts: AggConfigsOptions); + }, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined, opts: AggConfigsOptions); // (undocumented) aggs: IAggConfig[]; // (undocumented) @@ -2235,8 +2235,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "indexPatterns" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 6a96fd8209a8d..9789f3354e9ef 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1138,7 +1138,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; diff --git a/tsconfig.base.json b/tsconfig.base.json index 247813da51cfb..f8e07911e71ce 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,6 +14,8 @@ "strict": true, // save information about the project graph on disk "incremental": true, + // Do not check d.ts files by default + "skipLibCheck": true, // enables "core language features" "lib": [ "esnext", diff --git a/typings/@elastic/eui/index.d.ts b/typings/@elastic/eui/index.d.ts index 74f89608bc04f..e5cf7864a83b2 100644 --- a/typings/@elastic/eui/index.d.ts +++ b/typings/@elastic/eui/index.d.ts @@ -6,6 +6,12 @@ * Public License, v 1. */ -import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; - // TODO: Remove once typescript definitions are in EUI + +declare module '@elastic/eui/lib/services' { + export const RIGHT_ALIGNMENT: any; +} + +declare module '@elastic/eui/lib/services/format' { + export const dateFormatAliases: any; +} diff --git a/typings/@elastic/eui/lib/format.d.ts b/typings/@elastic/eui/lib/format.d.ts deleted file mode 100644 index 4be830ec2d98c..0000000000000 --- a/typings/@elastic/eui/lib/format.d.ts +++ /dev/null @@ -1,9 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -export const dateFormatAliases: any; diff --git a/typings/@elastic/eui/lib/services.d.ts b/typings/@elastic/eui/lib/services.d.ts deleted file mode 100644 index a667d111ceb72..0000000000000 --- a/typings/@elastic/eui/lib/services.d.ts +++ /dev/null @@ -1,9 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -export const RIGHT_ALIGNMENT: any; diff --git a/yarn.lock b/yarn.lock index 174f1284a3a6b..ed861b58773b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6861,28 +6861,29 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.8.1.tgz#b362abe0ee478a6c6d06c14552a6497f0b480769" - integrity sha512-d7LeQ7dbUrIv5YVFNzGgaW3IQKMmnmKFneRWagRlGYOSfLJVaRbj/FrBNOBC1a3tVO+TgNq1GbHvRtg1kwL0FQ== +"@typescript-eslint/eslint-plugin@^4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.1.tgz#22dd301ce228aaab3416b14ead10b1db3e7d3180" + integrity sha512-5JriGbYhtqMS1kRcZTQxndz1lKMwwEXKbwZbkUZNnp6MJX0+OVXnG0kOlBZP4LUAxEyzu3cs+EXd/97MJXsGfw== dependencies: - "@typescript-eslint/experimental-utils" "4.8.1" - "@typescript-eslint/scope-manager" "4.8.1" + "@typescript-eslint/experimental-utils" "4.14.1" + "@typescript-eslint/scope-manager" "4.14.1" debug "^4.1.1" functional-red-black-tree "^1.0.1" + lodash "^4.17.15" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.8.1.tgz#27275c20fa4336df99ebcf6195f7d7aa7aa9f22d" - integrity sha512-WigyLn144R3+lGATXW4nNcDJ9JlTkG8YdBWHkDlN0lC3gUGtDi7Pe3h5GPvFKMcRz8KbZpm9FJV9NTW8CpRHpg== +"@typescript-eslint/experimental-utils@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.1.tgz#a5c945cb24dabb96747180e1cfc8487f8066f471" + integrity sha512-2CuHWOJwvpw0LofbyG5gvYjEyoJeSvVH2PnfUQSn0KQr4v8Dql2pr43ohmx4fdPQ/eVoTSFjTi/bsGEXl/zUUQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.8.1" - "@typescript-eslint/types" "4.8.1" - "@typescript-eslint/typescript-estree" "4.8.1" + "@typescript-eslint/scope-manager" "4.14.1" + "@typescript-eslint/types" "4.14.1" + "@typescript-eslint/typescript-estree" "4.14.1" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -6898,16 +6899,24 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.8.1.tgz#4fe2fbdbb67485bafc4320b3ae91e34efe1219d1" - integrity sha512-QND8XSVetATHK9y2Ltc/XBl5Ro7Y62YuZKnPEwnNPB8E379fDsvzJ1dMJ46fg/VOmk0hXhatc+GXs5MaXuL5Uw== +"@typescript-eslint/parser@^4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.14.1.tgz#3bd6c24710cd557d8446625284bcc9c6d52817c6" + integrity sha512-mL3+gU18g9JPsHZuKMZ8Z0Ss9YP1S5xYZ7n68Z98GnPq02pYNQuRXL85b9GYhl6jpdvUc45Km7hAl71vybjUmw== dependencies: - "@typescript-eslint/scope-manager" "4.8.1" - "@typescript-eslint/types" "4.8.1" - "@typescript-eslint/typescript-estree" "4.8.1" + "@typescript-eslint/scope-manager" "4.14.1" + "@typescript-eslint/types" "4.14.1" + "@typescript-eslint/typescript-estree" "4.14.1" debug "^4.1.1" +"@typescript-eslint/scope-manager@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.1.tgz#8444534254c6f370e9aa974f035ced7fe713ce02" + integrity sha512-F4bjJcSqXqHnC9JGUlnqSa3fC2YH5zTtmACS1Hk+WX/nFB0guuynVK5ev35D4XZbdKjulXBAQMyRr216kmxghw== + dependencies: + "@typescript-eslint/types" "4.14.1" + "@typescript-eslint/visitor-keys" "4.14.1" + "@typescript-eslint/scope-manager@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz#c743227e087545968080d2362cfb1273842cb6a7" @@ -6916,23 +6925,29 @@ "@typescript-eslint/types" "4.3.0" "@typescript-eslint/visitor-keys" "4.3.0" -"@typescript-eslint/scope-manager@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.8.1.tgz#e343c475f8f1d15801b546cb17d7f309b768fdce" - integrity sha512-r0iUOc41KFFbZdPAdCS4K1mXivnSZqXS5D9oW+iykQsRlTbQRfuFRSW20xKDdYiaCoH+SkSLeIF484g3kWzwOQ== - dependencies: - "@typescript-eslint/types" "4.8.1" - "@typescript-eslint/visitor-keys" "4.8.1" +"@typescript-eslint/types@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.1.tgz#b3d2eb91dafd0fd8b3fce7c61512ac66bd0364aa" + integrity sha512-SkhzHdI/AllAgQSxXM89XwS1Tkic7csPdndUuTKabEwRcEfR8uQ/iPA3Dgio1rqsV3jtqZhY0QQni8rLswJM2w== "@typescript-eslint/types@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf" integrity sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw== -"@typescript-eslint/types@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.8.1.tgz#23829c73c5fc6f4fcd5346a7780b274f72fee222" - integrity sha512-ave2a18x2Y25q5K05K/U3JQIe2Av4+TNi/2YuzyaXLAsDx6UZkz1boZ7nR/N6Wwae2PpudTZmHFXqu7faXfHmA== +"@typescript-eslint/typescript-estree@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.1.tgz#20d3b8c8e3cdc8f764bdd5e5b0606dd83da6075b" + integrity sha512-M8+7MbzKC1PvJIA8kR2sSBnex8bsR5auatLCnVlNTJczmJgqRn8M+sAlQfkEq7M4IY3WmaNJ+LJjPVRrREVSHQ== + dependencies: + "@typescript-eslint/types" "4.14.1" + "@typescript-eslint/visitor-keys" "4.14.1" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" "@typescript-eslint/typescript-estree@4.3.0": version "4.3.0" @@ -6948,19 +6963,13 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.8.1.tgz#7307e3f2c9e95df7daa8dc0a34b8c43b7ec0dd32" - integrity sha512-bJ6Fn/6tW2g7WIkCWh3QRlaSU7CdUUK52shx36/J7T5oTQzANvi6raoTsbwGM11+7eBbeem8hCCKbyvAc0X3sQ== +"@typescript-eslint/visitor-keys@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.1.tgz#e93c2ff27f47ee477a929b970ca89d60a117da91" + integrity sha512-TAblbDXOI7bd0C/9PE1G+AFo7R5uc+ty1ArDoxmrC1ah61Hn6shURKy7gLdRb1qKJmjHkqu5Oq+e4Kt0jwf1IA== dependencies: - "@typescript-eslint/types" "4.8.1" - "@typescript-eslint/visitor-keys" "4.8.1" - debug "^4.1.1" - globby "^11.0.1" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" + "@typescript-eslint/types" "4.14.1" + eslint-visitor-keys "^2.0.0" "@typescript-eslint/visitor-keys@4.3.0": version "4.3.0" @@ -6970,14 +6979,6 @@ "@typescript-eslint/types" "4.3.0" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.8.1.tgz#794f68ee292d1b2e3aa9690ebedfcb3a8c90e3c3" - integrity sha512-3nrwXFdEYALQh/zW8rFwP4QltqsanCDz4CwWMPiIZmwlk9GlvBeueEIbq05SEq4ganqM0g9nh02xXgv5XI3PeQ== - dependencies: - "@typescript-eslint/types" "4.8.1" - eslint-visitor-keys "^2.0.0" - "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -28571,10 +28572,10 @@ typescript-compare@^0.0.2: dependencies: typescript-logic "^0.0.0" -typescript-fsa-reducers@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/typescript-fsa-reducers/-/typescript-fsa-reducers-1.2.1.tgz#2af1a85f7b88fb0dfb9fa59d5da51a5d7ac6543f" - integrity sha512-Qgn7zEnAU5n3YEWEL5ooEmIWZl9B4QyXD4Y/0DqpUzF0YuTrcsLa7Lht0gFXZ+xqLJXQwo3fEiTfQTDF1fBnMg== +typescript-fsa-reducers@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/typescript-fsa-reducers/-/typescript-fsa-reducers-1.2.2.tgz#1e2c8e8a50ca3e09e0d6fdf2a3b42bdd17de8094" + integrity sha512-IQ2VsIqUvmzVgWNDjxkeOxX97itl/rq+2u82jGsRdzCSFi9OtV4qf1Ec1urvj/eDlPHOaihIL7wMZzLYx9GvFg== typescript-fsa@^3.0.0: version "3.0.0" @@ -28593,10 +28594,10 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -typescript@4.1.2, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.5.3, typescript@~3.7.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9" - integrity sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ== +typescript@4.1.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.5.3, typescript@~3.7.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== ua-parser-js@^0.7.18: version "0.7.22" From c6cfdee5b02d30263972bf149601323883a8c351 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 26 Jan 2021 20:23:01 -0700 Subject: [PATCH 023/163] [core.logging] Add ops logs to the KP logging system (#88070) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/get_logging_config.ts | 5 +- src/core/README.md | 6 +- .../deprecation/core_deprecations.test.ts | 20 +++ .../config/deprecation/core_deprecations.ts | 12 ++ src/core/server/logging/README.md | 24 +++- src/core/server/logging/ecs.ts | 90 ++++++++++++ src/core/server/logging/index.ts | 7 + .../logging/get_ops_metrics_log.test.ts | 132 ++++++++++++++++++ .../metrics/logging/get_ops_metrics_log.ts | 78 +++++++++++ src/core/server/metrics/logging/index.ts | 9 ++ .../server/metrics/metrics_service.test.ts | 99 ++++++++++++- src/core/server/metrics/metrics_service.ts | 6 +- 12 files changed, 482 insertions(+), 6 deletions(-) create mode 100644 src/core/server/logging/ecs.ts create mode 100644 src/core/server/metrics/logging/get_ops_metrics_log.test.ts create mode 100644 src/core/server/metrics/logging/get_ops_metrics_log.ts create mode 100644 src/core/server/metrics/logging/index.ts diff --git a/packages/kbn-legacy-logging/src/get_logging_config.ts b/packages/kbn-legacy-logging/src/get_logging_config.ts index 5a28ab1271538..280b9f815c78c 100644 --- a/packages/kbn-legacy-logging/src/get_logging_config.ts +++ b/packages/kbn-legacy-logging/src/get_logging_config.ts @@ -28,7 +28,10 @@ export function getLoggingConfiguration(config: LegacyLoggingConfig, opsInterval } else if (config.verbose) { _.defaults(events, { log: '*', - ops: '*', + // To avoid duplicate logs, we explicitly disable this in verbose + // mode as it is already provided by the new logging config under + // the `metrics.ops` context. + ops: '!', request: '*', response: '*', error: '*', diff --git a/src/core/README.md b/src/core/README.md index e195bf30c054c..c73c6aa56bfd0 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -28,8 +28,10 @@ Even though the new validation system provided by the `core` is also based on Jo rules tailored to our needs (e.g. `byteSize`, `duration` etc.). That means that config values that were previously accepted by the "legacy" Kibana may be rejected by the `core` now. -Even though `core` has its own logging system it doesn't output log records directly (e.g. to file or terminal), but instead -forward them to the "legacy" Kibana so that they look the same as the rest of the log records throughout Kibana. +### Logging +`core` has its own [logging system](./server/logging/README.md) and will output log records directly (e.g. to file or terminal) when configured. When no +specific configuration is provided, logs are forwarded to the "legacy" Kibana so that they look the same as the rest of the +log records throughout Kibana. ## Core API Review To provide a stable API for plugin developers, it is important that the Core Public and Server API's are stable and diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index a7c6a63826523..a791362d9166f 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -236,4 +236,24 @@ describe('core deprecations', () => { ).toEqual([`worker-src blob:`]); }); }); + + describe('logging.events.ops', () => { + it('warns when ops events are used', () => { + const { messages } = applyCoreDeprecations({ + logging: { events: { ops: '*' } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.events.ops\\" has been deprecated and will be removed in 8.0. To access ops data moving forward, please enable debug logs for the \\"metrics.ops\\" context in your logging configuration.", + ] + `); + }); + + it('does not warn when other events are configured', () => { + const { messages } = applyCoreDeprecations({ + logging: { events: { log: '*' } }, + }); + expect(messages).toEqual([]); + }); + }); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 6f6e6c3e0522e..23a3518cd8eb6 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -103,6 +103,17 @@ const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath, return settings; }; +const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.events.ops')) { + log( + '"logging.events.ops" has been deprecated and will be removed ' + + 'in 8.0. To access ops data moving forward, please enable debug logs for the ' + + '"metrics.ops" context in your logging configuration.' + ); + } + return settings; +}; + export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ unusedFromRoot('savedObjects.indexCheckTimeout'), unusedFromRoot('server.xsrf.token'), @@ -137,4 +148,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unu rewriteBasePathDeprecation, cspRulesDeprecation, mapManifestServiceUrlDeprecation, + opsLoggingEventDeprecation, ]; diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 8cb704f09ce8c..cc2b6230d2d33 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -312,6 +312,9 @@ logging: - context: telemetry level: all appenders: [json-file-appender] + - context: metrics.ops + level: debug + appenders: [console] ``` Here is what we get with the config above: @@ -324,6 +327,7 @@ Here is what we get with the config above: | server | console, file | fatal | | optimize | console | error | | telemetry | json-file-appender | all | +| metrics.ops | console | debug | The `root` logger has a dedicated configuration node since this context is special and should always exist. By @@ -341,7 +345,25 @@ Or disable logging entirely with `off`: ```yaml logging.root.level: off ``` +### Dedicated loggers + +The `metrics.ops` logger is configured with `debug` level and will automatically output sample system and process information at a regular interval. +The metrics that are logged are a subset of the data collected and are formatted in the log message as follows: + +| Ops formatted log property | Location in metrics service | Log units +| :------------------------- | :-------------------------- | :-------------------------- | +| memory | process.memory.heap.used_in_bytes | [depends on the value](http://numeraljs.com/#format), typically MB or GB | +| uptime | process.uptime_in_millis | HH:mm:ss | +| load | os.load | [ "load for the last 1 min" "load for the last 5 min" "load for the last 15 min"] | +| delay | process.event_loop_delay | ms | + +The log interval is the same as the interval at which system and process information is refreshed and is configurable under `ops.interval`: + +```yaml +ops.interval: 5000 +``` +The minimum interval is 100ms and defaults to 5000ms. ## Usage Usage is very straightforward, one should just get a logger for a specific context and use it to log messages with @@ -478,4 +500,4 @@ TBD | meta | separate property `"meta": {"to": "v8"}` | merged in log record `{... "to": "v8"}` | | pid | `pid: 12345` | `pid: 12345` | | type | N/A | `type: log` | -| error | `{ message, name, stack }` | `{ message, name, stack, code, signal }` | \ No newline at end of file +| error | `{ message, name, stack }` | `{ message, name, stack, code, signal }` | diff --git a/src/core/server/logging/ecs.ts b/src/core/server/logging/ecs.ts new file mode 100644 index 0000000000000..0dbc403fca0b2 --- /dev/null +++ b/src/core/server/logging/ecs.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Typings for some ECS fields which core uses internally. + * These are not a complete set of ECS typings and should not + * be used externally; the only types included here are ones + * currently used in core. + * + * @internal + */ + +export interface EcsOpsMetricsEvent { + /** + * These typings were written as of ECS 1.7.0. + * Don't change this value without checking the rest + * of the types to conform to that ECS version. + * + * https://www.elastic.co/guide/en/ecs/1.7/index.html + */ + ecs: { version: '1.7.0' }; + + // base fields + ['@timestamp']?: string; + labels?: Record; + message?: string; + tags?: string[]; + // other fields + process?: EcsProcessField; + event?: EcsEventField; +} + +interface EcsProcessField { + uptime?: number; +} + +export interface EcsEventField { + kind?: EcsEventKind; + category?: EcsEventCategory[]; + type?: EcsEventType; +} + +export enum EcsEventKind { + ALERT = 'alert', + EVENT = 'event', + METRIC = 'metric', + STATE = 'state', + PIPELINE_ERROR = 'pipeline_error', + SIGNAL = 'signal', +} + +export enum EcsEventCategory { + AUTHENTICATION = 'authentication', + CONFIGURATION = 'configuration', + DATABASE = 'database', + DRIVER = 'driver', + FILE = 'file', + HOST = 'host', + IAM = 'iam', + INTRUSION_DETECTION = 'intrusion_detection', + MALWARE = 'malware', + NETWORK = 'network', + PACKAGE = 'package', + PROCESS = 'process', + WEB = 'web', +} + +export enum EcsEventType { + ACCESS = 'access', + ADMIN = 'admin', + ALLOWED = 'allowed', + CHANGE = 'change', + CONNECTION = 'connection', + CREATION = 'creation', + DELETION = 'deletion', + DENIED = 'denied', + END = 'end', + ERROR = 'error', + GROUP = 'group', + INFO = 'info', + INSTALLATION = 'installation', + PROTOCOL = 'protocol', + START = 'start', + USER = 'user', +} diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts index f024bea1bf358..18a903af0a9fd 100644 --- a/src/core/server/logging/index.ts +++ b/src/core/server/logging/index.ts @@ -17,6 +17,13 @@ export { LogLevelId, LogLevel, } from '@kbn/logging'; +export { + EcsOpsMetricsEvent, + EcsEventField, + EcsEventKind, + EcsEventCategory, + EcsEventType, +} from './ecs'; export { config, LoggingConfigType, diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts new file mode 100644 index 0000000000000..820959910e764 --- /dev/null +++ b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { OpsMetrics } from '..'; +import { getEcsOpsMetricsLog } from './get_ops_metrics_log'; + +function createBaseOpsMetrics(): OpsMetrics { + return { + collected_at: new Date('2020-01-01 01:00:00'), + process: { + memory: { + heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, + resident_set_size_in_bytes: 1, + }, + event_loop_delay: 1, + pid: 1, + uptime_in_millis: 1, + }, + os: { + platform: 'darwin' as const, + platformRelease: 'test', + load: { '1m': 1, '5m': 1, '15m': 1 }, + memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 }, + uptime_in_millis: 1, + }, + response_times: { avg_in_millis: 1, max_in_millis: 1 }, + requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } }, + concurrent_connections: 1, + }; +} + +function createMockOpsMetrics(testMetrics: Partial): OpsMetrics { + const base = createBaseOpsMetrics(); + return { + ...base, + ...testMetrics, + }; +} +const testMetrics = ({ + process: { + memory: { heap: { used_in_bytes: 100 } }, + uptime_in_millis: 1500, + event_loop_delay: 50, + }, + os: { + load: { + '1m': 10, + '5m': 20, + '15m': 30, + }, + }, +} as unknown) as Partial; + +describe('getEcsOpsMetricsLog', () => { + it('provides correctly formatted message', () => { + const result = getEcsOpsMetricsLog(createMockOpsMetrics(testMetrics)); + expect(result.message).toMatchInlineSnapshot( + `"memory: 100.0B uptime: 0:00:01 load: [10.00,20.00,30.00] delay: 50.000"` + ); + }); + + it('correctly formats process uptime', () => { + const logMeta = getEcsOpsMetricsLog(createMockOpsMetrics(testMetrics)); + expect(logMeta.process!.uptime).toEqual(1); + }); + + it('excludes values from the message if unavailable', () => { + const baseMetrics = createBaseOpsMetrics(); + const missingMetrics = ({ + ...baseMetrics, + process: {}, + os: {}, + } as unknown) as OpsMetrics; + const logMeta = getEcsOpsMetricsLog(missingMetrics); + expect(logMeta.message).toMatchInlineSnapshot(`""`); + }); + + it('specifies correct ECS version', () => { + const logMeta = getEcsOpsMetricsLog(createBaseOpsMetrics()); + expect(logMeta.ecs.version).toBe('1.7.0'); + }); + + it('provides an ECS-compatible response', () => { + const logMeta = getEcsOpsMetricsLog(createBaseOpsMetrics()); + expect(logMeta).toMatchInlineSnapshot(` + Object { + "ecs": Object { + "version": "1.7.0", + }, + "event": Object { + "category": Array [ + "process", + "host", + ], + "kind": "metric", + "type": "info", + }, + "host": Object { + "os": Object { + "load": Object { + "15m": 1, + "1m": 1, + "5m": 1, + }, + }, + }, + "message": "memory: 1.0B load: [1.00,1.00,1.00] delay: 1.000", + "process": Object { + "eventLoopDelay": 1, + "memory": Object { + "heap": Object { + "usedInBytes": 1, + }, + }, + "uptime": 0, + }, + } + `); + }); + + it('logs ECS fields in the log meta', () => { + const logMeta = getEcsOpsMetricsLog(createBaseOpsMetrics()); + expect(logMeta.event!.kind).toBe('metric'); + expect(logMeta.event!.category).toEqual(expect.arrayContaining(['process', 'host'])); + expect(logMeta.event!.type).toBe('info'); + }); +}); diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.ts b/src/core/server/metrics/logging/get_ops_metrics_log.ts new file mode 100644 index 0000000000000..361cac0bc310c --- /dev/null +++ b/src/core/server/metrics/logging/get_ops_metrics_log.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import numeral from '@elastic/numeral'; +import { EcsOpsMetricsEvent, EcsEventKind, EcsEventCategory, EcsEventType } from '../../logging'; +import { OpsMetrics } from '..'; + +const ECS_VERSION = '1.7.0'; +/** + * Converts ops metrics into ECS-compliant `LogMeta` for logging + * + * @internal + */ +export function getEcsOpsMetricsLog(metrics: OpsMetrics): EcsOpsMetricsEvent { + const { process, os } = metrics; + const processMemoryUsedInBytes = process?.memory?.heap?.used_in_bytes; + const processMemoryUsedInBytesMsg = processMemoryUsedInBytes + ? `memory: ${numeral(processMemoryUsedInBytes).format('0.0b')} ` + : ''; + + // ECS process.uptime is in seconds: + const uptimeVal = process?.uptime_in_millis + ? Math.floor(process.uptime_in_millis / 1000) + : undefined; + + // HH:mm:ss message format for backward compatibility + const uptimeValMsg = uptimeVal ? `uptime: ${numeral(uptimeVal).format('00:00:00')} ` : ''; + + // Event loop delay is in ms + const eventLoopDelayVal = process?.event_loop_delay; + const eventLoopDelayValMsg = eventLoopDelayVal + ? `delay: ${numeral(process?.event_loop_delay).format('0.000')}` + : ''; + + const loadEntries = { + '1m': os?.load ? os?.load['1m'] : undefined, + '5m': os?.load ? os?.load['5m'] : undefined, + '15m': os?.load ? os?.load['15m'] : undefined, + }; + + const loadVals = [...Object.values(os?.load ?? [])]; + const loadValsMsg = + loadVals.length > 0 + ? `load: [${loadVals.map((val: number) => { + return numeral(val).format('0.00'); + })}] ` + : ''; + + return { + ecs: { version: ECS_VERSION }, + message: `${processMemoryUsedInBytesMsg}${uptimeValMsg}${loadValsMsg}${eventLoopDelayValMsg}`, + event: { + kind: EcsEventKind.METRIC, + category: [EcsEventCategory.PROCESS, EcsEventCategory.HOST], + type: EcsEventType.INFO, + }, + process: { + uptime: uptimeVal, + // @ts-expect-error custom fields not yet part of ECS + memory: { + heap: { + usedInBytes: processMemoryUsedInBytes, + }, + }, + eventLoopDelay: eventLoopDelayVal, + }, + host: { + os: { + load: loadEntries, + }, + }, + }; +} diff --git a/src/core/server/metrics/logging/index.ts b/src/core/server/metrics/logging/index.ts new file mode 100644 index 0000000000000..5b3f9aed56be0 --- /dev/null +++ b/src/core/server/metrics/logging/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export { getEcsOpsMetricsLog } from './get_ops_metrics_log'; diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts index 5ba29606d8a80..e21bad1ef4be7 100644 --- a/src/core/server/metrics/metrics_service.test.ts +++ b/src/core/server/metrics/metrics_service.test.ts @@ -13,12 +13,15 @@ import { mockOpsCollector } from './metrics_service.test.mocks'; import { MetricsService } from './metrics_service'; import { mockCoreContext } from '../core_context.mock'; import { httpServiceMock } from '../http/http_service.mock'; +import { loggingSystemMock } from '../logging/logging_system.mock'; import { take } from 'rxjs/operators'; const testInterval = 100; const dummyMetrics = { metricA: 'value', metricB: 'otherValue' }; +const logger = loggingSystemMock.create(); + describe('MetricsService', () => { const httpMock = httpServiceMock.createInternalSetupContract(); let metricsService: MetricsService; @@ -29,7 +32,7 @@ describe('MetricsService', () => { const configService = configServiceMock.create({ atPath: { interval: moment.duration(testInterval) }, }); - const coreContext = mockCoreContext.create({ configService }); + const coreContext = mockCoreContext.create({ logger, configService }); metricsService = new MetricsService(coreContext); }); @@ -118,6 +121,100 @@ describe('MetricsService', () => { expect(await nextEmission()).toEqual({ metric: 'first' }); expect(await nextEmission()).toEqual({ metric: 'second' }); }); + + it('logs the metrics at every interval', async () => { + const firstMetrics = { + process: { + memory: { heap: { used_in_bytes: 100 } }, + uptime_in_millis: 1500, + event_loop_delay: 50, + }, + os: { + load: { + '1m': 10, + '5m': 20, + '15m': 30, + }, + }, + }; + const secondMetrics = { + process: { + memory: { heap: { used_in_bytes: 200 } }, + uptime_in_millis: 3000, + event_loop_delay: 100, + }, + os: { + load: { + '1m': 20, + '5m': 30, + '15m': 40, + }, + }, + }; + + const opsLogger = logger.get('metrics', 'ops'); + + mockOpsCollector.collect + .mockResolvedValueOnce(firstMetrics) + .mockResolvedValueOnce(secondMetrics); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); + + const nextEmission = async () => { + jest.advanceTimersByTime(testInterval); + const emission = await getOpsMetrics$().pipe(take(1)).toPromise(); + await new Promise((resolve) => process.nextTick(resolve)); + return emission; + }; + + await nextEmission(); + const opsLogs = loggingSystemMock.collect(opsLogger).debug; + expect(opsLogs.length).toEqual(2); + expect(opsLogs[0][1]).not.toEqual(opsLogs[1][1]); + }); + + it('omits metrics from log message if they are missing or malformed', async () => { + const opsLogger = logger.get('metrics', 'ops'); + mockOpsCollector.collect.mockResolvedValueOnce({ secondMetrics: 'metrics' }); + await metricsService.setup({ http: httpMock }); + await metricsService.start(); + expect(loggingSystemMock.collect(opsLogger).debug[0]).toMatchInlineSnapshot(` + Array [ + "", + Object { + "ecs": Object { + "version": "1.7.0", + }, + "event": Object { + "category": Array [ + "process", + "host", + ], + "kind": "metric", + "type": "info", + }, + "host": Object { + "os": Object { + "load": Object { + "15m": undefined, + "1m": undefined, + "5m": undefined, + }, + }, + }, + "process": Object { + "eventLoopDelay": undefined, + "memory": Object { + "heap": Object { + "usedInBytes": undefined, + }, + }, + "uptime": undefined, + }, + }, + ] + `); + }); }); describe('#stop', () => { diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts index 24b2b1b67a07a..460035ad2e298 100644 --- a/src/core/server/metrics/metrics_service.ts +++ b/src/core/server/metrics/metrics_service.ts @@ -15,6 +15,7 @@ import { InternalHttpServiceSetup } from '../http'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, OpsMetrics } from './types'; import { OpsMetricsCollector } from './ops_metrics_collector'; import { opsConfig, OpsConfigType } from './ops_config'; +import { getEcsOpsMetricsLog } from './logging'; interface MetricsServiceSetupDeps { http: InternalHttpServiceSetup; @@ -24,6 +25,7 @@ interface MetricsServiceSetupDeps { export class MetricsService implements CoreService { private readonly logger: Logger; + private readonly opsMetricsLogger: Logger; private metricsCollector?: OpsMetricsCollector; private collectInterval?: NodeJS.Timeout; private metrics$ = new ReplaySubject(1); @@ -31,6 +33,7 @@ export class MetricsService constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('metrics'); + this.opsMetricsLogger = coreContext.logger.get('metrics', 'ops'); } public async setup({ http }: MetricsServiceSetupDeps): Promise { @@ -69,8 +72,9 @@ export class MetricsService } private async refreshMetrics() { - this.logger.debug('Refreshing metrics'); const metrics = await this.metricsCollector!.collect(); + const { message, ...meta } = getEcsOpsMetricsLog(metrics); + this.opsMetricsLogger.debug(message!, meta); this.metricsCollector!.reset(); this.metrics$.next(metrics); } From 3fe2e95e350ce8d13272df633d97aa2eabc62a07 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 26 Jan 2021 22:53:14 -0500 Subject: [PATCH 024/163] Rename conversion function, extract to module scope and add tests. (#89018) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/requests/get_network_events.test.ts | 12 +++++++++++- .../uptime/server/lib/requests/get_network_events.ts | 12 +++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts index 2d590e80ca42d..e1c62f3469518 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts @@ -5,9 +5,19 @@ */ import { getUptimeESMockClient } from './helper'; -import { getNetworkEvents } from './get_network_events'; +import { getNetworkEvents, secondsToMillis } from './get_network_events'; describe('getNetworkEvents', () => { + describe('secondsToMillis conversion', () => { + it('returns -1 for -1 value', () => { + expect(secondsToMillis(-1)).toBe(-1); + }); + + it('returns a value of seconds as milliseconds', () => { + expect(secondsToMillis(10)).toBe(10_000); + }); + }); + let mockHits: any; beforeEach(() => { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index ec1fffd62350d..6b1bca8f2d7b7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -12,6 +12,10 @@ interface GetNetworkEventsParams { stepIndex: string; } +export const secondsToMillis = (seconds: number) => + // -1 is a special case where a value was unavailable + seconds === -1 ? -1 : seconds * 1000; + export const getNetworkEvents: UMElasticsearchQueryFn< GetNetworkEventsParams, { events: NetworkEvent[]; total: number } @@ -35,17 +39,15 @@ export const getNetworkEvents: UMElasticsearchQueryFn< const { body: result } = await uptimeEsClient.search({ body: params }); - const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); - return { total: result.hits.total.value, events: result.hits.hits.map((event: any) => { - const requestSentTime = microToMillis(event._source.synthetics.payload.request_sent_time); - const loadEndTime = microToMillis(event._source.synthetics.payload.load_end_time); + const requestSentTime = secondsToMillis(event._source.synthetics.payload.request_sent_time); + const loadEndTime = secondsToMillis(event._source.synthetics.payload.load_end_time); const requestStartTime = event._source.synthetics.payload.response && event._source.synthetics.payload.response.timing - ? microToMillis(event._source.synthetics.payload.response.timing.request_time) + ? secondsToMillis(event._source.synthetics.payload.response.timing.request_time) : undefined; return { From 053628cae862103b8c27bc60656bf45d76dc3f5d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 27 Jan 2021 09:54:56 +0100 Subject: [PATCH 025/163] [APM] Optimize API test order (#88654) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../fake_mocha_types.d.ts | 2 + .../snapshots/decorate_snapshot_ui.test.ts | 63 ++- .../lib/snapshots/decorate_snapshot_ui.ts | 89 ++-- .../test/apm_api_integration/basic/config.ts | 8 +- .../basic/tests/alerts/chart_preview.ts | 124 ----- .../apm_api_integration/basic/tests/index.ts | 72 --- .../tests/metrics_charts/metrics_charts.ts | 439 ---------------- .../basic/tests/service_maps/service_maps.ts | 32 -- .../basic/tests/services/annotations.ts | 48 -- .../basic/tests/services/throughput.ts | 85 --- .../basic/tests/services/top_services.ts | 259 --------- .../tests/settings/agent_configuration.ts | 489 ----------------- .../anomaly_detection/no_access_user.ts | 43 -- .../settings/anomaly_detection/read_user.ts | 46 -- .../settings/anomaly_detection/write_user.ts | 50 -- .../basic/tests/settings/custom_link.ts | 35 -- .../basic/tests/transactions/breakdown.ts | 123 ----- .../basic/tests/transactions/latency.ts | 102 ---- .../test/apm_api_integration/common/config.ts | 17 +- .../apm_api_integration/common/registry.ts | 160 ++++++ .../test/apm_api_integration/configs/index.ts | 25 + .../tests/alerts/chart_preview.ts | 70 +++ .../tests/correlations/slow_transactions.ts | 88 ++++ .../csm/__snapshots__/page_load_dist.snap | 8 +- .../tests/csm/__snapshots__/page_views.snap | 8 +- .../tests/csm/csm_services.ts | 40 ++ .../{trial => }/tests/csm/has_rum_data.ts | 39 +- .../{trial => }/tests/csm/js_errors.ts | 39 +- .../tests/csm/long_task_metrics.ts | 47 +- .../tests/csm/page_load_dist.ts | 58 ++ .../tests/csm/page_views.ts | 58 ++ .../tests/csm/url_search.ts | 82 +++ .../{trial => }/tests/csm/web_core_vitals.ts | 51 +- .../{basic => }/tests/feature_controls.ts | 5 +- .../test/apm_api_integration/tests/index.ts | 70 +++ .../tests/metrics_charts/metrics_charts.ts | 446 ++++++++++++++++ .../tests/observability_overview/has_data.ts | 37 +- .../observability_overview.ts | 42 +- .../__snapshots__/service_maps.snap | 6 +- .../tests/service_maps/service_maps.ts | 155 +++--- .../service_overview/dependencies/es_utils.ts | 0 .../service_overview/dependencies/index.ts | 44 +- .../tests/service_overview/error_groups.ts | 27 +- .../tests/service_overview/instances.ts | 65 +-- .../services/__snapshots__/throughput.snap | 2 +- .../{basic => }/tests/services/agent_name.ts | 35 +- .../{trial => }/tests/services/annotations.ts | 30 +- .../tests/services/service_details.ts | 27 +- .../tests/services/service_icons.ts | 63 ++- .../tests/services/throughput.ts | 82 +++ .../tests/services/top_services.ts | 364 +++++++++++++ .../tests/services/transaction_types.ts | 27 +- .../tests/settings/agent_configuration.ts | 495 ++++++++++++++++++ .../tests/settings/anomaly_detection/basic.ts | 53 ++ .../anomaly_detection/no_access_user.ts | 44 ++ .../settings/anomaly_detection/read_user.ts | 46 ++ .../settings/anomaly_detection/write_user.ts | 56 ++ .../tests/settings/custom_link.ts | 183 +++++++ .../traces/__snapshots__/top_traces.snap | 2 +- .../{basic => }/tests/traces/top_traces.ts | 36 +- .../transactions/__snapshots__/breakdown.snap | 4 +- .../__snapshots__/error_rate.snap | 2 +- .../transactions/__snapshots__/latency.snap | 46 ++ .../__snapshots__/top_transaction_groups.snap | 2 +- .../__snapshots__/transaction_charts.snap | 0 .../__snapshots__/transactions_charts.snap | 0 .../tests/transactions/breakdown.ts | 118 +++++ .../tests/transactions/distribution.ts | 26 +- .../tests/transactions/error_rate.ts | 43 +- .../tests/transactions/latency.ts | 243 +++++++++ .../tests/transactions/throughput.ts | 53 +- .../transactions/top_transaction_groups.ts | 26 +- .../transactions_groups_overview.ts | 27 +- .../test/apm_api_integration/trial/config.ts | 8 +- .../tests/correlations/slow_transactions.ts | 95 ---- .../trial/tests/csm/csm_services.ts | 47 -- .../trial/tests/csm/page_load_dist.ts | 64 --- .../trial/tests/csm/page_views.ts | 64 --- .../trial/tests/csm/url_search.ts | 89 ---- .../apm_api_integration/trial/tests/index.ts | 50 -- .../trial/tests/services/top_services.ts | 131 ----- .../anomaly_detection/no_access_user.ts | 41 -- .../settings/anomaly_detection/read_user.ts | 43 -- .../settings/anomaly_detection/write_user.ts | 53 -- .../trial/tests/settings/custom_link.ts | 159 ------ .../transactions/__snapshots__/latency.snap | 46 -- .../trial/tests/transactions/latency.ts | 159 ------ 87 files changed, 3447 insertions(+), 3533 deletions(-) delete mode 100644 x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/index.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/services/annotations.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/services/throughput.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/services/top_services.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/transactions/breakdown.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/transactions/latency.ts create mode 100644 x-pack/test/apm_api_integration/common/registry.ts create mode 100644 x-pack/test/apm_api_integration/configs/index.ts create mode 100644 x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts create mode 100644 x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts rename x-pack/test/apm_api_integration/{trial => }/tests/csm/__snapshots__/page_load_dist.snap (95%) rename x-pack/test/apm_api_integration/{trial => }/tests/csm/__snapshots__/page_views.snap (90%) create mode 100644 x-pack/test/apm_api_integration/tests/csm/csm_services.ts rename x-pack/test/apm_api_integration/{trial => }/tests/csm/has_rum_data.ts (53%) rename x-pack/test/apm_api_integration/{trial => }/tests/csm/js_errors.ts (72%) rename x-pack/test/apm_api_integration/{trial => }/tests/csm/long_task_metrics.ts (50%) create mode 100644 x-pack/test/apm_api_integration/tests/csm/page_load_dist.ts create mode 100644 x-pack/test/apm_api_integration/tests/csm/page_views.ts create mode 100644 x-pack/test/apm_api_integration/tests/csm/url_search.ts rename x-pack/test/apm_api_integration/{trial => }/tests/csm/web_core_vitals.ts (55%) rename x-pack/test/apm_api_integration/{basic => }/tests/feature_controls.ts (98%) create mode 100644 x-pack/test/apm_api_integration/tests/index.ts create mode 100644 x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts rename x-pack/test/apm_api_integration/{basic => }/tests/observability_overview/has_data.ts (67%) rename x-pack/test/apm_api_integration/{basic => }/tests/observability_overview/observability_overview.ts (69%) rename x-pack/test/apm_api_integration/{trial => }/tests/service_maps/__snapshots__/service_maps.snap (99%) rename x-pack/test/apm_api_integration/{trial => }/tests/service_maps/service_maps.ts (58%) rename x-pack/test/apm_api_integration/{basic => }/tests/service_overview/dependencies/es_utils.ts (100%) rename x-pack/test/apm_api_integration/{basic => }/tests/service_overview/dependencies/index.ts (91%) rename x-pack/test/apm_api_integration/{basic => }/tests/service_overview/error_groups.ts (94%) rename x-pack/test/apm_api_integration/{basic => }/tests/service_overview/instances.ts (83%) rename x-pack/test/apm_api_integration/{basic => }/tests/services/__snapshots__/throughput.snap (96%) rename x-pack/test/apm_api_integration/{basic => }/tests/services/agent_name.ts (55%) rename x-pack/test/apm_api_integration/{trial => }/tests/services/annotations.ts (92%) rename x-pack/test/apm_api_integration/{basic => }/tests/services/service_details.ts (87%) rename x-pack/test/apm_api_integration/{basic => }/tests/services/service_icons.ts (53%) create mode 100644 x-pack/test/apm_api_integration/tests/services/throughput.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/top_services.ts rename x-pack/test/apm_api_integration/{basic => }/tests/services/transaction_types.ts (75%) create mode 100644 x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts create mode 100644 x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.ts create mode 100644 x-pack/test/apm_api_integration/tests/settings/anomaly_detection/no_access_user.ts create mode 100644 x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.ts create mode 100644 x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts create mode 100644 x-pack/test/apm_api_integration/tests/settings/custom_link.ts rename x-pack/test/apm_api_integration/{basic => }/tests/traces/__snapshots__/top_traces.snap (99%) rename x-pack/test/apm_api_integration/{basic => }/tests/traces/top_traces.ts (81%) rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/__snapshots__/breakdown.snap (98%) rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/__snapshots__/error_rate.snap (95%) create mode 100644 x-pack/test/apm_api_integration/tests/transactions/__snapshots__/latency.snap rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/__snapshots__/top_transaction_groups.snap (96%) rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/__snapshots__/transaction_charts.snap (100%) rename x-pack/test/apm_api_integration/{trial => }/tests/transactions/__snapshots__/transactions_charts.snap (100%) create mode 100644 x-pack/test/apm_api_integration/tests/transactions/breakdown.ts rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/distribution.ts (85%) rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/error_rate.ts (71%) create mode 100644 x-pack/test/apm_api_integration/tests/transactions/latency.ts rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/throughput.ts (54%) rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/top_transaction_groups.ts (80%) rename x-pack/test/apm_api_integration/{basic => }/tests/transactions/transactions_groups_overview.ts (94%) delete mode 100644 x-pack/test/apm_api_integration/trial/tests/correlations/slow_transactions.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/csm_services.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/page_load_dist.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/index.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/services/top_services.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts delete mode 100644 x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap delete mode 100644 x-pack/test/apm_api_integration/trial/tests/transactions/latency.ts diff --git a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts index 6efc8774b0139..e78cbbb3aed02 100644 --- a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts +++ b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts @@ -20,6 +20,8 @@ export interface Suite { title: string; file?: string; parent?: Suite; + eachTest: (cb: (test: Test) => void) => void; + root: boolean; } export interface Test { diff --git a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts index 2a238cdeb5385..a8b0252e6f51c 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts @@ -6,21 +6,44 @@ * Public License, v 1. */ -import { Test } from '../../fake_mocha_types'; +import { Suite, Test } from '../../fake_mocha_types'; import { Lifecycle } from '../lifecycle'; import { decorateSnapshotUi, expectSnapshot } from './decorate_snapshot_ui'; import path from 'path'; import fs from 'fs'; +const createMockSuite = ({ tests, root = true }: { tests: Test[]; root?: boolean }) => { + const suite = { + tests, + root, + eachTest: (cb: (test: Test) => void) => { + suite.tests.forEach((test) => cb(test)); + }, + } as Suite; + + return suite; +}; + const createMockTest = ({ title = 'Test', passed = true, -}: { title?: string; passed?: boolean } = {}) => { - return { + filename = __filename, + parent, +}: { title?: string; passed?: boolean; filename?: string; parent?: Suite } = {}) => { + const test = ({ + file: filename, fullTitle: () => title, isPassed: () => passed, - parent: {}, - } as Test; + } as unknown) as Test; + + if (parent) { + parent.tests.push(test); + test.parent = parent; + } else { + test.parent = createMockSuite({ tests: [test] }); + } + + return test; }; describe('decorateSnapshotUi', () => { @@ -211,7 +234,7 @@ exports[\`Test2 1\`] = \`"bar"\`; expectSnapshot('bar').toMatch(); }).not.toThrow(); - const afterTestSuite = lifecycle.afterTestSuite.trigger({}); + const afterTestSuite = lifecycle.afterTestSuite.trigger(test.parent); await expect(afterTestSuite).resolves.toBe(undefined); }); @@ -225,7 +248,7 @@ exports[\`Test2 1\`] = \`"bar"\`; expectSnapshot('foo').toMatch(); }).not.toThrow(); - const afterTestSuite = lifecycle.afterTestSuite.trigger({}); + const afterTestSuite = lifecycle.afterTestSuite.trigger(test.parent); await expect(afterTestSuite).rejects.toMatchInlineSnapshot(` [Error: 1 obsolete snapshot(s) found: @@ -234,6 +257,32 @@ exports[\`Test2 1\`] = \`"bar"\`; Run tests again with \`--updateSnapshots\` to remove them.] `); }); + + it('does not throw on unused when some tests are skipped', async () => { + const root = createMockSuite({ tests: [] }); + + const test = createMockTest({ + title: 'Test', + parent: root, + passed: true, + }); + + createMockTest({ + title: 'Test2', + parent: root, + passed: false, + }); + + await lifecycle.beforeEachTest.trigger(test); + + expect(() => { + expectSnapshot('foo').toMatch(); + }).not.toThrow(); + + const afterTestSuite = lifecycle.afterTestSuite.trigger(root); + + await expect(afterTestSuite).resolves.toBeUndefined(); + }); }); }); }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts index 2111f1a6e5e90..4a52299ecf6d1 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts @@ -16,9 +16,8 @@ import path from 'path'; import prettier from 'prettier'; import babelTraverse from '@babel/traverse'; import { once } from 'lodash'; -import callsites from 'callsites'; import { Lifecycle } from '../lifecycle'; -import { Test } from '../../fake_mocha_types'; +import { Suite, Test } from '../../fake_mocha_types'; type ISnapshotState = InstanceType; @@ -33,12 +32,12 @@ const globalState: { updateSnapshot: SnapshotUpdateState; registered: boolean; currentTest: Test | null; - snapshots: Array<{ tests: Test[]; file: string; snapshotState: ISnapshotState }>; + snapshotStates: Record; } = { updateSnapshot: 'none', registered: false, currentTest: null, - snapshots: [], + snapshotStates: {}, }; const modifyStackTracePrepareOnce = once(() => { @@ -73,7 +72,7 @@ export function decorateSnapshotUi({ isCi: boolean; }) { globalState.registered = true; - globalState.snapshots.length = 0; + globalState.snapshotStates = {}; globalState.currentTest = null; if (isCi) { @@ -102,32 +101,36 @@ export function decorateSnapshotUi({ globalState.currentTest = test; }); - lifecycle.afterTestSuite.add(function (testSuite) { + lifecycle.afterTestSuite.add(function (testSuite: Suite) { // save snapshot & check unused after top-level test suite completes - if (testSuite.parent?.parent) { + if (!testSuite.root) { return; } - const unused: string[] = []; + testSuite.eachTest((test) => { + const file = test.file; - globalState.snapshots.forEach((snapshot) => { - const { tests, snapshotState } = snapshot; - tests.forEach((test) => { - const title = test.fullTitle(); - // If test is failed or skipped, mark snapshots as used. Otherwise, - // running a test in isolation will generate false positives. - if (!test.isPassed()) { - snapshotState.markSnapshotsAsCheckedForTest(title); - } - }); + if (!file) { + return; + } + + const snapshotState = globalState.snapshotStates[file]; + + if (snapshotState && !test.isPassed()) { + snapshotState.markSnapshotsAsCheckedForTest(test.fullTitle()); + } + }); + + const unused: string[] = []; - if (globalState.updateSnapshot !== 'all') { - unused.push(...snapshotState.getUncheckedKeys()); - } else { - snapshotState.removeUncheckedKeys(); + Object.values(globalState.snapshotStates).forEach((state) => { + if (globalState.updateSnapshot === 'all') { + state.removeUncheckedKeys(); } - snapshotState.save(); + unused.push(...state.getUncheckedKeys()); + + state.save(); }); if (unused.length) { @@ -138,7 +141,7 @@ export function decorateSnapshotUi({ ); } - globalState.snapshots.length = 0; + globalState.snapshotStates = {}; }); } @@ -161,43 +164,29 @@ function getSnapshotState(file: string, updateSnapshot: SnapshotUpdateState) { export function expectSnapshot(received: any) { if (!globalState.registered) { - throw new Error( - 'Mocha hooks were not registered before expectSnapshot was used. Call `registerMochaHooksForSnapshots` in your top-level describe().' - ); - } - - if (!globalState.currentTest) { - throw new Error('expectSnapshot can only be called inside of an it()'); + throw new Error('expectSnapshot UI was not initialized before calling expectSnapshot()'); } - const [, fileOfTest] = callsites().map((site) => site.getFileName()); + const test = globalState.currentTest; - if (!fileOfTest) { - throw new Error("Couldn't infer a filename for the current test"); + if (!test) { + throw new Error('expectSnapshot can only be called inside of an it()'); } - let snapshot = globalState.snapshots.find(({ file }) => file === fileOfTest); - - if (!snapshot) { - snapshot = { - file: fileOfTest, - tests: [], - snapshotState: getSnapshotState(fileOfTest, globalState.updateSnapshot), - }; - globalState.snapshots.unshift(snapshot!); + if (!test.file) { + throw new Error('File for test not found'); } - if (!snapshot) { - throw new Error('Snapshot is undefined'); - } + let snapshotState = globalState.snapshotStates[test.file]; - if (!snapshot.tests.includes(globalState.currentTest)) { - snapshot.tests.push(globalState.currentTest); + if (!snapshotState) { + snapshotState = getSnapshotState(test.file, globalState.updateSnapshot); + globalState.snapshotStates[test.file] = snapshotState; } const context: SnapshotContext = { - snapshotState: snapshot.snapshotState, - currentTestName: globalState.currentTest.fullTitle(), + snapshotState, + currentTestName: test.fullTitle(), }; return { diff --git a/x-pack/test/apm_api_integration/basic/config.ts b/x-pack/test/apm_api_integration/basic/config.ts index 03b8b21bf3232..f3e6ec29f3df2 100644 --- a/x-pack/test/apm_api_integration/basic/config.ts +++ b/x-pack/test/apm_api_integration/basic/config.ts @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createTestConfig } from '../common/config'; +import { configs } from '../configs'; -export default createTestConfig({ - license: 'basic', - name: 'X-Pack APM API integration tests (basic)', - testFiles: [require.resolve('./tests')], -}); +export default configs.basic; diff --git a/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts b/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts deleted file mode 100644 index 46c0dbeb8940f..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts +++ /dev/null @@ -1,124 +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 { format } from 'url'; -import archives from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - const archiveName = 'apm_8.0.0'; - const { end } = archives[archiveName]; - const start = new Date(Date.parse(end) - 600000).toISOString(); - - describe('Alerting chart previews', () => { - describe('GET /api/apm/alerts/chart_preview/transaction_error_rate', () => { - const url = format({ - pathname: '/api/apm/alerts/chart_preview/transaction_error_rate', - query: { - start, - end, - transactionType: 'request', - serviceName: 'opbeans-java', - }, - }); - - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns the correct data', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect( - response.body.some((item: { x: number; y: number | null }) => item.x && item.y) - ).to.equal(true); - }); - }); - }); - - describe('GET /api/apm/alerts/chart_preview/transaction_error_count', () => { - const url = format({ - pathname: '/api/apm/alerts/chart_preview/transaction_error_count', - query: { - start, - end, - serviceName: 'opbeans-java', - }, - }); - - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns the correct data', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect( - response.body.some((item: { x: number; y: number | null }) => item.x && item.y) - ).to.equal(true); - }); - }); - }); - - describe('GET /api/apm/alerts/chart_preview/transaction_duration', () => { - const url = format({ - pathname: '/api/apm/alerts/chart_preview/transaction_duration', - query: { - start, - end, - serviceName: 'opbeans-java', - transactionType: 'request', - }, - }); - - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns the correct data', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect( - response.body.some((item: { x: number; y: number | null }) => item.x && item.y) - ).to.equal(true); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts deleted file mode 100644 index 4e66c6e6f76c3..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('APM specs (basic)', function () { - this.tags('ciGroup1'); - - loadTestFile(require.resolve('./feature_controls')); - - describe('Alerts', function () { - loadTestFile(require.resolve('./alerts/chart_preview')); - }); - - describe('Service Maps', function () { - loadTestFile(require.resolve('./service_maps/service_maps')); - }); - - describe('Services', function () { - loadTestFile(require.resolve('./services/agent_name')); - loadTestFile(require.resolve('./services/annotations')); - loadTestFile(require.resolve('./services/throughput')); - loadTestFile(require.resolve('./services/top_services')); - loadTestFile(require.resolve('./services/transaction_types')); - loadTestFile(require.resolve('./services/service_details')); - loadTestFile(require.resolve('./services/service_icons')); - }); - - describe('Service overview', function () { - loadTestFile(require.resolve('./service_overview/error_groups')); - loadTestFile(require.resolve('./service_overview/dependencies')); - loadTestFile(require.resolve('./service_overview/instances')); - }); - - describe('Settings', function () { - loadTestFile(require.resolve('./settings/custom_link')); - loadTestFile(require.resolve('./settings/agent_configuration')); - - describe('Anomaly detection', function () { - loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); - loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); - loadTestFile(require.resolve('./settings/anomaly_detection/write_user')); - }); - }); - - describe('Traces', function () { - loadTestFile(require.resolve('./traces/top_traces')); - }); - - describe('Transactions', function () { - loadTestFile(require.resolve('./transactions/top_transaction_groups')); - loadTestFile(require.resolve('./transactions/latency')); - loadTestFile(require.resolve('./transactions/throughput')); - loadTestFile(require.resolve('./transactions/error_rate')); - loadTestFile(require.resolve('./transactions/breakdown')); - loadTestFile(require.resolve('./transactions/distribution')); - loadTestFile(require.resolve('./transactions/transactions_groups_overview')); - }); - - describe('Observability overview', function () { - loadTestFile(require.resolve('./observability_overview/has_data')); - loadTestFile(require.resolve('./observability_overview/observability_overview')); - }); - - describe('Metrics', function () { - loadTestFile(require.resolve('./metrics_charts/metrics_charts')); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts deleted file mode 100644 index d52aa2727d651..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts +++ /dev/null @@ -1,439 +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 { first } from 'lodash'; -import { MetricsChartsByAgentAPIResponse } from '../../../../../plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent'; -import { GenericMetricsChart } from '../../../../../plugins/apm/server/lib/metrics/transform_metrics_chart'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -interface ChartResponse { - body: MetricsChartsByAgentAPIResponse; - status: number; -} - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('when data is loaded', () => { - before(() => esArchiver.load('metrics_8.0.0')); - after(() => esArchiver.unload('metrics_8.0.0')); - - describe('for opbeans-node', () => { - const start = encodeURIComponent('2020-09-08T14:50:00.000Z'); - const end = encodeURIComponent('2020-09-08T14:55:00.000Z'); - const uiFilters = encodeURIComponent(JSON.stringify({})); - const agentName = 'nodejs'; - - describe('returns metrics data', () => { - let chartsResponse: ChartResponse; - before(async () => { - chartsResponse = await supertest.get( - `/api/apm/services/opbeans-node/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` - ); - }); - it('contains CPU usage and System memory usage chart data', async () => { - expect(chartsResponse.status).to.be(200); - expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` - Array [ - "CPU usage", - "System memory usage", - ] - `); - }); - - describe('CPU usage', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find(({ key }) => key === 'cpu_usage_chart'); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "System max", - "System average", - "Process max", - "Process average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.714, - 0.3877, - 0.75, - 0.2543, - ] - `); - }); - }); - - describe("System memory usage (using 'system.memory' fields to calculate the memory usage)", () => { - let systemMemoryUsageChart: GenericMetricsChart | undefined; - before(() => { - systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - }); - - it('has correct series', () => { - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.722093920925555, - 0.718173546796348, - ] - `); - }); - }); - }); - }); - - describe('for opbeans-java', () => { - const uiFilters = encodeURIComponent(JSON.stringify({})); - const agentName = 'java'; - - describe('returns metrics data', () => { - const start = encodeURIComponent('2020-09-08T14:55:30.000Z'); - const end = encodeURIComponent('2020-09-08T15:00:00.000Z'); - - let chartsResponse: ChartResponse; - before(async () => { - chartsResponse = await supertest.get( - `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` - ); - }); - - it('has correct chart data', async () => { - expect(chartsResponse.status).to.be(200); - expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` - Array [ - "CPU usage", - "System memory usage", - "Heap Memory", - "Non-Heap Memory", - "Thread Count", - "Garbage collection per minute", - "Garbage collection time spent per minute", - ] - `); - }); - - describe('CPU usage', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find(({ key }) => key === 'cpu_usage_chart'); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "System max", - "System average", - "Process max", - "Process average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.203, - 0.178777777777778, - 0.01, - 0.009, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.193, - 0.193, - 0.009, - 0.009, - ] - `); - }); - }); - - describe("System memory usage (using 'system.process.cgroup' fields to calculate the memory usage)", () => { - let systemMemoryUsageChart: GenericMetricsChart | undefined; - before(() => { - systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - }); - - it('has correct series', () => { - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.707924703557837, - 0.705395980841182, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.707924703557837, - 0.707924703557837, - ] - `); - }); - }); - - describe('Heap Memory', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'heap_memory_area_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. used", - "Avg. committed", - "Avg. limit", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 222501617.777778, - 374341632, - 1560281088, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 211472896, - 374341632, - 1560281088, - ] - `); - }); - }); - - describe('Non-Heap Memory', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'non_heap_memory_area_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. used", - "Avg. committed", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 138573397.333333, - 147677639.111111, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 138162752, - 147386368, - ] - `); - }); - }); - - describe('Thread Count', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'thread_count_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. count", - "Max count", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 44.4444444444444, - 45, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 44, - 44, - ] - `); - }); - }); - - describe('Garbage collection per minute', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'gc_rate_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "G1 Old Generation", - "G1 Young Generation", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0, - 15, - ] - `); - }); - }); - - describe('Garbage collection time spent per minute', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'gc_time_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "G1 Old Generation", - "G1 Young Generation", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0, - 187.5, - ] - `); - }); - }); - }); - - // 9223372036854771712 = memory limit for a c-group when no memory limit is specified - it('calculates system memory usage using system total field when cgroup limit is equal to 9223372036854771712', async () => { - const start = encodeURIComponent('2020-09-08T15:00:30.000Z'); - const end = encodeURIComponent('2020-09-08T15:05:00.000Z'); - - const chartsResponse: ChartResponse = await supertest.get( - `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` - ); - - const systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.114523896426499, - 0.114002376090415, - ] - `); - - const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.11383724014064, - 0.11383724014064, - ] - `); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts deleted file mode 100644 index f44b1561f2a5a..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts +++ /dev/null @@ -1,32 +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 archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function serviceMapsApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - // url parameters - const start = encodeURIComponent(metadata.start); - const end = encodeURIComponent(metadata.end); - - describe('Service Maps', () => { - it('is only be available to users with Platinum license (or higher)', async () => { - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); - - expect(response.status).to.be(403); - - expectSnapshot(response.body.message).toMatchInline( - `"In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data."` - ); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/services/annotations.ts b/x-pack/test/apm_api_integration/basic/tests/services/annotations.ts deleted file mode 100644 index 3136dcef2e985..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/services/annotations.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 expect from '@kbn/expect'; -import { JsonObject } from 'src/plugins/kibana_utils/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function annotationApiTests({ getService }: FtrProviderContext) { - const supertestWrite = getService('supertestAsApmAnnotationsWriteUser'); - - function request({ method, url, data }: { method: string; url: string; data?: JsonObject }) { - switch (method.toLowerCase()) { - case 'post': - return supertestWrite.post(url).send(data).set('kbn-xsrf', 'foo'); - - default: - throw new Error(`Unsupported method ${method}`); - } - } - - describe('APM annotations with a basic license', () => { - describe('when creating an annotation', () => { - it('fails with a 403 forbidden', async () => { - const response = await request({ - url: '/api/apm/services/opbeans-java/annotation', - method: 'POST', - data: { - '@timestamp': new Date().toISOString(), - message: 'New deployment', - tags: ['foo'], - service: { - version: '1.1', - environment: 'production', - }, - }, - }); - - expect(response.status).to.be(403); - expect(response.body.message).to.be( - 'Annotations require at least a gold license or a trial license.' - ); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/services/throughput.ts b/x-pack/test/apm_api_integration/basic/tests/services/throughput.ts deleted file mode 100644 index 07a1442d751b4..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/services/throughput.ts +++ /dev/null @@ -1,85 +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 qs from 'querystring'; -import { first, last } from 'lodash'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - describe('Throughput', () => { - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/throughput?${qs.stringify({ - start: metadata.start, - end: metadata.end, - uiFilters: encodeURIComponent('{}'), - transactionType: 'request', - })}` - ); - expect(response.status).to.be(200); - expect(response.body.throughput.length).to.be(0); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - describe('returns the service throughput', () => { - let throughputResponse: { - throughput: Array<{ x: number; y: number | null }>; - }; - before(async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/throughput?${qs.stringify({ - start: metadata.start, - end: metadata.end, - uiFilters: encodeURIComponent('{}'), - transactionType: 'request', - })}` - ); - throughputResponse = response.body; - }); - - it('returns some data', () => { - expect(throughputResponse.throughput.length).to.be.greaterThan(0); - - const nonNullDataPoints = throughputResponse.throughput.filter(({ y }) => y !== null); - - expect(nonNullDataPoints.length).to.be.greaterThan(0); - }); - - it('has the correct start date', () => { - expectSnapshot( - new Date(first(throughputResponse.throughput)?.x ?? NaN).toISOString() - ).toMatchInline(`"2020-12-08T13:57:30.000Z"`); - }); - - it('has the correct end date', () => { - expectSnapshot( - new Date(last(throughputResponse.throughput)?.x ?? NaN).toISOString() - ).toMatchInline(`"2020-12-08T14:27:30.000Z"`); - }); - - it('has the correct number of buckets', () => { - expectSnapshot(throughputResponse.throughput.length).toMatchInline(`61`); - }); - - it('has the correct throughput', () => { - expectSnapshot(throughputResponse.throughput).toMatch(); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts deleted file mode 100644 index 98bfe84cf56ee..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ /dev/null @@ -1,259 +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 { isEmpty, pick, sortBy } from 'lodash'; -import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - - const range = archives_metadata[archiveName]; - - // url parameters - const start = encodeURIComponent(range.start); - const end = encodeURIComponent(range.end); - - const uiFilters = encodeURIComponent(JSON.stringify({})); - - describe('APM Services Overview', () => { - describe('when data is not loaded ', () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - - expect(response.status).to.be(200); - expect(response.body.hasHistoricalData).to.be(false); - expect(response.body.hasLegacyData).to.be(false); - expect(response.body.items.length).to.be(0); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - describe('and fetching a list of services', () => { - let response: { - status: number; - body: APIReturnType<'GET /api/apm/services'>; - }; - - let sortedItems: typeof response.body.items; - - before(async () => { - response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - sortedItems = sortBy(response.body.items, 'serviceName'); - }); - - it('the response is successful', () => { - expect(response.status).to.eql(200); - }); - - it('returns hasHistoricalData: true', () => { - expect(response.body.hasHistoricalData).to.be(true); - }); - - it('returns hasLegacyData: false', () => { - expect(response.body.hasLegacyData).to.be(false); - }); - - it('returns the correct service names', () => { - expectSnapshot(sortedItems.map((item) => item.serviceName)).toMatchInline(` - Array [ - "kibana", - "kibana-frontend", - "opbeans-dotnet", - "opbeans-go", - "opbeans-java", - "opbeans-node", - "opbeans-python", - "opbeans-ruby", - "opbeans-rum", - ] - `); - }); - - it('returns the correct metrics averages', () => { - expectSnapshot( - sortedItems.map((item) => - pick( - item, - 'transactionErrorRate.value', - 'avgResponseTime.value', - 'transactionsPerMinute.value' - ) - ) - ).toMatchInline(` - Array [ - Object { - "avgResponseTime": Object { - "value": 420419.34550767, - }, - "transactionErrorRate": Object { - "value": 0, - }, - "transactionsPerMinute": Object { - "value": 45.6333333333333, - }, - }, - Object { - "avgResponseTime": Object { - "value": 2382833.33333333, - }, - "transactionErrorRate": Object { - "value": null, - }, - "transactionsPerMinute": Object { - "value": 0.2, - }, - }, - Object { - "avgResponseTime": Object { - "value": 631521.83908046, - }, - "transactionErrorRate": Object { - "value": 0.0229885057471264, - }, - "transactionsPerMinute": Object { - "value": 2.9, - }, - }, - Object { - "avgResponseTime": Object { - "value": 27946.1484375, - }, - "transactionErrorRate": Object { - "value": 0.015625, - }, - "transactionsPerMinute": Object { - "value": 4.26666666666667, - }, - }, - Object { - "avgResponseTime": Object { - "value": 237339.813333333, - }, - "transactionErrorRate": Object { - "value": 0.16, - }, - "transactionsPerMinute": Object { - "value": 2.5, - }, - }, - Object { - "avgResponseTime": Object { - "value": 24920.1052631579, - }, - "transactionErrorRate": Object { - "value": 0.0210526315789474, - }, - "transactionsPerMinute": Object { - "value": 3.16666666666667, - }, - }, - Object { - "avgResponseTime": Object { - "value": 29542.6607142857, - }, - "transactionErrorRate": Object { - "value": 0.0357142857142857, - }, - "transactionsPerMinute": Object { - "value": 1.86666666666667, - }, - }, - Object { - "avgResponseTime": Object { - "value": 70518.9328358209, - }, - "transactionErrorRate": Object { - "value": 0.0373134328358209, - }, - "transactionsPerMinute": Object { - "value": 4.46666666666667, - }, - }, - Object { - "avgResponseTime": Object { - "value": 2319812.5, - }, - "transactionErrorRate": Object { - "value": null, - }, - "transactionsPerMinute": Object { - "value": 0.533333333333333, - }, - }, - ] - `); - }); - - it('returns environments', () => { - expectSnapshot(sortedItems.map((item) => item.environments ?? [])).toMatchInline(` - Array [ - Array [ - "production", - ], - Array [ - "production", - ], - Array [ - "production", - ], - Array [ - "testing", - ], - Array [ - "production", - ], - Array [ - "testing", - ], - Array [], - Array [], - Array [ - "testing", - ], - ] - `); - }); - - it(`RUM services don't report any transaction error rates`, () => { - // RUM transactions don't have event.outcome set, - // so they should not have an error rate - - const rumServices = sortedItems.filter((item) => item.agentName === 'rum-js'); - - expect(rumServices.length).to.be.greaterThan(0); - - expect(rumServices.every((item) => isEmpty(item.transactionErrorRate?.value))); - }); - - it('non-RUM services all report transaction error rates', () => { - const nonRumServices = sortedItems.filter((item) => item.agentName !== 'rum-js'); - - expect( - nonRumServices.every((item) => { - return ( - typeof item.transactionErrorRate?.value === 'number' && - item.transactionErrorRate.timeseries.length > 0 - ); - }) - ).to.be(true); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts b/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts deleted file mode 100644 index 1817c7c4511fa..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts +++ /dev/null @@ -1,489 +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 { omit, orderBy } from 'lodash'; -import { AgentConfigurationIntake } from '../../../../../plugins/apm/common/agent_configuration/configuration_types'; -import { AgentConfigSearchParams } from '../../../../../plugins/apm/server/routes/settings/agent_configuration'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function agentConfigurationTests({ getService }: FtrProviderContext) { - const supertestRead = getService('supertestAsApmReadUser'); - const supertestWrite = getService('supertestAsApmWriteUser'); - const log = getService('log'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - - function getServices() { - return supertestRead - .get(`/api/apm/settings/agent-configuration/services`) - .set('kbn-xsrf', 'foo'); - } - - function getEnvironments(serviceName: string) { - return supertestRead - .get(`/api/apm/settings/agent-configuration/environments?serviceName=${serviceName}`) - .set('kbn-xsrf', 'foo'); - } - - function getAgentName(serviceName: string) { - return supertestRead - .get(`/api/apm/settings/agent-configuration/agent_name?serviceName=${serviceName}`) - .set('kbn-xsrf', 'foo'); - } - - function searchConfigurations(configuration: AgentConfigSearchParams) { - return supertestRead - .post(`/api/apm/settings/agent-configuration/search`) - .send(configuration) - .set('kbn-xsrf', 'foo'); - } - - function getAllConfigurations() { - return supertestRead.get(`/api/apm/settings/agent-configuration`).set('kbn-xsrf', 'foo'); - } - - async function createConfiguration(config: AgentConfigurationIntake, { user = 'write' } = {}) { - log.debug('creating configuration', config.service); - const supertestClient = user === 'read' ? supertestRead : supertestWrite; - - const res = await supertestClient - .put(`/api/apm/settings/agent-configuration`) - .send(config) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - async function updateConfiguration(config: AgentConfigurationIntake, { user = 'write' } = {}) { - log.debug('updating configuration', config.service); - const supertestClient = user === 'read' ? supertestRead : supertestWrite; - - const res = await supertestClient - .put(`/api/apm/settings/agent-configuration?overwrite=true`) - .send(config) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - async function deleteConfiguration( - { service }: AgentConfigurationIntake, - { user = 'write' } = {} - ) { - log.debug('deleting configuration', service); - const supertestClient = user === 'read' ? supertestRead : supertestWrite; - - const res = await supertestClient - .delete(`/api/apm/settings/agent-configuration`) - .send({ service }) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - function throwOnError(res: any) { - const { statusCode, req, body } = res; - if (statusCode !== 200) { - const e = new Error(` - Endpoint: ${req.method} ${req.path} - Service: ${JSON.stringify(res.request._data.service)} - Status code: ${statusCode} - Response: ${body.message}`); - - // @ts-ignore - e.res = res; - - throw e; - } - } - - describe('agent configuration', () => { - describe('when no data is loaded', () => { - it('handles the empty state for services', async () => { - const { body } = await getServices(); - expect(body).to.eql(['ALL_OPTION_VALUE']); - }); - - it('handles the empty state for environments', async () => { - const { body } = await getEnvironments('myservice'); - expect(body).to.eql([{ name: 'ALL_OPTION_VALUE', alreadyConfigured: false }]); - }); - - it('handles the empty state for agent names', async () => { - const { body } = await getAgentName('myservice'); - expect(body).to.eql({}); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns all services', async () => { - const { body } = await getServices(); - expectSnapshot(body).toMatchInline(` - Array [ - "ALL_OPTION_VALUE", - "kibana", - "kibana-frontend", - "opbeans-dotnet", - "opbeans-go", - "opbeans-java", - "opbeans-node", - "opbeans-python", - "opbeans-ruby", - "opbeans-rum", - ] - `); - }); - - it('returns the environments, all unconfigured', async () => { - const { body } = await getEnvironments('opbeans-node'); - - expect(body.map((item: { name: string }) => item.name)).to.contain('ALL_OPTION_VALUE'); - - expect( - body.every((item: { alreadyConfigured: boolean }) => item.alreadyConfigured === false) - ).to.be(true); - - expectSnapshot(body).toMatchInline(` - Array [ - Object { - "alreadyConfigured": false, - "name": "ALL_OPTION_VALUE", - }, - Object { - "alreadyConfigured": false, - "name": "testing", - }, - ] - `); - }); - - it('returns the agent names', async () => { - const { body } = await getAgentName('opbeans-node'); - expect(body).to.eql({ agentName: 'nodejs' }); - }); - }); - - describe('as a read-only user', () => { - const newConfig = { service: {}, settings: { transaction_sample_rate: '0.55' } }; - it('throws when attempting to create config', async () => { - try { - await createConfiguration(newConfig, { user: 'read' }); - - // ensure that `createConfiguration` throws - expect(true).to.be(false); - } catch (e) { - expect(e.res.statusCode).to.be(403); - } - }); - - describe('when a configuration already exists', () => { - before(async () => createConfiguration(newConfig)); - after(async () => deleteConfiguration(newConfig)); - - it('throws when attempting to update config', async () => { - try { - await updateConfiguration(newConfig, { user: 'read' }); - - // ensure that `updateConfiguration` throws - expect(true).to.be(false); - } catch (e) { - expect(e.res.statusCode).to.be(403); - } - }); - - it('throws when attempting to delete config', async () => { - try { - await deleteConfiguration(newConfig, { user: 'read' }); - - // ensure that `deleteConfiguration` throws - expect(true).to.be(false); - } catch (e) { - expect(e.res.statusCode).to.be(403); - } - }); - }); - }); - - describe('when creating one configuration', () => { - const newConfig = { - service: {}, - settings: { transaction_sample_rate: '0.55' }, - }; - - const searchParams = { - service: { name: 'myservice', environment: 'development' }, - etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39', - }; - - it('can create and delete config', async () => { - // assert that config does not exist - const res1 = await searchConfigurations(searchParams); - expect(res1.status).to.equal(404); - - // assert that config was created - await createConfiguration(newConfig); - const res2 = await searchConfigurations(searchParams); - expect(res2.status).to.equal(200); - - // assert that config was deleted - await deleteConfiguration(newConfig); - const res3 = await searchConfigurations(searchParams); - expect(res3.status).to.equal(404); - }); - - describe('when a configuration exists', () => { - before(async () => createConfiguration(newConfig)); - after(async () => deleteConfiguration(newConfig)); - - it('can find the config', async () => { - const { status, body } = await searchConfigurations(searchParams); - expect(status).to.equal(200); - expect(body._source.service).to.eql({}); - expect(body._source.settings).to.eql({ transaction_sample_rate: '0.55' }); - }); - - it('can list the config', async () => { - const { status, body } = await getAllConfigurations(); - expect(status).to.equal(200); - expect(omitTimestamp(body)).to.eql([ - { - service: {}, - settings: { transaction_sample_rate: '0.55' }, - applied_by_agent: false, - etag: 'eb88a8997666cc4b33745ef355a1bbd7c4782f2d', - }, - ]); - }); - - it('can update the config', async () => { - await updateConfiguration({ service: {}, settings: { transaction_sample_rate: '0.85' } }); - const { status, body } = await searchConfigurations(searchParams); - expect(status).to.equal(200); - expect(body._source.service).to.eql({}); - expect(body._source.settings).to.eql({ transaction_sample_rate: '0.85' }); - }); - }); - }); - - describe('when creating multiple configurations', () => { - const configs = [ - { - service: {}, - settings: { transaction_sample_rate: '0.1' }, - }, - { - service: { name: 'my_service' }, - settings: { transaction_sample_rate: '0.2' }, - }, - { - service: { name: 'my_service', environment: 'development' }, - settings: { transaction_sample_rate: '0.3' }, - }, - { - service: { environment: 'production' }, - settings: { transaction_sample_rate: '0.4' }, - }, - { - service: { environment: 'development' }, - settings: { transaction_sample_rate: '0.5' }, - }, - ]; - - before(async () => { - await Promise.all(configs.map((config) => createConfiguration(config))); - }); - - after(async () => { - await Promise.all(configs.map((config) => deleteConfiguration(config))); - }); - - const agentsRequests = [ - { - service: { name: 'non_existing_service', environment: 'non_existing_env' }, - expectedSettings: { transaction_sample_rate: '0.1' }, - }, - { - service: { name: 'my_service', environment: 'non_existing_env' }, - expectedSettings: { transaction_sample_rate: '0.2' }, - }, - { - service: { name: 'my_service', environment: 'production' }, - expectedSettings: { transaction_sample_rate: '0.2' }, - }, - { - service: { name: 'my_service', environment: 'development' }, - expectedSettings: { transaction_sample_rate: '0.3' }, - }, - { - service: { name: 'non_existing_service', environment: 'production' }, - expectedSettings: { transaction_sample_rate: '0.4' }, - }, - { - service: { name: 'non_existing_service', environment: 'development' }, - expectedSettings: { transaction_sample_rate: '0.5' }, - }, - ]; - - it('can list all configs', async () => { - const { status, body } = await getAllConfigurations(); - expect(status).to.equal(200); - expect(orderBy(omitTimestamp(body), ['settings.transaction_sample_rate'])).to.eql([ - { - service: {}, - settings: { transaction_sample_rate: '0.1' }, - applied_by_agent: false, - etag: '0758cb18817de60cca29e07480d472694239c4c3', - }, - { - service: { name: 'my_service' }, - settings: { transaction_sample_rate: '0.2' }, - applied_by_agent: false, - etag: 'e04737637056fdf1763bf0ef0d3fcb86e89ae5fc', - }, - { - service: { name: 'my_service', environment: 'development' }, - settings: { transaction_sample_rate: '0.3' }, - applied_by_agent: false, - etag: 'af4dac62621b6762e6281481d1f7523af1124120', - }, - { - service: { environment: 'production' }, - settings: { transaction_sample_rate: '0.4' }, - applied_by_agent: false, - etag: '8d1bf8e6b778b60af351117e2cf53fb1ee570068', - }, - { - service: { environment: 'development' }, - settings: { transaction_sample_rate: '0.5' }, - applied_by_agent: false, - etag: '4ce40da57e3c71daca704121c784b911ec05ae81', - }, - ]); - }); - - for (const agentRequest of agentsRequests) { - it(`${agentRequest.service.name} / ${agentRequest.service.environment}`, async () => { - const { status, body } = await searchConfigurations({ - service: agentRequest.service, - etag: 'abc', - }); - - expect(status).to.equal(200); - expect(body._source.settings).to.eql(agentRequest.expectedSettings); - }); - } - }); - - describe('when an agent retrieves a configuration', () => { - const config = { - service: { name: 'myservice', environment: 'development' }, - settings: { transaction_sample_rate: '0.9' }, - }; - const configProduction = { - service: { name: 'myservice', environment: 'production' }, - settings: { transaction_sample_rate: '0.9' }, - }; - let etag: string; - - before(async () => { - log.debug('creating agent configuration'); - await createConfiguration(config); - await createConfiguration(configProduction); - }); - - after(async () => { - await deleteConfiguration(config); - await deleteConfiguration(configProduction); - }); - - it(`should have 'applied_by_agent=false' before supplying etag`, async () => { - const res1 = await searchConfigurations({ - service: { name: 'myservice', environment: 'development' }, - }); - - etag = res1.body._source.etag; - - const res2 = await searchConfigurations({ - service: { name: 'myservice', environment: 'development' }, - etag, - }); - - expect(res1.body._source.applied_by_agent).to.be(false); - expect(res2.body._source.applied_by_agent).to.be(false); - }); - - it(`should have 'applied_by_agent=true' after supplying etag`, async () => { - await searchConfigurations({ - service: { name: 'myservice', environment: 'development' }, - etag, - }); - - async function hasBeenAppliedByAgent() { - const { body } = await searchConfigurations({ - service: { name: 'myservice', environment: 'development' }, - }); - - return body._source.applied_by_agent; - } - - // wait until `applied_by_agent` has been updated in elasticsearch - expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); - }); - it(`should have 'applied_by_agent=false' before marking as applied`, async () => { - const res1 = await searchConfigurations({ - service: { name: 'myservice', environment: 'production' }, - }); - - expect(res1.body._source.applied_by_agent).to.be(false); - }); - it(`should have 'applied_by_agent=true' when 'mark_as_applied_by_agent' attribute is true`, async () => { - await searchConfigurations({ - service: { name: 'myservice', environment: 'production' }, - mark_as_applied_by_agent: true, - }); - - async function hasBeenAppliedByAgent() { - const { body } = await searchConfigurations({ - service: { name: 'myservice', environment: 'production' }, - }); - - return body._source.applied_by_agent; - } - - // wait until `applied_by_agent` has been updated in elasticsearch - expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); - }); - }); - }); -} - -async function waitFor(cb: () => Promise, retries = 50): Promise { - if (retries === 0) { - throw new Error(`Maximum number of retries reached`); - } - - const res = await cb(); - if (!res) { - await new Promise((resolve) => setTimeout(resolve, 100)); - return waitFor(cb, retries - 1); - } - return res; -} - -function omitTimestamp(configs: AgentConfigurationIntake[]) { - return configs.map((config: AgentConfigurationIntake) => omit(config, '@timestamp')); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts deleted file mode 100644 index 5630bd195b6cd..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -export default function apiTest({ getService }: FtrProviderContext) { - const noAccessUser = getService('supertestAsNoAccessUser'); - - function getAnomalyDetectionJobs() { - return noAccessUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); - } - - function createAnomalyDetectionJobs(environments: string[]) { - return noAccessUser - .post(`/api/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); - } - - describe('when user does not have read access to ML', () => { - describe('when calling the endpoint for listing jobs', () => { - it('returns an error because the user does not have access', async () => { - const { body } = await getAnomalyDetectionJobs(); - - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - }); - }); - - describe('when calling create endpoint', () => { - it('returns an error because the user does not have access', async () => { - const { body } = await createAnomalyDetectionJobs(['production', 'staging']); - - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts deleted file mode 100644 index 30e097e791eaa..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts +++ /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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -export default function apiTest({ getService }: FtrProviderContext) { - const apmReadUser = getService('supertestAsApmReadUser'); - - function getAnomalyDetectionJobs() { - return apmReadUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); - } - - function createAnomalyDetectionJobs(environments: string[]) { - return apmReadUser - .post(`/api/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); - } - - describe('when user has read access to ML', () => { - describe('when calling the endpoint for listing jobs', () => { - it('returns an error because the user is on basic license', async () => { - const { body } = await getAnomalyDetectionJobs(); - - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - - expectSnapshot(body.message).toMatchInline( - `"To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning."` - ); - }); - }); - - describe('when calling create endpoint', () => { - it('returns an error because the user does not have access', async () => { - const { body } = await createAnomalyDetectionJobs(['production', 'staging']); - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts deleted file mode 100644 index 15659229a1917..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts +++ /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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -export default function apiTest({ getService }: FtrProviderContext) { - const apmWriteUser = getService('supertestAsApmWriteUser'); - - function getAnomalyDetectionJobs() { - return apmWriteUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); - } - - function createAnomalyDetectionJobs(environments: string[]) { - return apmWriteUser - .post(`/api/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); - } - - describe('when user has write access to ML', () => { - describe('when calling the endpoint for listing jobs', () => { - it('returns an error because the user is on basic license', async () => { - const { body } = await getAnomalyDetectionJobs(); - - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - expectSnapshot(body.message).toMatchInline( - `"To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning."` - ); - }); - }); - - describe('when calling create endpoint', () => { - it('returns an error because the user is on basic license', async () => { - const { body } = await createAnomalyDetectionJobs(['production', 'staging']); - - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - - expectSnapshot(body.message).toMatchInline( - `"To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning."` - ); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts deleted file mode 100644 index 8ac5566fc2c49..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts +++ /dev/null @@ -1,35 +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 { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function customLinksTests({ getService }: FtrProviderContext) { - const supertestWrite = getService('supertestAsApmWriteUser'); - - describe('custom links', () => { - it('is only be available to users with Gold license (or higher)', async () => { - const customLink = { - url: 'https://elastic.co', - label: 'with filters', - filters: [ - { key: 'service.name', value: 'baz' }, - { key: 'transaction.type', value: 'qux' }, - ], - } as CustomLink; - const response = await supertestWrite - .post(`/api/apm/settings/custom_links`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - - expect(response.status).to.be(403); - - expectSnapshot(response.body.message).toMatchInline( - `"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."` - ); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transactions/breakdown.ts deleted file mode 100644 index 947defca05d94..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/breakdown.ts +++ /dev/null @@ -1,123 +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 archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - const start = encodeURIComponent(metadata.start); - const end = encodeURIComponent(metadata.end); - const transactionType = 'request'; - const transactionName = 'GET /api'; - const uiFilters = encodeURIComponent(JSON.stringify({})); - - describe('Breakdown', () => { - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` - ); - expect(response.status).to.be(200); - expect(response.body).to.eql({ timeseries: [] }); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns the transaction breakdown for a service', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` - ); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatch(); - }); - it('returns the transaction breakdown for a transaction group', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}&transactionName=${transactionName}` - ); - - expect(response.status).to.be(200); - - const { timeseries } = response.body; - - const numberOfSeries = timeseries.length; - - expectSnapshot(numberOfSeries).toMatchInline(`1`); - - const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; - - const nonNullDataPoints = data.filter((y: number | null) => y !== null); - - expectSnapshot(nonNullDataPoints.length).toMatchInline(`61`); - - expectSnapshot( - data.slice(0, 5).map(({ x, y }: { x: number; y: number | null }) => { - return { - x: new Date(x ?? NaN).toISOString(), - y, - }; - }) - ).toMatchInline(` - Array [ - Object { - "x": "2020-12-08T13:57:30.000Z", - "y": null, - }, - Object { - "x": "2020-12-08T13:58:00.000Z", - "y": null, - }, - Object { - "x": "2020-12-08T13:58:30.000Z", - "y": 1, - }, - Object { - "x": "2020-12-08T13:59:00.000Z", - "y": 1, - }, - Object { - "x": "2020-12-08T13:59:30.000Z", - "y": null, - }, - ] - `); - - expectSnapshot(title).toMatchInline(`"app"`); - expectSnapshot(color).toMatchInline(`"#54b399"`); - expectSnapshot(type).toMatchInline(`"areaStacked"`); - expectSnapshot(hideLegend).toMatchInline(`false`); - expectSnapshot(legendValue).toMatchInline(`"100%"`); - - expectSnapshot(data).toMatch(); - }); - it('returns the transaction breakdown sorted by name', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` - ); - - expect(response.status).to.be(200); - expectSnapshot(response.body.timeseries.map((serie: { title: string }) => serie.title)) - .toMatchInline(` - Array [ - "app", - "http", - "postgresql", - "redis", - ] - `); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/latency.ts b/x-pack/test/apm_api_integration/basic/tests/transactions/latency.ts deleted file mode 100644 index 3088f4fd481d7..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/latency.ts +++ /dev/null @@ -1,102 +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 archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - // url parameters - const start = encodeURIComponent(metadata.start); - const end = encodeURIComponent(metadata.end); - const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'testing' })); - - describe('Latency', () => { - describe('when data is not loaded ', () => { - it('returns 400 when latencyAggregationType is not informed', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request` - ); - - expect(response.status).to.be(400); - }); - - it('returns 400 when transactionType is not informed', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&latencyAggregationType=avg` - ); - - expect(response.status).to.be(400); - }); - - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&latencyAggregationType=avg&transactionType=request` - ); - - expect(response.status).to.be(200); - - expect(response.body.overallAvgDuration).to.be(null); - expect(response.body.latencyTimeseries.length).to.be(0); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - let response: PromiseReturnType; - - describe('average latency type', () => { - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=avg` - ); - }); - - it('returns average duration and timeseries', async () => { - expect(response.status).to.be(200); - expect(response.body.overallAvgDuration).not.to.be(null); - expect(response.body.latencyTimeseries.length).to.be.eql(61); - }); - }); - - describe('95th percentile latency type', () => { - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=p95` - ); - }); - - it('returns average duration and timeseries', async () => { - expect(response.status).to.be(200); - expect(response.body.overallAvgDuration).not.to.be(null); - expect(response.body.latencyTimeseries.length).to.be.eql(61); - }); - }); - - describe('99th percentile latency type', () => { - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=p99` - ); - }); - - it('returns average duration and timeseries', async () => { - expect(response.status).to.be(200); - expect(response.body.overallAvgDuration).not.to.be(null); - expect(response.body.latencyTimeseries.length).to.be.eql(61); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index f94bd6bd3be6f..08333de15ec6d 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -11,11 +11,12 @@ import path from 'path'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; import { PromiseReturnType } from '../../../plugins/observability/typings/common'; import { createApmUser, APM_TEST_PASSWORD, ApmUser } from './authentication'; +import { APMFtrConfigName } from '../configs'; +import { registry } from './registry'; -interface Settings { +interface Config { + name: APMFtrConfigName; license: 'basic' | 'trial'; - testFiles: string[]; - name: string; } const supertestAsApmUser = (kibanaServer: UrlObject, apmUser: ApmUser) => async ( @@ -34,8 +35,8 @@ const supertestAsApmUser = (kibanaServer: UrlObject, apmUser: ApmUser) => async return supertestAsPromised(url); }; -export function createTestConfig(settings: Settings) { - const { testFiles, license, name } = settings; +export function createTestConfig(config: Config) { + const { license, name } = config; return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackAPITestsConfig = await readConfigFile( @@ -47,8 +48,10 @@ export function createTestConfig(settings: Settings) { const supertestAsApmReadUser = supertestAsApmUser(servers.kibana, ApmUser.apmReadUser); + registry.init(config.name); + return { - testFiles, + testFiles: [require.resolve('../tests')], servers, esArchiver: { directory: path.resolve(__dirname, './fixtures/es_archiver'), @@ -69,7 +72,7 @@ export function createTestConfig(settings: Settings) { ), }, junit: { - reportName: name, + reportName: `APM API Integration tests (${name})`, }, esTestCluster: { ...xPackAPITestsConfig.get('esTestCluster'), diff --git a/x-pack/test/apm_api_integration/common/registry.ts b/x-pack/test/apm_api_integration/common/registry.ts new file mode 100644 index 0000000000000..8c918eae5a5a8 --- /dev/null +++ b/x-pack/test/apm_api_integration/common/registry.ts @@ -0,0 +1,160 @@ +/* + * 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 { castArray, groupBy } from 'lodash'; +import callsites from 'callsites'; +import { maybe } from '../../../plugins/apm/common/utils/maybe'; +import { joinByKey } from '../../../plugins/apm/common/utils/join_by_key'; +import { APMFtrConfigName } from '../configs'; +import { FtrProviderContext } from './ftr_provider_context'; + +type ArchiveName = + | 'apm_8.0.0' + | '8.0.0' + | 'metrics_8.0.0' + | 'ml_8.0.0' + | 'observability_overview' + | 'rum_8.0.0' + | 'rum_test_data'; + +interface RunCondition { + config: APMFtrConfigName; + archives: ArchiveName[]; +} + +const callbacks: Array< + RunCondition & { + runs: Array<{ + cb: () => void; + }>; + } +> = []; + +let configName: APMFtrConfigName | undefined; + +let running: boolean = false; + +export const registry = { + init: (config: APMFtrConfigName) => { + configName = config; + callbacks.length = 0; + running = false; + }, + when: ( + title: string, + conditions: RunCondition | RunCondition[], + callback: (condition: RunCondition) => void + ) => { + const allConditions = castArray(conditions); + + if (!allConditions.length) { + throw new Error('At least one condition should be defined'); + } + + if (running) { + throw new Error("Can't add tests when running"); + } + + const frame = maybe(callsites()[1]); + + const file = frame?.getFileName(); + + if (!file) { + throw new Error('Could not infer file for suite'); + } + + allConditions.forEach((matchedCondition) => { + callbacks.push({ + ...matchedCondition, + runs: [ + { + cb: () => { + const suite = describe(title, () => { + callback(matchedCondition); + }); + + suite.file = file; + suite.eachTest((test) => { + test.file = file; + }); + }, + }, + ], + }); + }); + }, + run: (context: FtrProviderContext) => { + if (!configName) { + throw new Error(`registry was not init() before running`); + } + running = true; + const esArchiver = context.getService('esArchiver'); + const logger = context.getService('log'); + const logWithTimer = () => { + const start = process.hrtime(); + + return (message: string) => { + const diff = process.hrtime(start); + const time = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + logger.info(`(${time}) ${message}`); + }; + }; + + const groups = joinByKey(callbacks, ['config', 'archives'], (a, b) => ({ + ...a, + ...b, + runs: a.runs.concat(b.runs), + })); + + callbacks.length = 0; + + const byConfig = groupBy(groups, 'config'); + + Object.keys(byConfig).forEach((config) => { + const groupsForConfig = byConfig[config]; + // register suites for other configs, but skip them so tests are marked as such + // and their snapshots are not marked as obsolete + (config === configName ? describe : describe.skip)(config, () => { + groupsForConfig.forEach((group) => { + const { runs, ...condition } = group; + + const runBefore = async () => { + const log = logWithTimer(); + for (const archiveName of condition.archives) { + log(`Loading ${archiveName}`); + await esArchiver.load(archiveName); + } + if (condition.archives.length) { + log('Loaded all archives'); + } + }; + + const runAfter = async () => { + const log = logWithTimer(); + for (const archiveName of condition.archives) { + log(`Unloading ${archiveName}`); + await esArchiver.unload(archiveName); + } + if (condition.archives.length) { + log('Unloaded all archives'); + } + }; + + describe(condition.archives.join(',') || 'no data', () => { + before(runBefore); + + runs.forEach((run) => { + run.cb(); + }); + + after(runAfter); + }); + }); + }); + }); + + running = false; + }, +}; diff --git a/x-pack/test/apm_api_integration/configs/index.ts b/x-pack/test/apm_api_integration/configs/index.ts new file mode 100644 index 0000000000000..4bda5419c8729 --- /dev/null +++ b/x-pack/test/apm_api_integration/configs/index.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 { mapValues } from 'lodash'; +import { createTestConfig } from '../common/config'; + +const apmFtrConfigs = { + basic: { + license: 'basic' as const, + }, + trial: { + license: 'trial' as const, + }, +}; + +export type APMFtrConfigName = keyof typeof apmFtrConfigs; + +export const configs = mapValues(apmFtrConfigs, (value, key) => { + return createTestConfig({ + name: key as APMFtrConfigName, + ...value, + }); +}); diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts new file mode 100644 index 0000000000000..2b14e3b8a4a75 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { format } from 'url'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const { end } = archives[archiveName]; + const start = new Date(Date.parse(end) - 600000).toISOString(); + + const apis = [ + { + pathname: '/api/apm/alerts/chart_preview/transaction_error_rate', + params: { transactionType: 'request' }, + }, + { pathname: '/api/apm/alerts/chart_preview/transaction_error_count', params: {} }, + { + pathname: '/api/apm/alerts/chart_preview/transaction_duration', + params: { transactionType: 'request' }, + }, + ]; + + apis.forEach((api) => { + const url = format({ + pathname: api.pathname, + query: { + start, + end, + serviceName: 'opbeans-java', + ...api.params, + }, + }); + + registry.when( + `GET ${api.pathname} without data loaded`, + { config: 'basic', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + } + ); + + registry.when( + `GET ${api.pathname} with data loaded`, + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect( + response.body.some((item: { x: number; y: number | null }) => item.x && item.y) + ).to.equal(true); + }); + } + ); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts new file mode 100644 index 0000000000000..2439943a664ea --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts @@ -0,0 +1,88 @@ +/* + * 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 { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/slow_transactions`, + query: { + start: range.start, + end: range.end, + durationPercentile: 95, + fieldNames: + 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name', + }, + }); + + registry.when('without data', { config: 'trial', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + }); + + registry.when('with data and default args', { config: 'trial', archives: ['apm_8.0.0'] }, () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/slow_transactions'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns significant terms', () => { + const sorted = response.body?.significantTerms?.sort(); + expectSnapshot(sorted?.map((term) => term.fieldName)).toMatchInline(` + Array [ + "user_agent.name", + "url.domain", + "host.ip", + "service.node.name", + "container.id", + "url.domain", + "user_agent.name", + ] + `); + }); + + it('returns a distribution per term', () => { + expectSnapshot(response.body?.significantTerms?.map((term) => term.distribution.length)) + .toMatchInline(` + Array [ + 11, + 11, + 11, + 11, + 11, + 11, + 11, + ] + `); + }); + + it('returns overall distribution', () => { + expectSnapshot(response.body?.overall?.distribution.length).toMatchInline(`11`); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap b/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.snap similarity index 95% rename from x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap rename to x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.snap index c8681866169a5..e21069ba2f0a6 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap +++ b/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM specs (trial) CSM UX page load dist when there is data returns page load distribution 1`] = ` +exports[`APM API tests trial 8.0.0,rum_8.0.0 UX page load dist with data returns page load distribution 1`] = ` Object { "maxDuration": 54.46, "minDuration": 0, @@ -456,7 +456,7 @@ Object { } `; -exports[`APM specs (trial) CSM UX page load dist when there is data returns page load distribution with breakdown 1`] = ` +exports[`APM API tests trial 8.0.0,rum_8.0.0 UX page load dist with data returns page load distribution with breakdown 1`] = ` Array [ Object { "data": Array [ @@ -819,6 +819,6 @@ Array [ ] `; -exports[`APM specs (trial) CSM UX page load dist when there is no data returns empty list 1`] = `Object {}`; +exports[`APM API tests trial no data UX page load dist without data returns empty list 1`] = `Object {}`; -exports[`APM specs (trial) CSM UX page load dist when there is no data returns empty list with breakdowns 1`] = `Object {}`; +exports[`APM API tests trial no data UX page load dist without data returns empty list with breakdowns 1`] = `Object {}`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap b/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_views.snap similarity index 90% rename from x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap rename to x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_views.snap index 76e5180ba2141..01d0d7fb6139c 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap +++ b/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_views.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM specs (trial) CSM CSM page views when there is data returns page views 1`] = ` +exports[`APM API tests trial 8.0.0,rum_8.0.0 CSM page views with data returns page views 1`] = ` Object { "items": Array [ Object { @@ -128,7 +128,7 @@ Object { } `; -exports[`APM specs (trial) CSM CSM page views when there is data returns page views with breakdown 1`] = ` +exports[`APM API tests trial 8.0.0,rum_8.0.0 CSM page views with data returns page views with breakdown 1`] = ` Object { "items": Array [ Object { @@ -265,14 +265,14 @@ Object { } `; -exports[`APM specs (trial) CSM CSM page views when there is no data returns empty list 1`] = ` +exports[`APM API tests trial no data CSM page views without data returns empty list 1`] = ` Object { "items": Array [], "topItems": Array [], } `; -exports[`APM specs (trial) CSM CSM page views when there is no data returns empty list with breakdowns 1`] = ` +exports[`APM API tests trial no data CSM page views without data returns empty list with breakdowns 1`] = ` Object { "items": Array [], "topItems": Array [], diff --git a/x-pack/test/apm_api_integration/tests/csm/csm_services.ts b/x-pack/test/apm_api_integration/tests/csm/csm_services.ts new file mode 100644 index 0000000000000..6460c0832a947 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/csm/csm_services.ts @@ -0,0 +1,40 @@ +/* + * 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 '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + registry.when('CSM Services without data', { config: 'trial', archives: [] }, () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/services?start=2020-06-28T10%3A24%3A46.055Z&end=2020-07-29T10%3A24%3A46.055Z&uiFilters=%7B%22agentName%22%3A%5B%22js-base%22%2C%22rum-js%22%5D%7D' + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + + registry.when( + 'CSM services with data', + { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, + () => { + it('returns rum services list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/services?start=2020-06-28T10%3A24%3A46.055Z&end=2020-07-29T10%3A24%3A46.055Z&uiFilters=%7B%22agentName%22%3A%5B%22js-base%22%2C%22rum-js%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(`Array []`); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/has_rum_data.ts b/x-pack/test/apm_api_integration/tests/csm/has_rum_data.ts similarity index 53% rename from x-pack/test/apm_api_integration/trial/tests/csm/has_rum_data.ts rename to x-pack/test/apm_api_integration/tests/csm/has_rum_data.ts index f2033e03f5821..66f99dd29df5a 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/has_rum_data.ts +++ b/x-pack/test/apm_api_integration/tests/csm/has_rum_data.ts @@ -5,38 +5,31 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function rumHasDataApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('CSM has rum data api', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/observability_overview/has_rum_data?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=' - ); + registry.when('has_rum_data without data', { config: 'trial', archives: [] }, () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/observability_overview/has_rum_data?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=' + ); - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatchInline(` Object { "hasData": false, } `); - }); }); + }); - describe('when there is data', () => { - before(async () => { - await esArchiver.load('8.0.0'); - await esArchiver.load('rum_8.0.0'); - }); - after(async () => { - await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_8.0.0'); - }); - + registry.when( + 'has RUM data with data', + { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, + () => { it('returns that it has data and service name with most traffice', async () => { const response = await supertest.get( '/api/apm/observability_overview/has_rum_data?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=' @@ -51,6 +44,6 @@ export default function rumHasDataApiTests({ getService }: FtrProviderContext) { } `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts b/x-pack/test/apm_api_integration/tests/csm/js_errors.ts similarity index 72% rename from x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts rename to x-pack/test/apm_api_integration/tests/csm/js_errors.ts index 6abb701f98a1c..dcadc8424ef7e 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts +++ b/x-pack/test/apm_api_integration/tests/csm/js_errors.ts @@ -5,40 +5,33 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('CSM js errors', () => { - describe('when there is no data', () => { - it('returns no js errors', async () => { - const response = await supertest.get( - '/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' - ); + registry.when('CSM JS errors with data', { config: 'trial', archives: [] }, () => { + it('returns no js errors', async () => { + const response = await supertest.get( + '/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatchInline(` Object { "totalErrorGroups": 0, "totalErrorPages": 0, "totalErrors": 0, } `); - }); }); + }); - describe('when there is data', () => { - before(async () => { - await esArchiver.load('8.0.0'); - await esArchiver.load('rum_test_data'); - }); - after(async () => { - await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_test_data'); - }); - + registry.when( + 'CSM JS errors without data', + { config: 'trial', archives: ['8.0.0', 'rum_test_data'] }, + () => { it('returns js errors', async () => { const response = await supertest.get( '/api/apm/rum-client/js-errors?start=2021-01-18T12%3A20%3A17.202Z&end=2021-01-18T12%3A25%3A17.203Z&uiFilters=%7B%22environment%22%3A%22ENVIRONMENT_ALL%22%2C%22serviceName%22%3A%5B%22elastic-co-frontend%22%5D%7D&pageSize=5&pageIndex=0' @@ -81,6 +74,6 @@ export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) } `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/long_task_metrics.ts b/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.ts similarity index 50% rename from x-pack/test/apm_api_integration/trial/tests/csm/long_task_metrics.ts rename to x-pack/test/apm_api_integration/tests/csm/long_task_metrics.ts index 6db5de24baa99..0da0889c11775 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/long_task_metrics.ts +++ b/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.ts @@ -5,38 +5,31 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('CSM long task metrics', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/rum-client/long-task-metrics?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' - ); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - longestLongTask: 0, - noOfLongTasks: 0, - sumOfLongTasks: 0, - }); + registry.when('CSM long task metrics without data', { config: 'trial', archives: [] }, () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/long-task-metrics?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + longestLongTask: 0, + noOfLongTasks: 0, + sumOfLongTasks: 0, }); }); + }); - describe('when there is data', () => { - before(async () => { - await esArchiver.load('8.0.0'); - await esArchiver.load('rum_8.0.0'); - }); - after(async () => { - await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_8.0.0'); - }); - + registry.when( + 'CSM long task metrics with data', + { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, + () => { it('returns web core vitals values', async () => { const response = await supertest.get( '/api/apm/rum-client/long-task-metrics?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' @@ -52,6 +45,6 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) } `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/tests/csm/page_load_dist.ts b/x-pack/test/apm_api_integration/tests/csm/page_load_dist.ts new file mode 100644 index 0000000000000..28fa9cb87f516 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/csm/page_load_dist.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + registry.when('UX page load dist without data', { config: 'trial', archives: [] }, () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-load-distribution?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + + it('returns empty list with breakdowns', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-load-distribution/breakdown?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&breakdown=Browser' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + }); + + registry.when( + 'UX page load dist with data', + { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, + () => { + it('returns page load distribution', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-load-distribution?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + it('returns page load distribution with breakdown', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-load-distribution/breakdown?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&breakdown=Browser' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/csm/page_views.ts b/x-pack/test/apm_api_integration/tests/csm/page_views.ts new file mode 100644 index 0000000000000..43f9d278f694a --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/csm/page_views.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + registry.when('CSM page views without data', { config: 'trial', archives: [] }, () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + + it('returns empty list with breakdowns', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + }); + + registry.when( + 'CSM page views with data', + { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, + () => { + it('returns page views', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + it('returns page views with breakdown', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/csm/url_search.ts b/x-pack/test/apm_api_integration/tests/csm/url_search.ts new file mode 100644 index 0000000000000..906f1783cb90e --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/csm/url_search.ts @@ -0,0 +1,82 @@ +/* + * 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 '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + registry.when('CSM url search api without data', { config: 'trial', archives: [] }, () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&percentile=50' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [], + "total": 0, + } + `); + }); + }); + + registry.when( + 'CSM url search api with data', + { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, + () => { + it('returns top urls when no query', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&percentile=50' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [ + Object { + "count": 5, + "pld": 4924000, + "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", + }, + Object { + "count": 1, + "pld": 2760000, + "url": "http://localhost:5601/nfw/app/home", + }, + ], + "total": 2, + } + `); + }); + + it('returns specific results against query', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&urlQuery=csm&percentile=50' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [ + Object { + "count": 5, + "pld": 4924000, + "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", + }, + ], + "total": 1, + } + `); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.ts similarity index 55% rename from x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts rename to x-pack/test/apm_api_integration/tests/csm/web_core_vitals.ts index 50c261d2d37ad..3574ef065eef7 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.ts @@ -5,41 +5,34 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('CSM web core vitals', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/rum-client/web-core-vitals?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&percentile=50' - ); + registry.when('CSM web core vitals without data', { config: 'trial', archives: [] }, () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/web-core-vitals?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&percentile=50' + ); - expect(response.status).to.be(200); - expect(response.body).to.eql({ - coreVitalPages: 0, - cls: null, - tbt: 0, - lcpRanks: [100, 0, 0], - fidRanks: [100, 0, 0], - clsRanks: [100, 0, 0], - }); + expect(response.status).to.be(200); + expect(response.body).to.eql({ + coreVitalPages: 0, + cls: null, + tbt: 0, + lcpRanks: [100, 0, 0], + fidRanks: [100, 0, 0], + clsRanks: [100, 0, 0], }); }); + }); - describe('when there is data', () => { - before(async () => { - await esArchiver.load('8.0.0'); - await esArchiver.load('rum_8.0.0'); - }); - after(async () => { - await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_8.0.0'); - }); - + registry.when( + 'CSM web core vitals with data', + { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, + () => { it('returns web core vitals values', async () => { const response = await supertest.get( '/api/apm/rum-client/web-core-vitals?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&percentile=50' @@ -73,6 +66,6 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) } `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts b/x-pack/test/apm_api_integration/tests/feature_controls.ts similarity index 98% rename from x-pack/test/apm_api_integration/basic/tests/feature_controls.ts rename to x-pack/test/apm_api_integration/tests/feature_controls.ts index 35025fcbfd107..7a65d8114c73f 100644 --- a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.ts @@ -5,7 +5,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { FtrProviderContext } from '../common/ftr_provider_context'; +import { registry } from '../common/registry'; export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertestAsApmWriteUser'); @@ -270,7 +271,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) } } - describe('apm feature controls', () => { + registry.when('apm feature controls', { config: 'basic', archives: [] }, () => { const config = { service: { name: 'test-service' }, settings: { transaction_sample_rate: '0.5' }, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts new file mode 100644 index 0000000000000..eef82c714b2d0 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../common/ftr_provider_context'; +import { registry } from '../common/registry'; + +export default function apmApiIntegrationTests(providerContext: FtrProviderContext) { + const { loadTestFile } = providerContext; + + describe('APM API tests', function () { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./alerts/chart_preview')); + + loadTestFile(require.resolve('./correlations/slow_transactions')); + + loadTestFile(require.resolve('./csm/csm_services')); + loadTestFile(require.resolve('./csm/has_rum_data')); + loadTestFile(require.resolve('./csm/js_errors')); + loadTestFile(require.resolve('./csm/long_task_metrics')); + loadTestFile(require.resolve('./csm/page_load_dist')); + loadTestFile(require.resolve('./csm/page_views')); + loadTestFile(require.resolve('./csm/url_search')); + loadTestFile(require.resolve('./csm/web_core_vitals')); + + loadTestFile(require.resolve('./metrics_charts/metrics_charts')); + + loadTestFile(require.resolve('./observability_overview/has_data')); + loadTestFile(require.resolve('./observability_overview/observability_overview')); + + loadTestFile(require.resolve('./service_maps/service_maps')); + + loadTestFile(require.resolve('./service_overview/dependencies')); + loadTestFile(require.resolve('./service_overview/error_groups')); + loadTestFile(require.resolve('./service_overview/instances')); + + loadTestFile(require.resolve('./services/agent_name')); + loadTestFile(require.resolve('./services/annotations')); + loadTestFile(require.resolve('./services/service_details')); + loadTestFile(require.resolve('./services/service_icons')); + loadTestFile(require.resolve('./services/throughput')); + loadTestFile(require.resolve('./services/top_services')); + loadTestFile(require.resolve('./services/transaction_types')); + + loadTestFile(require.resolve('./settings/anomaly_detection/basic')); + loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); + loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); + loadTestFile(require.resolve('./settings/anomaly_detection/write_user')); + + loadTestFile(require.resolve('./settings/agent_configuration')); + + loadTestFile(require.resolve('./settings/custom_link')); + + loadTestFile(require.resolve('./traces/top_traces')); + + loadTestFile(require.resolve('./transactions/breakdown')); + loadTestFile(require.resolve('./transactions/distribution')); + loadTestFile(require.resolve('./transactions/error_rate')); + loadTestFile(require.resolve('./transactions/latency')); + loadTestFile(require.resolve('./transactions/throughput')); + loadTestFile(require.resolve('./transactions/top_transaction_groups')); + loadTestFile(require.resolve('./transactions/transactions_groups_overview')); + + loadTestFile(require.resolve('./feature_controls')); + + registry.run(providerContext); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts new file mode 100644 index 0000000000000..dda46f00d7c72 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts @@ -0,0 +1,446 @@ +/* + * 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 { first } from 'lodash'; +import { MetricsChartsByAgentAPIResponse } from '../../../../plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent'; +import { GenericMetricsChart } from '../../../../plugins/apm/server/lib/metrics/transform_metrics_chart'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +interface ChartResponse { + body: MetricsChartsByAgentAPIResponse; + status: number; +} + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + registry.when( + 'Metrics charts when data is loaded', + { config: 'basic', archives: ['metrics_8.0.0'] }, + () => { + describe('for opbeans-node', () => { + const start = encodeURIComponent('2020-09-08T14:50:00.000Z'); + const end = encodeURIComponent('2020-09-08T14:55:00.000Z'); + const uiFilters = encodeURIComponent(JSON.stringify({})); + const agentName = 'nodejs'; + + describe('returns metrics data', () => { + let chartsResponse: ChartResponse; + before(async () => { + chartsResponse = await supertest.get( + `/api/apm/services/opbeans-node/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` + ); + }); + it('contains CPU usage and System memory usage chart data', async () => { + expect(chartsResponse.status).to.be(200); + expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` + Array [ + "CPU usage", + "System memory usage", + ] + `); + }); + + describe('CPU usage', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'cpu_usage_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.714, + 0.3877, + 0.75, + 0.2543, + ] + `); + }); + }); + + describe("System memory usage (using 'system.memory' fields to calculate the memory usage)", () => { + let systemMemoryUsageChart: GenericMetricsChart | undefined; + before(() => { + systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + }); + + it('has correct series', () => { + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)) + .toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.722093920925555, + 0.718173546796348, + ] + `); + }); + }); + }); + }); + + describe('for opbeans-java', () => { + const uiFilters = encodeURIComponent(JSON.stringify({})); + const agentName = 'java'; + + describe('returns metrics data', () => { + const start = encodeURIComponent('2020-09-08T14:55:30.000Z'); + const end = encodeURIComponent('2020-09-08T15:00:00.000Z'); + + let chartsResponse: ChartResponse; + before(async () => { + chartsResponse = await supertest.get( + `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` + ); + }); + + it('has correct chart data', async () => { + expect(chartsResponse.status).to.be(200); + expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` + Array [ + "CPU usage", + "System memory usage", + "Heap Memory", + "Non-Heap Memory", + "Thread Count", + "Garbage collection per minute", + "Garbage collection time spent per minute", + ] + `); + }); + + describe('CPU usage', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'cpu_usage_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.203, + 0.178777777777778, + 0.01, + 0.009, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.193, + 0.193, + 0.009, + 0.009, + ] + `); + }); + }); + + describe("System memory usage (using 'system.process.cgroup' fields to calculate the memory usage)", () => { + let systemMemoryUsageChart: GenericMetricsChart | undefined; + before(() => { + systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + }); + + it('has correct series', () => { + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)) + .toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.707924703557837, + 0.705395980841182, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.707924703557837, + 0.707924703557837, + ] + `); + }); + }); + + describe('Heap Memory', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'heap_memory_area_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. used", + "Avg. committed", + "Avg. limit", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 222501617.777778, + 374341632, + 1560281088, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 211472896, + 374341632, + 1560281088, + ] + `); + }); + }); + + describe('Non-Heap Memory', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'non_heap_memory_area_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. used", + "Avg. committed", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 138573397.333333, + 147677639.111111, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 138162752, + 147386368, + ] + `); + }); + }); + + describe('Thread Count', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'thread_count_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. count", + "Max count", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 44.4444444444444, + 45, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 44, + 44, + ] + `); + }); + }); + + describe('Garbage collection per minute', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'gc_rate_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0, + 15, + ] + `); + }); + }); + + describe('Garbage collection time spent per minute', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'gc_time_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0, + 187.5, + ] + `); + }); + }); + }); + + // 9223372036854771712 = memory limit for a c-group when no memory limit is specified + it('calculates system memory usage using system total field when cgroup limit is equal to 9223372036854771712', async () => { + const start = encodeURIComponent('2020-09-08T15:00:30.000Z'); + const end = encodeURIComponent('2020-09-08T15:05:00.000Z'); + + const chartsResponse: ChartResponse = await supertest.get( + `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` + ); + + const systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.114523896426499, + 0.114002376090415, + ] + `); + + const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.11383724014064, + 0.11383724014064, + ] + `); + }); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts b/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts similarity index 67% rename from x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts rename to x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts index 6d0d2d3042625..9c88f75c6adbd 100644 --- a/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts @@ -4,40 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const archiveName = 'apm_8.0.0'; - - describe('Has data', () => { - describe('when data is not loaded', () => { + registry.when( + 'Observability overview when data is not loaded', + { config: 'basic', archives: [] }, + () => { it('returns false when there is no data', async () => { const response = await supertest.get('/api/apm/observability_overview/has_data'); expect(response.status).to.be(200); expect(response.body).to.eql(false); }); - }); - describe('when only onboarding data is loaded', () => { - before(() => esArchiver.load('observability_overview')); - after(() => esArchiver.unload('observability_overview')); + } + ); + + registry.when( + 'Observability overview when only onboarding data is loaded', + { config: 'basic', archives: ['observability_overview'] }, + () => { it('returns false when there is only onboarding data', async () => { const response = await supertest.get('/api/apm/observability_overview/has_data'); expect(response.status).to.be(200); expect(response.body).to.eql(false); }); - }); - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + } + ); + registry.when( + 'Observability overview when APM data is loaded', + { config: 'basic', archives: ['apm_8.0.0'] }, + () => { it('returns true when there is at least one document on transaction, error or metrics indices', async () => { const response = await supertest.get('/api/apm/observability_overview/has_data'); expect(response.status).to.be(200); expect(response.body).to.eql(true); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts similarity index 69% rename from x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts rename to x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts index f7c459029c7f5..7c9d8fa8b0f91 100644 --- a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; @@ -19,22 +19,28 @@ export default function ApiTest({ getService }: FtrProviderContext) { const end = encodeURIComponent(metadata.end); const bucketSize = '60s'; - describe('Observability overview', () => { - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` - ); - expect(response.status).to.be(200); + registry.when( + 'Observability overview when data is not loaded', + { config: 'basic', archives: [] }, + () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` + ); + expect(response.status).to.be(200); - expect(response.body.serviceCount).to.be(0); - expect(response.body.transactionCoordinates.length).to.be(0); + expect(response.body.serviceCount).to.be(0); + expect(response.body.transactionCoordinates.length).to.be(0); + }); }); - }); - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + } + ); + registry.when( + 'Observability overview when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { it('returns the service count and transaction coordinates', async () => { const response = await supertest.get( `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` @@ -80,6 +86,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { ] `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/tests/service_maps/__snapshots__/service_maps.snap similarity index 99% rename from x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap rename to x-pack/test/apm_api_integration/tests/service_maps/__snapshots__/service_maps.snap index 7639822eaa6f9..69bc039b67ed2 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ b/x-pack/test/apm_api_integration/tests/service_maps/__snapshots__/service_maps.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM specs (trial) Service Maps Service Maps with a trial license /api/apm/service-map when there is data returns service map elements filtering by environment not defined 1`] = ` +exports[`APM API tests trial apm_8.0.0 Service Map with data /api/apm/service-map returns service map elements filtering by environment not defined 1`] = ` Object { "elements": Array [ Object { @@ -514,7 +514,7 @@ Object { } `; -exports[`APM specs (trial) Service Maps Service Maps with a trial license /api/apm/service-map when there is data returns the correct data 3`] = ` +exports[`APM API tests trial apm_8.0.0 Service Map with data /api/apm/service-map returns the correct data 3`] = ` Array [ Object { "data": Object { @@ -1741,7 +1741,7 @@ Array [ ] `; -exports[`APM specs (trial) Service Maps Service Maps with a trial license when there is data with anomalies with the default apm user returns the correct anomaly stats 3`] = ` +exports[`APM API tests trial apm_8.0.0 Service Map with data /api/apm/service-map with ML data with the default apm user returns the correct anomaly stats 3`] = ` Object { "elements": Array [ Object { diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts similarity index 58% rename from x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts rename to x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts index 02acd34ad5666..a15f0442c7cde 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts @@ -7,57 +7,85 @@ import querystring from 'querystring'; import expect from '@kbn/expect'; import { isEmpty, uniq } from 'lodash'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { PromiseReturnType } from '../../../../plugins/observability/typings/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function serviceMapsApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess'); - const esArchiver = getService('esArchiver'); - const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; const start = encodeURIComponent(metadata.start); const end = encodeURIComponent(metadata.end); - describe('Service Maps with a trial license', () => { + registry.when('Service map with a basic license', { config: 'basic', archives: [] }, () => { + it('is only be available to users with Platinum license (or higher)', async () => { + const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + + expect(response.status).to.be(403); + + expectSnapshot(response.body.message).toMatchInline( + `"In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data."` + ); + }); + }); + + registry.when('Service map without data', { config: 'trial', archives: [] }, () => { describe('/api/apm/service-map', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + it('returns an empty list', async () => { + const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); - expect(response.status).to.be(200); - expect(response.body.elements.length).to.be(0); - }); + expect(response.status).to.be(200); + expect(response.body.elements.length).to.be(0); }); + }); - describe('when there is data', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + describe('/api/apm/service-map/service/{serviceName}', () => { + it('returns an object with nulls', async () => { + const q = querystring.stringify({ + start: metadata.start, + end: metadata.end, + uiFilters: encodeURIComponent('{}'), + }); + const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); - let response: PromiseReturnType; + expect(response.status).to.be(200); - before(async () => { - response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); - }); + expect(response.body.avgCpuUsage).to.be(null); + expect(response.body.avgErrorRate).to.be(null); + expect(response.body.avgMemoryUsage).to.be(null); + expect(response.body.transactionStats.avgRequestsPerMinute).to.be(null); + expect(response.body.transactionStats.avgTransactionDuration).to.be(null); + }); + }); + }); - it('returns service map elements', () => { - expect(response.status).to.be(200); - expect(response.body.elements.length).to.be.greaterThan(0); - }); + registry.when('Service Map with data', { config: 'trial', archives: ['apm_8.0.0'] }, () => { + describe('/api/apm/service-map', () => { + let response: PromiseReturnType; + + before(async () => { + response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + }); + + it('returns service map elements', () => { + expect(response.status).to.be(200); + expect(response.body.elements.length).to.be.greaterThan(0); + }); - it('returns the correct data', () => { - const elements: Array<{ data: Record }> = response.body.elements; + it('returns the correct data', () => { + const elements: Array<{ data: Record }> = response.body.elements; - const serviceNames = uniq( - elements - .filter((element) => element.data['service.name'] !== undefined) - .map((element) => element.data['service.name']) - ).sort(); + const serviceNames = uniq( + elements + .filter((element) => element.data['service.name'] !== undefined) + .map((element) => element.data['service.name']) + ).sort(); - expectSnapshot(serviceNames).toMatchInline(` + expectSnapshot(serviceNames).toMatchInline(` Array [ "kibana", "kibana-frontend", @@ -71,13 +99,13 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) ] `); - const externalDestinations = uniq( - elements - .filter((element) => element.data.target?.startsWith('>')) - .map((element) => element.data.target) - ).sort(); + const externalDestinations = uniq( + elements + .filter((element) => element.data.target?.startsWith('>')) + .map((element) => element.data.target) + ).sort(); - expectSnapshot(externalDestinations).toMatchInline(` + expectSnapshot(externalDestinations).toMatchInline(` Array [ ">elasticsearch", ">feeds.elastic.co:443", @@ -86,51 +114,26 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) ] `); - expectSnapshot(elements).toMatch(); - }); - - it('returns service map elements filtering by environment not defined', async () => { - const ENVIRONMENT_NOT_DEFINED = 'ENVIRONMENT_NOT_DEFINED'; - const { body, status } = await supertest.get( - `/api/apm/service-map?start=${start}&end=${end}&environment=${ENVIRONMENT_NOT_DEFINED}` - ); - expect(status).to.be(200); - const environments = new Set(); - body.elements.forEach((element: { data: Record }) => { - environments.add(element.data['service.environment']); - }); - - expect(environments.has(ENVIRONMENT_NOT_DEFINED)).to.eql(true); - expectSnapshot(body).toMatch(); - }); + expectSnapshot(elements).toMatch(); }); - }); - - describe('/api/apm/service-map/service/{serviceName}', () => { - describe('when there is no data', () => { - it('returns an object with nulls', async () => { - const q = querystring.stringify({ - start: metadata.start, - end: metadata.end, - uiFilters: encodeURIComponent('{}'), - }); - const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); - expect(response.status).to.be(200); - - expect(response.body.avgCpuUsage).to.be(null); - expect(response.body.avgErrorRate).to.be(null); - expect(response.body.avgMemoryUsage).to.be(null); - expect(response.body.transactionStats.avgRequestsPerMinute).to.be(null); - expect(response.body.transactionStats.avgTransactionDuration).to.be(null); + it('returns service map elements filtering by environment not defined', async () => { + const ENVIRONMENT_NOT_DEFINED = 'ENVIRONMENT_NOT_DEFINED'; + const { body, status } = await supertest.get( + `/api/apm/service-map?start=${start}&end=${end}&environment=${ENVIRONMENT_NOT_DEFINED}` + ); + expect(status).to.be(200); + const environments = new Set(); + body.elements.forEach((element: { data: Record }) => { + environments.add(element.data['service.environment']); }); + + expect(environments.has(ENVIRONMENT_NOT_DEFINED)).to.eql(true); + expectSnapshot(body).toMatch(); }); }); - describe('when there is data with anomalies', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - + describe('/api/apm/service-map with ML data', () => { describe('with the default apm user', () => { let response: PromiseReturnType; diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/es_utils.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/es_utils.ts similarity index 100% rename from x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/es_utils.ts rename to x-pack/test/apm_api_integration/tests/service_overview/dependencies/es_utils.ts diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/index.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts similarity index 91% rename from x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/index.ts rename to x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts index aeb5d1256796a..b3e7e0672fc7f 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/index.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts @@ -8,26 +8,28 @@ import expect from '@kbn/expect'; import url from 'url'; import { sortBy, pick, last } from 'lodash'; import { ValuesType } from 'utility-types'; -import { Maybe } from '../../../../../../plugins/apm/typings/common'; -import { isFiniteNumber } from '../../../../../../plugins/apm/common/utils/is_finite_number'; -import { APIReturnType } from '../../../../../../plugins/apm/public/services/rest/createCallApmApi'; -import { ENVIRONMENT_ALL } from '../../../../../../plugins/apm/common/environment_filter_values'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import archives from '../../../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../../common/registry'; +import { Maybe } from '../../../../../plugins/apm/typings/common'; +import { isFiniteNumber } from '../../../../../plugins/apm/common/utils/is_finite_number'; +import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { ENVIRONMENT_ALL } from '../../../../../plugins/apm/common/environment_filter_values'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives from '../../../common/fixtures/es_archiver/archives_metadata'; import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils'; const round = (num: Maybe): string => (isFiniteNumber(num) ? num.toPrecision(4) : ''); export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const es = getService('es'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; - describe('Service overview dependencies', () => { - describe('when data is not loaded', () => { + registry.when( + 'Service overview dependencies when data is not loaded', + { config: 'basic', archives: [] }, + () => { it('handles the empty state', async () => { const response = await supertest.get( url.format({ @@ -44,9 +46,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect(response.body).to.eql([]); }); - }); + } + ); - describe('when specific data is loaded', () => { + registry.when( + 'Service overview dependencies when specific data is loaded', + { config: 'basic', archives: [] }, + () => { let response: { status: number; body: APIReturnType<'GET /api/apm/services/{serviceName}/dependencies'>; @@ -285,17 +291,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { impact: 0, }); }); - }); + } + ); - describe('when data is loaded', () => { + registry.when( + 'Service overview dependencies when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { let response: { status: number; body: APIReturnType<'GET /api/apm/services/{serviceName}/dependencies'>; }; before(async () => { - await esArchiver.load(archiveName); - response = await supertest.get( url.format({ pathname: `/api/apm/services/opbeans-java/dependencies`, @@ -309,8 +317,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); }); - after(() => esArchiver.unload(archiveName)); - it('returns a successful response', () => { expect(response.status).to.be(200); }); @@ -380,6 +386,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { ] `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts similarity index 94% rename from x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts rename to x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts index 7d1c05960f3e6..fc649c60103c8 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts @@ -7,18 +7,20 @@ import expect from '@kbn/expect'; import qs from 'querystring'; import { pick, uniqBy } from 'lodash'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import archives from '../../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; - describe('Service overview error groups', () => { - describe('when data is not loaded', () => { + registry.when( + 'Service overview error groups when data is not loaded', + { config: 'basic', archives: [] }, + () => { it('handles the empty state', async () => { const response = await supertest.get( `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ @@ -41,12 +43,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { is_aggregation_accurate: true, }); }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + } + ); + registry.when( + 'Service overview error groups when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { it('returns the correct data', async () => { const response = await supertest.get( `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ @@ -220,6 +223,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(uniqBy(items, 'group_id').length).to.eql(totalItems); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances.ts similarity index 83% rename from x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts rename to x-pack/test/apm_api_integration/tests/service_overview/instances.ts index 2227a8c09a6cf..08d60f90900b8 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances.ts @@ -7,14 +7,14 @@ import expect from '@kbn/expect'; import url from 'url'; import { pick, sortBy } from 'lodash'; -import { isFiniteNumber } from '../../../../../plugins/apm/common/utils/is_finite_number'; -import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import archives from '../../../common/fixtures/es_archiver/archives_metadata'; +import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; @@ -24,31 +24,36 @@ export default function ApiTest({ getService }: FtrProviderContext) { body: APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances'>; } - describe('Service overview instances', () => { - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response: Response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/service_overview_instances`, - query: { - start, - end, - numBuckets: 20, - transactionType: 'request', - uiFilters: '{}', - }, - }) - ); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); + registry.when( + 'Service overview instances when data is not loaded', + { config: 'basic', archives: [] }, + () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response: Response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/service_overview_instances`, + query: { + start, + end, + numBuckets: 20, + transactionType: 'request', + uiFilters: '{}', + }, + }) + ); - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + } + ); + registry.when( + 'Service overview instances when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { describe('fetching java data', () => { let response: Response; @@ -209,6 +214,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(values); }); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap similarity index 96% rename from x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap rename to x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap index fe7f434aad2e1..f23601fccb174 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM specs (basic) Services Throughput when data is loaded returns the service throughput has the correct throughput 1`] = ` +exports[`APM API tests basic apm_8.0.0 Throughput when data is loaded has the correct throughput 1`] = ` Array [ Object { "x": 1607435850000, diff --git a/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts b/x-pack/test/apm_api_integration/tests/services/agent_name.ts similarity index 55% rename from x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts rename to x-pack/test/apm_api_integration/tests/services/agent_name.ts index cea8fb5da2428..538d7a448ea30 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts +++ b/x-pack/test/apm_api_integration/tests/services/agent_name.ts @@ -5,34 +5,33 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import archives from '../../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const range = archives[archiveName]; const start = encodeURIComponent(range.start); const end = encodeURIComponent(range.end); - describe('Agent name', () => { - describe('when data is not loaded ', () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/agent_name?start=${start}&end=${end}` - ); + registry.when('Agent name when data is not loaded', { config: 'basic', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/agent_name?start=${start}&end=${end}` + ); - expect(response.status).to.be(200); - expect(response.body).to.eql({}); - }); + expect(response.status).to.be(200); + expect(response.body).to.eql({}); }); + }); - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - + registry.when( + 'Agent name when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { it('returns the agent name', async () => { const response = await supertest.get( `/api/apm/services/opbeans-node/agent_name?start=${start}&end=${end}` @@ -42,6 +41,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.body).to.eql({ agentName: 'nodejs' }); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/trial/tests/services/annotations.ts b/x-pack/test/apm_api_integration/tests/services/annotations.ts similarity index 92% rename from x-pack/test/apm_api_integration/trial/tests/services/annotations.ts rename to x-pack/test/apm_api_integration/tests/services/annotations.ts index c2ddc10c5f1d2..4ff690fa01aa1 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/annotations.ts +++ b/x-pack/test/apm_api_integration/tests/services/annotations.ts @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { merge, cloneDeep, isPlainObject } from 'lodash'; import { JsonObject } from 'src/plugins/kibana_utils/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; const DEFAULT_INDEX_NAME = 'observability-annotations'; @@ -40,7 +41,32 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { } } - describe('APM annotations with a trial license', () => { + registry.when('Annotations with a basic license', { config: 'basic', archives: [] }, () => { + describe('when creating an annotation', () => { + it('fails with a 403 forbidden', async () => { + const response = await request({ + url: '/api/apm/services/opbeans-java/annotation', + method: 'POST', + data: { + '@timestamp': new Date().toISOString(), + message: 'New deployment', + tags: ['foo'], + service: { + version: '1.1', + environment: 'production', + }, + }, + }); + + expect(response.status).to.be(403); + expect(response.body.message).to.be( + 'Annotations require at least a gold license or a trial license.' + ); + }); + }); + }); + + registry.when('Annotations with a trial license', { config: 'trial', archives: [] }, () => { describe('when creating an annotation', () => { afterEach(async () => { const indexExists = (await es.indices.exists({ index: DEFAULT_INDEX_NAME })).body; diff --git a/x-pack/test/apm_api_integration/basic/tests/services/service_details.ts b/x-pack/test/apm_api_integration/tests/services/service_details.ts similarity index 87% rename from x-pack/test/apm_api_integration/basic/tests/services/service_details.ts rename to x-pack/test/apm_api_integration/tests/services/service_details.ts index 54bd16e6f78c4..77155c907f3b1 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/service_details.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_details.ts @@ -6,18 +6,20 @@ import expect from '@kbn/expect'; import url from 'url'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import archives from '../../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; - describe('Service details', () => { - describe('when data is not loaded ', () => { + registry.when( + 'Service details when data is not loaded', + { config: 'basic', archives: [] }, + () => { it('handles the empty state', async () => { const response = await supertest.get( url.format({ @@ -29,12 +31,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect(response.body).to.eql({}); }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + } + ); + registry.when( + 'Service details when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { it('returns java service details', async () => { const response = await supertest.get( url.format({ @@ -116,6 +119,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { } `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/services/service_icons.ts b/x-pack/test/apm_api_integration/tests/services/service_icons.ts similarity index 53% rename from x-pack/test/apm_api_integration/basic/tests/services/service_icons.ts rename to x-pack/test/apm_api_integration/tests/services/service_icons.ts index 4b79de14551d6..7926a2744e45c 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/service_icons.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_icons.ts @@ -6,35 +6,34 @@ import expect from '@kbn/expect'; import url from 'url'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import archives from '../../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; - describe('Service icons', () => { - describe('when data is not loaded ', () => { - it('handles the empty state', async () => { - const response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/metadata/icons`, - query: { start, end }, - }) - ); + registry.when('Service icons when data is not loaded', { config: 'basic', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/metadata/icons`, + query: { start, end }, + }) + ); - expect(response.status).to.be(200); - expect(response.body).to.eql({}); - }); + expect(response.status).to.be(200); + expect(response.body).to.eql({}); }); + }); - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - + registry.when( + 'Service icons when data is not loaded', + { config: 'basic', archives: [archiveName] }, + () => { it('returns java service icons', async () => { const response = await supertest.get( url.format({ @@ -46,11 +45,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expectSnapshot(response.body).toMatchInline(` - Object { - "agentName": "java", - "containerType": "Kubernetes", - } - `); + Object { + "agentName": "java", + "containerType": "Kubernetes", + } + `); }); it('returns python service icons', async () => { @@ -64,13 +63,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expectSnapshot(response.body).toMatchInline(` - Object { - "agentName": "python", - "cloudProvider": "gcp", - "containerType": "Kubernetes", - } - `); + Object { + "agentName": "python", + "cloudProvider": "gcp", + "containerType": "Kubernetes", + } + `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.ts b/x-pack/test/apm_api_integration/tests/services/throughput.ts new file mode 100644 index 0000000000000..fa94cbf600709 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/throughput.ts @@ -0,0 +1,82 @@ +/* + * 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 qs from 'querystring'; +import { first, last } from 'lodash'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + registry.when('Throughput when data is not loaded', { config: 'basic', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/throughput?${qs.stringify({ + start: metadata.start, + end: metadata.end, + uiFilters: encodeURIComponent('{}'), + transactionType: 'request', + })}` + ); + expect(response.status).to.be(200); + expect(response.body.throughput.length).to.be(0); + }); + }); + + registry.when( + 'Throughput when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + let throughputResponse: { + throughput: Array<{ x: number; y: number | null }>; + }; + before(async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/throughput?${qs.stringify({ + start: metadata.start, + end: metadata.end, + uiFilters: encodeURIComponent('{}'), + transactionType: 'request', + })}` + ); + throughputResponse = response.body; + }); + + it('returns some data', () => { + expect(throughputResponse.throughput.length).to.be.greaterThan(0); + + const nonNullDataPoints = throughputResponse.throughput.filter(({ y }) => y !== null); + + expect(nonNullDataPoints.length).to.be.greaterThan(0); + }); + + it('has the correct start date', () => { + expectSnapshot( + new Date(first(throughputResponse.throughput)?.x ?? NaN).toISOString() + ).toMatchInline(`"2020-12-08T13:57:30.000Z"`); + }); + + it('has the correct end date', () => { + expectSnapshot( + new Date(last(throughputResponse.throughput)?.x ?? NaN).toISOString() + ).toMatchInline(`"2020-12-08T14:27:30.000Z"`); + }); + + it('has the correct number of buckets', () => { + expectSnapshot(throughputResponse.throughput.length).toMatchInline(`61`); + }); + + it('has the correct throughput', () => { + expectSnapshot(throughputResponse.throughput).toMatch(); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.ts b/x-pack/test/apm_api_integration/tests/services/top_services.ts new file mode 100644 index 0000000000000..42797e3e7c87a --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/top_services.ts @@ -0,0 +1,364 @@ +/* + * 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 { sortBy, pick, isEmpty } from 'lodash'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { PromiseReturnType } from '../../../../plugins/observability/typings/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess'); + + const archiveName = 'apm_8.0.0'; + + const range = archives_metadata[archiveName]; + + // url parameters + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); + + const uiFilters = encodeURIComponent(JSON.stringify({})); + + registry.when( + 'APM Services Overview with a basic license when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + + expect(response.status).to.be(200); + expect(response.body.hasHistoricalData).to.be(false); + expect(response.body.hasLegacyData).to.be(false); + expect(response.body.items.length).to.be(0); + }); + } + ); + + registry.when( + 'APM Services Overview with a basic license when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + let response: { + status: number; + body: APIReturnType<'GET /api/apm/services'>; + }; + + let sortedItems: typeof response.body.items; + + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + sortedItems = sortBy(response.body.items, 'serviceName'); + }); + + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); + + it('returns hasHistoricalData: true', () => { + expect(response.body.hasHistoricalData).to.be(true); + }); + + it('returns hasLegacyData: false', () => { + expect(response.body.hasLegacyData).to.be(false); + }); + + it('returns the correct service names', () => { + expectSnapshot(sortedItems.map((item) => item.serviceName)).toMatchInline(` + Array [ + "kibana", + "kibana-frontend", + "opbeans-dotnet", + "opbeans-go", + "opbeans-java", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", + "opbeans-rum", + ] + `); + }); + + it('returns the correct metrics averages', () => { + expectSnapshot( + sortedItems.map((item) => + pick( + item, + 'transactionErrorRate.value', + 'avgResponseTime.value', + 'transactionsPerMinute.value' + ) + ) + ).toMatchInline(` + Array [ + Object { + "avgResponseTime": Object { + "value": 420419.34550767, + }, + "transactionErrorRate": Object { + "value": 0, + }, + "transactionsPerMinute": Object { + "value": 45.6333333333333, + }, + }, + Object { + "avgResponseTime": Object { + "value": 2382833.33333333, + }, + "transactionErrorRate": Object { + "value": null, + }, + "transactionsPerMinute": Object { + "value": 0.2, + }, + }, + Object { + "avgResponseTime": Object { + "value": 631521.83908046, + }, + "transactionErrorRate": Object { + "value": 0.0229885057471264, + }, + "transactionsPerMinute": Object { + "value": 2.9, + }, + }, + Object { + "avgResponseTime": Object { + "value": 27946.1484375, + }, + "transactionErrorRate": Object { + "value": 0.015625, + }, + "transactionsPerMinute": Object { + "value": 4.26666666666667, + }, + }, + Object { + "avgResponseTime": Object { + "value": 237339.813333333, + }, + "transactionErrorRate": Object { + "value": 0.16, + }, + "transactionsPerMinute": Object { + "value": 2.5, + }, + }, + Object { + "avgResponseTime": Object { + "value": 24920.1052631579, + }, + "transactionErrorRate": Object { + "value": 0.0210526315789474, + }, + "transactionsPerMinute": Object { + "value": 3.16666666666667, + }, + }, + Object { + "avgResponseTime": Object { + "value": 29542.6607142857, + }, + "transactionErrorRate": Object { + "value": 0.0357142857142857, + }, + "transactionsPerMinute": Object { + "value": 1.86666666666667, + }, + }, + Object { + "avgResponseTime": Object { + "value": 70518.9328358209, + }, + "transactionErrorRate": Object { + "value": 0.0373134328358209, + }, + "transactionsPerMinute": Object { + "value": 4.46666666666667, + }, + }, + Object { + "avgResponseTime": Object { + "value": 2319812.5, + }, + "transactionErrorRate": Object { + "value": null, + }, + "transactionsPerMinute": Object { + "value": 0.533333333333333, + }, + }, + ] + `); + }); + + it('returns environments', () => { + expectSnapshot(sortedItems.map((item) => item.environments ?? [])).toMatchInline(` + Array [ + Array [ + "production", + ], + Array [ + "production", + ], + Array [ + "production", + ], + Array [ + "testing", + ], + Array [ + "production", + ], + Array [ + "testing", + ], + Array [], + Array [], + Array [ + "testing", + ], + ] + `); + }); + + it(`RUM services don't report any transaction error rates`, () => { + // RUM transactions don't have event.outcome set, + // so they should not have an error rate + + const rumServices = sortedItems.filter((item) => item.agentName === 'rum-js'); + + expect(rumServices.length).to.be.greaterThan(0); + + expect(rumServices.every((item) => isEmpty(item.transactionErrorRate?.value))); + }); + + it('non-RUM services all report transaction error rates', () => { + const nonRumServices = sortedItems.filter((item) => item.agentName !== 'rum-js'); + + expect( + nonRumServices.every((item) => { + return ( + typeof item.transactionErrorRate?.value === 'number' && + item.transactionErrorRate.timeseries.length > 0 + ); + }) + ).to.be(true); + }); + } + ); + + registry.when( + 'APM Services overview with a trial license when data is loaded', + { config: 'trial', archives: [archiveName] }, + () => { + describe('with the default APM read user', () => { + describe('and fetching a list of services', () => { + let response: { + status: number; + body: APIReturnType<'GET /api/apm/services'>; + }; + + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); + + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); + + it('there is at least one service', () => { + expect(response.body.items.length).to.be.greaterThan(0); + }); + + it('some items have a health status set', () => { + // Under the assumption that the loaded archive has + // at least one APM ML job, and the time range is longer + // than 15m, at least one items should have a health status + // set. Note that we currently have a bug where healthy + // services report as unknown (so without any health status): + // https://github.com/elastic/kibana/issues/77083 + + const healthStatuses = sortBy(response.body.items, 'serviceName').map( + (item: any) => item.healthStatus + ); + + expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0); + + expectSnapshot(healthStatuses).toMatchInline(` + Array [ + "healthy", + "healthy", + "healthy", + "healthy", + "healthy", + "healthy", + "healthy", + "healthy", + "healthy", + ] + `); + }); + }); + }); + + describe('with a user that does not have access to ML', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertestAsApmReadUserWithoutMlAccess.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); + + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); + + it('there is at least one service', () => { + expect(response.body.items.length).to.be.greaterThan(0); + }); + + it('contains no health statuses', () => { + const definedHealthStatuses = response.body.items + .map((item: any) => item.healthStatus) + .filter(Boolean); + + expect(definedHealthStatuses.length).to.be(0); + }); + }); + + describe('and fetching a list of services with a filter', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${encodeURIComponent( + `{"kuery":"service.name:opbeans-java","environment":"ENVIRONMENT_ALL"}` + )}` + ); + }); + + it('does not return health statuses for services that are not found in APM data', () => { + expect(response.status).to.be(200); + + expect(response.body.items.length).to.be(1); + + expect(response.body.items[0].serviceName).to.be('opbeans-java'); + }); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts b/x-pack/test/apm_api_integration/tests/services/transaction_types.ts similarity index 75% rename from x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts rename to x-pack/test/apm_api_integration/tests/services/transaction_types.ts index fcfe1660d58e2..c45f4083ef8da 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts +++ b/x-pack/test/apm_api_integration/tests/services/transaction_types.ts @@ -5,12 +5,12 @@ */ import expect from '@kbn/expect'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; @@ -19,8 +19,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const start = encodeURIComponent(metadata.start); const end = encodeURIComponent(metadata.end); - describe('Transaction types', () => { - describe('when data is not loaded ', () => { + registry.when( + 'Transaction types when data is not loaded', + { config: 'basic', archives: [] }, + () => { it('handles empty state', async () => { const response = await supertest.get( `/api/apm/services/opbeans-node/transaction_types?start=${start}&end=${end}` @@ -30,12 +32,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.body.transactionTypes.length).to.be(0); }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + } + ); + registry.when( + 'Transaction types when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { it('handles empty state', async () => { const response = await supertest.get( `/api/apm/services/opbeans-node/transaction_types?start=${start}&end=${end}` @@ -53,6 +56,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { } `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts new file mode 100644 index 0000000000000..0b58dd5908c60 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts @@ -0,0 +1,495 @@ +/* + * 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, orderBy } from 'lodash'; +import { AgentConfigurationIntake } from '../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigSearchParams } from '../../../../plugins/apm/server/routes/settings/agent_configuration'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function agentConfigurationTests({ getService }: FtrProviderContext) { + const supertestRead = getService('supertestAsApmReadUser'); + const supertestWrite = getService('supertestAsApmWriteUser'); + const log = getService('log'); + + const archiveName = 'apm_8.0.0'; + + function getServices() { + return supertestRead + .get(`/api/apm/settings/agent-configuration/services`) + .set('kbn-xsrf', 'foo'); + } + + function getEnvironments(serviceName: string) { + return supertestRead + .get(`/api/apm/settings/agent-configuration/environments?serviceName=${serviceName}`) + .set('kbn-xsrf', 'foo'); + } + + function getAgentName(serviceName: string) { + return supertestRead + .get(`/api/apm/settings/agent-configuration/agent_name?serviceName=${serviceName}`) + .set('kbn-xsrf', 'foo'); + } + + function searchConfigurations(configuration: AgentConfigSearchParams) { + return supertestRead + .post(`/api/apm/settings/agent-configuration/search`) + .send(configuration) + .set('kbn-xsrf', 'foo'); + } + + function getAllConfigurations() { + return supertestRead.get(`/api/apm/settings/agent-configuration`).set('kbn-xsrf', 'foo'); + } + + async function createConfiguration(config: AgentConfigurationIntake, { user = 'write' } = {}) { + log.debug('creating configuration', config.service); + const supertestClient = user === 'read' ? supertestRead : supertestWrite; + + const res = await supertestClient + .put(`/api/apm/settings/agent-configuration`) + .send(config) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function updateConfiguration(config: AgentConfigurationIntake, { user = 'write' } = {}) { + log.debug('updating configuration', config.service); + const supertestClient = user === 'read' ? supertestRead : supertestWrite; + + const res = await supertestClient + .put(`/api/apm/settings/agent-configuration?overwrite=true`) + .send(config) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function deleteConfiguration( + { service }: AgentConfigurationIntake, + { user = 'write' } = {} + ) { + log.debug('deleting configuration', service); + const supertestClient = user === 'read' ? supertestRead : supertestWrite; + + const res = await supertestClient + .delete(`/api/apm/settings/agent-configuration`) + .send({ service }) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + function throwOnError(res: any) { + const { statusCode, req, body } = res; + if (statusCode !== 200) { + const e = new Error(` + Endpoint: ${req.method} ${req.path} + Service: ${JSON.stringify(res.request._data.service)} + Status code: ${statusCode} + Response: ${body.message}`); + + // @ts-ignore + e.res = res; + + throw e; + } + } + + registry.when( + 'agent configuration when no data is loaded', + { config: 'basic', archives: [] }, + () => { + it('handles the empty state for services', async () => { + const { body } = await getServices(); + expect(body).to.eql(['ALL_OPTION_VALUE']); + }); + + it('handles the empty state for environments', async () => { + const { body } = await getEnvironments('myservice'); + expect(body).to.eql([{ name: 'ALL_OPTION_VALUE', alreadyConfigured: false }]); + }); + + it('handles the empty state for agent names', async () => { + const { body } = await getAgentName('myservice'); + expect(body).to.eql({}); + }); + + describe('as a read-only user', () => { + const newConfig = { service: {}, settings: { transaction_sample_rate: '0.55' } }; + it('throws when attempting to create config', async () => { + try { + await createConfiguration(newConfig, { user: 'read' }); + + // ensure that `createConfiguration` throws + expect(true).to.be(false); + } catch (e) { + expect(e.res.statusCode).to.be(403); + } + }); + + describe('when a configuration already exists', () => { + before(async () => createConfiguration(newConfig)); + after(async () => deleteConfiguration(newConfig)); + + it('throws when attempting to update config', async () => { + try { + await updateConfiguration(newConfig, { user: 'read' }); + + // ensure that `updateConfiguration` throws + expect(true).to.be(false); + } catch (e) { + expect(e.res.statusCode).to.be(403); + } + }); + + it('throws when attempting to delete config', async () => { + try { + await deleteConfiguration(newConfig, { user: 'read' }); + + // ensure that `deleteConfiguration` throws + expect(true).to.be(false); + } catch (e) { + expect(e.res.statusCode).to.be(403); + } + }); + }); + }); + + describe('when creating one configuration', () => { + const newConfig = { + service: {}, + settings: { transaction_sample_rate: '0.55' }, + }; + + const searchParams = { + service: { name: 'myservice', environment: 'development' }, + etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39', + }; + + it('can create and delete config', async () => { + // assert that config does not exist + const res1 = await searchConfigurations(searchParams); + expect(res1.status).to.equal(404); + + // assert that config was created + await createConfiguration(newConfig); + const res2 = await searchConfigurations(searchParams); + expect(res2.status).to.equal(200); + + // assert that config was deleted + await deleteConfiguration(newConfig); + const res3 = await searchConfigurations(searchParams); + expect(res3.status).to.equal(404); + }); + + describe('when a configuration exists', () => { + before(async () => createConfiguration(newConfig)); + after(async () => deleteConfiguration(newConfig)); + + it('can find the config', async () => { + const { status, body } = await searchConfigurations(searchParams); + expect(status).to.equal(200); + expect(body._source.service).to.eql({}); + expect(body._source.settings).to.eql({ transaction_sample_rate: '0.55' }); + }); + + it('can list the config', async () => { + const { status, body } = await getAllConfigurations(); + expect(status).to.equal(200); + expect(omitTimestamp(body)).to.eql([ + { + service: {}, + settings: { transaction_sample_rate: '0.55' }, + applied_by_agent: false, + etag: 'eb88a8997666cc4b33745ef355a1bbd7c4782f2d', + }, + ]); + }); + + it('can update the config', async () => { + await updateConfiguration({ + service: {}, + settings: { transaction_sample_rate: '0.85' }, + }); + const { status, body } = await searchConfigurations(searchParams); + expect(status).to.equal(200); + expect(body._source.service).to.eql({}); + expect(body._source.settings).to.eql({ transaction_sample_rate: '0.85' }); + }); + }); + }); + + describe('when creating multiple configurations', () => { + const configs = [ + { + service: {}, + settings: { transaction_sample_rate: '0.1' }, + }, + { + service: { name: 'my_service' }, + settings: { transaction_sample_rate: '0.2' }, + }, + { + service: { name: 'my_service', environment: 'development' }, + settings: { transaction_sample_rate: '0.3' }, + }, + { + service: { environment: 'production' }, + settings: { transaction_sample_rate: '0.4' }, + }, + { + service: { environment: 'development' }, + settings: { transaction_sample_rate: '0.5' }, + }, + ]; + + before(async () => { + await Promise.all(configs.map((config) => createConfiguration(config))); + }); + + after(async () => { + await Promise.all(configs.map((config) => deleteConfiguration(config))); + }); + + const agentsRequests = [ + { + service: { name: 'non_existing_service', environment: 'non_existing_env' }, + expectedSettings: { transaction_sample_rate: '0.1' }, + }, + { + service: { name: 'my_service', environment: 'non_existing_env' }, + expectedSettings: { transaction_sample_rate: '0.2' }, + }, + { + service: { name: 'my_service', environment: 'production' }, + expectedSettings: { transaction_sample_rate: '0.2' }, + }, + { + service: { name: 'my_service', environment: 'development' }, + expectedSettings: { transaction_sample_rate: '0.3' }, + }, + { + service: { name: 'non_existing_service', environment: 'production' }, + expectedSettings: { transaction_sample_rate: '0.4' }, + }, + { + service: { name: 'non_existing_service', environment: 'development' }, + expectedSettings: { transaction_sample_rate: '0.5' }, + }, + ]; + + it('can list all configs', async () => { + const { status, body } = await getAllConfigurations(); + expect(status).to.equal(200); + expect(orderBy(omitTimestamp(body), ['settings.transaction_sample_rate'])).to.eql([ + { + service: {}, + settings: { transaction_sample_rate: '0.1' }, + applied_by_agent: false, + etag: '0758cb18817de60cca29e07480d472694239c4c3', + }, + { + service: { name: 'my_service' }, + settings: { transaction_sample_rate: '0.2' }, + applied_by_agent: false, + etag: 'e04737637056fdf1763bf0ef0d3fcb86e89ae5fc', + }, + { + service: { name: 'my_service', environment: 'development' }, + settings: { transaction_sample_rate: '0.3' }, + applied_by_agent: false, + etag: 'af4dac62621b6762e6281481d1f7523af1124120', + }, + { + service: { environment: 'production' }, + settings: { transaction_sample_rate: '0.4' }, + applied_by_agent: false, + etag: '8d1bf8e6b778b60af351117e2cf53fb1ee570068', + }, + { + service: { environment: 'development' }, + settings: { transaction_sample_rate: '0.5' }, + applied_by_agent: false, + etag: '4ce40da57e3c71daca704121c784b911ec05ae81', + }, + ]); + }); + + for (const agentRequest of agentsRequests) { + it(`${agentRequest.service.name} / ${agentRequest.service.environment}`, async () => { + const { status, body } = await searchConfigurations({ + service: agentRequest.service, + etag: 'abc', + }); + + expect(status).to.equal(200); + expect(body._source.settings).to.eql(agentRequest.expectedSettings); + }); + } + }); + + describe('when an agent retrieves a configuration', () => { + const config = { + service: { name: 'myservice', environment: 'development' }, + settings: { transaction_sample_rate: '0.9' }, + }; + const configProduction = { + service: { name: 'myservice', environment: 'production' }, + settings: { transaction_sample_rate: '0.9' }, + }; + let etag: string; + + before(async () => { + log.debug('creating agent configuration'); + await createConfiguration(config); + await createConfiguration(configProduction); + }); + + after(async () => { + await deleteConfiguration(config); + await deleteConfiguration(configProduction); + }); + + it(`should have 'applied_by_agent=false' before supplying etag`, async () => { + const res1 = await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + }); + + etag = res1.body._source.etag; + + const res2 = await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag, + }); + + expect(res1.body._source.applied_by_agent).to.be(false); + expect(res2.body._source.applied_by_agent).to.be(false); + }); + + it(`should have 'applied_by_agent=true' after supplying etag`, async () => { + await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag, + }); + + async function hasBeenAppliedByAgent() { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + }); + + return body._source.applied_by_agent; + } + + // wait until `applied_by_agent` has been updated in elasticsearch + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); + }); + it(`should have 'applied_by_agent=false' before marking as applied`, async () => { + const res1 = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + expect(res1.body._source.applied_by_agent).to.be(false); + }); + it(`should have 'applied_by_agent=true' when 'mark_as_applied_by_agent' attribute is true`, async () => { + await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + mark_as_applied_by_agent: true, + }); + + async function hasBeenAppliedByAgent() { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + return body._source.applied_by_agent; + } + + // wait until `applied_by_agent` has been updated in elasticsearch + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); + }); + }); + } + ); + + registry.when( + 'agent configuration when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns all services', async () => { + const { body } = await getServices(); + expectSnapshot(body).toMatchInline(` + Array [ + "ALL_OPTION_VALUE", + "kibana", + "kibana-frontend", + "opbeans-dotnet", + "opbeans-go", + "opbeans-java", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", + "opbeans-rum", + ] + `); + }); + + it('returns the environments, all unconfigured', async () => { + const { body } = await getEnvironments('opbeans-node'); + + expect(body.map((item: { name: string }) => item.name)).to.contain('ALL_OPTION_VALUE'); + + expect( + body.every((item: { alreadyConfigured: boolean }) => item.alreadyConfigured === false) + ).to.be(true); + + expectSnapshot(body).toMatchInline(` + Array [ + Object { + "alreadyConfigured": false, + "name": "ALL_OPTION_VALUE", + }, + Object { + "alreadyConfigured": false, + "name": "testing", + }, + ] + `); + }); + + it('returns the agent names', async () => { + const { body } = await getAgentName('opbeans-node'); + expect(body).to.eql({ agentName: 'nodejs' }); + }); + } + ); +} + +async function waitFor(cb: () => Promise, retries = 50): Promise { + if (retries === 0) { + throw new Error(`Maximum number of retries reached`); + } + + const res = await cb(); + if (!res) { + await new Promise((resolve) => setTimeout(resolve, 100)); + return waitFor(cb, retries - 1); + } + return res; +} + +function omitTimestamp(configs: AgentConfigurationIntake[]) { + return configs.map((config: AgentConfigurationIntake) => omit(config, '@timestamp')); +} diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.ts new file mode 100644 index 0000000000000..269375ef080de --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { registry } from '../../../common/registry'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const noAccessUser = getService('supertestAsNoAccessUser'); + const readUser = getService('supertestAsApmReadUser'); + const writeUser = getService('supertestAsApmWriteUser'); + + type SupertestAsUser = typeof noAccessUser | typeof readUser | typeof writeUser; + + function getJobs(user: SupertestAsUser) { + return user.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); + } + + function createJobs(user: SupertestAsUser, environments: string[]) { + return user + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + async function expectForbidden(user: SupertestAsUser) { + const { body: getJobsBody } = await getJobs(user); + expect(getJobsBody.statusCode).to.be(403); + expect(getJobsBody.error).to.be('Forbidden'); + + const { body: createJobsBody } = await createJobs(user, ['production', 'staging']); + + expect(createJobsBody.statusCode).to.be(403); + expect(getJobsBody.error).to.be('Forbidden'); + } + + registry.when('ML jobs return a 403 for', { config: 'basic', archives: [] }, () => { + it('user without access', async () => { + await expectForbidden(noAccessUser); + }); + + it('read user', async () => { + await expectForbidden(readUser); + }); + + it('write user', async () => { + await expectForbidden(writeUser); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/no_access_user.ts new file mode 100644 index 0000000000000..4878d5031f040 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/no_access_user.ts @@ -0,0 +1,44 @@ +/* + * 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 { registry } from '../../../common/registry'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const noAccessUser = getService('supertestAsNoAccessUser'); + + function getJobs() { + return noAccessUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); + } + + function createJobs(environments: string[]) { + return noAccessUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + registry.when('ML jobs', { config: 'trial', archives: [] }, () => { + describe('when user does not have read access to ML', () => { + describe('when calling the endpoint for listing jobs', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await getJobs(); + expect(body.statusCode).to.be(403); + expect(body.error).to.be('Forbidden'); + }); + }); + + describe('when calling create endpoint', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await createJobs(['production', 'staging']); + expect(body.statusCode).to.be(403); + expect(body.error).to.be('Forbidden'); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.ts new file mode 100644 index 0000000000000..a5fabe66af6f5 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.ts @@ -0,0 +1,46 @@ +/* + * 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 { registry } from '../../../common/registry'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const apmReadUser = getService('supertestAsApmReadUser'); + + function getJobs() { + return apmReadUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); + } + + function createJobs(environments: string[]) { + return apmReadUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + registry.when('ML jobs', { config: 'trial', archives: [] }, () => { + describe('when user has read access to ML', () => { + describe('when calling the endpoint for listing jobs', () => { + it('returns a list of jobs', async () => { + const { body } = await getJobs(); + + expect(body.jobs.length).to.be(0); + expect(body.hasLegacyJobs).to.be(false); + }); + }); + + describe('when calling create endpoint', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await createJobs(['production', 'staging']); + + expect(body.statusCode).to.be(403); + expect(body.error).to.be('Forbidden'); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts new file mode 100644 index 0000000000000..5260d234eb9c7 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { registry } from '../../../common/registry'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const apmWriteUser = getService('supertestAsApmWriteUser'); + + function getJobs() { + return apmWriteUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); + } + + function createJobs(environments: string[]) { + return apmWriteUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + function deleteJobs(jobIds: string[]) { + return apmWriteUser.post(`/api/ml/jobs/delete_jobs`).send({ jobIds }).set('kbn-xsrf', 'foo'); + } + + registry.when('ML jobs', { config: 'trial', archives: [] }, () => { + describe('when user has write access to ML', () => { + after(async () => { + const res = await getJobs(); + const jobIds = res.body.jobs.map((job: any) => job.job_id); + await deleteJobs(jobIds); + }); + + describe('when calling the endpoint for listing jobs', () => { + it('returns a list of jobs', async () => { + const { body } = await getJobs(); + expect(body.jobs.length).to.be(0); + expect(body.hasLegacyJobs).to.be(false); + }); + }); + + describe('when calling create endpoint', () => { + it('creates two jobs', async () => { + await createJobs(['production', 'staging']); + + const { body } = await getJobs(); + expect(body.hasLegacyJobs).to.be(false); + expect(body.jobs.map((job: any) => job.environment)).to.eql(['production', 'staging']); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/tests/settings/custom_link.ts new file mode 100644 index 0000000000000..3df905eabeace --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/custom_link.ts @@ -0,0 +1,183 @@ +/* + * 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 URL from 'url'; +import expect from '@kbn/expect'; +import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function customLinksTests({ getService }: FtrProviderContext) { + const supertestRead = getService('supertest'); + const supertestWrite = getService('supertestAsApmWriteUser'); + const log = getService('log'); + + const archiveName = 'apm_8.0.0'; + + registry.when('Custom links with a basic license', { config: 'basic', archives: [] }, () => { + it('returns a 403 forbidden', async () => { + const customLink = { + url: 'https://elastic.co', + label: 'with filters', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + } as CustomLink; + const response = await supertestWrite + .post(`/api/apm/settings/custom_links`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + expect(response.status).to.be(403); + + expectSnapshot(response.body.message).toMatchInline( + `"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."` + ); + }); + }); + + registry.when( + 'Custom links with a trial license and data', + { config: 'trial', archives: [archiveName] }, + () => { + before(async () => { + const customLink = { + url: 'https://elastic.co', + label: 'with filters', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + } as CustomLink; + await createCustomLink(customLink); + }); + it('fetches a custom link', async () => { + const { status, body } = await searchCustomLinks({ + 'service.name': 'baz', + 'transaction.type': 'qux', + }); + const { label, url, filters } = body[0]; + + expect(status).to.equal(200); + expect({ label, url, filters }).to.eql({ + label: 'with filters', + url: 'https://elastic.co', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + }); + }); + it('updates a custom link', async () => { + let { status, body } = await searchCustomLinks({ + 'service.name': 'baz', + 'transaction.type': 'qux', + }); + expect(status).to.equal(200); + await updateCustomLink(body[0].id, { + label: 'foo', + url: 'https://elastic.co?service.name={{service.name}}', + filters: [ + { key: 'service.name', value: 'quz' }, + { key: 'transaction.name', value: 'bar' }, + ], + }); + ({ status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + })); + const { label, url, filters } = body[0]; + expect(status).to.equal(200); + expect({ label, url, filters }).to.eql({ + label: 'foo', + url: 'https://elastic.co?service.name={{service.name}}', + filters: [ + { key: 'service.name', value: 'quz' }, + { key: 'transaction.name', value: 'bar' }, + ], + }); + }); + it('deletes a custom link', async () => { + let { status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + }); + expect(status).to.equal(200); + await deleteCustomLink(body[0].id); + ({ status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + })); + expect(status).to.equal(200); + expect(body).to.eql([]); + }); + + describe('transaction', () => { + it('fetches a transaction sample', async () => { + const response = await supertestRead.get( + '/api/apm/settings/custom_links/transaction?service.name=opbeans-java' + ); + expect(response.status).to.be(200); + expect(response.body.service.name).to.eql('opbeans-java'); + }); + }); + } + ); + + function searchCustomLinks(filters?: any) { + const path = URL.format({ + pathname: `/api/apm/settings/custom_links`, + query: filters, + }); + return supertestRead.get(path).set('kbn-xsrf', 'foo'); + } + + async function createCustomLink(customLink: CustomLink) { + log.debug('creating configuration', customLink); + const res = await supertestWrite + .post(`/api/apm/settings/custom_links`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function updateCustomLink(id: string, customLink: CustomLink) { + log.debug('updating configuration', id, customLink); + const res = await supertestWrite + .put(`/api/apm/settings/custom_links/${id}`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function deleteCustomLink(id: string) { + log.debug('deleting configuration', id); + const res = await supertestWrite + .delete(`/api/apm/settings/custom_links/${id}`) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + function throwOnError(res: any) { + const { statusCode, req, body } = res; + if (statusCode !== 200) { + throw new Error(` + Endpoint: ${req.method} ${req.path} + Service: ${JSON.stringify(res.request._data.service)} + Status code: ${statusCode} + Response: ${body.message}`); + } + } +} diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap b/x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.snap similarity index 99% rename from x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap rename to x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.snap index 56e82d752dccd..c8104b9858027 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap +++ b/x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM specs (basic) Traces Top traces when data is loaded returns the correct buckets 1`] = ` +exports[`APM API tests basic apm_8.0.0 Top traces when data is loaded returns the correct buckets 1`] = ` Array [ Object { "averageResponseTime": 1733, diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/tests/traces/top_traces.ts similarity index 81% rename from x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts rename to x-pack/test/apm_api_integration/tests/traces/top_traces.ts index 2ce3ba3838292..4754b3faa20ad 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/tests/traces/top_traces.ts @@ -5,12 +5,12 @@ */ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; @@ -20,28 +20,28 @@ export default function ApiTest({ getService }: FtrProviderContext) { const end = encodeURIComponent(metadata.end); const uiFilters = encodeURIComponent(JSON.stringify({})); - describe('Top traces', () => { - describe('when data is not loaded ', () => { - it('handles empty state', async () => { - const response = await supertest.get( - `/api/apm/traces?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); + registry.when('Top traces when data is not loaded', { config: 'basic', archives: [] }, () => { + it('handles empty state', async () => { + const response = await supertest.get( + `/api/apm/traces?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); - expect(response.status).to.be(200); - expect(response.body.items.length).to.be(0); - expect(response.body.isAggregationAccurate).to.be(true); - }); + expect(response.status).to.be(200); + expect(response.body.items.length).to.be(0); + expect(response.body.isAggregationAccurate).to.be(true); }); + }); - describe('when data is loaded', () => { + registry.when( + 'Top traces when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { let response: any; before(async () => { - await esArchiver.load(archiveName); response = await supertest.get( `/api/apm/traces?start=${start}&end=${end}&uiFilters=${uiFilters}` ); }); - after(() => esArchiver.unload(archiveName)); it('returns the correct status code', async () => { expect(response.status).to.be(200); @@ -116,6 +116,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { ] `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/breakdown.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/breakdown.snap similarity index 98% rename from x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/breakdown.snap rename to x-pack/test/apm_api_integration/tests/transactions/__snapshots__/breakdown.snap index 25aa68d2a86b1..cfdd27b594956 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/breakdown.snap +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/breakdown.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM specs (basic) Transactions Breakdown when data is loaded returns the transaction breakdown for a service 1`] = ` +exports[`APM API tests basic apm_8.0.0 when data is loaded returns the transaction breakdown for a service 1`] = ` Object { "timeseries": Array [ Object { @@ -1019,7 +1019,7 @@ Object { } `; -exports[`APM specs (basic) Transactions Breakdown when data is loaded returns the transaction breakdown for a transaction group 9`] = ` +exports[`APM API tests basic apm_8.0.0 when data is loaded returns the transaction breakdown for a transaction group 9`] = ` Array [ Object { "x": 1607435850000, diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/error_rate.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap similarity index 95% rename from x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/error_rate.snap rename to x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap index 3b67a86ba84e8..d97d39cda1b8d 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/error_rate.snap +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM specs (basic) Transactions Error rate when data is loaded returns the transaction error rate has the correct error rate 1`] = ` +exports[`APM API tests basic apm_8.0.0 Error rate when data is loaded returns the transaction error rate has the correct error rate 1`] = ` Array [ Object { "x": 1607435850000, diff --git a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/latency.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/latency.snap new file mode 100644 index 0000000000000..a384ca2c9364e --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/latency.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM API tests trial apm_8.0.0 Transaction latency with a trial license when data is loaded when not defined environments seleted should return the correct anomaly boundaries 1`] = ` +Array [ + Object { + "x": 1607436000000, + "y": 0, + "y0": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + "y0": 0, + }, +] +`; + +exports[`APM API tests trial apm_8.0.0 Transaction latency with a trial license when data is loaded with environment selected and empty kuery filter should return a non-empty anomaly series 1`] = ` +Array [ + Object { + "x": 1607436000000, + "y": 1625128.56211579, + "y0": 7533.02707532227, + }, + Object { + "x": 1607436900000, + "y": 1660982.24115757, + "y0": 5732.00699123528, + }, +] +`; + +exports[`APM API tests trial apm_8.0.0 Transaction latency with a trial license when data is loaded with environment selected in uiFilters should return a non-empty anomaly series 1`] = ` +Array [ + Object { + "x": 1607436000000, + "y": 1625128.56211579, + "y0": 7533.02707532227, + }, + Object { + "x": 1607436900000, + "y": 1660982.24115757, + "y0": 5732.00699123528, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/top_transaction_groups.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/top_transaction_groups.snap similarity index 96% rename from x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/top_transaction_groups.snap rename to x-pack/test/apm_api_integration/tests/transactions/__snapshots__/top_transaction_groups.snap index 473305f3e39af..67a02e416de51 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/top_transaction_groups.snap +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/top_transaction_groups.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM specs (basic) Transactions Top transaction groups when data is loaded returns the correct buckets (when ignoring samples) 1`] = ` +exports[`APM API tests basic apm_8.0.0 Top transaction groups when data is loaded returns the correct buckets (when ignoring samples) 1`] = ` Array [ Object { "averageResponseTime": 2722.75, diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/transaction_charts.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transaction_charts.snap similarity index 100% rename from x-pack/test/apm_api_integration/basic/tests/transactions/__snapshots__/transaction_charts.snap rename to x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transaction_charts.snap diff --git a/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/transactions_charts.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_charts.snap similarity index 100% rename from x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/transactions_charts.snap rename to x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_charts.snap diff --git a/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts b/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts new file mode 100644 index 0000000000000..4a9dcf9273003 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts @@ -0,0 +1,118 @@ +/* + * 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 archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); + const transactionType = 'request'; + const transactionName = 'GET /api'; + const uiFilters = encodeURIComponent(JSON.stringify({})); + + registry.when('Breakdown when data is not loaded', { config: 'basic', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` + ); + expect(response.status).to.be(200); + expect(response.body).to.eql({ timeseries: [] }); + }); + }); + + registry.when('when data is loaded', { config: 'basic', archives: [archiveName] }, () => { + it('returns the transaction breakdown for a service', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + it('returns the transaction breakdown for a transaction group', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}&transactionName=${transactionName}` + ); + + expect(response.status).to.be(200); + + const { timeseries } = response.body; + + const numberOfSeries = timeseries.length; + + expectSnapshot(numberOfSeries).toMatchInline(`1`); + + const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; + + const nonNullDataPoints = data.filter((y: number | null) => y !== null); + + expectSnapshot(nonNullDataPoints.length).toMatchInline(`61`); + + expectSnapshot( + data.slice(0, 5).map(({ x, y }: { x: number; y: number | null }) => { + return { + x: new Date(x ?? NaN).toISOString(), + y, + }; + }) + ).toMatchInline(` + Array [ + Object { + "x": "2020-12-08T13:57:30.000Z", + "y": null, + }, + Object { + "x": "2020-12-08T13:58:00.000Z", + "y": null, + }, + Object { + "x": "2020-12-08T13:58:30.000Z", + "y": 1, + }, + Object { + "x": "2020-12-08T13:59:00.000Z", + "y": 1, + }, + Object { + "x": "2020-12-08T13:59:30.000Z", + "y": null, + }, + ] + `); + + expectSnapshot(title).toMatchInline(`"app"`); + expectSnapshot(color).toMatchInline(`"#54b399"`); + expectSnapshot(type).toMatchInline(`"areaStacked"`); + expectSnapshot(hideLegend).toMatchInline(`false`); + expectSnapshot(legendValue).toMatchInline(`"100%"`); + + expectSnapshot(data).toMatch(); + }); + it('returns the transaction breakdown sorted by name', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body.timeseries.map((serie: { title: string }) => serie.title)) + .toMatchInline(` + Array [ + "app", + "http", + "postgresql", + "redis", + ] + `); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/distribution.ts similarity index 85% rename from x-pack/test/apm_api_integration/basic/tests/transactions/distribution.ts rename to x-pack/test/apm_api_integration/tests/transactions/distribution.ts index 890b6af728d5a..5d918b479111f 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/distribution.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/distribution.ts @@ -6,12 +6,12 @@ import expect from '@kbn/expect'; import qs from 'querystring'; import { isEmpty } from 'lodash'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; @@ -24,8 +24,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { transactionType: 'request', })}`; - describe('Transaction groups distribution', () => { - describe('when data is not loaded ', () => { + registry.when( + 'Transaction groups distribution when data is not loaded', + { config: 'basic', archives: [] }, + () => { it('handles empty state', async () => { const response = await supertest.get(url); @@ -34,15 +36,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.body.noHits).to.be(true); expect(response.body.buckets.length).to.be(0); }); - }); + } + ); - describe('when data is loaded', () => { + registry.when( + 'Transaction groups distribution when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { let response: any; before(async () => { - await esArchiver.load(archiveName); response = await supertest.get(url); }); - after(() => esArchiver.unload(archiveName)); it('returns the correct metadata', () => { expect(response.status).to.be(200); @@ -91,6 +95,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { ] `); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/error_rate.ts b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts similarity index 71% rename from x-pack/test/apm_api_integration/basic/tests/transactions/error_rate.ts rename to x-pack/test/apm_api_integration/tests/transactions/error_rate.ts index 22d9a7eba7fbf..487f2efbae400 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/error_rate.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts @@ -6,12 +6,12 @@ import expect from '@kbn/expect'; import { first, last } from 'lodash'; import { format } from 'url'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; @@ -20,28 +20,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { const uiFilters = '{}'; const transactionType = 'request'; - describe('Error rate', () => { - const url = format({ - pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', - query: { start, end, uiFilters, transactionType }, - }); + const url = format({ + pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', + query: { start, end, uiFilters, transactionType }, + }); - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - expect(response.status).to.be(200); + registry.when('Error rate when data is not loaded', { config: 'basic', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + expect(response.status).to.be(200); - expect(response.body.noHits).to.be(true); + expect(response.body.noHits).to.be(true); - expect(response.body.transactionErrorRate.length).to.be(0); - expect(response.body.average).to.be(null); - }); + expect(response.body.transactionErrorRate.length).to.be(0); + expect(response.body.average).to.be(null); }); + }); - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - + registry.when( + 'Error rate when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { describe('returns the transaction error rate', () => { let errorRateResponse: { transactionErrorRate: Array<{ x: number; y: number | null }>; @@ -89,6 +88,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(errorRateResponse.transactionErrorRate).toMatch(); }); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency.ts b/x-pack/test/apm_api_integration/tests/transactions/latency.ts new file mode 100644 index 0000000000000..c860b0e75495c --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/latency.ts @@ -0,0 +1,243 @@ +/* + * 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 { PromiseReturnType } from '../../../../plugins/observability/typings/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + + const range = archives_metadata[archiveName]; + + // url parameters + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); + + registry.when( + 'Latency with a basic license when data is not loaded ', + { config: 'basic', archives: [] }, + () => { + const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'testing' })); + it('returns 400 when latencyAggregationType is not informed', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request` + ); + + expect(response.status).to.be(400); + }); + + it('returns 400 when transactionType is not informed', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&latencyAggregationType=avg` + ); + + expect(response.status).to.be(400); + }); + + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&latencyAggregationType=avg&transactionType=request` + ); + + expect(response.status).to.be(200); + + expect(response.body.overallAvgDuration).to.be(null); + expect(response.body.latencyTimeseries.length).to.be(0); + }); + } + ); + + registry.when( + 'Latency with a basic license when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + let response: PromiseReturnType; + + const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'testing' })); + + describe('average latency type', () => { + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=avg` + ); + }); + + it('returns average duration and timeseries', async () => { + expect(response.status).to.be(200); + expect(response.body.overallAvgDuration).not.to.be(null); + expect(response.body.latencyTimeseries.length).to.be.eql(61); + }); + }); + + describe('95th percentile latency type', () => { + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=p95` + ); + }); + + it('returns average duration and timeseries', async () => { + expect(response.status).to.be(200); + expect(response.body.overallAvgDuration).not.to.be(null); + expect(response.body.latencyTimeseries.length).to.be.eql(61); + }); + }); + + describe('99th percentile latency type', () => { + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=p99` + ); + }); + + it('returns average duration and timeseries', async () => { + expect(response.status).to.be(200); + expect(response.body.overallAvgDuration).not.to.be(null); + expect(response.body.latencyTimeseries.length).to.be.eql(61); + }); + }); + } + ); + + registry.when( + 'Transaction latency with a trial license when data is loaded', + { config: 'trial', archives: [archiveName] }, + () => { + let response: PromiseReturnType; + + const transactionType = 'request'; + + describe('without environment', () => { + const uiFilters = encodeURIComponent(JSON.stringify({})); + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` + ); + }); + it('should return an error response', () => { + expect(response.status).to.eql(400); + }); + }); + + describe('without uiFilters', () => { + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&latencyAggregationType=avg` + ); + }); + it('should return an error response', () => { + expect(response.status).to.eql(400); + }); + }); + + describe('with environment selected in uiFilters', () => { + const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'production' })); + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` + ); + }); + + it('should have a successful response', () => { + expect(response.status).to.eql(200); + }); + + it('should return the ML job id for anomalies of the selected environment', () => { + expect(response.body).to.have.property('anomalyTimeseries'); + expect(response.body.anomalyTimeseries).to.have.property('jobId'); + expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline( + `"apm-production-1369-high_mean_transaction_duration"` + ); + }); + + it('should return a non-empty anomaly series', () => { + expect(response.body).to.have.property('anomalyTimeseries'); + expect(response.body.anomalyTimeseries.anomalyBoundaries?.length).to.be.greaterThan(0); + expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch(); + }); + }); + + describe('when not defined environments seleted', () => { + const uiFilters = encodeURIComponent( + JSON.stringify({ environment: 'ENVIRONMENT_NOT_DEFINED' }) + ); + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-python/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` + ); + }); + + it('should have a successful response', () => { + expect(response.status).to.eql(200); + }); + + it('should return the ML job id for anomalies with no defined environment', () => { + expect(response.body).to.have.property('anomalyTimeseries'); + expect(response.body.anomalyTimeseries).to.have.property('jobId'); + expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline( + `"apm-environment_not_defined-5626-high_mean_transaction_duration"` + ); + }); + + it('should return the correct anomaly boundaries', () => { + expect(response.body).to.have.property('anomalyTimeseries'); + expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch(); + }); + }); + + describe('with all environments selected', () => { + const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'ENVIRONMENT_ALL' })); + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` + ); + }); + + it('should have a successful response', () => { + expect(response.status).to.eql(200); + }); + + it('should not return anomaly timeseries data', () => { + expect(response.body).to.not.have.property('anomalyTimeseries'); + }); + }); + + describe('with environment selected and empty kuery filter', () => { + const uiFilters = encodeURIComponent( + JSON.stringify({ kuery: '', environment: 'production' }) + ); + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` + ); + }); + + it('should have a successful response', () => { + expect(response.status).to.eql(200); + }); + + it('should return the ML job id for anomalies of the selected environment', () => { + expect(response.body).to.have.property('anomalyTimeseries'); + expect(response.body.anomalyTimeseries).to.have.property('jobId'); + expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline( + `"apm-production-1369-high_mean_transaction_duration"` + ); + }); + + it('should return a non-empty anomaly series', () => { + expect(response.body).to.have.property('anomalyTimeseries'); + expect(response.body.anomalyTimeseries.anomalyBoundaries?.length).to.be.greaterThan(0); + expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch(); + }); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/throughput.ts b/x-pack/test/apm_api_integration/tests/transactions/throughput.ts similarity index 54% rename from x-pack/test/apm_api_integration/basic/tests/transactions/throughput.ts rename to x-pack/test/apm_api_integration/tests/transactions/throughput.ts index 1013f3a19d71f..475bef4e9b549 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/throughput.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/throughput.ts @@ -5,13 +5,13 @@ */ import expect from '@kbn/expect'; import url from 'url'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { PromiseReturnType } from '../../../../plugins/observability/typings/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; @@ -20,31 +20,30 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { start, end } = metadata; const uiFilters = JSON.stringify({ environment: 'testing' }); - describe('Throughput', () => { - describe('when data is not loaded ', () => { - it('handles the empty state', async () => { - const response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-node/transactions/charts/throughput`, - query: { - start, - end, - uiFilters, - transactionType: 'request', - }, - }) - ); + registry.when('Throughput when data is not loaded', { config: 'basic', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-node/transactions/charts/throughput`, + query: { + start, + end, + uiFilters, + transactionType: 'request', + }, + }) + ); - expect(response.status).to.be(200); + expect(response.status).to.be(200); - expect(response.body.throughputTimeseries.length).to.be(0); - }); + expect(response.body.throughputTimeseries.length).to.be(0); }); + }); - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - + registry.when( + 'Throughput when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { let response: PromiseReturnType; before(async () => { @@ -66,6 +65,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.body.throughputTimeseries.length).to.be.greaterThan(0); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/top_transaction_groups.ts b/x-pack/test/apm_api_integration/tests/transactions/top_transaction_groups.ts similarity index 80% rename from x-pack/test/apm_api_integration/basic/tests/transactions/top_transaction_groups.ts rename to x-pack/test/apm_api_integration/tests/transactions/top_transaction_groups.ts index dac36ae8b3303..70afb2ee384c4 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/top_transaction_groups.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/top_transaction_groups.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; function sortTransactionGroups(items: any[]) { return sortBy(items, 'impact'); @@ -14,7 +15,6 @@ function sortTransactionGroups(items: any[]) { export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; @@ -25,8 +25,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const uiFilters = encodeURIComponent(JSON.stringify({})); const transactionType = 'request'; - describe('Top transaction groups', () => { - describe('when data is not loaded ', () => { + registry.when( + 'Top transaction groups when data is not loaded', + { config: 'basic', archives: [] }, + () => { it('handles empty state', async () => { const response = await supertest.get( `/api/apm/services/opbeans-node/transactions/groups?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` @@ -37,17 +39,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.body.isAggregationAccurate).to.be(true); expect(response.body.items.length).to.be(0); }); - }); + } + ); - describe('when data is loaded', () => { + registry.when( + 'Top transaction groups when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { let response: any; before(async () => { - await esArchiver.load(archiveName); response = await supertest.get( `/api/apm/services/opbeans-node/transactions/groups?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` ); }); - after(() => esArchiver.unload(archiveName)); it('returns the correct metadata', () => { expect(response.status).to.be(200); @@ -62,6 +66,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the correct buckets (when ignoring samples)', async () => { expectSnapshot(sortTransactionGroups(response.body.items)).toMatch(); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/transactions_groups_overview.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_overview.ts similarity index 94% rename from x-pack/test/apm_api_integration/basic/tests/transactions/transactions_groups_overview.ts rename to x-pack/test/apm_api_integration/tests/transactions/transactions_groups_overview.ts index be978b2a82618..8818aaccdec01 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/transactions_groups_overview.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_overview.ts @@ -7,18 +7,20 @@ import expect from '@kbn/expect'; import { pick, uniqBy, sortBy } from 'lodash'; import url from 'url'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import archives from '../../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; - describe('Transactions groups overview', () => { - describe('when data is not loaded', () => { + registry.when( + 'Transaction groups overview when data is not loaded', + { config: 'basic', archives: [] }, + () => { it('handles the empty state', async () => { const response = await supertest.get( url.format({ @@ -45,12 +47,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { isAggregationAccurate: true, }); }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + } + ); + registry.when( + 'Top transaction groups when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { it('returns the correct data', async () => { const response = await supertest.get( url.format({ @@ -264,6 +267,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(uniqBy(items, 'name').length).to.eql(totalItems); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/trial/config.ts b/x-pack/test/apm_api_integration/trial/config.ts index 94a6f808603c1..ba9ef4ca42597 100644 --- a/x-pack/test/apm_api_integration/trial/config.ts +++ b/x-pack/test/apm_api_integration/trial/config.ts @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createTestConfig } from '../common/config'; +import { configs } from '../configs'; -export default createTestConfig({ - license: 'trial', - name: 'X-Pack APM API integration tests (trial)', - testFiles: [require.resolve('./tests')], -}); +export default configs.trial; diff --git a/x-pack/test/apm_api_integration/trial/tests/correlations/slow_transactions.ts b/x-pack/test/apm_api_integration/trial/tests/correlations/slow_transactions.ts deleted file mode 100644 index 9a868373292f6..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/correlations/slow_transactions.ts +++ /dev/null @@ -1,95 +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 { format } from 'url'; -import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - describe('Slow durations', () => { - const url = format({ - pathname: `/api/apm/correlations/slow_transactions`, - query: { - start: range.start, - end: range.end, - durationPercentile: 95, - fieldNames: - 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name', - }, - }); - - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - describe('making request with default args', () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/slow_transactions'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns significant terms', () => { - const sorted = response.body?.significantTerms?.sort(); - expectSnapshot(sorted?.map((term) => term.fieldName)).toMatchInline(` - Array [ - "user_agent.name", - "url.domain", - "host.ip", - "service.node.name", - "container.id", - "url.domain", - "user_agent.name", - ] - `); - }); - - it('returns a distribution per term', () => { - expectSnapshot(response.body?.significantTerms?.map((term) => term.distribution.length)) - .toMatchInline(` - Array [ - 11, - 11, - 11, - 11, - 11, - 11, - 11, - ] - `); - }); - - it('returns overall distribution', () => { - expectSnapshot(response.body?.overall?.distribution.length).toMatchInline(`11`); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/csm_services.ts b/x-pack/test/apm_api_integration/trial/tests/csm/csm_services.ts deleted file mode 100644 index 05c6439508ece..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/csm/csm_services.ts +++ /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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('CSM Services', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/rum-client/services?start=2020-06-28T10%3A24%3A46.055Z&end=2020-07-29T10%3A24%3A46.055Z&uiFilters=%7B%22agentName%22%3A%5B%22js-base%22%2C%22rum-js%22%5D%7D' - ); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); - - describe('when there is data', () => { - before(async () => { - await esArchiver.load('8.0.0'); - await esArchiver.load('rum_8.0.0'); - }); - after(async () => { - await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_8.0.0'); - }); - - it('returns rum services list', async () => { - const response = await supertest.get( - '/api/apm/rum-client/services?start=2020-06-28T10%3A24%3A46.055Z&end=2020-07-29T10%3A24%3A46.055Z&uiFilters=%7B%22agentName%22%3A%5B%22js-base%22%2C%22rum-js%22%5D%7D' - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(`Array []`); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/page_load_dist.ts b/x-pack/test/apm_api_integration/trial/tests/csm/page_load_dist.ts deleted file mode 100644 index fa5fcbcb6a7c3..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/csm/page_load_dist.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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('UX page load dist', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/rum-client/page-load-distribution?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' - ); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatch(); - }); - it('returns empty list with breakdowns', async () => { - const response = await supertest.get( - '/api/apm/rum-client/page-load-distribution/breakdown?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&breakdown=Browser' - ); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatch(); - }); - }); - - describe('when there is data', () => { - before(async () => { - await esArchiver.load('8.0.0'); - await esArchiver.load('rum_8.0.0'); - }); - after(async () => { - await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_8.0.0'); - }); - - it('returns page load distribution', async () => { - const response = await supertest.get( - '/api/apm/rum-client/page-load-distribution?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatch(); - }); - it('returns page load distribution with breakdown', async () => { - const response = await supertest.get( - '/api/apm/rum-client/page-load-distribution/breakdown?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&breakdown=Browser' - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatch(); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts deleted file mode 100644 index 5d910862843d5..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/csm/page_views.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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('CSM page views', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' - ); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatch(); - }); - it('returns empty list with breakdowns', async () => { - const response = await supertest.get( - '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' - ); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatch(); - }); - }); - - describe('when there is data', () => { - before(async () => { - await esArchiver.load('8.0.0'); - await esArchiver.load('rum_8.0.0'); - }); - after(async () => { - await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_8.0.0'); - }); - - it('returns page views', async () => { - const response = await supertest.get( - '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatch(); - }); - it('returns page views with breakdown', async () => { - const response = await supertest.get( - '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatch(); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts deleted file mode 100644 index 961c783434639..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts +++ /dev/null @@ -1,89 +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 '../../../common/ftr_provider_context'; - -export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('CSM url search api', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&percentile=50' - ); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` - Object { - "items": Array [], - "total": 0, - } - `); - }); - }); - - describe('when there is data', () => { - before(async () => { - await esArchiver.load('8.0.0'); - await esArchiver.load('rum_8.0.0'); - }); - after(async () => { - await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_8.0.0'); - }); - - it('returns top urls when no query', async () => { - const response = await supertest.get( - '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&percentile=50' - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(` - Object { - "items": Array [ - Object { - "count": 5, - "pld": 4924000, - "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", - }, - Object { - "count": 1, - "pld": 2760000, - "url": "http://localhost:5601/nfw/app/home", - }, - ], - "total": 2, - } - `); - }); - - it('returns specific results against query', async () => { - const response = await supertest.get( - '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&urlQuery=csm&percentile=50' - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(` - Object { - "items": Array [ - Object { - "count": 5, - "pld": 4924000, - "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", - }, - ], - "total": 1, - } - `); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts deleted file mode 100644 index c8ee858d9ceb0..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ /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 { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; - -export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('APM specs (trial)', function () { - this.tags('ciGroup1'); - - describe('Services', function () { - loadTestFile(require.resolve('./services/annotations')); - loadTestFile(require.resolve('./services/top_services.ts')); - }); - - describe('Transactions', function () { - loadTestFile(require.resolve('./transactions/latency')); - }); - - describe('Settings', function () { - loadTestFile(require.resolve('./settings/custom_link.ts')); - describe('Anomaly detection', function () { - loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); - loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); - loadTestFile(require.resolve('./settings/anomaly_detection/write_user')); - }); - }); - - describe('Service Maps', function () { - loadTestFile(require.resolve('./service_maps/service_maps')); - }); - - describe('CSM', function () { - loadTestFile(require.resolve('./csm/csm_services.ts')); - loadTestFile(require.resolve('./csm/web_core_vitals.ts')); - loadTestFile(require.resolve('./csm/long_task_metrics.ts')); - loadTestFile(require.resolve('./csm/url_search.ts')); - loadTestFile(require.resolve('./csm/page_views.ts')); - loadTestFile(require.resolve('./csm/js_errors.ts')); - loadTestFile(require.resolve('./csm/has_rum_data.ts')); - loadTestFile(require.resolve('./csm/page_load_dist.ts')); - }); - - describe('Correlations', function () { - loadTestFile(require.resolve('./correlations/slow_transactions')); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts deleted file mode 100644 index e37d98b41b7af..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ /dev/null @@ -1,131 +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 { sortBy } from 'lodash'; -import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - - const range = archives_metadata[archiveName]; - - // url parameters - const start = encodeURIComponent(range.start); - const end = encodeURIComponent(range.end); - - const uiFilters = encodeURIComponent(JSON.stringify({})); - - describe('APM Services Overview', () => { - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - describe('with the default APM read user', () => { - describe('and fetching a list of services', () => { - let response: { - status: number; - body: APIReturnType<'GET /api/apm/services'>; - }; - - before(async () => { - response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - }); - - it('the response is successful', () => { - expect(response.status).to.eql(200); - }); - - it('there is at least one service', () => { - expect(response.body.items.length).to.be.greaterThan(0); - }); - - it('some items have a health status set', () => { - // Under the assumption that the loaded archive has - // at least one APM ML job, and the time range is longer - // than 15m, at least one items should have a health status - // set. Note that we currently have a bug where healthy - // services report as unknown (so without any health status): - // https://github.com/elastic/kibana/issues/77083 - - const healthStatuses = sortBy(response.body.items, 'serviceName').map( - (item: any) => item.healthStatus - ); - - expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0); - - expectSnapshot(healthStatuses).toMatchInline(` - Array [ - "healthy", - "healthy", - "healthy", - "healthy", - "healthy", - "healthy", - "healthy", - "healthy", - "healthy", - ] - `); - }); - }); - }); - - describe('with a user that does not have access to ML', () => { - let response: PromiseReturnType; - before(async () => { - response = await supertestAsApmReadUserWithoutMlAccess.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - }); - - it('the response is successful', () => { - expect(response.status).to.eql(200); - }); - - it('there is at least one service', () => { - expect(response.body.items.length).to.be.greaterThan(0); - }); - - it('contains no health statuses', () => { - const definedHealthStatuses = response.body.items - .map((item: any) => item.healthStatus) - .filter(Boolean); - - expect(definedHealthStatuses.length).to.be(0); - }); - }); - - describe('and fetching a list of services with a filter', () => { - let response: PromiseReturnType; - before(async () => { - response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${encodeURIComponent( - `{"kuery":"service.name:opbeans-java","environment":"ENVIRONMENT_ALL"}` - )}` - ); - }); - - it('does not return health statuses for services that are not found in APM data', () => { - expect(response.status).to.be(200); - - expect(response.body.items.length).to.be(1); - - expect(response.body.items[0].serviceName).to.be('opbeans-java'); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts deleted file mode 100644 index a917bdb3cea23..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts +++ /dev/null @@ -1,41 +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 '../../../../common/ftr_provider_context'; - -export default function apiTest({ getService }: FtrProviderContext) { - const noAccessUser = getService('supertestAsNoAccessUser'); - - function getJobs() { - return noAccessUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); - } - - function createJobs(environments: string[]) { - return noAccessUser - .post(`/api/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); - } - - describe('when user does not have read access to ML', () => { - describe('when calling the endpoint for listing jobs', () => { - it('returns an error because the user does not have access', async () => { - const { body } = await getJobs(); - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - }); - }); - - describe('when calling create endpoint', () => { - it('returns an error because the user does not have access', async () => { - const { body } = await createJobs(['production', 'staging']); - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts deleted file mode 100644 index 2265c4dc0a41d..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -export default function apiTest({ getService }: FtrProviderContext) { - const apmReadUser = getService('supertestAsApmReadUser'); - - function getJobs() { - return apmReadUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); - } - - function createJobs(environments: string[]) { - return apmReadUser - .post(`/api/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); - } - - describe('when user has read access to ML', () => { - describe('when calling the endpoint for listing jobs', () => { - it('returns a list of jobs', async () => { - const { body } = await getJobs(); - - expect(body.jobs.length).to.be(0); - expect(body.hasLegacyJobs).to.be(false); - }); - }); - - describe('when calling create endpoint', () => { - it('returns an error because the user does not have access', async () => { - const { body } = await createJobs(['production', 'staging']); - - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts deleted file mode 100644 index 720d66e1efcc8..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts +++ /dev/null @@ -1,53 +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 '../../../../common/ftr_provider_context'; - -export default function apiTest({ getService }: FtrProviderContext) { - const apmWriteUser = getService('supertestAsApmWriteUser'); - - function getJobs() { - return apmWriteUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); - } - - function createJobs(environments: string[]) { - return apmWriteUser - .post(`/api/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); - } - - function deleteJobs(jobIds: string[]) { - return apmWriteUser.post(`/api/ml/jobs/delete_jobs`).send({ jobIds }).set('kbn-xsrf', 'foo'); - } - - describe('when user has write access to ML', () => { - after(async () => { - const res = await getJobs(); - const jobIds = res.body.jobs.map((job: any) => job.job_id); - await deleteJobs(jobIds); - }); - - describe('when calling the endpoint for listing jobs', () => { - it('returns a list of jobs', async () => { - const { body } = await getJobs(); - expect(body.jobs.length).to.be(0); - expect(body.hasLegacyJobs).to.be(false); - }); - }); - - describe('when calling create endpoint', () => { - it('creates two jobs', async () => { - await createJobs(['production', 'staging']); - - const { body } = await getJobs(); - expect(body.hasLegacyJobs).to.be(false); - expect(body.jobs.map((job: any) => job.environment)).to.eql(['production', 'staging']); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts deleted file mode 100644 index bcfe8fce4b948..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts +++ /dev/null @@ -1,159 +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 URL from 'url'; -import expect from '@kbn/expect'; -import { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function customLinksTests({ getService }: FtrProviderContext) { - const supertestRead = getService('supertest'); - const supertestWrite = getService('supertestAsApmWriteUser'); - const log = getService('log'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - - function searchCustomLinks(filters?: any) { - const path = URL.format({ - pathname: `/api/apm/settings/custom_links`, - query: filters, - }); - return supertestRead.get(path).set('kbn-xsrf', 'foo'); - } - - async function createCustomLink(customLink: CustomLink) { - log.debug('creating configuration', customLink); - const res = await supertestWrite - .post(`/api/apm/settings/custom_links`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - async function updateCustomLink(id: string, customLink: CustomLink) { - log.debug('updating configuration', id, customLink); - const res = await supertestWrite - .put(`/api/apm/settings/custom_links/${id}`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - async function deleteCustomLink(id: string) { - log.debug('deleting configuration', id); - const res = await supertestWrite - .delete(`/api/apm/settings/custom_links/${id}`) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - function throwOnError(res: any) { - const { statusCode, req, body } = res; - if (statusCode !== 200) { - throw new Error(` - Endpoint: ${req.method} ${req.path} - Service: ${JSON.stringify(res.request._data.service)} - Status code: ${statusCode} - Response: ${body.message}`); - } - } - - describe('custom links', () => { - before(async () => { - const customLink = { - url: 'https://elastic.co', - label: 'with filters', - filters: [ - { key: 'service.name', value: 'baz' }, - { key: 'transaction.type', value: 'qux' }, - ], - } as CustomLink; - await createCustomLink(customLink); - }); - it('fetches a custom link', async () => { - const { status, body } = await searchCustomLinks({ - 'service.name': 'baz', - 'transaction.type': 'qux', - }); - const { label, url, filters } = body[0]; - - expect(status).to.equal(200); - expect({ label, url, filters }).to.eql({ - label: 'with filters', - url: 'https://elastic.co', - filters: [ - { key: 'service.name', value: 'baz' }, - { key: 'transaction.type', value: 'qux' }, - ], - }); - }); - it('updates a custom link', async () => { - let { status, body } = await searchCustomLinks({ - 'service.name': 'baz', - 'transaction.type': 'qux', - }); - expect(status).to.equal(200); - await updateCustomLink(body[0].id, { - label: 'foo', - url: 'https://elastic.co?service.name={{service.name}}', - filters: [ - { key: 'service.name', value: 'quz' }, - { key: 'transaction.name', value: 'bar' }, - ], - }); - ({ status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - })); - const { label, url, filters } = body[0]; - expect(status).to.equal(200); - expect({ label, url, filters }).to.eql({ - label: 'foo', - url: 'https://elastic.co?service.name={{service.name}}', - filters: [ - { key: 'service.name', value: 'quz' }, - { key: 'transaction.name', value: 'bar' }, - ], - }); - }); - it('deletes a custom link', async () => { - let { status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - }); - expect(status).to.equal(200); - await deleteCustomLink(body[0].id); - ({ status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - })); - expect(status).to.equal(200); - expect(body).to.eql([]); - }); - - describe('transaction', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('fetches a transaction sample', async () => { - const response = await supertestRead.get( - '/api/apm/settings/custom_links/transaction?service.name=opbeans-java' - ); - expect(response.status).to.be(200); - expect(response.body.service.name).to.eql('opbeans-java'); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap b/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap deleted file mode 100644 index 9475670387a08..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`APM specs (trial) Transactions Latency when data is loaded and fetching transaction charts with uiFilters when not defined environments seleted should return the correct anomaly boundaries 1`] = ` -Array [ - Object { - "x": 1607436000000, - "y": 0, - "y0": 0, - }, - Object { - "x": 1607436900000, - "y": 0, - "y0": 0, - }, -] -`; - -exports[`APM specs (trial) Transactions Latency when data is loaded and fetching transaction charts with uiFilters with environment selected and empty kuery filter should return a non-empty anomaly series 1`] = ` -Array [ - Object { - "x": 1607436000000, - "y": 1625128.56211579, - "y0": 7533.02707532227, - }, - Object { - "x": 1607436900000, - "y": 1660982.24115757, - "y0": 5732.00699123528, - }, -] -`; - -exports[`APM specs (trial) Transactions Latency when data is loaded and fetching transaction charts with uiFilters with environment selected in uiFilters should return a non-empty anomaly series 1`] = ` -Array [ - Object { - "x": 1607436000000, - "y": 1625128.56211579, - "y0": 7533.02707532227, - }, - Object { - "x": 1607436900000, - "y": 1660982.24115757, - "y0": 5732.00699123528, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/trial/tests/transactions/latency.ts b/x-pack/test/apm_api_integration/trial/tests/transactions/latency.ts deleted file mode 100644 index e0b9559be7208..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/transactions/latency.ts +++ /dev/null @@ -1,159 +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 { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - - const range = archives_metadata[archiveName]; - - // url parameters - const start = encodeURIComponent(range.start); - const end = encodeURIComponent(range.end); - const transactionType = 'request'; - - describe('Latency', () => { - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - describe('and fetching transaction charts with uiFilters', () => { - let response: PromiseReturnType; - - describe('without environment', () => { - const uiFilters = encodeURIComponent(JSON.stringify({})); - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` - ); - }); - it('should return an error response', () => { - expect(response.status).to.eql(400); - }); - }); - - describe('without uiFilters', () => { - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&latencyAggregationType=avg` - ); - }); - it('should return an error response', () => { - expect(response.status).to.eql(400); - }); - }); - - describe('with environment selected in uiFilters', () => { - const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'production' })); - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` - ); - }); - - it('should have a successful response', () => { - expect(response.status).to.eql(200); - }); - - it('should return the ML job id for anomalies of the selected environment', () => { - expect(response.body).to.have.property('anomalyTimeseries'); - expect(response.body.anomalyTimeseries).to.have.property('jobId'); - expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline( - `"apm-production-1369-high_mean_transaction_duration"` - ); - }); - - it('should return a non-empty anomaly series', () => { - expect(response.body).to.have.property('anomalyTimeseries'); - expect(response.body.anomalyTimeseries.anomalyBoundaries?.length).to.be.greaterThan(0); - expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch(); - }); - }); - - describe('when not defined environments seleted', () => { - const uiFilters = encodeURIComponent( - JSON.stringify({ environment: 'ENVIRONMENT_NOT_DEFINED' }) - ); - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-python/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` - ); - }); - - it('should have a successful response', () => { - expect(response.status).to.eql(200); - }); - - it('should return the ML job id for anomalies with no defined environment', () => { - expect(response.body).to.have.property('anomalyTimeseries'); - expect(response.body.anomalyTimeseries).to.have.property('jobId'); - expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline( - `"apm-environment_not_defined-5626-high_mean_transaction_duration"` - ); - }); - - it('should return the correct anomaly boundaries', () => { - expect(response.body).to.have.property('anomalyTimeseries'); - expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch(); - }); - }); - - describe('with all environments selected', () => { - const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'ENVIRONMENT_ALL' })); - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` - ); - }); - - it('should have a successful response', () => { - expect(response.status).to.eql(200); - }); - - it('should not return anomaly timeseries data', () => { - expect(response.body).to.not.have.property('anomalyTimeseries'); - }); - }); - - describe('with environment selected and empty kuery filter', () => { - const uiFilters = encodeURIComponent( - JSON.stringify({ kuery: '', environment: 'production' }) - ); - before(async () => { - response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg` - ); - }); - - it('should have a successful response', () => { - expect(response.status).to.eql(200); - }); - - it('should return the ML job id for anomalies of the selected environment', () => { - expect(response.body).to.have.property('anomalyTimeseries'); - expect(response.body.anomalyTimeseries).to.have.property('jobId'); - expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline( - `"apm-production-1369-high_mean_transaction_duration"` - ); - }); - - it('should return a non-empty anomaly series', () => { - expect(response.body).to.have.property('anomalyTimeseries'); - expect(response.body.anomalyTimeseries.anomalyBoundaries?.length).to.be.greaterThan(0); - expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch(); - }); - }); - }); - }); - }); -} From 64e9cf0440e1e60b25a8b04939cb9d0097efcb53 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 27 Jan 2021 12:45:49 +0200 Subject: [PATCH 026/163] Cleanup OSS code from visualizations wizard (#89092) * Cleanup OSS code from visualizations wizard * Remove unecessary translations * Remove unused translation * Fix functional test * Disable the functional test for OSS * Remove from oss correctly :D * Fix ci --- .github/CODEOWNERS | 2 - .i18nrc.json | 2 - docs/developer/plugin-list.asciidoc | 10 --- .../deprecation/deprecation_factory.test.ts | 85 +------------------ .../src/deprecation/deprecation_factory.ts | 26 ------ packages/kbn-config/src/deprecation/index.ts | 2 +- packages/kbn-config/src/index.ts | 1 - packages/kbn-optimizer/limits.yml | 2 - src/plugins/lens_oss/README.md | 6 -- src/plugins/lens_oss/common/constants.ts | 12 --- src/plugins/lens_oss/common/index.ts | 9 -- src/plugins/lens_oss/config.ts | 15 ---- src/plugins/lens_oss/kibana.json | 10 --- src/plugins/lens_oss/public/index.ts | 11 --- src/plugins/lens_oss/public/plugin.ts | 32 ------- src/plugins/lens_oss/public/vis_type_alias.ts | 37 -------- src/plugins/lens_oss/server/index.ts | 21 ----- src/plugins/lens_oss/tsconfig.json | 20 ----- src/plugins/maps_oss/README.md | 6 -- src/plugins/maps_oss/common/constants.ts | 12 --- src/plugins/maps_oss/common/index.ts | 9 -- src/plugins/maps_oss/config.ts | 15 ---- src/plugins/maps_oss/kibana.json | 10 --- src/plugins/maps_oss/public/index.ts | 11 --- src/plugins/maps_oss/public/plugin.ts | 32 ------- src/plugins/maps_oss/public/vis_type_alias.ts | 33 ------- src/plugins/maps_oss/server/index.ts | 21 ----- .../vis_types/vis_type_alias_registry.ts | 7 -- .../group_selection/group_selection.test.tsx | 40 --------- .../group_selection/group_selection.tsx | 26 ------ .../functional/apps/visualize/_chart_types.ts | 18 +--- test/functional/apps/visualize/index.ts | 6 +- tsconfig.json | 2 - tsconfig.refs.json | 1 - x-pack/plugins/lens/kibana.json | 2 +- x-pack/plugins/lens/public/plugin.ts | 3 - x-pack/plugins/lens/tsconfig.json | 1 - x-pack/plugins/maps/kibana.json | 2 +- x-pack/plugins/maps/public/plugin.ts | 4 - .../translations/translations/ja-JP.json | 8 -- .../translations/translations/zh-CN.json | 8 -- 41 files changed, 10 insertions(+), 570 deletions(-) delete mode 100644 src/plugins/lens_oss/README.md delete mode 100644 src/plugins/lens_oss/common/constants.ts delete mode 100644 src/plugins/lens_oss/common/index.ts delete mode 100644 src/plugins/lens_oss/config.ts delete mode 100644 src/plugins/lens_oss/kibana.json delete mode 100644 src/plugins/lens_oss/public/index.ts delete mode 100644 src/plugins/lens_oss/public/plugin.ts delete mode 100644 src/plugins/lens_oss/public/vis_type_alias.ts delete mode 100644 src/plugins/lens_oss/server/index.ts delete mode 100644 src/plugins/lens_oss/tsconfig.json delete mode 100644 src/plugins/maps_oss/README.md delete mode 100644 src/plugins/maps_oss/common/constants.ts delete mode 100644 src/plugins/maps_oss/common/index.ts delete mode 100644 src/plugins/maps_oss/config.ts delete mode 100644 src/plugins/maps_oss/kibana.json delete mode 100644 src/plugins/maps_oss/public/index.ts delete mode 100644 src/plugins/maps_oss/public/plugin.ts delete mode 100644 src/plugins/maps_oss/public/vis_type_alias.ts delete mode 100644 src/plugins/maps_oss/server/index.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b47d78ea6d691..0630937d5ac4b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,7 +13,6 @@ /src/plugins/advanced_settings/ @elastic/kibana-app /src/plugins/charts/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app -/src/plugins/lens_oss/ @elastic/kibana-app /src/plugins/management/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app @@ -127,7 +126,6 @@ /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis -#CC# /src/plugins/maps_oss/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis #CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis diff --git a/.i18nrc.json b/.i18nrc.json index b425dd99857dc..0cdcae08e54e0 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -61,8 +61,6 @@ "visTypeVislib": "src/plugins/vis_type_vislib", "visTypeXy": "src/plugins/vis_type_xy", "visualizations": "src/plugins/visualizations", - "lensOss": "src/plugins/lens_oss", - "mapsOss": "src/plugins/maps_oss", "visualize": "src/plugins/visualize", "apmOss": "src/plugins/apm_oss", "usageCollection": "src/plugins/usage_collection" diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 7ce4896a8bce4..fd4ed75352b1f 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -130,11 +130,6 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |The legacyExport plugin adds support for the legacy saved objects export format. -|{kib-repo}blob/{branch}/src/plugins/lens_oss/README.md[lensOss] -|The lens_oss plugin registers the lens visualization on OSS. -It is registered as disabled. The x-pack plugin should unregister this. - - |{kib-repo}blob/{branch}/src/plugins/management/README.md[management] |This plugins contains the "Stack Management" page framework. It offers navigation and an API to link individual managment section into it. This plugin does not contain any individual @@ -145,11 +140,6 @@ management section itself. |Internal objects used by the Coordinate, Region, and Vega visualizations. -|{kib-repo}blob/{branch}/src/plugins/maps_oss/README.md[mapsOss] -|The maps_oss plugin registers the maps visualization on OSS. -It is registered as disabled. The x-pack plugin should unregister this. - - |{kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] |The navigation plugins exports the TopNavMenu component. It also provides a stateful version of it on the start contract. diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts index 5184494dd74bf..1c13c539c3746 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts @@ -7,7 +7,7 @@ */ import { ConfigDeprecationLogger } from './types'; -import { configDeprecationFactory, copyFromRoot } from './deprecation_factory'; +import { configDeprecationFactory } from './deprecation_factory'; describe('DeprecationFactory', () => { const { rename, unused, renameFromRoot, unusedFromRoot } = configDeprecationFactory; @@ -239,89 +239,6 @@ describe('DeprecationFactory', () => { }); }); - describe('copyFromRoot', () => { - it('copies a property to a different namespace', () => { - const rawConfig = { - originplugin: { - deprecated: 'toberenamed', - valid: 'valid', - }, - destinationplugin: { - property: 'value', - }, - }; - const processed = copyFromRoot('originplugin.deprecated', 'destinationplugin.renamed')( - rawConfig, - 'does-not-matter', - logger - ); - expect(processed).toEqual({ - originplugin: { - deprecated: 'toberenamed', - valid: 'valid', - }, - destinationplugin: { - renamed: 'toberenamed', - property: 'value', - }, - }); - expect(deprecationMessages.length).toEqual(0); - }); - - it('does not alter config if origin property is not present', () => { - const rawConfig = { - myplugin: { - new: 'new', - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, - }; - const processed = copyFromRoot('myplugin.deprecated', 'myplugin.new')( - rawConfig, - 'does-not-matter', - logger - ); - expect(processed).toEqual({ - myplugin: { - new: 'new', - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, - }); - expect(deprecationMessages.length).toEqual(0); - }); - - it('does not alter config if they both exist', () => { - const rawConfig = { - myplugin: { - deprecated: 'deprecated', - renamed: 'renamed', - }, - someOtherPlugin: { - property: 'value', - }, - }; - const processed = copyFromRoot('myplugin.deprecated', 'someOtherPlugin.property')( - rawConfig, - 'does-not-matter', - logger - ); - expect(processed).toEqual({ - myplugin: { - deprecated: 'deprecated', - renamed: 'renamed', - }, - someOtherPlugin: { - property: 'value', - }, - }); - }); - }); - describe('unused', () => { it('removes the unused property from the config and logs a warning is present', () => { const rawConfig = { diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.ts b/packages/kbn-config/src/deprecation/deprecation_factory.ts index 40e29f9739156..04826446dc1ad 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.ts @@ -45,26 +45,6 @@ const _rename = ( return config; }; -const _copy = ( - config: Record, - rootPath: string, - originKey: string, - destinationKey: string -) => { - const originPath = getPath(rootPath, originKey); - const originValue = get(config, originPath); - if (originValue === undefined) { - return config; - } - - const destinationPath = getPath(rootPath, destinationKey); - const destinationValue = get(config, destinationPath); - if (destinationValue === undefined) { - set(config, destinationPath, originValue); - } - return config; -}; - const _unused = ( config: Record, rootPath: string, @@ -89,12 +69,6 @@ const renameFromRoot = (oldKey: string, newKey: string, silent?: boolean): Confi log ) => _rename(config, '', log, oldKey, newKey, silent); -export const copyFromRoot = (originKey: string, destinationKey: string): ConfigDeprecation => ( - config, - rootPath, - log -) => _copy(config, '', originKey, destinationKey); - const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => _unused(config, rootPath, log, unusedKey); diff --git a/packages/kbn-config/src/deprecation/index.ts b/packages/kbn-config/src/deprecation/index.ts index 87ec43d1fb09f..6deebd27399bb 100644 --- a/packages/kbn-config/src/deprecation/index.ts +++ b/packages/kbn-config/src/deprecation/index.ts @@ -13,5 +13,5 @@ export { ConfigDeprecationFactory, ConfigDeprecationProvider, } from './types'; -export { configDeprecationFactory, copyFromRoot } from './deprecation_factory'; +export { configDeprecationFactory } from './deprecation_factory'; export { applyDeprecations } from './apply_deprecations'; diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index e1743270f62c7..a803e3fd2dc8e 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -14,7 +14,6 @@ export { ConfigDeprecationLogger, ConfigDeprecationProvider, ConfigDeprecationWithContext, - copyFromRoot, } from './deprecation'; export { RawConfigurationProvider, RawConfigService, getConfigFromFiles } from './raw'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index ef672d6cbeb2e..1a4fb390d0c17 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -44,7 +44,6 @@ pageLoadAssetSize: kibanaReact: 161921 kibanaUtils: 198829 lens: 96624 - lensOss: 19341 licenseManagement: 41817 licensing: 39008 lists: 202261 @@ -53,7 +52,6 @@ pageLoadAssetSize: maps: 183610 mapsLegacy: 116817 mapsLegacyLicensing: 20214 - mapsOss: 19284 ml: 82187 monitoring: 50000 navigation: 37269 diff --git a/src/plugins/lens_oss/README.md b/src/plugins/lens_oss/README.md deleted file mode 100644 index 187da2497026e..0000000000000 --- a/src/plugins/lens_oss/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# lens_oss - -The lens_oss plugin registers the lens visualization on OSS. -It is registered as disabled. The x-pack plugin should unregister this. - -`visualizations.unregisterAlias('lensOss')` \ No newline at end of file diff --git a/src/plugins/lens_oss/common/constants.ts b/src/plugins/lens_oss/common/constants.ts deleted file mode 100644 index 0ff5cdd78bb1b..0000000000000 --- a/src/plugins/lens_oss/common/constants.ts +++ /dev/null @@ -1,12 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -export const APP_NAME = 'lens'; -export const PLUGIN_ID_OSS = 'lensOss'; -export const APP_PATH = '#/'; -export const APP_ICON = 'lensApp'; diff --git a/src/plugins/lens_oss/common/index.ts b/src/plugins/lens_oss/common/index.ts deleted file mode 100644 index 7f60b8508dc27..0000000000000 --- a/src/plugins/lens_oss/common/index.ts +++ /dev/null @@ -1,9 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -export * from './constants'; diff --git a/src/plugins/lens_oss/config.ts b/src/plugins/lens_oss/config.ts deleted file mode 100644 index 58c50f0104f46..0000000000000 --- a/src/plugins/lens_oss/config.ts +++ /dev/null @@ -1,15 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; - -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), -}); - -export type ConfigSchema = TypeOf; diff --git a/src/plugins/lens_oss/kibana.json b/src/plugins/lens_oss/kibana.json deleted file mode 100644 index 3e3d3585f37fb..0000000000000 --- a/src/plugins/lens_oss/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "lensOss", - "version": "kibana", - "ui": true, - "server": true, - "requiredPlugins": [ - "visualizations" - ], - "extraPublicDirs": ["common/constants"] -} diff --git a/src/plugins/lens_oss/public/index.ts b/src/plugins/lens_oss/public/index.ts deleted file mode 100644 index 2f68f6d183a22..0000000000000 --- a/src/plugins/lens_oss/public/index.ts +++ /dev/null @@ -1,11 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { LensOSSPlugin } from './plugin'; - -export const plugin = () => new LensOSSPlugin(); diff --git a/src/plugins/lens_oss/public/plugin.ts b/src/plugins/lens_oss/public/plugin.ts deleted file mode 100644 index 5a441614b7e24..0000000000000 --- a/src/plugins/lens_oss/public/plugin.ts +++ /dev/null @@ -1,32 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { DocLinksStart, CoreSetup } from 'src/core/public'; -import { VisualizationsSetup } from '../../visualizations/public'; -import { getLensAliasConfig } from './vis_type_alias'; - -export interface LensPluginSetupDependencies { - visualizations: VisualizationsSetup; -} - -export interface LensPluginStartDependencies { - docLinks: DocLinksStart; -} - -export class LensOSSPlugin { - setup( - core: CoreSetup, - { visualizations }: LensPluginSetupDependencies - ) { - core.getStartServices().then(([coreStart]) => { - visualizations.registerAlias(getLensAliasConfig(coreStart.docLinks)); - }); - } - - start() {} -} diff --git a/src/plugins/lens_oss/public/vis_type_alias.ts b/src/plugins/lens_oss/public/vis_type_alias.ts deleted file mode 100644 index b9806bbf3b4e5..0000000000000 --- a/src/plugins/lens_oss/public/vis_type_alias.ts +++ /dev/null @@ -1,37 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { VisTypeAlias } from 'src/plugins/visualizations/public'; -import { DocLinksStart } from 'src/core/public'; -import { APP_NAME, PLUGIN_ID_OSS, APP_PATH, APP_ICON } from '../common'; - -export const getLensAliasConfig = ({ links }: DocLinksStart): VisTypeAlias => ({ - aliasPath: APP_PATH, - aliasApp: APP_NAME, - name: PLUGIN_ID_OSS, - title: i18n.translate('lensOss.visTypeAlias.title', { - defaultMessage: 'Lens', - }), - description: i18n.translate('lensOss.visTypeAlias.description', { - defaultMessage: - 'Create visualizations with our drag-and-drop editor. Switch between visualization types at any time. Best for most visualizations.', - }), - icon: APP_ICON, - stage: 'production', - disabled: true, - note: i18n.translate('lensOss.visTypeAlias.note', { - defaultMessage: 'Recommended for most users.', - }), - promoTooltip: { - description: i18n.translate('lensOss.visTypeAlias.promoTooltip.description', { - defaultMessage: 'Try Lens for free with Elastic. Learn more.', - }), - link: `${links.visualize.lens}?blade=kibanaossvizwizard`, - }, -}); diff --git a/src/plugins/lens_oss/server/index.ts b/src/plugins/lens_oss/server/index.ts deleted file mode 100644 index d13a9b2caaeb2..0000000000000 --- a/src/plugins/lens_oss/server/index.ts +++ /dev/null @@ -1,21 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { PluginConfigDescriptor } from 'kibana/server'; -import { copyFromRoot } from '@kbn/config'; -import { configSchema, ConfigSchema } from '../config'; - -export const config: PluginConfigDescriptor = { - schema: configSchema, - deprecations: () => [copyFromRoot('xpack.lens.enabled', 'lens_oss.enabled')], -}; - -export const plugin = () => ({ - setup() {}, - start() {}, -}); diff --git a/src/plugins/lens_oss/tsconfig.json b/src/plugins/lens_oss/tsconfig.json deleted file mode 100644 index d7bbc593fa87b..0000000000000 --- a/src/plugins/lens_oss/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - "*.ts" - ], - "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../visualizations/tsconfig.json" } - ] -} diff --git a/src/plugins/maps_oss/README.md b/src/plugins/maps_oss/README.md deleted file mode 100644 index ed91de500fbfb..0000000000000 --- a/src/plugins/maps_oss/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# maps_oss - -The maps_oss plugin registers the maps visualization on OSS. -It is registered as disabled. The x-pack plugin should unregister this. - -`visualizations.unregisterAlias('mapsOss')` \ No newline at end of file diff --git a/src/plugins/maps_oss/common/constants.ts b/src/plugins/maps_oss/common/constants.ts deleted file mode 100644 index db29f541a03df..0000000000000 --- a/src/plugins/maps_oss/common/constants.ts +++ /dev/null @@ -1,12 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -export const APP_NAME = 'maps'; -export const PLUGIN_ID_OSS = 'mapsOss'; -export const APP_PATH = '/map'; -export const APP_ICON = 'gisApp'; diff --git a/src/plugins/maps_oss/common/index.ts b/src/plugins/maps_oss/common/index.ts deleted file mode 100644 index 7f60b8508dc27..0000000000000 --- a/src/plugins/maps_oss/common/index.ts +++ /dev/null @@ -1,9 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -export * from './constants'; diff --git a/src/plugins/maps_oss/config.ts b/src/plugins/maps_oss/config.ts deleted file mode 100644 index 58c50f0104f46..0000000000000 --- a/src/plugins/maps_oss/config.ts +++ /dev/null @@ -1,15 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; - -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), -}); - -export type ConfigSchema = TypeOf; diff --git a/src/plugins/maps_oss/kibana.json b/src/plugins/maps_oss/kibana.json deleted file mode 100644 index 19770dcffaadd..0000000000000 --- a/src/plugins/maps_oss/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "mapsOss", - "version": "kibana", - "ui": true, - "server": true, - "requiredPlugins": [ - "visualizations" - ], - "extraPublicDirs": ["common/constants"] -} diff --git a/src/plugins/maps_oss/public/index.ts b/src/plugins/maps_oss/public/index.ts deleted file mode 100644 index 1d27dc4b6d996..0000000000000 --- a/src/plugins/maps_oss/public/index.ts +++ /dev/null @@ -1,11 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { MapsOSSPlugin } from './plugin'; - -export const plugin = () => new MapsOSSPlugin(); diff --git a/src/plugins/maps_oss/public/plugin.ts b/src/plugins/maps_oss/public/plugin.ts deleted file mode 100644 index 5e27ae34257bf..0000000000000 --- a/src/plugins/maps_oss/public/plugin.ts +++ /dev/null @@ -1,32 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { DocLinksStart, CoreSetup } from 'src/core/public'; -import { VisualizationsSetup } from '../../visualizations/public'; -import { getMapsAliasConfig } from './vis_type_alias'; - -export interface MapsPluginSetupDependencies { - visualizations: VisualizationsSetup; -} - -export interface MapsPluginStartDependencies { - docLinks: DocLinksStart; -} - -export class MapsOSSPlugin { - setup( - core: CoreSetup, - { visualizations }: MapsPluginSetupDependencies - ) { - core.getStartServices().then(([coreStart]) => { - visualizations.registerAlias(getMapsAliasConfig(coreStart.docLinks)); - }); - } - - start() {} -} diff --git a/src/plugins/maps_oss/public/vis_type_alias.ts b/src/plugins/maps_oss/public/vis_type_alias.ts deleted file mode 100644 index a27c628755cf6..0000000000000 --- a/src/plugins/maps_oss/public/vis_type_alias.ts +++ /dev/null @@ -1,33 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { VisTypeAlias } from 'src/plugins/visualizations/public'; -import { DocLinksStart } from 'src/core/public'; -import { APP_NAME, PLUGIN_ID_OSS, APP_PATH, APP_ICON } from '../common'; - -export const getMapsAliasConfig = ({ links }: DocLinksStart): VisTypeAlias => ({ - aliasPath: APP_PATH, - aliasApp: APP_NAME, - name: PLUGIN_ID_OSS, - title: i18n.translate('mapsOss.visTypeAlias.title', { - defaultMessage: 'Maps', - }), - description: i18n.translate('mapsOss.visTypeAlias.description', { - defaultMessage: 'Plot and style your geo data in a multi layer map.', - }), - icon: APP_ICON, - stage: 'production', - disabled: true, - promoTooltip: { - description: i18n.translate('mapsOss.visTypeAlias.promoTooltip.description', { - defaultMessage: 'Try maps for free with Elastic. Learn more.', - }), - link: `${links.visualize.maps}?blade=kibanaossvizwizard`, - }, -}); diff --git a/src/plugins/maps_oss/server/index.ts b/src/plugins/maps_oss/server/index.ts deleted file mode 100644 index 8f07beee705a6..0000000000000 --- a/src/plugins/maps_oss/server/index.ts +++ /dev/null @@ -1,21 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { PluginConfigDescriptor } from 'kibana/server'; -import { copyFromRoot } from '@kbn/config'; -import { configSchema, ConfigSchema } from '../config'; - -export const config: PluginConfigDescriptor = { - schema: configSchema, - deprecations: () => [copyFromRoot('xpack.maps.enabled', 'maps_oss.enabled')], -}; - -export const plugin = () => ({ - setup() {}, - start() {}, -}); diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index 839d2b1f57c34..cb6bc624801d9 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -31,11 +31,6 @@ export interface VisualizationsAppExtension { toListItem: (savedObject: SavedObject) => VisualizationListItem; } -export interface VisTypeAliasPromoTooltip { - description: string; - link: string; -} - export interface VisTypeAlias { aliasPath: string; aliasApp: string; @@ -43,10 +38,8 @@ export interface VisTypeAlias { title: string; icon: string; promotion?: boolean; - promoTooltip?: VisTypeAliasPromoTooltip; description: string; note?: string; - disabled?: boolean; getSupportedTriggers?: () => string[]; stage: VisualizationStage; diff --git a/src/plugins/visualizations/public/wizard/group_selection/group_selection.test.tsx b/src/plugins/visualizations/public/wizard/group_selection/group_selection.test.tsx index 74163296e31fd..396be30aca6d0 100644 --- a/src/plugins/visualizations/public/wizard/group_selection/group_selection.test.tsx +++ b/src/plugins/visualizations/public/wizard/group_selection/group_selection.test.tsx @@ -39,11 +39,6 @@ describe('GroupSelection', () => { title: 'Vis with alias Url', aliasApp: 'aliasApp', aliasPath: '#/aliasApp', - disabled: true, - promoTooltip: { - description: 'Learn More', - link: '#/anotherUrl', - }, description: 'Vis with alias Url', stage: 'production', group: VisGroups.PROMOTED, @@ -227,41 +222,6 @@ describe('GroupSelection', () => { ]); }); - it('should render disabled aliases with a disabled class', () => { - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="visType-visWithAliasUrl"]').exists()).toBe(true); - expect( - wrapper - .find('[data-test-subj="visType-visWithAliasUrl"]') - .at(1) - .hasClass('euiCard-isDisabled') - ).toBe(true); - }); - - it('should render a basic badge with link for disabled aliases with promoTooltip', () => { - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="visTypeBadge"]').exists()).toBe(true); - expect(wrapper.find('[data-test-subj="visTypeBadge"]').at(0).prop('tooltipContent')).toBe( - 'Learn More' - ); - }); - it('should not show tools experimental visualizations if showExperimentalis false', () => { const expVis = { name: 'visExp', diff --git a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx index 9b61b2c415e9f..594e37f6a7608 100644 --- a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx +++ b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx @@ -48,10 +48,6 @@ interface VisCardProps { showExperimental?: boolean | undefined; } -function isVisTypeAlias(type: BaseVisType | VisTypeAlias): type is VisTypeAlias { - return 'aliasPath' in type; -} - function GroupSelection(props: GroupSelectionProps) { const visualizeGuideLink = props.docLinks.links.dashboard.guide; const promotedVisGroups = useMemo( @@ -185,29 +181,8 @@ const VisGroup = ({ visType, onVisTypeSelected }: VisCardProps) => { const onClick = useCallback(() => { onVisTypeSelected(visType); }, [onVisTypeSelected, visType]); - const shouldDisableCard = isVisTypeAlias(visType) && visType.disabled; - const betaBadgeContent = - shouldDisableCard && 'promoTooltip' in visType ? ( - - - - ) : undefined; return ( - {betaBadgeContent} { } onClick={onClick} - isDisabled={shouldDisableCard} data-test-subj={`visType-${visType.name}`} data-vis-stage={!('aliasPath' in visType) ? visType.stage : 'alias'} aria-label={`visType-${visType.name}`} diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/_chart_types.ts index 55b68b7370148..69403f2090594 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/_chart_types.ts @@ -12,21 +12,17 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const deployment = getService('deployment'); const log = getService('log'); const PageObjects = getPageObjects(['visualize']); - let isOss = true; describe('chart types', function () { before(async function () { log.debug('navigateToApp visualize'); - isOss = await deployment.isOss(); await PageObjects.visualize.navigateToNewVisualization(); }); it('should show the promoted vis types for the first step', async function () { const expectedChartTypes = ['Custom visualization', 'Lens', 'Maps', 'TSVB']; - log.debug('oss= ' + isOss); // find all the chart types and make sure there all there const chartTypes = (await PageObjects.visualize.getPromotedVisTypes()).sort(); @@ -37,9 +33,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show the correct agg based chart types', async function () { await PageObjects.visualize.clickAggBasedVisualizations(); - let expectedChartTypes = [ + const expectedChartTypes = [ 'Area', - 'Coordinate Map', 'Data table', 'Gauge', 'Goal', @@ -48,21 +43,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Line', 'Metric', 'Pie', - 'Region Map', 'Tag cloud', 'Timelion', 'Vertical bar', ]; - if (!isOss) { - expectedChartTypes = _.remove(expectedChartTypes, function (n) { - return n !== 'Coordinate Map'; - }); - expectedChartTypes = _.remove(expectedChartTypes, function (n) { - return n !== 'Region Map'; - }); - expectedChartTypes.sort(); - } - log.debug('oss= ' + isOss); // find all the chart types and make sure there all there const chartTypes = (await PageObjects.visualize.getChartTypes()).sort(); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 8dd2854419693..4170ada692e67 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -67,11 +67,15 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags('ciGroup9'); loadTestFile(require.resolve('./_embedding_chart')); - loadTestFile(require.resolve('./_chart_types')); loadTestFile(require.resolve('./_area_chart')); loadTestFile(require.resolve('./_data_table')); loadTestFile(require.resolve('./_data_table_nontimeindex')); loadTestFile(require.resolve('./_data_table_notimeindex_filters')); + + // this check is not needed when the CI doesn't run anymore for the OSS + if (!isOss) { + loadTestFile(require.resolve('./_chart_types')); + } }); describe('', function () { diff --git a/tsconfig.json b/tsconfig.json index b6742bffeab55..e7856aa0c8747 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,6 @@ "src/plugins/kibana_react/**/*", "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", - "src/plugins/lens_oss/**/*", "src/plugins/management/**/*", "src/plugins/navigation/**/*", "src/plugins/newsfeed/**/*", @@ -81,7 +80,6 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, - { "path": "./src/plugins/lens_oss/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 1bce19e2ee44a..5edfd4231a6d5 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -22,7 +22,6 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, - { "path": "./src/plugins/lens_oss/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index dc0a92ac702d0..9df3f41fbd855 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -19,5 +19,5 @@ "optionalPlugins": ["usageCollection", "taskManager", "globalSearch", "savedObjectsTagging"], "configPath": ["xpack", "lens"], "extraPublicDirs": ["common/constants"], - "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "lensOss", "presentationUtil"] + "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "presentationUtil"] } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index cdffdb342fd23..3fb7186aeac59 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -39,7 +39,6 @@ import { VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; import { getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; -import { PLUGIN_ID_OSS } from '../../../../src/plugins/lens_oss/common/constants'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; @@ -208,8 +207,6 @@ export class LensPlugin { start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart { const frameStart = this.editorFrameService.start(core, startDependencies); this.createEditorFrame = frameStart.createInstance; - // unregisters the OSS alias - startDependencies.visualizations.unRegisterAlias(PLUGIN_ID_OSS); // unregisters the Visualize action and registers the lens one if (startDependencies.uiActions.hasAction(ACTION_VISUALIZE_FIELD)) { startDependencies.uiActions.unregisterAction(ACTION_VISUALIZE_FIELD); diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 7ac5a2980d0ba..636d2f44b0217 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -35,7 +35,6 @@ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/embeddable/tsconfig.json"}, - { "path": "../../../src/plugins/lens_oss/tsconfig.json"}, { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, ] } \ No newline at end of file diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 5f6f5224e32a3..2536601d0e6b1 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -23,5 +23,5 @@ "ui": true, "server": true, "extraPublicDirs": ["common/constants"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "mapsOss", "presentationUtil"] + "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "presentationUtil"] } diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index dd256126fae62..4173328a41d57 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -34,7 +34,6 @@ import { VisualizationsStart, } from '../../../../src/plugins/visualizations/public'; import { APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; -import { PLUGIN_ID_OSS } from '../../../../src/plugins/maps_oss/common/constants'; import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../src/plugins/ui_actions/public'; import { createMapsUrlGenerator, @@ -162,9 +161,6 @@ export class MapsPlugin setLicensingPluginStart(plugins.licensing); setStartServices(core, plugins); - // unregisters the OSS alias - plugins.visualizations.unRegisterAlias(PLUGIN_ID_OSS); - if (core.application.capabilities.maps.show) { plugins.uiActions.addTriggerAction(VISUALIZE_GEO_FIELD_TRIGGER, visualizeGeoFieldAction); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e28b41e0ce5e3..da9228335a3f3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3032,10 +3032,6 @@ "kibanaOverview.manageData.sectionTitle": "データを管理", "kibanaOverview.more.title": "Elasticではさまざまなことが可能です", "kibanaOverview.news.title": "新機能", - "lensOss.visTypeAlias.description": "ドラッグアンドドロップエディターでビジュアライゼーションを作成します。いつでもビジュアライゼーションタイプを切り替えることができます。ほとんどのビジュアライゼーションに最適です。", - "lensOss.visTypeAlias.note": "ほとんどのユーザーに推奨されます。", - "lensOss.visTypeAlias.promoTooltip.description": "Elastic では Lens を無料でお試しいただけます。詳細情報", - "lensOss.visTypeAlias.title": "レンズ", "management.breadcrumb": "スタック管理", "management.landing.header": "Stack Management {version}へようこそ", "management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibanaの設定、その他を管理します。", @@ -3089,9 +3085,6 @@ "maps_legacy.wmsOptions.wmsStylesLabel": "WMSスタイル", "maps_legacy.wmsOptions.wmsUrlLabel": "WMS URL", "maps_legacy.wmsOptions.wmsVersionLabel": "WMS バージョン", - "mapsOss.visTypeAlias.description": "マルチレイヤーマップで地理データをプロットしてスタイル設定できます。", - "mapsOss.visTypeAlias.promoTooltip.description": "Elastic では Maps を無料でお試しいただけます。詳細情報", - "mapsOss.visTypeAlias.title": "マップ", "monaco.painlessLanguage.autocomplete.docKeywordDescription": "doc['field_name'] 構文を使用して、スクリプトからフィールド値にアクセスします", "monaco.painlessLanguage.autocomplete.emitKeywordDescription": "戻らずに値を発行します。", "monaco.painlessLanguage.autocomplete.fieldValueDescription": "フィールド「{fieldName}」の値を取得します", @@ -4692,7 +4685,6 @@ "visualizations.initializeWithoutIndexPatternErrorMessage": "インデックスパターンなしで集約を初期化しようとしています", "visualizations.newVisWizard.aggBasedGroupDescription": "クラシック Visualize ライブラリを使用して、アグリゲーションに基づいてグラフを作成します。", "visualizations.newVisWizard.aggBasedGroupTitle": "アグリゲーションに基づく", - "visualizations.newVisWizard.basicTitle": "基本", "visualizations.newVisWizard.chooseSourceTitle": "ソースの選択", "visualizations.newVisWizard.experimentalTitle": "実験的", "visualizations.newVisWizard.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 49fd9ce095f66..679edaf9e0cdd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3036,10 +3036,6 @@ "kibanaOverview.manageData.sectionTitle": "管理您的数据", "kibanaOverview.more.title": "Elastic 让您事半功倍", "kibanaOverview.news.title": "最新动态", - "lensOss.visTypeAlias.description": "使用我们支持拖放的编辑器来创建可视化。随时在可视化类型之间切换。绝大多数可视化的最佳选择。", - "lensOss.visTypeAlias.note": "适合绝大多数用户。", - "lensOss.visTypeAlias.promoTooltip.description": "免费试用 Elastic 的 Lens。了解详情。", - "lensOss.visTypeAlias.title": "Lens", "management.breadcrumb": "Stack Management", "management.landing.header": "欢迎使用 Stack Management {version}", "management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", @@ -3093,9 +3089,6 @@ "maps_legacy.wmsOptions.wmsStylesLabel": "WMS 样式", "maps_legacy.wmsOptions.wmsUrlLabel": "WMS url", "maps_legacy.wmsOptions.wmsVersionLabel": "WMS 版本", - "mapsOss.visTypeAlias.description": "在多层地图中绘制地理数据并设置其样式。", - "mapsOss.visTypeAlias.promoTooltip.description": "免费试用 Elastic 的地图。了解详情。", - "mapsOss.visTypeAlias.title": "地图", "monaco.painlessLanguage.autocomplete.docKeywordDescription": "使用 doc['field_name'] 语法,从脚本中访问字段值", "monaco.painlessLanguage.autocomplete.emitKeywordDescription": "发出值,而不返回值。", "monaco.painlessLanguage.autocomplete.fieldValueDescription": "检索字段“{fieldName}”的值", @@ -4697,7 +4690,6 @@ "visualizations.initializeWithoutIndexPatternErrorMessage": "正在尝试在不使用索引模式的情况下初始化聚合", "visualizations.newVisWizard.aggBasedGroupDescription": "使用我们的经典可视化库,基于聚合创建图表。", "visualizations.newVisWizard.aggBasedGroupTitle": "基于聚合", - "visualizations.newVisWizard.basicTitle": "基本级", "visualizations.newVisWizard.chooseSourceTitle": "选择源", "visualizations.newVisWizard.experimentalTitle": "实验性", "visualizations.newVisWizard.experimentalTooltip": "未来版本可能会更改或删除此可视化,其不受支持 SLA 的约束。", From b8947e3e1574d69cc91b297743908b48daf6acba Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 27 Jan 2021 11:52:13 +0100 Subject: [PATCH 027/163] [Search Sessions] Make search session indicator UI opt-in, refactor per-app capabilities (#88699) --- .../kibana-plugin-plugins-data-public.md | 1 + ...nosearchsessionstoragecapabilitymessage.md | 13 ++++ .../public/application/dashboard_router.tsx | 1 + .../embeddable/dashboard_container.tsx | 3 +- .../hooks/use_dashboard_state_manager.ts | 15 ++++- .../dashboard/public/application/types.ts | 1 + src/plugins/data/public/index.ts | 1 + src/plugins/data/public/public.api.md | 35 ++++++----- src/plugins/data/public/search/index.ts | 1 + .../data/public/search/session/i18n.ts | 20 +++++++ .../data/public/search/session/index.ts | 1 + .../data/public/search/session/mocks.ts | 4 +- .../search/session/session_service.test.ts | 60 ++++++++++++++++++- .../public/search/session/session_service.ts | 59 ++++++++++++++---- .../public/application/angular/discover.js | 14 ++++- ...onnected_search_session_indicator.test.tsx | 37 +++++++++--- .../connected_search_session_indicator.tsx | 29 +++------ .../config.ts | 1 + .../services/index.ts | 4 +- ...nd_to_background.ts => search_sessions.ts} | 26 +++++--- .../async_search/sessions_in_space.ts | 57 +++++++++++++++++- .../tests/apps/discover/sessions_in_space.ts | 59 +++++++++++++++++- .../tests/apps/lens/index.ts | 22 +++++++ .../tests/apps/lens/search_sessions.ts | 35 +++++++++++ 24 files changed, 424 insertions(+), 75 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md create mode 100644 src/plugins/data/public/search/session/i18n.ts rename x-pack/test/send_search_to_background_integration/services/{send_to_background.ts => search_sessions.ts} (81%) create mode 100644 x-pack/test/send_search_to_background_integration/tests/apps/lens/index.ts create mode 100644 x-pack/test/send_search_to_background_integration/tests/apps/lens/search_sessions.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 65a722868b37f..4bbc76b78ba03 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -125,6 +125,7 @@ | [isPartialResponse](./kibana-plugin-plugins-data-public.ispartialresponse.md) | | | [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | | | [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | | +| [noSearchSessionStorageCapabilityMessage](./kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md) | Message to display in case storing session session is disabled due to turned off capability | | [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | | | [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | | | [search](./kibana-plugin-plugins-data-public.search.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md new file mode 100644 index 0000000000000..2bb0a0db8f9b3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [noSearchSessionStorageCapabilityMessage](./kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md) + +## noSearchSessionStorageCapabilityMessage variable + +Message to display in case storing session session is disabled due to turned off capability + +Signature: + +```typescript +noSearchSessionStorageCapabilityMessage: string +``` diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index 9141f2e592fd7..5206c76f50be2 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -104,6 +104,7 @@ export async function mountApp({ mapsCapabilities: { save: Boolean(coreStart.application.capabilities.maps?.save) }, createShortUrl: Boolean(coreStart.application.capabilities.dashboard.createShortUrl), visualizeCapabilities: { save: Boolean(coreStart.application.capabilities.visualize?.save) }, + storeSearchSession: Boolean(coreStart.application.capabilities.dashboard.storeSearchSession), }, }; diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index fa520ec22497b..780eb1bad8c2b 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -89,7 +89,7 @@ export interface InheritedChildInput extends IndexSignature { export type DashboardReactContextValue = KibanaReactContextValue; export type DashboardReactContext = KibanaReactContext; -const defaultCapabilities = { +const defaultCapabilities: DashboardCapabilities = { show: false, createNew: false, saveQuery: false, @@ -97,6 +97,7 @@ const defaultCapabilities = { hideWriteControls: true, mapsCapabilities: { save: false }, visualizeCapabilities: { save: false }, + storeSearchSession: true, }; export class DashboardContainer extends Container { diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts index a4044e8668e59..93fbb50950850 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts @@ -16,6 +16,7 @@ import { useKibana } from '../../services/kibana_react'; import { connectToQueryState, esFilters, + noSearchSessionStorageCapabilityMessage, QueryState, syncQueryStateWithUrl, } from '../../services/data'; @@ -159,13 +160,22 @@ export const useDashboardStateManager = ( stateManager.isNew() ); - searchSession.setSearchSessionInfoProvider( + searchSession.enableStorage( createSessionRestorationDataProvider({ data: dataPlugin, getDashboardTitle: () => dashboardTitle, getDashboardId: () => savedDashboard?.id || '', getAppState: () => stateManager.getAppState(), - }) + }), + { + isDisabled: () => + dashboardCapabilities.storeSearchSession + ? { disabled: false } + : { + disabled: true, + reasonText: noSearchSessionStorageCapabilityMessage, + }, + } ); setDashboardStateManager(stateManager); @@ -192,6 +202,7 @@ export const useDashboardStateManager = ( toasts, uiSettings, usageCollection, + dashboardCapabilities.storeSearchSession, ]); return { dashboardStateManager, viewMode, setViewMode }; diff --git a/src/plugins/dashboard/public/application/types.ts b/src/plugins/dashboard/public/application/types.ts index 61e16beed61f4..e4f9388a919d1 100644 --- a/src/plugins/dashboard/public/application/types.ts +++ b/src/plugins/dashboard/public/application/types.ts @@ -55,6 +55,7 @@ export interface DashboardCapabilities { saveQuery: boolean; createNew: boolean; show: boolean; + storeSearchSession: boolean; } export interface DashboardAppServices { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index ff3e2ebc89a41..fc8c44e8d1870 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -384,6 +384,7 @@ export { SearchTimeoutError, TimeoutErrorMode, PainlessError, + noSearchSessionStorageCapabilityMessage, } from './search'; export type { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 002f033365790..9e493f46b0781 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1834,6 +1834,11 @@ export enum METRIC_TYPES { TOP_HITS = "top_hits" } +// Warning: (ae-missing-release-tag) "noSearchSessionStorageCapabilityMessage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const noSearchSessionStorageCapabilityMessage: string; + // Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2629,21 +2634,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:41:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 1deffc9c2d55e..3d87411883a67 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -37,6 +37,7 @@ export { SearchSessionState, SessionsClient, ISessionsClient, + noSearchSessionStorageCapabilityMessage, } from './session'; export { getEsPreference } from './es_search'; diff --git a/src/plugins/data/public/search/session/i18n.ts b/src/plugins/data/public/search/session/i18n.ts new file mode 100644 index 0000000000000..2ee36b46dfd5a --- /dev/null +++ b/src/plugins/data/public/search/session/i18n.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +/** + * Message to display in case storing + * session session is disabled due to turned off capability + */ +export const noSearchSessionStorageCapabilityMessage = i18n.translate( + 'data.searchSessionIndicator.noCapability', + { + defaultMessage: "You don't have permissions to create search sessions.", + } +); diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts index ab311d56fe096..f3f0c34c1be75 100644 --- a/src/plugins/data/public/search/session/index.ts +++ b/src/plugins/data/public/search/session/index.ts @@ -9,3 +9,4 @@ export { SessionService, ISessionService, SearchSessionInfoProvider } from './session_service'; export { SearchSessionState } from './search_session_state'; export { SessionsClient, ISessionsClient } from './sessions_client'; +export { noSearchSessionStorageCapabilityMessage } from './i18n'; diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index 6a7a207b90d46..679898e3e51dd 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -29,7 +29,6 @@ export function getSessionServiceMock(): jest.Mocked { getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), state$: new BehaviorSubject(SearchSessionState.None).asObservable(), - setSearchSessionInfoProvider: jest.fn(), trackSearch: jest.fn((searchDescriptor) => () => {}), destroy: jest.fn(), onRefresh$: new Subject(), @@ -40,5 +39,8 @@ export function getSessionServiceMock(): jest.Mocked { save: jest.fn(), isCurrentSession: jest.fn(), getSearchOptions: jest.fn(), + enableStorage: jest.fn(), + isSessionStorageReady: jest.fn(() => true), + getSearchSessionIndicatorUiConfig: jest.fn(() => ({ isDisabled: () => ({ disabled: false }) })), }; } diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 7797d92c4e07d..21c38c68e6a83 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -113,7 +113,7 @@ describe('Session service', () => { sessionId, }); - sessionService.setSearchSessionInfoProvider({ + sessionService.enableStorage({ getName: async () => 'Name', getUrlGeneratorData: async () => ({ urlGeneratorId: 'id', @@ -156,4 +156,62 @@ describe('Session service', () => { expect(sessionService.isCurrentSession('some-other')).toBeFalsy(); expect(sessionService.isCurrentSession(sessionId)).toBeTruthy(); }); + + test('enableStorage() enables storage capabilities', async () => { + sessionService.start(); + await expect(() => sessionService.save()).rejects.toThrowErrorMatchingInlineSnapshot( + `"No info provider for current session"` + ); + + expect(sessionService.isSessionStorageReady()).toBe(false); + + sessionService.enableStorage({ + getName: async () => 'Name', + getUrlGeneratorData: async () => ({ + urlGeneratorId: 'id', + initialState: {}, + restoreState: {}, + }), + }); + + expect(sessionService.isSessionStorageReady()).toBe(true); + + await expect(() => sessionService.save()).resolves; + + sessionService.clear(); + expect(sessionService.isSessionStorageReady()).toBe(false); + }); + + test('can provide config for search session indicator', () => { + expect(sessionService.getSearchSessionIndicatorUiConfig().isDisabled().disabled).toBe(false); + sessionService.enableStorage( + { + getName: async () => 'Name', + getUrlGeneratorData: async () => ({ + urlGeneratorId: 'id', + initialState: {}, + restoreState: {}, + }), + }, + { + isDisabled: () => ({ disabled: true, reasonText: 'text' }), + } + ); + + expect(sessionService.getSearchSessionIndicatorUiConfig().isDisabled().disabled).toBe(true); + + sessionService.clear(); + expect(sessionService.getSearchSessionIndicatorUiConfig().isDisabled().disabled).toBe(false); + }); + + test('save() throws in case getUrlGeneratorData returns throws', async () => { + sessionService.enableStorage({ + getName: async () => 'Name', + getUrlGeneratorData: async () => { + throw new Error('Haha'); + }, + }); + sessionService.start(); + await expect(() => sessionService.save()).rejects.toMatchInlineSnapshot(`[Error: Haha]`); + }); }); diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 6269398036e76..23129a9afc460 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -43,6 +43,20 @@ export interface SearchSessionInfoProvider; } +/** + * Configure a "Search session indicator" UI + */ +export interface SearchSessionIndicatorUiConfig { + /** + * App controls if "Search session indicator" UI should be disabled. + * reasonText will appear in a tooltip. + * + * Could be used, for example, to disable "Search session indicator" UI + * in case user doesn't have permissions to store a search session + */ + isDisabled: () => { disabled: true; reasonText: string } | { disabled: false }; +} + /** * Responsible for tracking a current search session. Supports only a single session at a time. */ @@ -51,6 +65,7 @@ export class SessionService { private readonly state: SessionStateContainer; private searchSessionInfoProvider?: SearchSessionInfoProvider; + private searchSessionIndicatorUiConfig?: Partial; private subscription = new Subscription(); private curApp?: string; @@ -102,17 +117,6 @@ export class SessionService { }); } - /** - * Set a provider of info about current session - * This will be used for creating a search session saved object - * @param searchSessionInfoProvider - */ - public setSearchSessionInfoProvider( - searchSessionInfoProvider: SearchSessionInfoProvider | undefined - ) { - this.searchSessionInfoProvider = searchSessionInfoProvider; - } - /** * Used to track pending searches within current session * @@ -185,7 +189,8 @@ export class SessionService { */ public clear() { this.state.transitions.clear(); - this.setSearchSessionInfoProvider(undefined); + this.searchSessionInfoProvider = undefined; + this.searchSessionIndicatorUiConfig = undefined; } private refresh$ = new Subject(); @@ -269,4 +274,34 @@ export class SessionService { isStored: isCurrentSession ? this.isStored() : false, }; } + + /** + * Provide an info about current session which is needed for storing a search session. + * To opt-into "Search session indicator" UI app has to call {@link enableStorage}. + * + * @param searchSessionInfoProvider - info provider for saving a search session + * @param searchSessionIndicatorUiConfig - config for "Search session indicator" UI + */ + public enableStorage( + searchSessionInfoProvider: SearchSessionInfoProvider, + searchSessionIndicatorUiConfig?: SearchSessionIndicatorUiConfig + ) { + this.searchSessionInfoProvider = searchSessionInfoProvider; + this.searchSessionIndicatorUiConfig = searchSessionIndicatorUiConfig; + } + + /** + * If the current app explicitly called {@link enableStorage} and provided all configuration needed + * for storing its search sessions + */ + public isSessionStorageReady(): boolean { + return !!this.searchSessionInfoProvider; + } + + public getSearchSessionIndicatorUiConfig(): SearchSessionIndicatorUiConfig { + return { + isDisabled: () => ({ disabled: false }), + ...this.searchSessionIndicatorUiConfig, + }; + } } diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 946baa7f4ecb1..5c26680c7cc45 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -18,6 +18,7 @@ import { connectToQueryState, esFilters, indexPatterns as indexPatternsUtils, + noSearchSessionStorageCapabilityMessage, syncQueryStateWithUrl, } from '../../../../data/public'; import { getSortArray } from './doc_table'; @@ -284,12 +285,21 @@ function discoverController($route, $scope, Promise) { } }); - data.search.session.setSearchSessionInfoProvider( + data.search.session.enableStorage( createSearchSessionRestorationDataProvider({ appStateContainer, data, getSavedSearch: () => savedSearch, - }) + }), + { + isDisabled: () => + capabilities.discover.storeSearchSession + ? { disabled: false } + : { + disabled: true, + reasonText: noSearchSessionStorageCapabilityMessage, + }, + } ); $scope.setIndexPattern = async (id) => { diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index 2c74f9c995a5a..f4bb7577bee53 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -28,6 +28,12 @@ timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue beforeEach(() => { refreshInterval$.next({ value: 0, pause: true }); + sessionService.isSessionStorageReady.mockImplementation(() => true); + sessionService.getSearchSessionIndicatorUiConfig.mockImplementation(() => ({ + isDisabled: () => ({ + disabled: false, + }), + })); }); test("shouldn't show indicator in case no active search session", async () => { @@ -45,6 +51,22 @@ test("shouldn't show indicator in case no active search session", async () => { expect(container).toMatchInlineSnapshot(`
`); }); +test("shouldn't show indicator in case app hasn't opt-in", async () => { + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ + sessionService, + application: coreStart.application, + timeFilter, + }); + const { getByTestId, container } = render(); + sessionService.isSessionStorageReady.mockImplementation(() => false); + + // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) + await expect( + waitFor(() => getByTestId('searchSessionIndicator'), { timeout: 100 }) + ).rejects.toThrow(); + expect(container).toMatchInlineSnapshot(`
`); +}); + test('should show indicator in case there is an active search session', async () => { const state$ = new BehaviorSubject(SearchSessionState.Loading); const SearchSessionIndicator = createConnectedSearchSessionIndicator({ @@ -57,7 +79,7 @@ test('should show indicator in case there is an active search session', async () await waitFor(() => getByTestId('searchSessionIndicator')); }); -test('should be disabled when permissions are off', async () => { +test('should be disabled in case uiConfig says so ', async () => { const state$ = new BehaviorSubject(SearchSessionState.Loading); coreStart.application.currentAppId$ = new BehaviorSubject('discover'); (coreStart.application.capabilities as any) = { @@ -65,6 +87,12 @@ test('should be disabled when permissions are off', async () => { storeSearchSession: false, }, }; + sessionService.getSearchSessionIndicatorUiConfig.mockImplementation(() => ({ + isDisabled: () => ({ + disabled: true, + reasonText: 'reason', + }), + })); const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application: coreStart.application, @@ -80,12 +108,7 @@ test('should be disabled when permissions are off', async () => { test('should be disabled during auto-refresh', async () => { const state$ = new BehaviorSubject(SearchSessionState.Loading); - coreStart.application.currentAppId$ = new BehaviorSubject('discover'); - (coreStart.application.capabilities as any) = { - discover: { - storeSearchSession: true, - }, - }; + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application: coreStart.application, diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index 5c8c01064bff4..59c1bb4a223b1 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -29,35 +29,14 @@ export const createConnectedSearchSessionIndicator = ({ .getRefreshIntervalUpdate$() .pipe(map(isAutoRefreshEnabled), distinctUntilChanged()); - const getCapabilitiesByAppId = ( - capabilities: ApplicationStart['capabilities'], - appId?: string - ) => { - switch (appId) { - case 'dashboards': - return capabilities.dashboard; - case 'discover': - return capabilities.discover; - default: - return undefined; - } - }; - return () => { const state = useObservable(sessionService.state$.pipe(debounceTime(500))); const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); - const appId = useObservable(application.currentAppId$, undefined); + const isDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled(); let disabled = false; let disabledReasonText: string = ''; - if (getCapabilitiesByAppId(application.capabilities, appId)?.storeSearchSession !== true) { - disabled = true; - disabledReasonText = i18n.translate('xpack.data.searchSessionIndicator.noCapability', { - defaultMessage: "You don't have permissions to send to background.", - }); - } - if (autoRefreshEnabled) { disabled = true; disabledReasonText = i18n.translate( @@ -68,6 +47,12 @@ export const createConnectedSearchSessionIndicator = ({ ); } + if (isDisabledByApp.disabled) { + disabled = true; + disabledReasonText = isDisabledByApp.reasonText; + } + + if (!sessionService.isSessionStorageReady()) return null; if (!state) return null; return ( diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index bad818bb69664..c788cc38477e6 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -24,6 +24,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { resolve(__dirname, './tests/apps/dashboard/async_search'), resolve(__dirname, './tests/apps/discover'), resolve(__dirname, './tests/apps/management/search_sessions'), + resolve(__dirname, './tests/apps/lens'), ], kbnTestServer: { diff --git a/x-pack/test/send_search_to_background_integration/services/index.ts b/x-pack/test/send_search_to_background_integration/services/index.ts index 35eed5a218b42..b177ac3a01cb0 100644 --- a/x-pack/test/send_search_to_background_integration/services/index.ts +++ b/x-pack/test/send_search_to_background_integration/services/index.ts @@ -5,9 +5,9 @@ */ import { services as functionalServices } from '../../functional/services'; -import { SendToBackgroundProvider } from './send_to_background'; +import { SearchSessionsProvider } from './search_sessions'; export const services = { ...functionalServices, - searchSessions: SendToBackgroundProvider, + searchSessions: SearchSessionsProvider, }; diff --git a/x-pack/test/send_search_to_background_integration/services/send_to_background.ts b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts similarity index 81% rename from x-pack/test/send_search_to_background_integration/services/send_to_background.ts rename to x-pack/test/send_search_to_background_integration/services/search_sessions.ts index 8c3261c2074ae..7041de6462243 100644 --- a/x-pack/test/send_search_to_background_integration/services/send_to_background.ts +++ b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; import { SavedObjectsFindResponse } from 'src/core/server'; import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -20,7 +21,7 @@ type SessionStateType = | 'restored' | 'canceled'; -export function SendToBackgroundProvider({ getService }: FtrProviderContext) { +export function SearchSessionsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); const retry = getService('retry'); @@ -36,6 +37,17 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) { return testSubjects.exists(SEARCH_SESSION_INDICATOR_TEST_SUBJ); } + public async missingOrFail(): Promise { + return testSubjects.missingOrFail(SEARCH_SESSION_INDICATOR_TEST_SUBJ); + } + + public async disabledOrFail() { + await this.exists(); + await expect(await (await (await this.find()).findByTagName('button')).isEnabled()).to.be( + false + ); + } + public async expectState(state: SessionStateType) { return retry.waitFor(`searchSessions indicator to get into state = ${state}`, async () => { const currentState = await ( @@ -93,12 +105,12 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) { } /* - * This cleanup function should be used by tests that create new background sesions. - * Tests should not end with new background sessions remaining in storage since that interferes with functional tests that check the _find API. - * Alternatively, a test can navigate to `Managment > Search Sessions` and use the UI to delete any created tests. + * This cleanup function should be used by tests that create new search sessions. + * Tests should not end with new search sessions remaining in storage since that interferes with functional tests that check the _find API. + * Alternatively, a test can navigate to `Management > Search Sessions` and use the UI to delete any created tests. */ public async deleteAllSearchSessions() { - log.debug('Deleting created background sessions'); + log.debug('Deleting created searcg sessions'); // ignores 409 errs and keeps retrying await retry.tryForTime(10000, async () => { const { body } = await supertest @@ -109,10 +121,10 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) { .expect(200); const { saved_objects: savedObjects } = body as SavedObjectsFindResponse; - log.debug(`Found created background sessions: ${savedObjects.map(({ id }) => id)}`); + log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); await Promise.all( savedObjects.map(async (so) => { - log.debug(`Deleting background session: ${so.id}`); + log.debug(`Deleting search session: ${so.id}`); await supertest .delete(`/internal/session/${so.id}`) .set(`kbn-xsrf`, `anything`) diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index f590e44138642..6aea4368a2f3f 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const searchSessions = getService('searchSessions'); describe('dashboard in space', () => { - describe('Send to background in space', () => { + describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('dashboard/session_in_space'); @@ -92,5 +92,60 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.missingOrFail('embeddableErrorLabel'); }); }); + + describe('Disabled storing search sessions', () => { + before(async () => { + await esArchiver.load('dashboard/session_in_space'); + + await security.role.create('data_analyst', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['all'] }], + }, + kibana: [ + { + feature: { + dashboard: ['minimal_read'], + }, + spaces: ['another-space'], + }, + ], + }); + + await security.user.create('analyst', { + password: 'analyst-password', + roles: ['data_analyst'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('analyst', 'analyst-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await security.role.delete('data_analyst'); + await security.user.delete('analyst'); + + await esArchiver.unload('dashboard/session_in_space'); + await PageObjects.security.forceLogout(); + }); + + it("Doesn't allow to store a session", async () => { + await PageObjects.common.navigateToApp('dashboard', { basePath: 's/another-space' }); + await PageObjects.dashboard.loadSavedDashboard('A Dashboard in another space'); + + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 1, 2015 @ 00:00:00.000', + 'Oct 1, 2015 @ 00:00:00.000' + ); + + await PageObjects.dashboard.waitForRenderComplete(); + + await searchSessions.expectState('completed'); + await searchSessions.disabledOrFail(); + }); + }); }); } diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts index 6384afb179593..733b2edd4cd06 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const searchSessions = getService('searchSessions'); describe('discover in space', () => { - describe('Send to background in space', () => { + describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('dashboard/session_in_space'); @@ -93,7 +93,62 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Check that session is restored await searchSessions.expectState('restored'); - await testSubjects.missingOrFail('embeddableErrorLabel'); + await testSubjects.missingOrFail('discoverNoResultsError'); // expect error because of fake searchSessionId + }); + }); + describe('Disabled storing search sessions in space', () => { + before(async () => { + await esArchiver.load('dashboard/session_in_space'); + + await security.role.create('data_analyst', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['all'] }], + }, + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['another-space'], + }, + ], + }); + + await security.user.create('analyst', { + password: 'analyst-password', + roles: ['data_analyst'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('analyst', 'analyst-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await security.role.delete('data_analyst'); + await security.user.delete('analyst'); + + await esArchiver.unload('dashboard/session_in_space'); + await PageObjects.security.forceLogout(); + }); + + it("Doesn't allow to store a session", async () => { + await PageObjects.common.navigateToApp('discover', { basePath: 's/another-space' }); + + await PageObjects.discover.selectIndexPattern('logstash-*'); + + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 1, 2015 @ 00:00:00.000', + 'Oct 1, 2015 @ 00:00:00.000' + ); + + await PageObjects.discover.waitForDocTableLoadingComplete(); + + await searchSessions.expectState('completed'); + await searchSessions.disabledOrFail(); }); }); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/lens/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/lens/index.ts new file mode 100644 index 0000000000000..e84cede4bc87d --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/lens/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile, getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + + describe('lens search sessions', function () { + this.tags('ciGroup3'); + + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + }); + + loadTestFile(require.resolve('./search_sessions.ts')); + }); +} diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/lens/search_sessions.ts b/x-pack/test/send_search_to_background_integration/tests/apps/lens/search_sessions.ts new file mode 100644 index 0000000000000..6bd79f0f98883 --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/lens/search_sessions.ts @@ -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. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const searchSession = getService('searchSessions'); + const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'timePicker', 'header']); + const listingTable = getService('listingTable'); + + describe('lens search sessions', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('lens/basic'); + }); + after(async () => { + await esArchiver.unload('lens/basic'); + }); + + it("doesn't shows search sessions indicator UI", async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.isShowingNoResults()).to.be(false); + + await searchSession.missingOrFail(); + }); + }); +} From c8afae8a5120850565ae3fa7f9db460a3f7be653 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 27 Jan 2021 12:13:15 +0100 Subject: [PATCH 028/163] Enable v2 so migrations, disable in FTR tests (#89297) * Enable v2 so migrations, disable in FTR tests * Disable v2 migrations for ui_settings integration tests * Disable v2 migrations for reporting without serucity api integration test --- src/core/server/saved_objects/saved_objects_config.ts | 2 +- src/core/server/ui_settings/integration_tests/lib/servers.ts | 3 +++ test/common/config.js | 2 ++ .../reporting_without_security.config.ts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 82db64daf2bd3..0885a52078f5c 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -18,7 +18,7 @@ export const savedObjectsMigrationConfig = { pollInterval: schema.number({ defaultValue: 1500 }), skip: schema.boolean({ defaultValue: false }), // TODO migrationsV2: remove/deprecate once we release migrations v2 - enableV2: schema.boolean({ defaultValue: false }), + enableV2: schema.boolean({ defaultValue: true }), }), }; diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index f181272030ae1..b5198b19007d0 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -37,6 +37,9 @@ export async function startServers() { adjustTimeout: (t) => jest.setTimeout(t), settings: { kbn: { + migrations: { + enableV2: false, + }, uiSettings: { overrides: { foo: 'bar', diff --git a/test/common/config.js b/test/common/config.js index 8a42e6c87b214..b6d12444b7017 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -61,6 +61,8 @@ export default function () { ...(!!process.env.CODE_COVERAGE ? [`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'coverage')}`] : []), + // Disable v2 migrations in tests for now + '--migrations.enableV2=false', ], }, services, diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index 4a95a15169b59..11182bbcdb3b0 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -32,6 +32,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { kbnTestServer: { ...apiConfig.get('kbnTestServer'), serverArgs: [ + `--migrations.enableV2=false`, `--elasticsearch.hosts=${formatUrl(esTestConfig.getUrlParts())}`, `--logging.json=false`, `--server.maxPayloadBytes=1679958`, From 9e6897505447d7a5911b568eb334e658bd95df78 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 27 Jan 2021 12:35:38 +0100 Subject: [PATCH 029/163] [APM] Upgrade ES client (#86594) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/apm/public/hooks/use_fetcher.tsx | 32 ++++---- .../scripts/upload-telemetry-data/index.ts | 12 +-- .../collect_data_telemetry/index.ts | 18 +++-- .../apm/server/lib/apm_telemetry/index.ts | 22 +++--- ...with_debug.ts => call_async_with_debug.ts} | 58 +++++++------- .../cancel_es_request_on_abort.ts | 27 +++++++ .../create_apm_event_client/index.test.ts | 77 +++++++++++++++++++ .../create_apm_event_client/index.ts | 45 +++++++---- .../create_internal_es_client/index.ts | 77 ++++++++++++------- .../server/lib/helpers/setup_request.test.ts | 56 +++++++------- .../apm/server/lib/helpers/setup_request.ts | 2 +- .../annotations/get_stored_annotations.ts | 17 ++-- .../server/lib/services/annotations/index.ts | 8 +- .../create_agent_config_index.ts | 8 +- .../custom_link/create_custom_link_index.ts | 8 +- x-pack/plugins/apm/server/plugin.ts | 6 +- .../apm/server/routes/create_api/index.ts | 7 +- x-pack/plugins/apm/server/routes/services.ts | 2 +- x-pack/plugins/observability/server/index.ts | 2 + .../lib/annotations/bootstrap_annotations.ts | 2 +- .../annotations/create_annotations_client.ts | 70 ++++++++++------- .../annotations/register_annotation_apis.ts | 4 +- .../server/utils/create_or_update_index.ts | 28 +++---- .../server/utils/unwrap_es_response.ts | 13 ++++ x-pack/typings/elasticsearch/index.d.ts | 5 +- 25 files changed, 396 insertions(+), 210 deletions(-) rename x-pack/plugins/apm/server/lib/helpers/create_es_client/{call_client_with_debug.ts => call_async_with_debug.ts} (51%) create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts create mode 100644 x-pack/plugins/observability/server/utils/unwrap_es_response.ts diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx index 8174f06e06b8b..2b58f30a9ec64 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx @@ -24,6 +24,21 @@ export interface FetcherResult { error?: IHttpFetchError; } +function getDetailsFromErrorResponse(error: IHttpFetchError) { + const message = error.body?.message ?? error.response?.statusText; + return ( + <> + {message} ({error.response?.status}) +
+ {i18n.translate('xpack.apm.fetcher.error.url', { + defaultMessage: `URL`, + })} +
+ {error.response?.url} + + ); +} + // fetcher functions can return undefined OR a promise. Previously we had a more simple type // but it led to issues when using object destructuring with default values type InferResponseType = Exclude extends Promise< @@ -82,25 +97,14 @@ export function useFetcher( if (!didCancel) { const errorDetails = - 'response' in err ? ( - <> - {err.response?.statusText} ({err.response?.status}) -
- {i18n.translate('xpack.apm.fetcher.error.url', { - defaultMessage: `URL`, - })} -
- {err.response?.url} - - ) : ( - err.message - ); + 'response' in err ? getDetailsFromErrorResponse(err) : err.message; if (showToastOnError) { - notifications.toasts.addWarning({ + notifications.toasts.addDanger({ title: i18n.translate('xpack.apm.fetcher.error.title', { defaultMessage: `Error while fetching resource`, }), + text: toMountPoint(
diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index 8c64c37d9b7f7..e3221c17f3f2a 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -17,6 +17,8 @@ import { argv } from 'yargs'; import { Logger } from 'kibana/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { unwrapEsResponse } from '../../../observability/server/utils/unwrap_es_response'; import { downloadTelemetryTemplate } from '../shared/download-telemetry-template'; import { mergeApmTelemetryMapping } from '../../common/apm_telemetry'; import { generateSampleDocuments } from './generate-sample-documents'; @@ -80,18 +82,18 @@ async function uploadData() { apmAgentConfigurationIndex: '.apm-agent-configuration', }, search: (body) => { - return client.search(body as any).then((res) => res.body as any); + return unwrapEsResponse(client.search(body)); }, indicesStats: (body) => { - return client.indices.stats(body as any).then((res) => res.body); + return unwrapEsResponse(client.indices.stats(body)); }, transportRequest: ((params) => { - return client.transport - .request({ + return unwrapEsResponse( + client.transport.request({ method: params.method, path: params.path, }) - .then((res) => res.body); + ); }) as CollectTelemetryParams['transportRequest'], }, }); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index 730645c609cb6..90aad48fe20b9 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { merge } from 'lodash'; -import { Logger, LegacyCallAPIOptions } from 'kibana/server'; -import { IndicesStatsParams, Client } from 'elasticsearch'; +import { Logger } from 'kibana/server'; +import { RequestParams } from '@elastic/elasticsearch'; import { ESSearchRequest, ESSearchResponse, @@ -20,9 +20,17 @@ type TelemetryTaskExecutor = (params: { params: TSearchRequest ): Promise>; indicesStats( - params: IndicesStatsParams, - options?: LegacyCallAPIOptions - ): ReturnType; + params: RequestParams.IndicesStats + // promise returned by client has an abort property + // so we cannot use its ReturnType + ): Promise<{ + _all?: { + total?: { store?: { size_in_bytes?: number }; docs?: { count?: number } }; + }; + _shards?: { + total?: number; + }; + }>; transportRequest: (params: { path: string; method: 'get'; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index 6d91e64be034d..98abff08dab5e 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -11,6 +11,7 @@ import { Logger, SavedObjectsErrorHelpers, } from '../../../../../../src/core/server'; +import { unwrapEsResponse } from '../../../../observability/server'; import { APMConfig } from '../..'; import { TaskManagerSetupContract, @@ -65,27 +66,22 @@ export async function createApmTelemetry({ const collectAndStore = async () => { const config = await config$.pipe(take(1)).toPromise(); const [{ elasticsearch }] = await core.getStartServices(); - const esClient = elasticsearch.legacy.client; + const esClient = elasticsearch.client; const indices = await getApmIndices({ config, savedObjectsClient, }); - const search = esClient.callAsInternalUser.bind( - esClient, - 'search' - ) as CollectTelemetryParams['search']; + const search: CollectTelemetryParams['search'] = (params) => + unwrapEsResponse(esClient.asInternalUser.search(params)); - const indicesStats = esClient.callAsInternalUser.bind( - esClient, - 'indices.stats' - ) as CollectTelemetryParams['indicesStats']; + const indicesStats: CollectTelemetryParams['indicesStats'] = (params) => + unwrapEsResponse(esClient.asInternalUser.indices.stats(params)); - const transportRequest = esClient.callAsInternalUser.bind( - esClient, - 'transport.request' - ) as CollectTelemetryParams['transportRequest']; + const transportRequest: CollectTelemetryParams['transportRequest'] = ( + params + ) => unwrapEsResponse(esClient.asInternalUser.transport.request(params)); const dataTelemetry = await collectDataTelemetry({ search, 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_async_with_debug.ts similarity index 51% rename from x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts rename to x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 9f7aaafbefb8c..9d612d82d99bb 100644 --- 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_async_with_debug.ts @@ -7,34 +7,31 @@ /* eslint-disable no-console */ import chalk from 'chalk'; -import { - LegacyAPICaller, - KibanaRequest, -} from '../../../../../../../src/core/server'; +import { KibanaRequest } from '../../../../../../../src/core/server'; function formatObj(obj: Record) { return JSON.stringify(obj, null, 2); } -export async function callClientWithDebug({ - apiCaller, - operationName, - params, +export async function callAsyncWithDebug({ + cb, + getDebugMessage, debug, - request, }: { - apiCaller: LegacyAPICaller; - operationName: string; - params: Record; + cb: () => Promise; + getDebugMessage: () => { body: string; title: string }; debug: boolean; - request: KibanaRequest; }) { + if (!debug) { + return cb(); + } + const startTime = process.hrtime(); let res: any; let esError = null; try { - res = await apiCaller(operationName, params); + res = await cb(); } catch (e) { // catch error and throw after outputting debug info esError = e; @@ -44,23 +41,14 @@ export async function callClientWithDebug({ 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 - }`; + + const { title, body } = getDebugMessage(); console.log( - chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + chalk.bold[highlightColor](`=== Debug: ${title} (${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(body); console.log(`\n`); } @@ -70,3 +58,19 @@ export async function callClientWithDebug({ return res; } + +export const getDebugBody = ( + params: Record, + operationName: string +) => { + if (operationName === 'search') { + return `GET ${params.index}/_search\n${formatObj(params.body)}`; + } + + return `${chalk.bold('ES operation:')} ${operationName}\n${chalk.bold( + 'ES query:' + )}\n${formatObj(params)}`; +}; + +export const getDebugTitle = (request: KibanaRequest) => + `${request.route.method.toUpperCase()} ${request.route.path}`; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts new file mode 100644 index 0000000000000..e9b61a27f4380 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts @@ -0,0 +1,27 @@ +/* + * 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 { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { KibanaRequest } from 'src/core/server'; + +export function cancelEsRequestOnAbort>( + promise: T, + request: KibanaRequest +) { + const subscription = request.events.aborted$.subscribe(() => { + promise.abort(); + }); + + // using .catch() here means unsubscribe will be called + // after it has thrown an error, so we use .then(onSuccess, onFailure) + // syntax + promise.then( + () => subscription.unsubscribe(), + () => subscription.unsubscribe() + ); + + return promise; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts new file mode 100644 index 0000000000000..f58e04061254d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts @@ -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 { contextServiceMock } from 'src/core/server/mocks'; +import { createHttpServer } from 'src/core/server/test_utils'; +import supertest from 'supertest'; +import { createApmEventClient } from '.'; + +describe('createApmEventClient', () => { + let server: ReturnType; + + beforeEach(() => { + server = createHttpServer(); + }); + + afterEach(async () => { + await server.stop(); + }); + it('cancels a search when a request is aborted', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextServiceMock.createSetupContract(), + }); + const router = createRouter('/'); + + const abort = jest.fn(); + router.get( + { path: '/', validate: false }, + async (context, request, res) => { + const eventClient = createApmEventClient({ + esClient: { + search: () => { + return Object.assign( + new Promise((resolve) => setTimeout(resolve, 3000)), + { abort } + ); + }, + } as any, + debug: false, + request, + indices: {} as any, + options: { + includeFrozen: false, + }, + }); + + await eventClient.search({ + apm: { + events: [], + }, + }); + + return res.ok({ body: 'ok' }); + } + ); + + await server.start(); + + const incomingRequest = supertest(innerServer.listener) + .get('/') + // end required to send request + .end(); + + await new Promise((resolve) => { + setTimeout(() => { + incomingRequest.abort(); + setTimeout(() => { + resolve(undefined); + }, 0); + }, 50); + }); + + expect(abort).toHaveBeenCalled(); + }); +}); 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 index b7c38068eb93e..b2e25994d6fe6 100644 --- 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 @@ -5,10 +5,11 @@ */ import { ValuesType } from 'utility-types'; +import { unwrapEsResponse } from '../../../../../../observability/server'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { + ElasticsearchClient, KibanaRequest, - LegacyScopedClusterClient, } from '../../../../../../../../src/core/server'; import { ProcessorEvent } from '../../../../../common/processor_event'; import { @@ -17,11 +18,16 @@ import { } from '../../../../../../../typings/elasticsearch'; import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; 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 { Metric } from '../../../../../typings/es_schemas/ui/metric'; import { unpackProcessorEvents } from './unpack_processor_events'; +import { + callAsyncWithDebug, + getDebugTitle, + getDebugBody, +} from '../call_async_with_debug'; +import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; export type APMEventESSearchRequest = Omit & { apm: { @@ -59,10 +65,7 @@ export function createApmEventClient({ indices, options: { includeFrozen } = { includeFrozen: false }, }: { - esClient: Pick< - LegacyScopedClusterClient, - 'callAsInternalUser' | 'callAsCurrentUser' - >; + esClient: ElasticsearchClient; debug: boolean; request: KibanaRequest; indices: ApmIndicesConfig; @@ -71,9 +74,9 @@ export function createApmEventClient({ }; }) { return { - search( + async search( params: TParams, - { includeLegacyData } = { includeLegacyData: false } + { includeLegacyData = false } = {} ): Promise> { const withProcessorEventFilter = unpackProcessorEvents(params, indices); @@ -81,15 +84,25 @@ export function createApmEventClient({ ? addFilterToExcludeLegacyData(withProcessorEventFilter) : withProcessorEventFilter; - return callClientWithDebug({ - apiCaller: esClient.callAsCurrentUser, - operationName: 'search', - params: { - ...withPossibleLegacyDataFilter, - ignore_throttled: !includeFrozen, - ignore_unavailable: true, + const searchParams = { + ...withPossibleLegacyDataFilter, + ignore_throttled: !includeFrozen, + ignore_unavailable: true, + }; + + return callAsyncWithDebug({ + cb: () => { + const searchPromise = cancelEsRequestOnAbort( + esClient.search(searchParams), + request + ); + + return unwrapEsResponse(searchPromise); }, - request, + getDebugMessage: () => ({ + body: getDebugBody(searchParams, 'search'), + title: getDebugTitle(request), + }), debug, }); }, 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 index 8e74a7992e9ea..69f596520d216 100644 --- 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 @@ -3,23 +3,23 @@ * 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 { RequestParams } from '@elastic/elasticsearch'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { unwrapEsResponse } from '../../../../../../observability/server'; import { APMRequestHandlerContext } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, } from '../../../../../../../typings/elasticsearch'; -import { callClientWithDebug } from '../call_client_with_debug'; +import { + callAsyncWithDebug, + getDebugBody, + getDebugTitle, +} from '../call_async_with_debug'; +import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; -// `type` was deprecated in 7.0 -export type APMIndexDocumentParams = Omit, 'type'>; +export type APMIndexDocumentParams = RequestParams.Index; export type APMInternalClient = ReturnType; @@ -30,17 +30,26 @@ export function createInternalESClient({ context: APMRequestHandlerContext; request: KibanaRequest; }) { - const { callAsInternalUser } = context.core.elasticsearch.legacy.client; + const { asInternalUser } = context.core.elasticsearch.client; - const callEs = (operationName: string, params: Record) => { - return callClientWithDebug({ - apiCaller: callAsInternalUser, - operationName, - params, - request, + function callEs({ + cb, + operationName, + params, + }: { + operationName: string; + cb: () => TransportRequestPromise; + params: Record; + }) { + return callAsyncWithDebug({ + cb: () => unwrapEsResponse(cancelEsRequestOnAbort(cb(), request)), + getDebugMessage: () => ({ + title: getDebugTitle(request), + body: getDebugBody(params, operationName), + }), debug: context.params.query._debug, }); - }; + } return { search: async < @@ -49,18 +58,32 @@ export function createInternalESClient({ >( params: TSearchRequest ): Promise> => { - return callEs('search', params); + return callEs({ + operationName: 'search', + cb: () => asInternalUser.search(params), + params, + }); }, - index: (params: APMIndexDocumentParams) => { - return callEs('index', params); + index: (params: APMIndexDocumentParams) => { + return callEs({ + operationName: 'index', + cb: () => asInternalUser.index(params), + params, + }); }, - delete: ( - params: Omit - ): Promise => { - return callEs('delete', params); + delete: (params: RequestParams.Delete): Promise<{ result: string }> => { + return callEs({ + operationName: 'delete', + cb: () => asInternalUser.delete(params), + params, + }); }, - indicesCreate: (params: IndicesCreateParams) => { - return callEs('indices.create', params); + indicesCreate: (params: RequestParams.IndicesCreate) => { + return callEs({ + operationName: 'indices.create', + cb: () => asInternalUser.indices.create(params), + 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 f2d291cd053bb..f00941d6e6800 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 @@ -31,6 +31,15 @@ jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({ })); function getMockRequest() { + const esClientMock = { + asCurrentUser: { + search: jest.fn().mockResolvedValue({ body: {} }), + }, + asInternalUser: { + search: jest.fn().mockResolvedValue({ body: {} }), + }, + }; + const mockContext = ({ config: new Proxy( {}, @@ -45,12 +54,7 @@ function getMockRequest() { }, core: { elasticsearch: { - legacy: { - client: { - callAsCurrentUser: jest.fn(), - callAsInternalUser: jest.fn(), - }, - }, + client: esClientMock, }, uiSettings: { client: { @@ -69,12 +73,7 @@ function getMockRequest() { } as unknown) as APMRequestHandlerContext & { core: { elasticsearch: { - legacy: { - client: { - callAsCurrentUser: jest.Mock; - callAsInternalUser: jest.Mock; - }; - }; + client: typeof esClientMock; }; uiSettings: { client: { @@ -91,6 +90,11 @@ function getMockRequest() { const mockRequest = ({ url: '', + events: { + aborted$: { + subscribe: jest.fn(), + }, + }, } as unknown) as KibanaRequest; return { mockContext, mockRequest }; @@ -106,8 +110,8 @@ describe('setupRequest', () => { body: { foo: 'bar' }, }); expect( - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser - ).toHaveBeenCalledWith('search', { + mockContext.core.elasticsearch.client.asCurrentUser.search + ).toHaveBeenCalledWith({ index: ['apm-*'], body: { foo: 'bar', @@ -133,8 +137,8 @@ describe('setupRequest', () => { body: { foo: 'bar' }, } as any); expect( - mockContext.core.elasticsearch.legacy.client.callAsInternalUser - ).toHaveBeenCalledWith('search', { + mockContext.core.elasticsearch.client.asInternalUser.search + ).toHaveBeenCalledWith({ index: ['apm-*'], body: { foo: 'bar', @@ -154,8 +158,8 @@ describe('setupRequest', () => { body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, }); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -184,8 +188,8 @@ describe('setupRequest', () => { } ); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -214,8 +218,8 @@ describe('without a bool filter', () => { }, }); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.body).toEqual({ query: { bool: { @@ -245,8 +249,8 @@ describe('with includeFrozen=false', () => { }); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; expect(params.ignore_throttled).toBe(true); }); }); @@ -265,8 +269,8 @@ describe('with includeFrozen=true', () => { }); const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; + mockContext.core.elasticsearch.client.asCurrentUser.search.mock + .calls[0][0]; 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 47529de1042a1..947eb68e10093 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -86,7 +86,7 @@ export async function setupRequest( const coreSetupRequest = { indices, apmEventClient: createApmEventClient({ - esClient: context.core.elasticsearch.legacy.client, + esClient: context.core.elasticsearch.client.asCurrentUser, debug: context.params.query._debug, request, indices, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 3903298415aed..55395f3a4ca4e 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, Logger } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; +import { unwrapEsResponse } from '../../../../../observability/server'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; @@ -18,14 +19,14 @@ export async function getStoredAnnotations({ setup, serviceName, environment, - apiCaller, + client, annotationsClient, logger, }: { setup: Setup & SetupTimeRange; serviceName: string; environment?: string; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; annotationsClient: ScopedAnnotationsClient; logger: Logger; }): Promise { @@ -50,10 +51,12 @@ export async function getStoredAnnotations({ const response: ESSearchResponse< ESAnnotation, { body: typeof body } - > = (await apiCaller('search', { - index: annotationsClient.index, - body, - })) as any; + > = await unwrapEsResponse( + client.search({ + index: annotationsClient.index, + body, + }) + ); return response.hits.hits.map((hit) => { return { diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.ts index 9516ed3777297..304485822be28 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, Logger } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { getDerivedServiceAnnotations } from './get_derived_service_annotations'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -15,7 +15,7 @@ export async function getServiceAnnotations({ serviceName, environment, annotationsClient, - apiCaller, + client, logger, }: { serviceName: string; @@ -23,7 +23,7 @@ export async function getServiceAnnotations({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; annotationsClient?: ScopedAnnotationsClient; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; logger: Logger; }) { // start fetching derived annotations (based on transactions), but don't wait on it @@ -41,7 +41,7 @@ export async function getServiceAnnotations({ serviceName, environment, annotationsClient, - apiCaller, + client, logger, }) : []; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index 83117db1167b5..190c99d0002d8 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyClusterClient, Logger } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { createOrUpdateIndex, MappingsDefinition, @@ -13,18 +13,18 @@ import { APMConfig } from '../../..'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; export async function createApmAgentConfigurationIndex({ - esClient, + client, config, logger, }: { - esClient: ILegacyClusterClient; + client: ElasticsearchClient; config: APMConfig; logger: Logger; }) { const index = getApmIndicesConfig(config).apmAgentConfigurationIndex; return createOrUpdateIndex({ index, - apiCaller: esClient.callAsInternalUser, + client, logger, mappings, }); diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts index 2bfe0d620e4e8..aa9e7411d1014 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyClusterClient, Logger } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { createOrUpdateIndex, MappingsDefinition, @@ -13,18 +13,18 @@ import { APMConfig } from '../../..'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; export const createApmCustomLinkIndex = async ({ - esClient, + client, config, logger, }: { - esClient: ILegacyClusterClient; + client: ElasticsearchClient; config: APMConfig; logger: Logger; }) => { const index = getApmIndicesConfig(config).apmCustomLinkIndex; return createOrUpdateIndex({ index, - apiCaller: esClient.callAsInternalUser, + client, logger, mappings, }); diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 09b75137e12df..3e01523aa8e31 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -173,7 +173,7 @@ export class APMPlugin implements Plugin { context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); - const esClient = context.core.elasticsearch.legacy.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; return createApmEventClient({ debug: debug ?? false, @@ -195,13 +195,13 @@ export class APMPlugin implements Plugin { // create agent configuration index without blocking start lifecycle createApmAgentConfigurationIndex({ - esClient: core.elasticsearch.legacy.client, + client: core.elasticsearch.client.asInternalUser, config: this.currentConfig, logger: this.logger, }); // create custom action index without blocking start lifecycle createApmCustomLinkIndex({ - esClient: core.elasticsearch.legacy.client, + client: core.elasticsearch.client.asInternalUser, config: this.currentConfig, logger: this.logger, }); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index cfb31670bd521..721badf7fc025 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -147,7 +147,7 @@ function convertBoomToKibanaResponse( error: Boom.Boom, response: KibanaResponseFactory ) { - const opts = { body: error.message }; + const opts = { body: { message: error.message } }; switch (error.output.statusCode) { case 404: return response.notFound(opts); @@ -159,9 +159,6 @@ function convertBoomToKibanaResponse( return response.forbidden(opts); default: - return response.custom({ - statusCode: error.output.statusCode, - ...opts, - }); + throw error; } } diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index bfc2ebf062ac3..ef74437f5f0e7 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -194,7 +194,7 @@ export const serviceAnnotationsRoute = createRoute({ serviceName, environment, annotationsClient, - apiCaller: context.core.elasticsearch.legacy.client.callAsCurrentUser, + client: context.core.elasticsearch.client.asCurrentUser, logger: context.logger, }); }, diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index 78550b781b411..e88541f69d6cf 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -9,6 +9,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { ObservabilityPlugin, ObservabilityPluginSetup } from './plugin'; import { createOrUpdateIndex, MappingsDefinition } from './utils/create_or_update_index'; import { ScopedAnnotationsClient } from './lib/annotations/bootstrap_annotations'; +import { unwrapEsResponse } from './utils/unwrap_es_response'; export const config = { schema: schema.object({ @@ -30,4 +31,5 @@ export { MappingsDefinition, ObservabilityPluginSetup, ScopedAnnotationsClient, + unwrapEsResponse, }; diff --git a/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts b/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts index 6fcd780d5af29..90084611d7efc 100644 --- a/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts +++ b/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts @@ -38,7 +38,7 @@ export async function bootstrapAnnotations({ index, core, context }: Params) { ) => { return createAnnotationsClient({ index, - apiCaller: requestContext.core.elasticsearch.legacy.client.callAsCurrentUser, + esClient: requestContext.core.elasticsearch.client.asCurrentUser, logger, license: requestContext.licensing?.license, }); diff --git a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts index 41f45683d244c..76890cbd587e9 100644 --- a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts +++ b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, Logger } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import * as t from 'io-ts'; -import { Client } from 'elasticsearch'; import Boom from '@hapi/boom'; import { ILicense } from '../../../../licensing/server'; import { @@ -15,9 +14,9 @@ import { Annotation, getAnnotationByIdRt, } from '../../../common/annotations'; -import { PromiseReturnType } from '../../../typings/common'; import { createOrUpdateIndex } from '../../utils/create_or_update_index'; import { mappings } from './mappings'; +import { unwrapEsResponse } from '../../utils/unwrap_es_response'; type CreateParams = t.TypeOf; type DeleteParams = t.TypeOf; @@ -38,19 +37,25 @@ interface IndexDocumentResponse { result: string; } +interface GetResponse { + _id: string; + _index: string; + _source: Annotation; +} + export function createAnnotationsClient(params: { index: string; - apiCaller: LegacyAPICaller; + esClient: ElasticsearchClient; logger: Logger; license?: ILicense; }) { - const { index, apiCaller, logger, license } = params; + const { index, esClient, logger, license } = params; const initIndex = () => createOrUpdateIndex({ index, mappings, - apiCaller, + client: esClient, logger, }); @@ -71,9 +76,11 @@ export function createAnnotationsClient(params: { async ( createParams: CreateParams ): Promise<{ _id: string; _index: string; _source: Annotation }> => { - const indexExists = await apiCaller('indices.exists', { - index, - }); + const indexExists = await unwrapEsResponse( + esClient.indices.exists({ + index, + }) + ); if (!indexExists) { await initIndex(); @@ -86,35 +93,42 @@ export function createAnnotationsClient(params: { }, }; - const response = (await apiCaller('index', { - index, - body: annotation, - refresh: 'wait_for', - })) as IndexDocumentResponse; + const body = await unwrapEsResponse( + esClient.index({ + index, + body: annotation, + refresh: 'wait_for', + }) + ); - return apiCaller('get', { - index, - id: response._id, - }); + return ( + await esClient.get({ + index, + id: body._id, + }) + ).body; } ), getById: ensureGoldLicense(async (getByIdParams: GetByIdParams) => { const { id } = getByIdParams; - return apiCaller('get', { - id, - index, - }); + return unwrapEsResponse( + esClient.get({ + id, + index, + }) + ); }), delete: ensureGoldLicense(async (deleteParams: DeleteParams) => { const { id } = deleteParams; - const response = (await apiCaller('delete', { - index, - id, - refresh: 'wait_for', - })) as PromiseReturnType; - return response; + return unwrapEsResponse( + esClient.delete({ + index, + id, + refresh: 'wait_for', + }) + ); }), }; } diff --git a/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts b/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts index 8f0b53b5a3df2..6ae80880d22b5 100644 --- a/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts +++ b/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts @@ -55,11 +55,11 @@ export function registerAnnotationAPIs({ }); } - const apiCaller = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const esClient = context.core.elasticsearch.client.asCurrentUser; const client = createAnnotationsClient({ index, - apiCaller, + esClient, logger, license: context.licensing?.license, }); diff --git a/x-pack/plugins/observability/server/utils/create_or_update_index.ts b/x-pack/plugins/observability/server/utils/create_or_update_index.ts index 1898331451262..248488b4a5ebe 100644 --- a/x-pack/plugins/observability/server/utils/create_or_update_index.ts +++ b/x-pack/plugins/observability/server/utils/create_or_update_index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import pRetry from 'p-retry'; -import { Logger, LegacyAPICaller } from 'src/core/server'; +import { Logger, ElasticsearchClient } from 'src/core/server'; export interface MappingsObject { type: string; @@ -24,12 +24,12 @@ export interface MappingsDefinition { export async function createOrUpdateIndex({ index, mappings, - apiCaller, + client, logger, }: { index: string; mappings: MappingsDefinition; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; logger: Logger; }) { try { @@ -43,21 +43,21 @@ export async function createOrUpdateIndex({ */ await pRetry( async () => { - const indexExists = await apiCaller('indices.exists', { index }); + const indexExists = (await client.indices.exists({ index })).body; const result = indexExists ? await updateExistingIndex({ index, - apiCaller, + client, mappings, }) : await createNewIndex({ index, - apiCaller, + client, mappings, }); - if (!result.acknowledged) { - const resultError = result && result.error && JSON.stringify(result.error); + if (!result.body.acknowledged) { + const resultError = result && result.body.error && JSON.stringify(result.body.error); throw new Error(resultError); } }, @@ -75,14 +75,14 @@ export async function createOrUpdateIndex({ function createNewIndex({ index, - apiCaller, + client, mappings, }: { index: string; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; mappings: MappingsDefinition; }) { - return apiCaller('indices.create', { + return client.indices.create<{ acknowledged: boolean; error: any }>({ index, body: { // auto_expand_replicas: Allows cluster to not have replicas for this index @@ -94,14 +94,14 @@ function createNewIndex({ function updateExistingIndex({ index, - apiCaller, + client, mappings, }: { index: string; - apiCaller: LegacyAPICaller; + client: ElasticsearchClient; mappings: MappingsDefinition; }) { - return apiCaller('indices.putMapping', { + return client.indices.putMapping<{ acknowledged: boolean; error: any }>({ index, body: mappings, }); diff --git a/x-pack/plugins/observability/server/utils/unwrap_es_response.ts b/x-pack/plugins/observability/server/utils/unwrap_es_response.ts new file mode 100644 index 0000000000000..418ceeb64cc87 --- /dev/null +++ b/x-pack/plugins/observability/server/utils/unwrap_es_response.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PromiseValueType } from '../../../apm/typings/common'; + +export function unwrapEsResponse>( + responsePromise: T +): Promise['body']> { + return responsePromise.then((res) => res.body); +} diff --git a/x-pack/typings/elasticsearch/index.d.ts b/x-pack/typings/elasticsearch/index.d.ts index ff20ce39d6446..049e1e52c66d9 100644 --- a/x-pack/typings/elasticsearch/index.d.ts +++ b/x-pack/typings/elasticsearch/index.d.ts @@ -5,6 +5,7 @@ */ import { ValuesType } from 'utility-types'; import { Explanation, SearchParams, SearchResponse } from 'elasticsearch'; +import { RequestParams } from '@elastic/elasticsearch'; import { AggregationResponseMap, AggregationInputMap, SortOptions } from './aggregations'; export { AggregationInputMap, @@ -72,9 +73,7 @@ export interface ESSearchBody { _source?: ESSourceOptions; } -export type ESSearchRequest = Omit & { - body?: ESSearchBody; -}; +export type ESSearchRequest = RequestParams.Search; export interface ESSearchOptions { restTotalHitsAsInt: boolean; From ab1af57f0564e64907ac88f1a7a6cde4262b94a7 Mon Sep 17 00:00:00 2001 From: Silvia Mitter Date: Wed, 27 Jan 2021 13:34:11 +0100 Subject: [PATCH 030/163] update apm index pattern (#89395) --- src/plugins/apm_oss/server/tutorial/index_pattern.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/apm_oss/server/tutorial/index_pattern.json b/src/plugins/apm_oss/server/tutorial/index_pattern.json index 6eb040f2758af..93a2393b70fa4 100644 --- a/src/plugins/apm_oss/server/tutorial/index_pattern.json +++ b/src/plugins/apm_oss/server/tutorial/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.build.original\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reason\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"file.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.file.path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hosts\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.client.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.server.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.changes.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.effective.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.email\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.full_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.target.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.root\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.cls\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.fid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.tbt\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.sum\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.max\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.histogram\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"metricset.period\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.build.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reason\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"file.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hosts\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.client.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.server.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.roles\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.version_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.root\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.cls\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.fid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.tbt\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.sum\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.max\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.histogram\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"metricset.period\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, From b09e010b16970239a61dc18363d3bc9c0e6db146 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 27 Jan 2021 13:43:55 +0100 Subject: [PATCH 031/163] [Lens] Clean up usage collector (#89109) --- x-pack/plugins/lens/server/usage/collectors.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/server/usage/collectors.ts b/x-pack/plugins/lens/server/usage/collectors.ts index c32fc0371ed8a..71a699aadb865 100644 --- a/x-pack/plugins/lens/server/usage/collectors.ts +++ b/x-pack/plugins/lens/server/usage/collectors.ts @@ -16,11 +16,6 @@ export function registerLensUsageCollector( usageCollection: UsageCollectionSetup, taskManager: Promise ) { - let isCollectorReady = false; - taskManager.then(() => { - // mark lensUsageCollector as ready to collect when the TaskManager is ready - isCollectorReady = true; - }); const lensUsageCollector = usageCollection.makeUsageCollector({ type: 'lens', async fetch() { @@ -55,7 +50,10 @@ export function registerLensUsageCollector( }; } }, - isReady: () => isCollectorReady, + isReady: async () => { + await taskManager; + return true; + }, schema: lensUsageSchema, }); From ba1e795b3f4341547d3acc4cf9df79646e2b412d Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Wed, 27 Jan 2021 14:23:47 +0100 Subject: [PATCH 032/163] [Lens] Fix indexpattern checks for missing references (#88840) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../dimension_panel/reference_editor.test.tsx | 76 +++++++++++++++- .../dimension_panel/reference_editor.tsx | 42 ++++++--- .../indexpattern_datasource/indexpattern.tsx | 11 ++- .../public/indexpattern_datasource/mocks.ts | 78 +++++++++++++++- .../operations/definitions/index.ts | 4 +- .../operations/layer_helpers.test.ts | 89 +++++++++++++------ .../operations/layer_helpers.ts | 30 ++++--- .../public/indexpattern_datasource/utils.ts | 26 +++++- 8 files changed, 294 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index 0891dd27fcf17..ed1b695640922 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -13,7 +13,7 @@ import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'k import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import { OperationMetadata } from '../../types'; -import { createMockedIndexPattern } from '../mocks'; +import { createMockedIndexPattern, createMockedIndexPatternWithoutType } from '../mocks'; import { ReferenceEditor, ReferenceEditorProps } from './reference_editor'; import { insertOrReplaceColumn } from '../operations'; import { FieldSelect } from './field_select'; @@ -260,6 +260,48 @@ describe('reference editor', () => { ); }); + it("should show the sub-function as invalid if there's no field compatible with it", () => { + // This may happen for saved objects after changing the type of a field + wrapper = mount( + true, + }} + /> + ); + + const subFunctionSelect = wrapper + .find('[data-test-subj="indexPattern-reference-function"]') + .first(); + + expect(subFunctionSelect.prop('isInvalid')).toEqual(true); + expect(subFunctionSelect.prop('selectedOptions')).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + 'data-test-subj': 'lns-indexPatternDimension-avg incompatible', + label: 'Average', + value: 'avg', + }), + ]) + ); + }); + it('should hide the function selector when using a field-only selection style', () => { wrapper = mount( { expect(fieldSelect.prop('markAllFieldsCompatible')).toEqual(true); }); + it('should show the FieldSelect as invalid if the selected field is missing', () => { + wrapper = mount( + true, + }} + /> + ); + + const fieldSelect = wrapper.find(FieldSelect); + expect(fieldSelect.prop('fieldIsInvalid')).toEqual(true); + expect(fieldSelect.prop('selectedField')).toEqual('missing'); + expect(fieldSelect.prop('selectedOperationType')).toEqual('avg'); + expect(fieldSelect.prop('incompleteOperation')).toBeUndefined(); + expect(fieldSelect.prop('markAllFieldsCompatible')).toEqual(false); + }); + it('should show the ParamEditor for functions that offer one', () => { wrapper = mount( value === incompleteInfo.operationType)!] + const selectedOption = incompleteOperation + ? [functionOptions.find(({ value }) => value === incompleteOperation)!] : column ? [functionOptions.find(({ value }) => value === column.operationType)!] : []; // If the operationType is incomplete, the user needs to select a field- so // the function is marked as valid. - const showOperationInvalid = !column && !Boolean(incompleteInfo?.operationType); + const showOperationInvalid = !column && !Boolean(incompleteOperation); // The field is invalid if the operation has been updated without a field, // or if we are in a field-only mode but empty state - const showFieldInvalid = - Boolean(incompleteInfo?.operationType) || (selectionStyle === 'field' && !column); + const showFieldInvalid = Boolean(incompleteOperation) || (selectionStyle === 'field' && !column); + // Check if the field still exists to protect from changes + const showFieldMissingInvalid = !currentIndexPattern.getFieldByName( + incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField + ); + + // what about a field changing type and becoming invalid? + // Let's say this change makes the indexpattern without any number field but the operation was set to a numeric operation. + // At this point the ComboBox will crash. + // Therefore check if the selectedOption is in functionOptions and in case fill it in as disabled option + const showSelectionFunctionInvalid = Boolean(selectedOption.length && selectedOption[0] == null); + if (showSelectionFunctionInvalid) { + const selectedOperationType = incompleteOperation || column.operationType; + const brokenFunctionOption = { + label: operationPanels[selectedOperationType].displayName, + value: selectedOperationType, + className: 'lnsIndexPatternDimensionEditor__operation', + 'data-test-subj': `lns-indexPatternDimension-${selectedOperationType} incompatible`, + }; + functionOptions.push(brokenFunctionOption); + selectedOption[0] = brokenFunctionOption; + } return (
@@ -216,7 +236,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { defaultMessage: 'Choose a sub-function', })} fullWidth - isInvalid={showOperationInvalid} + isInvalid={showOperationInvalid || showSelectionFunctionInvalid} > { @@ -258,11 +278,11 @@ export function ReferenceEditor(props: ReferenceEditorProps) { defaultMessage: 'Select a field', })} fullWidth - isInvalid={showFieldInvalid} + isInvalid={showFieldInvalid || showFieldMissingInvalid} labelAppend={labelAppend} > - (getErrorMessages(layer) ?? []).map((message) => ({ - shortMessage: '', // Not displayed currently - longMessage: message, - })) + (getErrorMessages(layer, state.indexPatterns[layer.indexPatternId]) ?? []).map( + (message) => ({ + shortMessage: '', // Not displayed currently + longMessage: message, + }) + ) ); // Single layer case, no need to explain more diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index d0cbcee61db6f..4aea9e8ac67a9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -6,7 +6,83 @@ import { DragContextState } from '../drag_drop'; import { getFieldByNameFactory } from './pure_helpers'; -import type { IndexPattern } from './types'; +import type { IndexPattern, IndexPatternField } from './types'; + +export const createMockedIndexPatternWithoutType = ( + typeToFilter: IndexPatternField['type'] +): IndexPattern => { + const fields = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'start_date', + displayName: 'start_date', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + esTypes: ['keyword'], + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + esTypes: ['keyword'], + }, + { + name: 'scripted', + displayName: 'Scripted', + type: 'string', + searchable: true, + aggregatable: true, + scripted: true, + lang: 'painless', + script: '1234', + }, + ].filter(({ type }) => type !== typeToFilter); + return { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields, + getFieldByName: getFieldByNameFactory(fields), + }; +}; export const createMockedIndexPattern = (): IndexPattern => { const fields = [ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 7dbc7d3b986a5..1cdaff53c5458 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -197,7 +197,7 @@ interface BaseOperationDefinitionProps { getErrorMessage?: ( layer: IndexPatternLayer, columnId: string, - indexPattern?: IndexPattern + indexPattern: IndexPattern ) => string[] | undefined; /* @@ -301,7 +301,7 @@ interface FieldBasedOperationDefinition { getErrorMessage: ( layer: IndexPatternLayer, columnId: string, - indexPattern?: IndexPattern + indexPattern: IndexPattern ) => string[] | undefined; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index c0e71dc1509e7..94cf13a5c50a4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -2147,14 +2147,17 @@ describe('state_helpers', () => { it('should collect errors from metric-type operation definitions', () => { const mock = jest.fn().mockReturnValue(['error 1']); operationDefinitionMap.avg.getErrorMessage = mock; - const errors = getErrorMessages({ - indexPatternId: '1', - columnOrder: [], - columns: { - // @ts-expect-error invalid column - col1: { operationType: 'avg' }, + const errors = getErrorMessages( + { + indexPatternId: '1', + columnOrder: [], + columns: { + // @ts-expect-error invalid column + col1: { operationType: 'avg' }, + }, }, - }); + indexPattern + ); expect(mock).toHaveBeenCalled(); expect(errors).toHaveLength(1); }); @@ -2162,15 +2165,18 @@ describe('state_helpers', () => { it('should collect errors from reference-type operation definitions', () => { const mock = jest.fn().mockReturnValue(['error 1']); operationDefinitionMap.testReference.getErrorMessage = mock; - const errors = getErrorMessages({ - indexPatternId: '1', - columnOrder: [], - columns: { - col1: - // @ts-expect-error not statically analyzed - { operationType: 'testReference', references: [] }, + const errors = getErrorMessages( + { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: + // @ts-expect-error not statically analyzed + { operationType: 'testReference', references: [] }, + }, }, - }); + indexPattern + ); expect(mock).toHaveBeenCalled(); expect(errors).toHaveLength(1); }); @@ -2184,24 +2190,55 @@ describe('state_helpers', () => { getErrorMessage: incompleteRef, }; - const errors = getErrorMessages({ - indexPatternId: '1', - columnOrder: [], - columns: { - col1: + const errors = getErrorMessages( + { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: + // @ts-expect-error not statically analyzed + { operationType: 'testReference', references: [] }, + }, + incompleteColumns: { // @ts-expect-error not statically analyzed - { operationType: 'testReference', references: [] }, - }, - incompleteColumns: { - // @ts-expect-error not statically analyzed - col1: { operationType: 'testIncompleteReference' }, + col1: { operationType: 'testIncompleteReference' }, + }, }, - }); + indexPattern + ); expect(savedRef).not.toHaveBeenCalled(); expect(incompleteRef).toHaveBeenCalled(); expect(errors).toBeUndefined(); delete operationDefinitionMap.testIncompleteReference; }); + + it('should forward the indexpattern when available', () => { + const mock = jest.fn(); + operationDefinitionMap.testReference.getErrorMessage = mock; + getErrorMessages( + { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: + // @ts-expect-error not statically analyzed + { operationType: 'testReference', references: [] }, + }, + }, + indexPattern + ); + expect(mock).toHaveBeenCalledWith( + { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: { operationType: 'testReference', references: [] }, + }, + }, + 'col1', + indexPattern + ); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index d8244f3902a6e..10618cc754556 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -864,18 +864,24 @@ export function updateLayerIndexPattern( * - All column references are valid * - All prerequisites are met */ -export function getErrorMessages(layer: IndexPatternLayer): string[] | undefined { - const errors: string[] = []; - Object.entries(layer.columns).forEach(([columnId, column]) => { - // If we're transitioning to another operation, check for "new" incompleteColumns rather - // than "old" saved operation on the layer - const columnFinalRef = - layer.incompleteColumns?.[columnId]?.operationType || column.operationType; - const def = operationDefinitionMap[columnFinalRef]; - if (def.getErrorMessage) { - errors.push(...(def.getErrorMessage(layer, columnId) ?? [])); - } - }); +export function getErrorMessages( + layer: IndexPatternLayer, + indexPattern: IndexPattern +): string[] | undefined { + const errors: string[] = Object.entries(layer.columns) + .flatMap(([columnId, column]) => { + // If we're transitioning to another operation, check for "new" incompleteColumns rather + // than "old" saved operation on the layer + const columnFinalRef = + layer.incompleteColumns?.[columnId]?.operationType || column.operationType; + const def = operationDefinitionMap[columnFinalRef]; + + if (def.getErrorMessage) { + return def.getErrorMessage(layer, columnId, indexPattern); + } + }) + // remove the undefined values + .filter((v: string | undefined): v is string => v != null); return errors.length ? errors : undefined; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 57cc4abeb723a..b5a4905a29738 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -7,9 +7,10 @@ import { DataType } from '../types'; import { IndexPattern, IndexPatternLayer } from './types'; import { DraggedField } from './indexpattern'; -import { +import type { BaseIndexPatternColumn, FieldBasedIndexPatternColumn, + ReferenceBasedIndexPatternColumn, } from './operations/definitions/column_types'; import { operationDefinitionMap, IndexPatternColumn } from './operations'; @@ -53,12 +54,29 @@ export function isColumnInvalid( if (!column) return; const operationDefinition = column.operationType && operationDefinitionMap[column.operationType]; - return !!( - operationDefinition.getErrorMessage && - operationDefinition.getErrorMessage(layer, columnId, indexPattern) + // check also references for errors + const referencesHaveErrors = + true && + 'references' in column && + Boolean(getReferencesErrors(layer, column, indexPattern).filter(Boolean).length); + + return ( + !!operationDefinition.getErrorMessage?.(layer, columnId, indexPattern) || referencesHaveErrors ); } +function getReferencesErrors( + layer: IndexPatternLayer, + column: ReferenceBasedIndexPatternColumn, + indexPattern: IndexPattern +) { + return column.references?.map((referenceId: string) => { + const referencedOperation = layer.columns[referenceId]?.operationType; + const referencedDefinition = operationDefinitionMap[referencedOperation]; + return referencedDefinition?.getErrorMessage?.(layer, referenceId, indexPattern); + }); +} + export function fieldIsInvalid(column: IndexPatternColumn | undefined, indexPattern: IndexPattern) { if (!column || !hasField(column)) { return false; From 6425c5d6ae343551e953c31b4d9e4ed0b231353a Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Wed, 27 Jan 2021 14:07:20 +0000 Subject: [PATCH 033/163] Fixed regex bug in Safari (#89399) * Fixed regex bug in Safari * Added extra unit test --- x-pack/plugins/security/common/constants.ts | 2 +- .../management/users/edit_user/create_user_page.test.tsx | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index c235c296bcbae..372f539d812fd 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -29,7 +29,7 @@ export const NEXT_URL_QUERY_STRING_PARAMETER = 'next'; * - Must contain only letters, numbers, spaces, punctuation and printable symbols. * - Must not contain leading or trailing spaces. */ -export const NAME_REGEX = /^(?! )[a-zA-Z0-9 !"#$%&'()*+,\-./\\:;<=>?@\[\]^_`{|}~]+(??@\[\]^_`{|}~]*[a-zA-Z0-9!"#$%&'()*+,\-./\\:;<=>?@\[\]^_`{|}~]$/; /** * Maximum length of usernames and role names. diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx index e7e3e1164ae14..99c67201b2b56 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx @@ -88,6 +88,12 @@ describe('CreateUserPage', () => { await findAllByText(/Username must not contain leading or trailing spaces/i); + fireEvent.change(await findByLabelText('Username'), { + target: { value: 'username_with_trailing_space ' }, + }); + + await findAllByText(/Username must not contain leading or trailing spaces/i); + fireEvent.change(await findByLabelText('Username'), { target: { value: '€' }, }); From cadcbf88452165a8edf7f8c5328dc74cd200d287 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 27 Jan 2021 08:52:44 -0600 Subject: [PATCH 034/163] [Enterprise Search] Add links to doc links service (#89260) * [Enterprise Search] Add links to doc links service * Remove extra slash * Add types * Update API docs and API review --- .../kibana-plugin-core-public.doclinksstart.links.md | 5 +++++ .../public/kibana-plugin-core-public.doclinksstart.md | 2 +- src/core/public/doc_links/doc_links_service.ts | 10 ++++++++++ src/core/public/public.api.md | 5 +++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index ff2a8a2b5f75f..79c603165cae4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -30,6 +30,11 @@ readonly links: { readonly metricbeat: { readonly base: string; }; + readonly enterpriseSearch: { + readonly base: string; + readonly appSearchBase: string; + readonly workplaceSearchBase: string; + }; readonly heartbeat: { readonly base: string; }; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 8404326f773e6..f4bce8b51ebb1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index b82254e5a1416..c732fc7823b62 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -46,6 +46,11 @@ export class DocLinksService { auditbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/auditbeat/${DOC_LINK_VERSION}`, }, + enterpriseSearch: { + base: `${ELASTIC_WEBSITE_URL}guide/en/enterprise-search/${DOC_LINK_VERSION}`, + appSearchBase: `${ELASTIC_WEBSITE_URL}guide/en/app-search/${DOC_LINK_VERSION}`, + workplaceSearchBase: `${ELASTIC_WEBSITE_URL}guide/en/workplace-search/${DOC_LINK_VERSION}`, + }, metricbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}`, }, @@ -260,6 +265,11 @@ export interface DocLinksStart { readonly metricbeat: { readonly base: string; }; + readonly enterpriseSearch: { + readonly base: string; + readonly appSearchBase: string; + readonly workplaceSearchBase: string; + }; readonly heartbeat: { readonly base: string; }; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 52fc8fbf33910..d72b2aa5afd1e 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -483,6 +483,11 @@ export interface DocLinksStart { readonly metricbeat: { readonly base: string; }; + readonly enterpriseSearch: { + readonly base: string; + readonly appSearchBase: string; + readonly workplaceSearchBase: string; + }; readonly heartbeat: { readonly base: string; }; From d1b88afd3b0c15a65158f39992d2ff7d8e129e75 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 27 Jan 2021 17:01:02 +0200 Subject: [PATCH 035/163] Increase the time needed to locate the save viz toast (#89301) --- test/functional/page_objects/common_page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 1149f1b100788..ed817b8b55e80 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -376,7 +376,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } async closeToast() { - const toast = await find.byCssSelector('.euiToast', 2 * defaultFindTimeout); + const toast = await find.byCssSelector('.euiToast', 6 * defaultFindTimeout); await toast.moveMouseTo(); const title = await (await find.byCssSelector('.euiToastHeader__title')).getVisibleText(); From 0abf45fcf1673a714c2b4fa894cf58fb9b24e265 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 27 Jan 2021 17:03:44 +0200 Subject: [PATCH 036/163] [Vega Docs] Add experimental flag on the vega maps title (#89402) * [Vega Docs] Add experimental flag on the vega maps title * Add the experimental warning on the initial paragraph of vega maps to be more visible --- docs/user/dashboard/vega-reference.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 99c5e62697cfd..4a0598cc569cd 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -237,7 +237,7 @@ format: {property: "features"} [[vega-with-a-map]] ==== Vega with a Map -To enable *Maps*, the graph must specify `type=map` in the host configuration: +experimental[] To enable *Maps*, the graph must specify `type=map` in the host configuration: [source,yaml] ---- @@ -335,7 +335,7 @@ Use the contextual *Inspect* tool to gain insights into different elements. [float] [[inspect-elasticsearch-requests]] -======= Inspect {es} requests +====== Inspect {es} requests *Vega* uses the {ref}/search-search.html[{es} search API] to get documents and aggregation results from {es}. To troubleshoot these requests, click *Inspect*, which shows the most recent requests. From da8d6b939a9a4780292cdc6cd243752e597b0846 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 27 Jan 2021 08:18:54 -0700 Subject: [PATCH 037/163] Migrate maps_legacy, maps_oss, region_map, and tile_map plugions to TS projects (#89351) --- src/plugins/maps_legacy/public/index.ts | 3 --- .../maps_legacy/public/map/color_util.d.ts | 11 ++++++++ .../public/map/kibana_map_layer.d.ts | 25 +++++++++++++++++++ .../maps_legacy/public/tooltip_provider.d.ts | 9 +++++++ src/plugins/maps_legacy/tsconfig.json | 14 +++++++++++ src/plugins/maps_oss/tsconfig.json | 14 +++++++++++ src/plugins/region_map/tsconfig.json | 15 +++++++++++ src/plugins/tile_map/tsconfig.json | 15 +++++++++++ tsconfig.json | 8 ++++++ tsconfig.refs.json | 4 +++ 10 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/plugins/maps_legacy/public/map/color_util.d.ts create mode 100644 src/plugins/maps_legacy/public/map/kibana_map_layer.d.ts create mode 100644 src/plugins/maps_legacy/public/tooltip_provider.d.ts create mode 100644 src/plugins/maps_legacy/tsconfig.json create mode 100644 src/plugins/maps_oss/tsconfig.json create mode 100644 src/plugins/region_map/tsconfig.json create mode 100644 src/plugins/tile_map/tsconfig.json diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index 95550fab1ba17..9268f14995f44 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -8,9 +8,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { MapsLegacyPlugin } from './plugin'; -// @ts-ignore import * as colorUtil from './map/color_util'; -// @ts-ignore import { KibanaMapLayer } from './map/kibana_map_layer'; import { VectorLayer, @@ -19,7 +17,6 @@ import { TmsLayer, IServiceSettings, } from './map/service_settings_types'; -// @ts-ignore import { mapTooltipProvider } from './tooltip_provider'; import './map/index.scss'; diff --git a/src/plugins/maps_legacy/public/map/color_util.d.ts b/src/plugins/maps_legacy/public/map/color_util.d.ts new file mode 100644 index 0000000000000..9ec6b3c1fb007 --- /dev/null +++ b/src/plugins/maps_legacy/public/map/color_util.d.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export function getLegendColors(colorRamp: unknown, numLegendColors?: number): string[]; + +export function getColor(colorRamp: unknown, i: number): string; diff --git a/src/plugins/maps_legacy/public/map/kibana_map_layer.d.ts b/src/plugins/maps_legacy/public/map/kibana_map_layer.d.ts new file mode 100644 index 0000000000000..222cb6b215f9a --- /dev/null +++ b/src/plugins/maps_legacy/public/map/kibana_map_layer.d.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export class KibanaMapLayer { + constructor(); + + getBounds(): Promise; + + addToLeafletMap(leafletMap: unknown): void; + + removeFromLeafletMap(leafletMap: unknown): void; + + appendLegendContents(): void; + + updateExtent(): void; + + movePointer(): void; + + getAttributions(): unknown; +} diff --git a/src/plugins/maps_legacy/public/tooltip_provider.d.ts b/src/plugins/maps_legacy/public/tooltip_provider.d.ts new file mode 100644 index 0000000000000..4082a6ef83c4d --- /dev/null +++ b/src/plugins/maps_legacy/public/tooltip_provider.d.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export function mapTooltipProvider(element: unknown, formatter: unknown): () => unknown; diff --git a/src/plugins/maps_legacy/tsconfig.json b/src/plugins/maps_legacy/tsconfig.json new file mode 100644 index 0000000000000..e7ea06706b64f --- /dev/null +++ b/src/plugins/maps_legacy/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts"], + "references": [ + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/src/plugins/maps_oss/tsconfig.json b/src/plugins/maps_oss/tsconfig.json new file mode 100644 index 0000000000000..03c30c3c49fd3 --- /dev/null +++ b/src/plugins/maps_oss/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts"], + "references": [ + { "path": "../visualizations/tsconfig.json" }, + ] +} diff --git a/src/plugins/region_map/tsconfig.json b/src/plugins/region_map/tsconfig.json new file mode 100644 index 0000000000000..40f76ece2a6ff --- /dev/null +++ b/src/plugins/region_map/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*"], + "references": [ + { "path": "../maps_legacy/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/src/plugins/tile_map/tsconfig.json b/src/plugins/tile_map/tsconfig.json new file mode 100644 index 0000000000000..40f76ece2a6ff --- /dev/null +++ b/src/plugins/tile_map/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*"], + "references": [ + { "path": "../maps_legacy/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/tsconfig.json b/tsconfig.json index e7856aa0c8747..334a3febfddda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,8 +27,11 @@ "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", "src/plugins/management/**/*", + "src/plugins/maps_legacy/**/*", + "src/plugins/maps_oss/**/*", "src/plugins/navigation/**/*", "src/plugins/newsfeed/**/*", + "src/plugins/region_map/**/*", "src/plugins/saved_objects/**/*", "src/plugins/saved_objects_management/**/*", "src/plugins/saved_objects_tagging_oss/**/*", @@ -37,6 +40,7 @@ "src/plugins/spaces_oss/**/*", "src/plugins/telemetry/**/*", "src/plugins/telemetry_collection_manager/**/*", + "src/plugins/tile_map/**/*", "src/plugins/timelion/**/*", "src/plugins/ui_actions/**/*", "src/plugins/url_forwarding/**/*", @@ -81,8 +85,11 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, + { "path": "./src/plugins/maps_legacy/tsconfig.json" }, + { "path": "./src/plugins/maps_oss/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/region_map/tsconfig.json" }, { "path": "./src/plugins/saved_objects/tsconfig.json" }, { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, @@ -91,6 +98,7 @@ { "path": "./src/plugins/spaces_oss/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/tile_map/tsconfig.json" }, { "path": "./src/plugins/timelion/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 5edfd4231a6d5..a8eecd278160c 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -23,8 +23,11 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, + { "path": "./src/plugins/maps_legacy/tsconfig.json" }, + { "path": "./src/plugins/maps_oss/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/region_map/tsconfig.json" }, { "path": "./src/plugins/saved_objects/tsconfig.json" }, { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, @@ -34,6 +37,7 @@ { "path": "./src/plugins/spaces_oss/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/tile_map/tsconfig.json" }, { "path": "./src/plugins/timelion/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, From 007e7e4506a9256c62446a8035ae22160e69f203 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 27 Jan 2021 10:22:36 -0500 Subject: [PATCH 038/163] [Upgrade Assistant] Migrate server to new es-js client (#89207) --- .../plugins/upgrade_assistant/common/types.ts | 19 +- .../components/tabs/checkup/constants.tsx | 3 +- .../components/tabs/checkup/controls.tsx | 3 +- .../checkup/deprecations/grouped.test.tsx | 4 +- .../tabs/checkup/deprecations/grouped.tsx | 4 +- .../tabs/checkup/deprecations/health.tsx | 3 +- .../tabs/checkup/deprecations/list.tsx | 4 +- .../tabs/checkup/filter_bar.test.tsx | 3 +- .../components/tabs/checkup/filter_bar.tsx | 3 +- .../lib/es_deprecation_logging_apis.test.ts | 14 +- .../server/lib/es_deprecation_logging_apis.ts | 12 +- .../server/lib/es_indices_state_check.ts | 13 +- .../server/lib/es_migration_apis.test.ts | 111 +++-- .../server/lib/es_migration_apis.ts | 24 +- .../server/lib/es_version_precheck.test.ts | 94 ++-- .../server/lib/es_version_precheck.ts | 22 +- .../lib/reindexing/reindex_actions.test.ts | 33 +- .../server/lib/reindexing/reindex_actions.ts | 13 +- .../lib/reindexing/reindex_service.test.ts | 418 ++++++++++-------- .../server/lib/reindexing/reindex_service.ts | 107 ++--- .../server/lib/reindexing/worker.ts | 13 +- .../lib/telemetry/usage_collector.test.ts | 20 +- .../server/lib/telemetry/usage_collector.ts | 17 +- .../server/routes/__mocks__/routes.mock.ts | 4 +- .../server/routes/cluster_checkup.test.ts | 2 +- .../server/routes/cluster_checkup.ts | 12 +- .../server/routes/deprecation_logging.test.ts | 24 +- .../server/routes/deprecation_logging.ts | 8 +- .../routes/reindex_indices/reindex_handler.ts | 6 +- .../routes/reindex_indices/reindex_indices.ts | 37 +- .../upgrade_assistant/upgrade_assistant.ts | 2 +- .../upgrade_assistant_integration/config.js | 2 - .../services/index.js | 7 - .../services/legacy_es.js | 18 - .../upgrade_assistant/reindexing.js | 22 +- 35 files changed, 570 insertions(+), 531 deletions(-) delete mode 100644 x-pack/test/upgrade_assistant_integration/services/index.js delete mode 100644 x-pack/test/upgrade_assistant_integration/services/legacy_es.js diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index 2a94b39ca6c66..9625ca89b11d0 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -5,8 +5,6 @@ */ import { SavedObject, SavedObjectAttributes } from 'src/core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../src/core/server/elasticsearch/legacy/api_types'; export enum ReindexStep { // Enum values are spaced out by 10 to give us room to insert steps in between. @@ -165,6 +163,23 @@ export interface UpgradeAssistantTelemetrySavedObjectAttributes { [key: string]: any; } +export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; +export interface DeprecationInfo { + level: MIGRATION_DEPRECATION_LEVEL; + message: string; + url: string; + details?: string; +} + +export interface IndexSettingsDeprecationInfo { + [indexName: string]: DeprecationInfo[]; +} +export interface DeprecationAPIResponse { + cluster_settings: DeprecationInfo[]; + ml_settings: DeprecationInfo[]; + node_settings: DeprecationInfo[]; + index_settings: IndexSettingsDeprecationInfo; +} export interface EnrichedDeprecationInfo extends DeprecationInfo { index?: string; node?: string; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx index 66c802097055b..8637099b77c9b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx @@ -6,8 +6,7 @@ import { IconColor } from '@elastic/eui'; import { invert } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; +import { DeprecationInfo } from '../../../../../common/types'; export const LEVEL_MAP: { [level: string]: number } = { warning: 0, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx index d75a25a95d67f..c75db7e2f96e1 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx @@ -8,8 +8,7 @@ import React, { FunctionComponent, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; +import { DeprecationInfo } from '../../../../../common/types'; import { GroupByOption, LevelFilterOption, LoadingState } from '../../types'; import { FilterBar } from './filter_bar'; import { GroupByBar } from './group_by_bar'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx index 6bdb5df036224..727d959f49a71 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx @@ -9,9 +9,7 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { EuiBadge, EuiPagination } from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx index de1a5a996d75f..20ffbae143672 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx @@ -18,9 +18,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationCountSummary } from './count_summary'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx index 3ce40d0c4fdf0..c866c1e1f6847 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx @@ -10,8 +10,7 @@ import React, { FunctionComponent } from 'react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; +import { DeprecationInfo } from '../../../../../../common/types'; import { COLOR_MAP, LEVEL_MAP, REVERSE_LEVEL_MAP } from '../constants'; const LocalizedLevels: { [level: string]: string } = { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx index 038f05aace4c3..afc443b8c3b7d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx @@ -6,9 +6,7 @@ import React, { FunctionComponent } from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption } from '../../../types'; import { COLOR_MAP, LEVEL_MAP } from '../constants'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx index 053ef21d6b309..231b15fc52d72 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx @@ -6,9 +6,8 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; +import { DeprecationInfo } from '../../../../../common/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { LevelFilterOption } from '../../types'; import { FilterBar } from './filter_bar'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx index 6939c547fee57..abcd02d5a5ce4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx @@ -10,8 +10,7 @@ import React from 'react'; import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; +import { DeprecationInfo } from '../../../../../common/types'; import { LevelFilterOption } from '../../types'; const LocalizedOptions: { [option: string]: string } = { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts index b0dec299b2b12..dee05c97f11af 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts @@ -12,10 +12,10 @@ import { describe('getDeprecationLoggingStatus', () => { it('calls cluster.getSettings', async () => { - const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); await getDeprecationLoggingStatus(dataClient); - expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.getSettings', { - includeDefaults: true, + expect(dataClient.asCurrentUser.cluster.getSettings).toHaveBeenCalledWith({ + include_defaults: true, }); }); }); @@ -23,9 +23,9 @@ describe('getDeprecationLoggingStatus', () => { describe('setDeprecationLogging', () => { describe('isEnabled = true', () => { it('calls cluster.putSettings with logger.deprecation = WARN', async () => { - const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); await setDeprecationLogging(dataClient, true); - expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.putSettings', { + expect(dataClient.asCurrentUser.cluster.putSettings).toHaveBeenCalledWith({ body: { transient: { 'logger.deprecation': 'WARN' } }, }); }); @@ -33,9 +33,9 @@ describe('setDeprecationLogging', () => { describe('isEnabled = false', () => { it('calls cluster.putSettings with logger.deprecation = ERROR', async () => { - const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); await setDeprecationLogging(dataClient, false); - expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.putSettings', { + expect(dataClient.asCurrentUser.cluster.putSettings).toHaveBeenCalledWith({ body: { transient: { 'logger.deprecation': 'ERROR' } }, }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts index 348eebb97e384..c545d12ac1b82 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { ILegacyScopedClusterClient } from 'src/core/server'; +import { IScopedClusterClient } from 'src/core/server'; interface DeprecationLoggingStatus { isEnabled: boolean; } export async function getDeprecationLoggingStatus( - dataClient: ILegacyScopedClusterClient + dataClient: IScopedClusterClient ): Promise { - const response = await dataClient.callAsCurrentUser('cluster.getSettings', { - includeDefaults: true, + const { body: response } = await dataClient.asCurrentUser.cluster.getSettings({ + include_defaults: true, }); return { @@ -23,10 +23,10 @@ export async function getDeprecationLoggingStatus( } export async function setDeprecationLogging( - dataClient: ILegacyScopedClusterClient, + dataClient: IScopedClusterClient, isEnabled: boolean ): Promise { - const response = await dataClient.callAsCurrentUser('cluster.putSettings', { + const { body: response } = await dataClient.asCurrentUser.cluster.putSettings({ body: { transient: { 'logger.deprecation': isEnabled ? 'WARN' : 'ERROR', diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts index bce48b152700f..739499e2235b5 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts @@ -4,22 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { getIndexState } from '../../common/get_index_state'; import { ResolveIndexResponseFromES } from '../../common/types'; type StatusCheckResult = Record; export const esIndicesStateCheck = async ( - callAsUser: LegacyAPICaller, + asCurrentUser: ElasticsearchClient, indices: string[] ): Promise => { - const response: ResolveIndexResponseFromES = await callAsUser('transport.request', { - method: 'GET', - path: `/_resolve/index/*`, - query: { - expand_wildcards: 'all', - }, + const { body: response } = await asCurrentUser.indices.resolveIndex({ + name: '*', + expand_wildcards: 'all', }); const result: StatusCheckResult = {}; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index 312a7275382b8..6e8a729b4d3bb 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -5,95 +5,94 @@ */ import _ from 'lodash'; +import { RequestEvent } from '@elastic/elasticsearch/lib/Transport'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationAPIResponse } from '../../../../../src/core/server/elasticsearch/legacy/api_types'; +import { DeprecationAPIResponse } from '../../common/types'; import { getUpgradeAssistantStatus } from './es_migration_apis'; import fakeDeprecations from './__fixtures__/fake_deprecations.json'; const fakeIndexNames = Object.keys(fakeDeprecations.index_settings); +const asApiResponse = (body: T): RequestEvent => + ({ + body, + } as RequestEvent); + describe('getUpgradeAssistantStatus', () => { const resolvedIndices = { indices: fakeIndexNames.map((f) => ({ name: f, attributes: ['open'] })), }; - let deprecationsResponse: DeprecationAPIResponse; - - const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - (dataClient.callAsCurrentUser as jest.Mock).mockImplementation(async (api, { path }) => { - if (path === '/_migration/deprecations') { - return deprecationsResponse; - } else if (path === '/_resolve/index/*') { - return resolvedIndices; - } else if (api === 'indices.getMapping') { - return {}; - } else { - throw new Error(`Unexpected API call: ${path}`); - } - }); + // @ts-expect-error mock data is too loosely typed + const deprecationsResponse: DeprecationAPIResponse = _.cloneDeep(fakeDeprecations); - beforeEach(() => { - // @ts-expect-error mock data is too loosely typed - deprecationsResponse = _.cloneDeep(fakeDeprecations); - }); + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + + esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + asApiResponse(deprecationsResponse) + ); + + esClient.asCurrentUser.indices.resolveIndex.mockResolvedValue(asApiResponse(resolvedIndices)); it('calls /_migration/deprecations', async () => { - await getUpgradeAssistantStatus(dataClient, false); - expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('transport.request', { - path: '/_migration/deprecations', - method: 'GET', - }); + await getUpgradeAssistantStatus(esClient, false); + expect(esClient.asCurrentUser.migration.deprecations).toHaveBeenCalled(); }); it('returns the correct shape of data', async () => { - const resp = await getUpgradeAssistantStatus(dataClient, false); + const resp = await getUpgradeAssistantStatus(esClient, false); expect(resp).toMatchSnapshot(); }); it('returns readyForUpgrade === false when critical issues found', async () => { - deprecationsResponse = { - cluster_settings: [{ level: 'critical', message: 'Do count me', url: 'https://...' }], - node_settings: [], - ml_settings: [], - index_settings: {}, - }; - - await expect(getUpgradeAssistantStatus(dataClient, false)).resolves.toHaveProperty( + esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + asApiResponse({ + cluster_settings: [{ level: 'critical', message: 'Do count me', url: 'https://...' }], + node_settings: [], + ml_settings: [], + index_settings: {}, + }) + ); + + await expect(getUpgradeAssistantStatus(esClient, false)).resolves.toHaveProperty( 'readyForUpgrade', false ); }); it('returns readyForUpgrade === true when no critical issues found', async () => { - deprecationsResponse = { - cluster_settings: [{ level: 'warning', message: 'Do not count me', url: 'https://...' }], - node_settings: [], - ml_settings: [], - index_settings: {}, - }; - - await expect(getUpgradeAssistantStatus(dataClient, false)).resolves.toHaveProperty( + esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + asApiResponse({ + cluster_settings: [{ level: 'warning', message: 'Do not count me', url: 'https://...' }], + node_settings: [], + ml_settings: [], + index_settings: {}, + }) + ); + + await expect(getUpgradeAssistantStatus(esClient, false)).resolves.toHaveProperty( 'readyForUpgrade', true ); }); it('filters out security realm deprecation on Cloud', async () => { - deprecationsResponse = { - cluster_settings: [ - { - level: 'critical', - message: 'Security realm settings structure changed', - url: 'https://...', - }, - ], - node_settings: [], - ml_settings: [], - index_settings: {}, - }; - - const result = await getUpgradeAssistantStatus(dataClient, true); + esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + asApiResponse({ + cluster_settings: [ + { + level: 'critical', + message: 'Security realm settings structure changed', + url: 'https://...', + }, + ], + node_settings: [], + ml_settings: [], + index_settings: {}, + }) + ); + + const result = await getUpgradeAssistantStatus(esClient, true); expect(result).toHaveProperty('readyForUpgrade', true); expect(result).toHaveProperty('cluster', []); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts index 9f55b9d049735..22b9956fc957a 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts @@ -4,21 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'src/core/server'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { DeprecationAPIResponse } from '../../../../../src/core/server/elasticsearch/legacy/api_types'; -import { EnrichedDeprecationInfo, UpgradeAssistantStatus } from '../../common/types'; +import { IScopedClusterClient } from 'src/core/server'; +import { + DeprecationAPIResponse, + EnrichedDeprecationInfo, + UpgradeAssistantStatus, +} from '../../common/types'; import { esIndicesStateCheck } from './es_indices_state_check'; export async function getUpgradeAssistantStatus( - dataClient: ILegacyScopedClusterClient, + dataClient: IScopedClusterClient, isCloudEnabled: boolean ): Promise { - const deprecations = await dataClient.callAsCurrentUser('transport.request', { - path: '/_migration/deprecations', - method: 'GET', - }); + const { + body: deprecations, + } = await dataClient.asCurrentUser.migration.deprecations(); const cluster = getClusterDeprecations(deprecations, isCloudEnabled); const indices = getCombinedIndexInfos(deprecations); @@ -28,10 +29,7 @@ export async function getUpgradeAssistantStatus( // If we have found deprecation information for index/indices check whether the index is // open or closed. if (indexNames.length) { - const indexStates = await esIndicesStateCheck( - dataClient.callAsCurrentUser.bind(dataClient), - indexNames - ); + const indexStates = await esIndicesStateCheck(dataClient.asCurrentUser, indexNames); indices.forEach((indexData) => { indexData.blockerForReindexing = diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts index 78f159ed98867..2310f993ce27d 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts @@ -5,7 +5,7 @@ */ import { SemVer } from 'semver'; -import { ILegacyScopedClusterClient, kibanaResponseFactory } from 'src/core/server'; +import { IScopedClusterClient, kibanaResponseFactory } from 'src/core/server'; import { xpackMocks } from '../../../../mocks'; import { CURRENT_VERSION } from '../../common/version'; import { @@ -17,14 +17,20 @@ import { describe('getAllNodeVersions', () => { it('returns a list of unique node versions', async () => { const adminClient = ({ - callAsInternalUser: jest.fn().mockResolvedValue({ + asInternalUser: { nodes: { - node1: { version: '7.0.0' }, - node2: { version: '7.0.0' }, - node3: { version: '6.0.0' }, + info: jest.fn().mockResolvedValue({ + body: { + nodes: { + node1: { version: '7.0.0' }, + node2: { version: '7.0.0' }, + node3: { version: '6.0.0' }, + }, + }, + }), }, - }), - } as unknown) as ILegacyScopedClusterClient; + }, + } as unknown) as IScopedClusterClient; await expect(getAllNodeVersions(adminClient)).resolves.toEqual([ new SemVer('6.0.0'), @@ -73,12 +79,18 @@ describe('verifyAllMatchKibanaVersion', () => { describe('EsVersionPrecheck', () => { it('returns a 403 when callCluster fails with a 403', async () => { - const fakeCall = jest.fn().mockRejectedValue({ status: 403 }); + const fakeCall = jest.fn().mockRejectedValue({ statusCode: 403 }); const ctx = xpackMocks.createRequestHandlerContext(); - ctx.core.elasticsearch.legacy.client = { - callAsCurrentUser: jest.fn(), - callAsInternalUser: fakeCall, + ctx.core.elasticsearch.client = { + asInternalUser: { + ...ctx.core.elasticsearch.client.asInternalUser, + nodes: { + ...ctx.core.elasticsearch.client.asInternalUser.nodes, + info: fakeCall, + }, + }, + asCurrentUser: ctx.core.elasticsearch.client.asCurrentUser, }; const result = await esVersionCheck(ctx, kibanaResponseFactory); @@ -87,14 +99,22 @@ describe('EsVersionPrecheck', () => { it('returns a 426 message w/ allNodesUpgraded = false when nodes are not on same version', async () => { const ctx = xpackMocks.createRequestHandlerContext(); - ctx.core.elasticsearch.legacy.client = { - callAsCurrentUser: jest.fn(), - callAsInternalUser: jest.fn().mockResolvedValue({ + ctx.core.elasticsearch.client = { + asInternalUser: { + ...ctx.core.elasticsearch.client.asInternalUser, nodes: { - node1: { version: CURRENT_VERSION.raw }, - node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, + ...ctx.core.elasticsearch.client.asInternalUser.nodes, + info: jest.fn().mockResolvedValue({ + body: { + nodes: { + node1: { version: CURRENT_VERSION.raw }, + node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, + }, + }, + }), }, - }), + }, + asCurrentUser: ctx.core.elasticsearch.client.asCurrentUser, }; const result = await esVersionCheck(ctx, kibanaResponseFactory); @@ -104,14 +124,22 @@ describe('EsVersionPrecheck', () => { it('returns a 426 message w/ allNodesUpgraded = true when nodes are on next version', async () => { const ctx = xpackMocks.createRequestHandlerContext(); - ctx.core.elasticsearch.legacy.client = { - callAsCurrentUser: jest.fn(), - callAsInternalUser: jest.fn().mockResolvedValue({ + ctx.core.elasticsearch.client = { + asInternalUser: { + ...ctx.core.elasticsearch.client.asInternalUser, nodes: { - node1: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, - node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, + ...ctx.core.elasticsearch.client.asInternalUser.nodes, + info: jest.fn().mockResolvedValue({ + body: { + nodes: { + node1: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, + node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, + }, + }, + }), }, - }), + }, + asCurrentUser: ctx.core.elasticsearch.client.asCurrentUser, }; const result = await esVersionCheck(ctx, kibanaResponseFactory); @@ -121,14 +149,22 @@ describe('EsVersionPrecheck', () => { it('returns undefined when nodes are on same version', async () => { const ctx = xpackMocks.createRequestHandlerContext(); - ctx.core.elasticsearch.legacy.client = { - callAsCurrentUser: jest.fn(), - callAsInternalUser: jest.fn().mockResolvedValue({ + ctx.core.elasticsearch.client = { + asInternalUser: { + ...ctx.core.elasticsearch.client.asInternalUser, nodes: { - node1: { version: CURRENT_VERSION.raw }, - node2: { version: CURRENT_VERSION.raw }, + ...ctx.core.elasticsearch.client.asInternalUser.nodes, + info: jest.fn().mockResolvedValue({ + body: { + nodes: { + node1: { version: CURRENT_VERSION.raw }, + node2: { version: CURRENT_VERSION.raw }, + }, + }, + }), }, - }), + }, + asCurrentUser: ctx.core.elasticsearch.client.asCurrentUser, }; await expect(esVersionCheck(ctx, kibanaResponseFactory)).resolves.toBe(undefined); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts index 2b49d4c286f61..be6c4f5ff0230 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts @@ -7,7 +7,7 @@ import { uniq } from 'lodash'; import { SemVer } from 'semver'; import { - ILegacyScopedClusterClient, + IScopedClusterClient, KibanaRequest, KibanaResponseFactory, RequestHandler, @@ -15,14 +15,22 @@ import { } from 'src/core/server'; import { CURRENT_VERSION } from '../../common/version'; +interface Nodes { + nodes: { + [nodeId: string]: { version: string }; + }; +} + /** * Returns an array of all the unique Elasticsearch Node Versions in the Elasticsearch cluster. */ -export const getAllNodeVersions = async (adminClient: ILegacyScopedClusterClient) => { +export const getAllNodeVersions = async (adminClient: IScopedClusterClient) => { // Get the version information for all nodes in the cluster. - const { nodes } = (await adminClient.callAsInternalUser('nodes.info', { - filterPath: 'nodes.*.version', - })) as { nodes: { [nodeId: string]: { version: string } } }; + const response = await adminClient.asInternalUser.nodes.info({ + filter_path: 'nodes.*.version', + }); + + const nodes = response.body.nodes; const versionStrings = Object.values(nodes).map(({ version }) => version); @@ -62,13 +70,13 @@ export const esVersionCheck = async ( ctx: RequestHandlerContext, response: KibanaResponseFactory ) => { - const { client } = ctx.core.elasticsearch.legacy; + const { client } = ctx.core.elasticsearch; let allNodeVersions: SemVer[]; try { allNodeVersions = await getAllNodeVersions(client); } catch (e) { - if (e.status === 403) { + if (e.statusCode === 403) { return response.forbidden({ body: e.message }); } diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 525c3781be749..d059c03bcecb1 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -3,7 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; +import { RequestEvent } from '@elastic/elasticsearch/lib/Transport'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ScopedClusterClientMock } from 'src/core/server/elasticsearch/client/mocks'; import moment from 'moment'; import { @@ -18,7 +22,7 @@ import { LOCK_WINDOW, ReindexActions, reindexActionsFactory } from './reindex_ac describe('ReindexActions', () => { let client: jest.Mocked; - let callCluster: jest.Mock; + let clusterClient: ScopedClusterClientMock; let actions: ReindexActions; const unimplemented = (name: string) => () => @@ -38,8 +42,8 @@ describe('ReindexActions', () => { Promise.resolve({ id, attributes } as ReindexSavedObject) ) as any, }; - callCluster = jest.fn(); - actions = reindexActionsFactory(client, callCluster); + clusterClient = elasticsearchServiceMock.createScopedClusterClient(); + actions = reindexActionsFactory(client, clusterClient.asCurrentUser); }); describe('createReindexOp', () => { @@ -281,13 +285,20 @@ describe('ReindexActions', () => { }); describe('getFlatSettings', () => { + const asApiResponse = (body: T): RequestEvent => + ({ + body, + } as RequestEvent); + it('returns flat settings', async () => { - callCluster.mockResolvedValueOnce({ - myIndex: { - settings: { 'index.mySetting': '1' }, - mappings: {}, - }, - }); + clusterClient.asCurrentUser.indices.getSettings.mockResolvedValueOnce( + asApiResponse({ + myIndex: { + settings: { 'index.mySetting': '1' }, + mappings: {}, + }, + }) + ); await expect(actions.getFlatSettings('myIndex')).resolves.toEqual({ settings: { 'index.mySetting': '1' }, mappings: {}, @@ -295,7 +306,7 @@ describe('ReindexActions', () => { }); it('returns null if index does not exist', async () => { - callCluster.mockResolvedValueOnce({}); + clusterClient.asCurrentUser.indices.getSettings.mockResolvedValueOnce(asApiResponse({})); await expect(actions.getFlatSettings('myIndex')).resolves.toBeNull(); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts index 6d8afee1ff950..611ab3c92b72b 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { SavedObjectsFindResponse, SavedObjectsClientContract, - LegacyAPICaller, + ElasticsearchClient, } from 'src/core/server'; import { IndexGroup, @@ -116,7 +116,7 @@ export interface ReindexActions { export const reindexActionsFactory = ( client: SavedObjectsClientContract, - callAsUser: LegacyAPICaller + esClient: ElasticsearchClient ): ReindexActions => { // ----- Internal functions const isLocked = (reindexOp: ReindexSavedObject) => { @@ -236,9 +236,12 @@ export const reindexActionsFactory = ( }, async getFlatSettings(indexName: string) { - const flatSettings = (await callAsUser('transport.request', { - path: `/${encodeURIComponent(indexName)}?flat_settings=true`, - })) as { [indexName: string]: FlatSettings }; + const { body: flatSettings } = await esClient.indices.getSettings<{ + [indexName: string]: FlatSettings; + }>({ + index: indexName, + flat_settings: true, + }); if (!flatSettings[indexName]) { return null; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index dea9974791a88..8a7033c1594da 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -5,8 +5,11 @@ */ jest.mock('../es_indices_state_check', () => ({ esIndicesStateCheck: jest.fn() })); import { BehaviorSubject } from 'rxjs'; +import { RequestEvent } from '@elastic/elasticsearch/lib/Transport'; import { Logger } from 'src/core/server'; -import { loggingSystemMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ScopedClusterClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { IndexGroup, @@ -28,9 +31,14 @@ import { reindexServiceFactory, } from './reindex_service'; +const asApiResponse = (body: T): RequestEvent => + ({ + body, + } as RequestEvent); + describe('reindexService', () => { let actions: jest.Mocked; - let callCluster: jest.Mock; + let clusterClient: ScopedClusterClientMock; let log: Logger; let service: ReindexService; let licensingPluginSetup: LicensingPluginSetup; @@ -59,7 +67,7 @@ describe('reindexService', () => { decrementIndexGroupReindexes: jest.fn(unimplemented('decrementIndexGroupReindexes')), runWhileIndexGroupLocked: jest.fn(async (group: string, f: any) => f({ attributes: {} })), }; - callCluster = jest.fn(); + clusterClient = elasticsearchServiceMock.createScopedClusterClient(); log = loggingSystemMock.create().get(); licensingPluginSetup = licensingMock.createSetup(); licensingPluginSetup.license$ = new BehaviorSubject( @@ -68,7 +76,12 @@ describe('reindexService', () => { }) ); - service = reindexServiceFactory(callCluster as any, actions, log, licensingPluginSetup); + service = reindexServiceFactory( + clusterClient.asCurrentUser, + actions, + log, + licensingPluginSetup + ); }); describe('hasRequiredPrivileges', () => { @@ -83,13 +96,13 @@ describe('reindexService', () => { }); it('calls security API with basic requirements', async () => { - callCluster.mockResolvedValueOnce({ has_all_requested: true }); + clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( + asApiResponse({ has_all_requested: true }) + ); const hasRequired = await service.hasRequiredPrivileges('anIndex'); expect(hasRequired).toBe(true); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_security/user/_has_privileges', - method: 'POST', + expect(clusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ body: { cluster: ['manage'], index: [ @@ -108,12 +121,12 @@ describe('reindexService', () => { }); it('includes manage_ml for ML indices', async () => { - callCluster.mockResolvedValueOnce({ has_all_requested: true }); + clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( + asApiResponse({ has_all_requested: true }) + ); await service.hasRequiredPrivileges('.ml-anomalies'); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_security/user/_has_privileges', - method: 'POST', + expect(clusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ body: { cluster: ['manage', 'manage_ml'], index: [ @@ -132,15 +145,15 @@ describe('reindexService', () => { }); it('includes checking for permissions on the baseName which could be an alias', async () => { - callCluster.mockResolvedValueOnce({ has_all_requested: true }); + clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( + asApiResponse({ has_all_requested: true }) + ); const hasRequired = await service.hasRequiredPrivileges( `reindexed-v${PREV_MAJOR_VERSION}-anIndex` ); expect(hasRequired).toBe(true); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_security/user/_has_privileges', - method: 'POST', + expect(clusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ body: { cluster: ['manage'], index: [ @@ -163,12 +176,14 @@ describe('reindexService', () => { }); it('includes manage_watcher for watcher indices', async () => { - callCluster.mockResolvedValueOnce({ has_all_requested: true }); + clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( + asApiResponse({ + has_all_requested: true, + }) + ); await service.hasRequiredPrivileges('.watches'); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_security/user/_has_privileges', - method: 'POST', + expect(clusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ body: { cluster: ['manage', 'manage_watcher'], index: [ @@ -212,7 +227,7 @@ describe('reindexService', () => { describe('createReindexOperation', () => { it('creates new reindex operation', async () => { - callCluster.mockResolvedValueOnce(true); // indices.exist + clusterClient.asCurrentUser.indices.exists.mockResolvedValueOnce(asApiResponse(true)); actions.findReindexOperations.mockResolvedValueOnce({ total: 0 }); actions.createReindexOp.mockResolvedValueOnce(); @@ -222,13 +237,13 @@ describe('reindexService', () => { }); it('fails if index does not exist', async () => { - callCluster.mockResolvedValueOnce(false); // indices.exist + clusterClient.asCurrentUser.indices.exists.mockResolvedValueOnce(asApiResponse(false)); await expect(service.createReindexOperation('myIndex')).rejects.toThrow(); expect(actions.createReindexOp).not.toHaveBeenCalled(); }); it('deletes existing operation if it failed', async () => { - callCluster.mockResolvedValueOnce(true); // indices.exist + clusterClient.asCurrentUser.indices.exists.mockResolvedValueOnce(asApiResponse(true)); actions.findReindexOperations.mockResolvedValueOnce({ saved_objects: [{ id: 1, attributes: { status: ReindexStatus.failed } }], total: 1, @@ -244,7 +259,7 @@ describe('reindexService', () => { }); it('deletes existing operation if it was cancelled', async () => { - callCluster.mockResolvedValueOnce(true); // indices.exist + clusterClient.asCurrentUser.indices.exists.mockResolvedValueOnce(asApiResponse(true)); actions.findReindexOperations.mockResolvedValueOnce({ saved_objects: [{ id: 1, attributes: { status: ReindexStatus.cancelled } }], total: 1, @@ -260,7 +275,7 @@ describe('reindexService', () => { }); it('fails if existing operation did not fail', async () => { - callCluster.mockResolvedValueOnce(true); // indices.exist + clusterClient.asCurrentUser.indices.exists.mockResolvedValueOnce(asApiResponse(true)); actions.findReindexOperations.mockResolvedValueOnce({ saved_objects: [{ id: 1, attributes: { status: ReindexStatus.inProgress } }], total: 1, @@ -418,10 +433,11 @@ describe('reindexService', () => { reindexTaskId: '999333', }, } as any); - callCluster.mockResolvedValueOnce(true); + + clusterClient.asCurrentUser.tasks.cancel.mockResolvedValueOnce(asApiResponse(true)); await service.cancelReindexing('myIndex'); - expect(callCluster).toHaveBeenCalledWith('tasks.cancel', { taskId: '999333' }); + expect(clusterClient.asCurrentUser.tasks.cancel).toHaveBeenCalledWith({ task_id: '999333' }); findSpy.mockRestore(); }); @@ -433,7 +449,11 @@ describe('reindexService', () => { const findSpy = jest.spyOn(service, 'findReindexOperation').mockResolvedValueOnce(reindexOp); await expect(service.cancelReindexing('myIndex')).rejects.toThrow(); - expect(callCluster).not.toHaveBeenCalledWith('tasks.cancel', { taskId: '999333' }); + expect(clusterClient.asCurrentUser.tasks.cancel).not.toHaveBeenCalledWith( + asApiResponse({ + taskId: '999333', + }) + ); findSpy.mockRestore(); }); @@ -450,7 +470,11 @@ describe('reindexService', () => { const findSpy = jest.spyOn(service, 'findReindexOperation').mockResolvedValueOnce(reindexOp); await expect(service.cancelReindexing('myIndex')).rejects.toThrow(); - expect(callCluster).not.toHaveBeenCalledWith('tasks.cancel', { taskId: '999333' }); + expect(clusterClient.asCurrentUser.tasks.cancel).not.toHaveBeenCalledWith( + asApiResponse({ + taskId: '999333', + }) + ); findSpy.mockRestore(); }); @@ -525,7 +549,7 @@ describe('reindexService', () => { ); expect(actions.incrementIndexGroupReindexes).not.toHaveBeenCalled(); expect(actions.runWhileIndexGroupLocked).not.toHaveBeenCalled(); - expect(callCluster).not.toHaveBeenCalled(); + expect(clusterClient.asCurrentUser.nodes.info).not.toHaveBeenCalled(); }); it('supports an already migrated ML index', async () => { @@ -533,11 +557,12 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f() ); - callCluster - // Mock call to /_nodes for version check - .mockResolvedValueOnce({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) - // Mock call to /_ml/set_upgrade_mode?enabled=true - .mockResolvedValueOnce({ acknowledged: true }); + clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( + asApiResponse({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) + ); + clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const mlReindexedOp = { id: '2', @@ -550,9 +575,8 @@ describe('reindexService', () => { ); expect(actions.incrementIndexGroupReindexes).toHaveBeenCalled(); expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=true', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ + enabled: true, }); }); @@ -561,11 +585,13 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f() ); - callCluster - // Mock call to /_nodes for version check - .mockResolvedValueOnce({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) - // Mock call to /_ml/set_upgrade_mode?enabled=true - .mockResolvedValueOnce({ acknowledged: true }); + + clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( + asApiResponse({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) + ); + clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(mlReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual( @@ -573,9 +599,8 @@ describe('reindexService', () => { ); expect(actions.incrementIndexGroupReindexes).toHaveBeenCalled(); expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=true', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ + enabled: true, }); }); @@ -587,9 +612,8 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=true', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ + enabled: true, }); }); @@ -602,9 +626,8 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=true', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ + enabled: true, }); }); @@ -613,11 +636,12 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f() ); - callCluster - // Mock call to /_nodes for version check - .mockResolvedValueOnce({ nodes: { nodeX: { version: '6.7.0' } } }) - // Mock call to /_ml/set_upgrade_mode?enabled=true - .mockResolvedValueOnce({ acknowledged: false }); + clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( + asApiResponse({ nodes: { nodeX: { version: '6.7.0' } } }) + ); + clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( + asApiResponse({ acknowledged: false }) + ); const updatedOp = await service.processNextStep(mlReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); @@ -626,9 +650,8 @@ describe('reindexService', () => { updatedOp.attributes.errorMessage!.includes('Could not stop ML jobs') ).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=true', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ + enabled: true, }); }); @@ -637,9 +660,9 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f() ); - callCluster - // Mock call to /_nodes for version check - .mockResolvedValueOnce({ nodes: { nodeX: { version: '6.6.0' } } }); + clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( + asApiResponse({ nodes: { nodeX: { version: '6.6.0' } } }) + ); const updatedOp = await service.processNextStep(mlReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); @@ -649,9 +672,8 @@ describe('reindexService', () => { ).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); // Should not have called ML endpoint at all - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=true', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ + enabled: true, }); }); }); @@ -669,7 +691,7 @@ describe('reindexService', () => { ); expect(actions.incrementIndexGroupReindexes).not.toHaveBeenCalled(); expect(actions.runWhileIndexGroupLocked).not.toHaveBeenCalled(); - expect(callCluster).not.toHaveBeenCalled(); + expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalled(); }); it('increments ML reindexes and calls watcher stop endpoint', async () => { @@ -677,9 +699,9 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (type: string, f: any) => f() ); - callCluster - // Mock call to /_watcher/_stop - .mockResolvedValueOnce({ acknowledged: true }); + clusterClient.asCurrentUser.watcher.stop.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(watcherReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual( @@ -687,10 +709,7 @@ describe('reindexService', () => { ); expect(actions.incrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.watcher); expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_stop', - method: 'POST', - }); + expect(clusterClient.asCurrentUser.watcher.stop).toHaveBeenCalled(); }); it('fails if watcher reindexes cannot be incremented', async () => { @@ -701,9 +720,8 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_stop', - method: 'POST', + expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalledWith({ + enabled: true, }); }); @@ -716,10 +734,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_stop', - method: 'POST', - }); + expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalled(); }); it('fails if watcher endpoint fails', async () => { @@ -727,9 +742,9 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (type: string, f: any) => f() ); - callCluster - // Mock call to /_watcher/_stop - .mockResolvedValueOnce({ acknowledged: false }); + clusterClient.asCurrentUser.watcher.stop.mockResolvedValueOnce( + asApiResponse({ acknowledged: false }) + ); const updatedOp = await service.processNextStep(watcherReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); @@ -738,10 +753,7 @@ describe('reindexService', () => { updatedOp.attributes.errorMessage!.includes('Could not stop Watcher') ).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_stop', - method: 'POST', - }); + expect(clusterClient.asCurrentUser.watcher.stop).toHaveBeenCalled(); }); }); }); @@ -756,17 +768,21 @@ describe('reindexService', () => { } as ReindexSavedObject; it('blocks writes and updates lastCompletedStep', async () => { - callCluster.mockResolvedValueOnce({ acknowledged: true }); + clusterClient.asCurrentUser.indices.putSettings.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); - expect(callCluster).toHaveBeenCalledWith('indices.putSettings', { + expect(clusterClient.asCurrentUser.indices.putSettings).toHaveBeenCalledWith({ index: 'myIndex', body: { 'index.blocks.write': true }, }); }); it('fails if setting updates are not acknowledged', async () => { - callCluster.mockResolvedValueOnce({ acknowledged: false }); + clusterClient.asCurrentUser.indices.putSettings.mockResolvedValueOnce( + asApiResponse({ acknowledged: false }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual( ReindexStep.indexGroupServicesStopped @@ -777,7 +793,7 @@ describe('reindexService', () => { }); it('fails if setting updates fail', async () => { - callCluster.mockRejectedValueOnce(new Error('blah!')); + clusterClient.asCurrentUser.indices.putSettings.mockRejectedValueOnce(new Error('blah!')); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual( ReindexStep.indexGroupServicesStopped @@ -797,11 +813,12 @@ describe('reindexService', () => { // The more intricate details of how the settings are chosen are test separately. it('creates new index with settings and mappings and updates lastCompletedStep', async () => { actions.getFlatSettings.mockResolvedValueOnce(settingsMappings); - callCluster.mockResolvedValueOnce({ acknowledged: true }); // indices.create - + clusterClient.asCurrentUser.indices.create.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.newIndexCreated); - expect(callCluster).toHaveBeenCalledWith('indices.create', { + expect(clusterClient.asCurrentUser.indices.create).toHaveBeenCalledWith({ index: 'myIndex-reindex-0', body: { // index.blocks.write should be removed from the settings for the new index. @@ -812,9 +829,13 @@ describe('reindexService', () => { }); it('fails if create index is not acknowledged', async () => { - callCluster - .mockResolvedValueOnce({ myIndex: settingsMappings }) - .mockResolvedValueOnce({ acknowledged: false }); + clusterClient.asCurrentUser.indices.getSettings.mockResolvedValueOnce( + asApiResponse({ myIndex: settingsMappings }) + ); + + clusterClient.asCurrentUser.indices.create.mockResolvedValueOnce( + asApiResponse({ acknowledged: false }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); @@ -823,10 +844,16 @@ describe('reindexService', () => { }); it('fails if create index fails', async () => { - callCluster - .mockResolvedValueOnce({ myIndex: settingsMappings }) - .mockRejectedValueOnce(new Error(`blah!`)) - .mockResolvedValueOnce({ acknowledged: true }); + clusterClient.asCurrentUser.indices.getSettings.mockResolvedValueOnce( + asApiResponse({ myIndex: settingsMappings }) + ); + + clusterClient.asCurrentUser.indices.create.mockRejectedValueOnce(new Error(`blah!`)); + + clusterClient.asCurrentUser.indices.putSettings.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); + const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); @@ -834,7 +861,7 @@ describe('reindexService', () => { expect(log.error).toHaveBeenCalledWith(expect.any(String)); // Original index should have been set back to allow reads. - expect(callCluster).toHaveBeenCalledWith('indices.putSettings', { + expect(clusterClient.asCurrentUser.indices.putSettings).toHaveBeenCalledWith({ index: 'myIndex', body: { 'index.blocks.write': false }, }); @@ -858,14 +885,14 @@ describe('reindexService', () => { }); it('starts reindex, saves taskId, and updates lastCompletedStep', async () => { - callCluster.mockResolvedValueOnce({ task: 'xyz' }); // reindex + clusterClient.asCurrentUser.reindex.mockResolvedValueOnce(asApiResponse({ task: 'xyz' })); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexStarted); expect(updatedOp.attributes.reindexTaskId).toEqual('xyz'); expect(updatedOp.attributes.reindexTaskPercComplete).toEqual(0); - expect(callCluster).toHaveBeenLastCalledWith('reindex', { + expect(clusterClient.asCurrentUser.reindex).toHaveBeenLastCalledWith({ refresh: true, - waitForCompletion: false, + wait_for_completion: false, body: { source: { index: 'myIndex' }, dest: { index: 'myIndex-reindex-0' }, @@ -874,7 +901,7 @@ describe('reindexService', () => { }); it('fails if starting reindex fails', async () => { - callCluster.mockRejectedValueOnce(new Error('blah!')).mockResolvedValueOnce({}); + clusterClient.asCurrentUser.reindex.mockRejectedValueOnce(new Error('blah!')); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.newIndexCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); @@ -895,10 +922,13 @@ describe('reindexService', () => { describe('reindex task is not complete', () => { it('updates reindexTaskPercComplete', async () => { - callCluster.mockResolvedValueOnce({ - completed: false, - task: { status: { created: 10, total: 100 } }, - }); + clusterClient.asCurrentUser.tasks.get.mockResolvedValueOnce( + asApiResponse({ + completed: false, + task: { status: { created: 10, total: 100 } }, + }) + ); + const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexStarted); expect(updatedOp.attributes.reindexTaskPercComplete).toEqual(0.1); // 10 / 100 = 0.1 @@ -907,30 +937,47 @@ describe('reindexService', () => { describe('reindex task is complete', () => { it('deletes task, updates reindexTaskPercComplete, updates lastCompletedStep', async () => { - callCluster - .mockResolvedValueOnce({ + clusterClient.asCurrentUser.tasks.get.mockResolvedValueOnce( + asApiResponse({ completed: true, task: { status: { created: 100, total: 100 } }, }) - .mockResolvedValueOnce({ count: 100 }) - .mockResolvedValueOnce({ result: 'deleted' }); + ); + + clusterClient.asCurrentUser.count.mockResolvedValueOnce( + asApiResponse({ + count: 100, + }) + ); + + clusterClient.asCurrentUser.delete.mockResolvedValueOnce( + asApiResponse({ + result: 'deleted', + }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexCompleted); expect(updatedOp.attributes.reindexTaskPercComplete).toEqual(1); - expect(callCluster).toHaveBeenCalledWith('delete', { + expect(clusterClient.asCurrentUser.delete).toHaveBeenCalledWith({ index: '.tasks', id: 'xyz', }); }); it('fails if docs created is less than count in source index', async () => { - callCluster - .mockResolvedValueOnce({ + clusterClient.asCurrentUser.tasks.get.mockResolvedValueOnce( + asApiResponse({ completed: true, task: { status: { created: 95, total: 95 } }, }) - .mockReturnValueOnce({ count: 100 }); + ); + + clusterClient.asCurrentUser.count.mockResolvedValueOnce( + asApiResponse({ + count: 100, + }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexStarted); @@ -941,18 +988,22 @@ describe('reindexService', () => { }); describe('reindex task is cancelled', () => { - it('deletes tsk, updates status to cancelled', async () => { - callCluster - .mockResolvedValueOnce({ + it('deletes task, updates status to cancelled', async () => { + clusterClient.asCurrentUser.tasks.get.mockResolvedValueOnce( + asApiResponse({ completed: true, task: { status: { created: 100, total: 100, canceled: 'by user request' } }, }) - .mockResolvedValue({ result: 'deleted' }); + ); + + clusterClient.asCurrentUser.delete.mockResolvedValue( + asApiResponse({ result: 'deleted' }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexStarted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.cancelled); - expect(callCluster).toHaveBeenCalledWith('delete', { + expect(clusterClient.asCurrentUser.delete).toHaveBeenLastCalledWith({ index: '.tasks', id: 'xyz', }); @@ -971,12 +1022,16 @@ describe('reindexService', () => { } as ReindexSavedObject; it('switches aliases, sets as complete, and updates lastCompletedStep', async () => { - callCluster - .mockResolvedValueOnce({ myIndex: { aliases: {} } }) - .mockResolvedValueOnce({ acknowledged: true }); + clusterClient.asCurrentUser.indices.getAlias.mockResolvedValue( + asApiResponse({ myIndex: { aliases: {} } }) + ); + + clusterClient.asCurrentUser.indices.updateAliases.mockResolvedValue( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(callCluster).toHaveBeenCalledWith('indices.updateAliases', { + expect(clusterClient.asCurrentUser.indices.updateAliases).toHaveBeenCalledWith({ body: { actions: [ { add: { index: 'myIndex-reindex-0', alias: 'myIndex' } }, @@ -987,8 +1042,8 @@ describe('reindexService', () => { }); it('moves existing aliases over to new index', async () => { - callCluster - .mockResolvedValueOnce({ + clusterClient.asCurrentUser.indices.getAlias.mockResolvedValue( + asApiResponse({ myIndex: { aliases: { myAlias: {}, @@ -996,10 +1051,15 @@ describe('reindexService', () => { }, }, }) - .mockResolvedValueOnce({ acknowledged: true }); + ); + + clusterClient.asCurrentUser.indices.updateAliases.mockResolvedValue( + asApiResponse({ acknowledged: true }) + ); + const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(callCluster).toHaveBeenCalledWith('indices.updateAliases', { + expect(clusterClient.asCurrentUser.indices.updateAliases).toHaveBeenCalledWith({ body: { actions: [ { add: { index: 'myIndex-reindex-0', alias: 'myIndex' } }, @@ -1018,7 +1078,9 @@ describe('reindexService', () => { }); it('fails if switching aliases is not acknowledged', async () => { - callCluster.mockResolvedValueOnce({ acknowledged: false }); + clusterClient.asCurrentUser.indices.updateAliases.mockResolvedValue( + asApiResponse({ acknowledged: false }) + ); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexCompleted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); @@ -1027,7 +1089,7 @@ describe('reindexService', () => { }); it('fails if switching aliases fails', async () => { - callCluster.mockRejectedValueOnce(new Error('blah!')); + clusterClient.asCurrentUser.indices.updateAliases.mockRejectedValueOnce(new Error('blah!')); const updatedOp = await service.processNextStep(reindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexCompleted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); @@ -1053,7 +1115,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual( ReindexStep.indexGroupServicesStarted ); - expect(callCluster).not.toHaveBeenCalled(); + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalled(); }); it('decrements ML reindexes and calls ML start endpoint if no remaining ML jobs', async () => { @@ -1061,17 +1123,17 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f({ attributes: { runningReindexCount: 0 } }) ); - // Mock call to /_ml/set_upgrade_mode?enabled=false - callCluster.mockResolvedValueOnce({ acknowledged: true }); + clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(mlReindexOp); expect(actions.decrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.ml); expect(updatedOp.attributes.lastCompletedStep).toEqual( ReindexStep.indexGroupServicesStarted ); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=false', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ + enabled: false, }); }); @@ -1080,16 +1142,16 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f({ attributes: { runningReindexCount: 2 } }) ); - // Mock call to /_ml/set_upgrade_mode?enabled=false - callCluster.mockResolvedValueOnce({ acknowledged: true }); + clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(mlReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual( ReindexStep.indexGroupServicesStarted ); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=false', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ + enabled: false, }); }); @@ -1102,9 +1164,8 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=false', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ + enabled: false, }); }); @@ -1118,9 +1179,8 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=false', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ + enabled: false, }); }); @@ -1129,9 +1189,9 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f({ attributes: { runningReindexCount: 0 } }) ); - // Mock call to /_ml/set_upgrade_mode?enabled=true - callCluster.mockResolvedValueOnce({ acknowledged: false }); - + clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( + asApiResponse({ acknowledged: false }) + ); const updatedOp = await service.processNextStep(mlReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); @@ -1139,9 +1199,8 @@ describe('reindexService', () => { updatedOp.attributes.errorMessage!.includes('Could not resume ML jobs') ).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=false', - method: 'POST', + expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ + enabled: false, }); }); }); @@ -1157,7 +1216,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual( ReindexStep.indexGroupServicesStarted ); - expect(callCluster).not.toHaveBeenCalled(); + expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalled(); }); it('decrements watcher reindexes and calls wathcer start endpoint if no remaining watcher reindexes', async () => { @@ -1165,36 +1224,31 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f({ attributes: { runningReindexCount: 0 } }) ); - // Mock call to /_watcher/_start - callCluster.mockResolvedValueOnce({ acknowledged: true }); + clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(watcherReindexOp); expect(actions.decrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.watcher); expect(updatedOp.attributes.lastCompletedStep).toEqual( ReindexStep.indexGroupServicesStarted ); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_start', - method: 'POST', - }); + expect(clusterClient.asCurrentUser.watcher.start).toHaveBeenCalled(); }); - it('does not call wathcer start endpoint if there are remaining wathcer reindexes', async () => { + it('does not call watcher start endpoint if there are remaining watcher reindexes', async () => { actions.decrementIndexGroupReindexes.mockResolvedValue(); actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f({ attributes: { runningReindexCount: 2 } }) ); - // Mock call to /_watcher/_start - callCluster.mockResolvedValueOnce({ acknowledged: true }); - + clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( + asApiResponse({ acknowledged: true }) + ); const updatedOp = await service.processNextStep(watcherReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual( ReindexStep.indexGroupServicesStarted ); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_start', - method: 'POST', - }); + expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); }); it('fails if watcher reindexes cannot be decremented', async () => { @@ -1206,10 +1260,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_start', - method: 'POST', - }); + expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); }); it('fails if watcher doc cannot be locked', async () => { @@ -1222,10 +1273,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).not.toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_start', - method: 'POST', - }); + expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); }); it('fails if watcher endpoint fails', async () => { @@ -1233,9 +1281,10 @@ describe('reindexService', () => { actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => f({ attributes: { runningReindexCount: 0 } }) ); - // Mock call to /_watcher/_start - callCluster.mockResolvedValueOnce({ acknowledged: false }); + clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( + asApiResponse({ acknowledged: false }) + ); const updatedOp = await service.processNextStep(watcherReindexOp); expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); @@ -1243,10 +1292,7 @@ describe('reindexService', () => { updatedOp.attributes.errorMessage!.includes('Could not start Watcher') ).toBeTruthy(); expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_watcher/_start', - method: 'POST', - }); + expect(clusterClient.asCurrentUser.watcher.start).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index e784f42867d57..f59dc66f40612 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, Logger } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { first } from 'rxjs/operators'; import { LicensingPluginSetup } from '../../../../licensing/server'; @@ -127,7 +127,7 @@ export interface ReindexService { } export const reindexServiceFactory = ( - callAsUser: LegacyAPICaller, + esClient: ElasticsearchClient, actions: ReindexActions, log: Logger, licensing: LicensingPluginSetup @@ -144,12 +144,11 @@ export const reindexServiceFactory = ( await actions.runWhileIndexGroupLocked(IndexGroup.ml, async (mlDoc) => { await validateNodesMinimumVersion(6, 7); - const res = await callAsUser('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=true', - method: 'POST', + const { body } = await esClient.ml.setUpgradeMode({ + enabled: true, }); - if (!res.acknowledged) { + if (!body.acknowledged) { throw new Error(`Could not stop ML jobs`); } @@ -164,12 +163,11 @@ export const reindexServiceFactory = ( await actions.decrementIndexGroupReindexes(IndexGroup.ml); await actions.runWhileIndexGroupLocked(IndexGroup.ml, async (mlDoc) => { if (mlDoc.attributes.runningReindexCount === 0) { - const res = await callAsUser('transport.request', { - path: '/_ml/set_upgrade_mode?enabled=false', - method: 'POST', + const { body } = await esClient.ml.setUpgradeMode({ + enabled: false, }); - if (!res.acknowledged) { + if (!body.acknowledged) { throw new Error(`Could not resume ML jobs`); } } @@ -184,12 +182,9 @@ export const reindexServiceFactory = ( const stopWatcher = async () => { await actions.incrementIndexGroupReindexes(IndexGroup.watcher); await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async (watcherDoc) => { - const { acknowledged } = await callAsUser('transport.request', { - path: '/_watcher/_stop', - method: 'POST', - }); + const { body } = await esClient.watcher.stop(); - if (!acknowledged) { + if (!body.acknowledged) { throw new Error('Could not stop Watcher'); } @@ -204,12 +199,9 @@ export const reindexServiceFactory = ( await actions.decrementIndexGroupReindexes(IndexGroup.watcher); await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async (watcherDoc) => { if (watcherDoc.attributes.runningReindexCount === 0) { - const { acknowledged } = await callAsUser('transport.request', { - path: '/_watcher/_start', - method: 'POST', - }); + const { body } = await esClient.watcher.start(); - if (!acknowledged) { + if (!body.acknowledged) { throw new Error('Could not start Watcher'); } } @@ -221,14 +213,16 @@ export const reindexServiceFactory = ( const cleanupChanges = async (reindexOp: ReindexSavedObject) => { // Cancel reindex task if it was started but not completed if (reindexOp.attributes.lastCompletedStep === ReindexStep.reindexStarted) { - await callAsUser('tasks.cancel', { - taskId: reindexOp.attributes.reindexTaskId, - }).catch((e) => undefined); // Ignore any exceptions trying to cancel (it may have already completed). + await esClient.tasks + .cancel({ + task_id: reindexOp.attributes.reindexTaskId ?? undefined, + }) + .catch((e) => undefined); // Ignore any exceptions trying to cancel (it may have already completed). } // Set index back to writable if we ever got past this point. if (reindexOp.attributes.lastCompletedStep >= ReindexStep.readonly) { - await callAsUser('indices.putSettings', { + await esClient.indices.putSettings({ index: reindexOp.attributes.indexName, body: { 'index.blocks.write': false }, }); @@ -238,7 +232,9 @@ export const reindexServiceFactory = ( reindexOp.attributes.lastCompletedStep >= ReindexStep.newIndexCreated && reindexOp.attributes.lastCompletedStep < ReindexStep.aliasCreated ) { - await callAsUser('indices.delete', { index: reindexOp.attributes.newIndexName }); + await esClient.indices.delete({ + index: reindexOp.attributes.newIndexName, + }); } // Resume consumers if we ever got past this point. @@ -252,10 +248,7 @@ export const reindexServiceFactory = ( // ------ Functions used to process the state machine const validateNodesMinimumVersion = async (minMajor: number, minMinor: number) => { - const nodesResponse = await callAsUser('transport.request', { - path: '/_nodes', - method: 'GET', - }); + const { body: nodesResponse } = await esClient.nodes.info(); const outDatedNodes = Object.values(nodesResponse.nodes).filter((node: any) => { const matches = node.version.match(VERSION_REGEX); @@ -293,7 +286,7 @@ export const reindexServiceFactory = ( */ const setReadonly = async (reindexOp: ReindexSavedObject) => { const { indexName } = reindexOp.attributes; - const putReadonly = await callAsUser('indices.putSettings', { + const { body: putReadonly } = await esClient.indices.putSettings({ index: indexName, body: { 'index.blocks.write': true }, }); @@ -319,7 +312,7 @@ export const reindexServiceFactory = ( const { settings, mappings } = transformFlatSettings(flatSettings); - const createIndex = await callAsUser('indices.create', { + const { body: createIndex } = await esClient.indices.create({ index: newIndexName, body: { settings, @@ -345,25 +338,25 @@ export const reindexServiceFactory = ( // Where possible, derive reindex options at the last moment before reindexing // to prevent them from becoming stale as they wait in the queue. - const indicesState = await esIndicesStateCheck(callAsUser, [indexName]); + const indicesState = await esIndicesStateCheck(esClient, [indexName]); const shouldOpenAndClose = indicesState[indexName] === 'closed'; if (shouldOpenAndClose) { log.debug(`Detected closed index ${indexName}, opening...`); - await callAsUser('indices.open', { index: indexName }); + await esClient.indices.open({ index: indexName }); } - const startReindex = (await callAsUser('reindex', { + const { body: startReindexResponse } = await esClient.reindex({ refresh: true, - waitForCompletion: false, + wait_for_completion: false, body: { source: { index: indexName }, dest: { index: reindexOp.attributes.newIndexName }, }, - })) as any; + }); return actions.updateReindexOp(reindexOp, { lastCompletedStep: ReindexStep.reindexStarted, - reindexTaskId: startReindex.task, + reindexTaskId: startReindexResponse.task, reindexTaskPercComplete: 0, reindexOptions: { ...(reindexOptions ?? {}), @@ -379,12 +372,12 @@ export const reindexServiceFactory = ( * @param reindexOp */ const updateReindexStatus = async (reindexOp: ReindexSavedObject) => { - const taskId = reindexOp.attributes.reindexTaskId; + const taskId = reindexOp.attributes.reindexTaskId!; // Check reindexing task progress - const taskResponse = await callAsUser('tasks.get', { - taskId, - waitForCompletion: false, + const { body: taskResponse } = await esClient.tasks.get({ + task_id: taskId, + wait_for_completion: false, }); if (!taskResponse.completed) { @@ -403,7 +396,7 @@ export const reindexServiceFactory = ( reindexOp = await cleanupChanges(reindexOp); } else { // Check that it reindexed all documents - const { count } = await callAsUser('count', { index: reindexOp.attributes.indexName }); + const { body: count } = await esClient.count({ index: reindexOp.attributes.indexName }); if (taskResponse.task.status.created < count) { // Include the entire task result in the error message. This should be guaranteed @@ -419,7 +412,7 @@ export const reindexServiceFactory = ( } // Delete the task from ES .tasks index - const deleteTaskResp = await callAsUser('delete', { + const { body: deleteTaskResp } = await esClient.delete({ index: '.tasks', id: taskId, }); @@ -438,22 +431,22 @@ export const reindexServiceFactory = ( const switchAlias = async (reindexOp: ReindexSavedObject) => { const { indexName, newIndexName, reindexOptions } = reindexOp.attributes; - const existingAliases = ( - await callAsUser('indices.getAlias', { - index: indexName, - }) - )[indexName].aliases; + const { body: response } = await esClient.indices.getAlias({ + index: indexName, + }); + + const existingAliases = response[indexName].aliases; - const extraAlises = Object.keys(existingAliases).map((aliasName) => ({ + const extraAliases = Object.keys(existingAliases).map((aliasName) => ({ add: { index: newIndexName, alias: aliasName, ...existingAliases[aliasName] }, })); - const aliasResponse = await callAsUser('indices.updateAliases', { + const { body: aliasResponse } = await esClient.indices.updateAliases({ body: { actions: [ { add: { index: newIndexName, alias: indexName } }, { remove_index: { index: indexName } }, - ...extraAlises, + ...extraAliases, ], }, }); @@ -463,7 +456,7 @@ export const reindexServiceFactory = ( } if (reindexOptions?.openAndClose === true) { - await callAsUser('indices.close', { index: indexName }); + await esClient.indices.close({ index: indexName }); } return actions.updateReindexOp(reindexOp, { @@ -540,9 +533,7 @@ export const reindexServiceFactory = ( body.cluster = [...body.cluster, 'manage_watcher']; } - const resp = await callAsUser('transport.request', { - path: '/_security/user/_has_privileges', - method: 'POST', + const { body: resp } = await esClient.security.hasPrivileges({ body, }); @@ -567,7 +558,7 @@ export const reindexServiceFactory = ( }, async createReindexOperation(indexName: string, opts?: { enqueue: boolean }) { - const indexExists = await callAsUser('indices.exists', { index: indexName }); + const { body: indexExists } = await esClient.indices.exists({ index: indexName }); if (!indexExists) { throw error.indexNotFound(`Index ${indexName} does not exist in this cluster.`); } @@ -752,8 +743,8 @@ export const reindexServiceFactory = ( ); } - const resp = await callAsUser('tasks.cancel', { - taskId: reindexOp.attributes.reindexTaskId, + const { body: resp } = await esClient.tasks.cancel({ + task_id: reindexOp.attributes.reindexTaskId!, }); if (resp.node_failures && resp.node_failures.length > 0) { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts index 756ce17b60199..1a30f98296c3a 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts @@ -3,12 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - ILegacyClusterClient, - Logger, - SavedObjectsClientContract, - FakeRequest, -} from 'src/core/server'; +import { IClusterClient, Logger, SavedObjectsClientContract, FakeRequest } from 'src/core/server'; import moment from 'moment'; import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; import { Credential, CredentialStore } from './credential_store'; @@ -53,7 +48,7 @@ export class ReindexWorker { constructor( private client: SavedObjectsClientContract, private credentialStore: CredentialStore, - private clusterClient: ILegacyClusterClient, + private clusterClient: IClusterClient, log: Logger, private licensing: LicensingPluginSetup ) { @@ -62,7 +57,7 @@ export class ReindexWorker { throw new Error(`More than one ReindexWorker cannot be created.`); } - const callAsInternalUser = this.clusterClient.callAsInternalUser.bind(this.clusterClient); + const callAsInternalUser = this.clusterClient.asInternalUser; this.reindexService = reindexServiceFactory( callAsInternalUser, @@ -151,7 +146,7 @@ export class ReindexWorker { private getCredentialScopedReindexService = (credential: Credential) => { const fakeRequest: FakeRequest = { headers: credential }; const scopedClusterClient = this.clusterClient.asScoped(fakeRequest); - const callAsCurrentUser = scopedClusterClient.callAsCurrentUser.bind(scopedClusterClient); + const callAsCurrentUser = scopedClusterClient.asCurrentUser; const actions = reindexActionsFactory(this.client, callAsCurrentUser); return reindexServiceFactory(callAsCurrentUser, actions, this.log, this.licensing); }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index e14056439ca6b..9b5223d07bfbf 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -5,7 +5,7 @@ */ import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { registerUpgradeAssistantUsageCollector } from './usage_collector'; -import { ILegacyClusterClient } from 'src/core/server'; +import { IClusterClient } from 'src/core/server'; /** * Since these route callbacks are so thin, these serve simply as integration tests @@ -18,15 +18,17 @@ describe('Upgrade Assistant Usage Collector', () => { let dependencies: any; let callClusterStub: any; let usageCollection: any; - let clusterClient: ILegacyClusterClient; + let clusterClient: IClusterClient; beforeEach(() => { - clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); - (clusterClient.callAsInternalUser as jest.Mock).mockResolvedValue({ - persistent: {}, - transient: { - logger: { - deprecation: 'WARN', + clusterClient = elasticsearchServiceMock.createClusterClient(); + (clusterClient.asInternalUser.cluster.getSettings as jest.Mock).mockResolvedValue({ + body: { + persistent: {}, + transient: { + logger: { + deprecation: 'WARN', + }, }, }, }); @@ -59,7 +61,7 @@ describe('Upgrade Assistant Usage Collector', () => { }), }, elasticsearch: { - legacy: { client: clusterClient }, + client: clusterClient, }, }; }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 276e639678fd8..1eafefb4238c2 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { - LegacyAPICaller, + ElasticsearchClient, ElasticsearchServiceStart, ISavedObjectsRepository, SavedObjectsServiceStart, @@ -38,12 +38,10 @@ async function getSavedObjectAttributesFromRepo( } } -async function getDeprecationLoggingStatusValue( - callAsCurrentUser: LegacyAPICaller -): Promise { +async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): Promise { try { - const loggerDeprecationCallResult = await callAsCurrentUser('cluster.getSettings', { - includeDefaults: true, + const { body: loggerDeprecationCallResult } = await esClient.cluster.getSettings({ + include_defaults: true, }); return isDeprecationLoggingEnabled(loggerDeprecationCallResult); @@ -53,7 +51,7 @@ async function getDeprecationLoggingStatusValue( } export async function fetchUpgradeAssistantMetrics( - { legacy: { client: esClient } }: ElasticsearchServiceStart, + { client: esClient }: ElasticsearchServiceStart, savedObjects: SavedObjectsServiceStart ): Promise { const savedObjectsRepository = savedObjects.createInternalRepository(); @@ -62,8 +60,9 @@ export async function fetchUpgradeAssistantMetrics( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID ); - const callAsInternalUser = esClient.callAsInternalUser.bind(esClient); - const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue(callAsInternalUser); + const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue( + esClient.asInternalUser + ); const getTelemetrySavedObject = ( upgradeAssistantTelemetrySavedObjectAttrs: UpgradeAssistantTelemetrySavedObjectAttributes | null diff --git a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts index 2df770c3ce45c..43fe8af18c392 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts @@ -12,9 +12,7 @@ import { export const routeHandlerContextMock = ({ core: { elasticsearch: { - legacy: { - client: elasticsearchServiceMock.createLegacyScopedClusterClient(), - }, + client: elasticsearchServiceMock.createScopedClusterClient(), }, savedObjects: { client: savedObjectsClientMock.create() }, }, diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts index 16f8001f8e1de..630ee9157dc90 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts @@ -80,7 +80,7 @@ describe('cluster checkup API', () => { it('returns an 403 error if it throws forbidden', async () => { const e: any = new Error(`you can't go here!`); - e.status = 403; + e.statusCode = 403; MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(e); const resp = await routeDependencies.router.getHandler({ diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts index ff673adaf1642..6eb0378b21baa 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts @@ -23,9 +23,7 @@ export function registerClusterCheckupRoutes({ cloud, router, licensing, log }: { core: { savedObjects: { client: savedObjectsClient }, - elasticsearch: { - legacy: { client }, - }, + elasticsearch: { client }, }, }, request, @@ -34,10 +32,10 @@ export function registerClusterCheckupRoutes({ cloud, router, licensing, log }: try { const status = await getUpgradeAssistantStatus(client, isCloudEnabled); - const callAsCurrentUser = client.callAsCurrentUser.bind(client); - const reindexActions = reindexActionsFactory(savedObjectsClient, callAsCurrentUser); + const asCurrentUser = client.asCurrentUser; + const reindexActions = reindexActionsFactory(savedObjectsClient, asCurrentUser); const reindexService = reindexServiceFactory( - callAsCurrentUser, + asCurrentUser, reindexActions, log, licensing @@ -52,7 +50,7 @@ export function registerClusterCheckupRoutes({ cloud, router, licensing, log }: body: status, }); } catch (e) { - if (e.status === 403) { + if (e.statusCode === 403) { return response.forbidden(e.message); } diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts index b8021eeef75e6..f76a86704e2c4 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts @@ -37,9 +37,9 @@ describe('deprecation logging API', () => { describe('GET /api/upgrade_assistant/deprecation_logging', () => { it('returns isEnabled', async () => { - (routeHandlerContextMock.core.elasticsearch.legacy.client - .callAsCurrentUser as jest.Mock).mockResolvedValue({ - default: { logger: { deprecation: 'WARN' } }, + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock).mockResolvedValue({ + body: { default: { logger: { deprecation: 'WARN' } } }, }); const resp = await routeDependencies.router.getHandler({ method: 'get', @@ -51,8 +51,8 @@ describe('deprecation logging API', () => { }); it('returns an error if it throws', async () => { - (routeHandlerContextMock.core.elasticsearch.legacy.client - .callAsCurrentUser as jest.Mock).mockRejectedValue(new Error(`scary error!`)); + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock).mockRejectedValue(new Error(`scary error!`)); const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/deprecation_logging', @@ -64,21 +64,21 @@ describe('deprecation logging API', () => { describe('PUT /api/upgrade_assistant/deprecation_logging', () => { it('returns isEnabled', async () => { - (routeHandlerContextMock.core.elasticsearch.legacy.client - .callAsCurrentUser as jest.Mock).mockResolvedValue({ - default: { logger: { deprecation: 'ERROR' } }, + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .putSettings as jest.Mock).mockResolvedValue({ + body: { default: { logger: { deprecation: 'ERROR' } } }, }); const resp = await routeDependencies.router.getHandler({ - method: 'get', + method: 'put', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, { body: { isEnabled: true } }, kibanaResponseFactory); expect(resp.payload).toEqual({ isEnabled: false }); }); it('returns an error if it throws', async () => { - (routeHandlerContextMock.core.elasticsearch.legacy.client - .callAsCurrentUser as jest.Mock).mockRejectedValue(new Error(`scary error!`)); + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .putSettings as jest.Mock).mockRejectedValue(new Error(`scary error!`)); const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/deprecation_logging', diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts index 38f244a4e1154..4bdb1364b071a 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts @@ -23,9 +23,7 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) async ( { core: { - elasticsearch: { - legacy: { client }, - }, + elasticsearch: { client }, }, }, request, @@ -54,9 +52,7 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) async ( { core: { - elasticsearch: { - legacy: { client }, - }, + elasticsearch: { client }, }, }, request, diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts index fffde339c59e5..e33398fdcc93b 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { ILegacyScopedClusterClient, Logger, SavedObjectsClientContract } from 'kibana/server'; +import { IScopedClusterClient, Logger, SavedObjectsClientContract } from 'kibana/server'; import { LicensingPluginSetup } from '../../../../licensing/server'; @@ -17,7 +17,7 @@ import { error } from '../../lib/reindexing/error'; interface ReindexHandlerArgs { savedObjects: SavedObjectsClientContract; - dataClient: ILegacyScopedClusterClient; + dataClient: IScopedClusterClient; indexName: string; log: Logger; licensing: LicensingPluginSetup; @@ -38,7 +38,7 @@ export const reindexHandler = async ({ savedObjects, reindexOptions, }: ReindexHandlerArgs): Promise => { - const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const callAsCurrentUser = dataClient.asCurrentUser; const reindexActions = reindexActionsFactory(savedObjects, callAsCurrentUser); const reindexService = reindexServiceFactory(callAsCurrentUser, reindexActions, log, licensing); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index c745def5e8936..52157f39743e8 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -51,7 +51,7 @@ export function createReindexWorker({ savedObjects, licensing, }: CreateReindexWorker) { - const esClient = elasticsearchService.legacy.client; + const esClient = elasticsearchService.client; return new ReindexWorker(savedObjects, credentialStore, esClient, logger, licensing); } @@ -100,9 +100,7 @@ export function registerReindexIndicesRoutes( { core: { savedObjects: { client: savedObjectsClient }, - elasticsearch: { - legacy: { client: esClient }, - }, + elasticsearch: { client: esClient }, }, }, request, @@ -142,9 +140,7 @@ export function registerReindexIndicesRoutes( async ( { core: { - elasticsearch: { - legacy: { client: esClient }, - }, + elasticsearch: { client: esClient }, savedObjects, }, }, @@ -152,7 +148,7 @@ export function registerReindexIndicesRoutes( response ) => { const { client } = savedObjects; - const callAsCurrentUser = esClient.callAsCurrentUser.bind(esClient); + const callAsCurrentUser = esClient.asCurrentUser; const reindexActions = reindexActionsFactory(client, callAsCurrentUser); try { const inProgressOps = await reindexActions.findAllByStatus(ReindexStatus.inProgress); @@ -184,9 +180,7 @@ export function registerReindexIndicesRoutes( { core: { savedObjects: { client: savedObjectsClient }, - elasticsearch: { - legacy: { client: esClient }, - }, + elasticsearch: { client: esClient }, }, }, request, @@ -245,9 +239,7 @@ export function registerReindexIndicesRoutes( { core: { savedObjects, - elasticsearch: { - legacy: { client: esClient }, - }, + elasticsearch: { client: esClient }, }, }, request, @@ -255,14 +247,9 @@ export function registerReindexIndicesRoutes( ) => { const { client } = savedObjects; const { indexName } = request.params; - const callAsCurrentUser = esClient.callAsCurrentUser.bind(esClient); - const reindexActions = reindexActionsFactory(client, callAsCurrentUser); - const reindexService = reindexServiceFactory( - callAsCurrentUser, - reindexActions, - log, - licensing - ); + const asCurrentUser = esClient.asCurrentUser; + const reindexActions = reindexActionsFactory(client, asCurrentUser); + const reindexService = reindexServiceFactory(asCurrentUser, reindexActions, log, licensing); try { const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(indexName); @@ -303,9 +290,7 @@ export function registerReindexIndicesRoutes( { core: { savedObjects, - elasticsearch: { - legacy: { client: esClient }, - }, + elasticsearch: { client: esClient }, }, }, request, @@ -313,7 +298,7 @@ export function registerReindexIndicesRoutes( ) => { const { indexName } = request.params; const { client } = savedObjects; - const callAsCurrentUser = esClient.callAsCurrentUser.bind(esClient); + const callAsCurrentUser = esClient.asCurrentUser; const reindexActions = reindexActionsFactory(client, callAsCurrentUser); const reindexService = reindexServiceFactory( callAsCurrentUser, diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/api_integration/apis/upgrade_assistant/upgrade_assistant.ts index daeb71ef12382..bdfbff61bc5db 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/upgrade_assistant.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/upgrade_assistant.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { reindexOperationWithLargeErrorMessage } from './reindex_operation_with_large_error_message'; export default function ({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); describe('Reindex operation saved object', function () { const dotKibanaIndex = '.kibana'; diff --git a/x-pack/test/upgrade_assistant_integration/config.js b/x-pack/test/upgrade_assistant_integration/config.js index d11b39ff74e35..5409d8a470502 100644 --- a/x-pack/test/upgrade_assistant_integration/config.js +++ b/x-pack/test/upgrade_assistant_integration/config.js @@ -5,7 +5,6 @@ */ import path from 'path'; -import { LegacyEsProvider } from './services'; export default async function ({ readConfigFile }) { // Read the Kibana API integration tests config file so that we can utilize its services. @@ -25,7 +24,6 @@ export default async function ({ readConfigFile }) { services: { ...kibanaCommonConfig.get('services'), supertest: kibanaAPITestsConfig.get('services.supertest'), - legacyEs: LegacyEsProvider, }, esArchiver: xPackFunctionalTestsConfig.get('esArchiver'), junit: { diff --git a/x-pack/test/upgrade_assistant_integration/services/index.js b/x-pack/test/upgrade_assistant_integration/services/index.js deleted file mode 100644 index 83424ad1eb50c..0000000000000 --- a/x-pack/test/upgrade_assistant_integration/services/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { LegacyEsProvider } from './legacy_es'; diff --git a/x-pack/test/upgrade_assistant_integration/services/legacy_es.js b/x-pack/test/upgrade_assistant_integration/services/legacy_es.js deleted file mode 100644 index 78bfd63ded3c9..0000000000000 --- a/x-pack/test/upgrade_assistant_integration/services/legacy_es.js +++ /dev/null @@ -1,18 +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 { format as formatUrl } from 'url'; - -import * as legacyElasticsearch from 'elasticsearch'; - -export function LegacyEsProvider({ getService }) { - const config = getService('config'); - - return new legacyElasticsearch.Client({ - host: formatUrl(config.get('servers.elasticsearch')), - requestTimeout: config.get('timeouts.esRequestTimeout'), - }); -} diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js index 10074f68bb59e..f57d2184d4b8d 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js @@ -13,7 +13,7 @@ import { getIndexState } from '../../../plugins/upgrade_assistant/common/get_ind export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('legacyEs'); + const es = getService('es'); // Utility function that keeps polling API until reindex operation has completed or failed. const waitForReindexToComplete = async (indexName) => { @@ -65,14 +65,14 @@ export default function ({ getService }) { expect(lastState.status).to.equal(ReindexStatus.completed); const { newIndexName } = lastState; - const indexSummary = await es.indices.get({ index: 'dummydata' }); + const { body: indexSummary } = await es.indices.get({ index: 'dummydata' }); // The new index was created expect(indexSummary[newIndexName]).to.be.an('object'); // The original index name is aliased to the new one expect(indexSummary[newIndexName].aliases.dummydata).to.be.an('object'); // The number of documents in the new index matches what we expect - expect((await es.count({ index: lastState.newIndexName })).count).to.be(3); + expect((await es.count({ index: lastState.newIndexName })).body.count).to.be(3); // Cleanup newly created index await es.indices.delete({ @@ -95,9 +95,9 @@ export default function ({ getService }) { ], }, }); - expect((await es.count({ index: 'myAlias' })).count).to.be(3); - expect((await es.count({ index: 'wildcardAlias' })).count).to.be(3); - expect((await es.count({ index: 'myHttpsAlias' })).count).to.be(2); + expect((await es.count({ index: 'myAlias' })).body.count).to.be(3); + expect((await es.count({ index: 'wildcardAlias' })).body.count).to.be(3); + expect((await es.count({ index: 'myHttpsAlias' })).body.count).to.be(2); // Reindex await supertest @@ -107,10 +107,10 @@ export default function ({ getService }) { const lastState = await waitForReindexToComplete('dummydata'); // The regular aliases should still return 3 docs - expect((await es.count({ index: 'myAlias' })).count).to.be(3); - expect((await es.count({ index: 'wildcardAlias' })).count).to.be(3); + expect((await es.count({ index: 'myAlias' })).body.count).to.be(3); + expect((await es.count({ index: 'wildcardAlias' })).body.count).to.be(3); // The filtered alias should still return 2 docs - expect((await es.count({ index: 'myHttpsAlias' })).count).to.be(2); + expect((await es.count({ index: 'myHttpsAlias' })).body.count).to.be(2); // Cleanup newly created index await es.indices.delete({ @@ -204,8 +204,8 @@ export default function ({ getService }) { await assertQueueState(undefined, 0); // Check that the closed index is still closed after reindexing - const resolvedIndices = await es.transport.request({ - path: `_resolve/index/${nameOfIndexThatShouldBeClosed}`, + const { body: resolvedIndices } = await es.indices.resolveIndex({ + name: nameOfIndexThatShouldBeClosed, }); const test1ReindexedState = getIndexState(nameOfIndexThatShouldBeClosed, resolvedIndices); From 46ac4ed7a2827b2dd689d30a68c54e29b726ee0c Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 27 Jan 2021 07:36:10 -0800 Subject: [PATCH 039/163] [CI] Combines Jest test jobs (#85850) Signed-off-by: Tyler Smalley --- .ci/es-snapshots/Jenkinsfile_verify_es | 1 - .ci/jobs.yml | 1 - .ci/teamcity/default/jest.sh | 10 ++------ .ci/teamcity/oss/jest.sh | 7 ++++-- .ci/teamcity/tests/jest.sh | 10 ++++++++ .teamcity/src/Extensions.kt | 15 ++++++------ .teamcity/src/builds/test/AllTests.kt | 2 +- .teamcity/src/builds/test/Jest.kt | 2 +- .teamcity/src/builds/test/XPackJest.kt | 19 --------------- .teamcity/src/projects/Kibana.kt | 1 - jest.config.integration.js | 5 +++- jest.config.js | 10 +++++++- jest.config.oss.js | 19 --------------- packages/kbn-test/jest-preset.js | 2 +- .../shell_scripts/extract_archives.sh | 2 +- test/scripts/jenkins_unit.sh | 17 +++----------- test/scripts/jenkins_xpack.sh | 23 ------------------- test/scripts/test/jest_integration.sh | 2 +- test/scripts/test/jest_unit.sh | 2 +- test/scripts/test/xpack_jest_unit.sh | 6 ----- vars/kibanaCoverage.groovy | 7 ------ vars/kibanaPipeline.groovy | 15 ++++++------ vars/tasks.groovy | 1 - x-pack/jest.config.js | 11 --------- 24 files changed, 55 insertions(+), 135 deletions(-) create mode 100755 .ci/teamcity/tests/jest.sh delete mode 100644 .teamcity/src/builds/test/XPackJest.kt delete mode 100644 jest.config.oss.js delete mode 100755 test/scripts/jenkins_xpack.sh delete mode 100755 test/scripts/test/xpack_jest_unit.sh delete mode 100644 x-pack/jest.config.js diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 11a39faa9aed0..b40cd91a45c57 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -29,7 +29,6 @@ kibanaPipeline(timeoutMinutes: 150) { withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { 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-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), diff --git a/.ci/jobs.yml b/.ci/jobs.yml index b05e834f5a459..6aa93d4a1056a 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -2,7 +2,6 @@ JOB: - kibana-intake - - x-pack-intake - kibana-firefoxSmoke - kibana-ciGroup1 - kibana-ciGroup2 diff --git a/.ci/teamcity/default/jest.sh b/.ci/teamcity/default/jest.sh index b900d1b6d6b4e..dac1cc8986a1c 100755 --- a/.ci/teamcity/default/jest.sh +++ b/.ci/teamcity/default/jest.sh @@ -1,10 +1,4 @@ #!/bin/bash -set -euo pipefail - -source "$(dirname "${0}")/../util.sh" - -export JOB=kibana-default-jest - -checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest x-pack --ci --verbose --maxWorkers=5 +# This file is temporary and can be removed once #85850 has been +# merged and the changes included in open PR's (~3 days after merging) diff --git a/.ci/teamcity/oss/jest.sh b/.ci/teamcity/oss/jest.sh index 0dee07d00d2be..b323a88ef06bc 100755 --- a/.ci/teamcity/oss/jest.sh +++ b/.ci/teamcity/oss/jest.sh @@ -1,10 +1,13 @@ #!/bin/bash +# This file is temporary and can be removed once #85850 has been +# merged and the changes included in open PR's (~3 days after merging) + set -euo pipefail source "$(dirname "${0}")/../util.sh" export JOB=kibana-oss-jest -checks-reporter-with-killswitch "OSS Jest Unit Tests" \ - node scripts/jest --config jest.config.oss.js --ci --verbose --maxWorkers=5 +checks-reporter-with-killswitch "Jest Unit Tests" \ + node scripts/jest --ci --verbose diff --git a/.ci/teamcity/tests/jest.sh b/.ci/teamcity/tests/jest.sh new file mode 100755 index 0000000000000..c8b9b075e0e61 --- /dev/null +++ b/.ci/teamcity/tests/jest.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-jest + +checks-reporter-with-killswitch "Jest Unit Tests" \ + node scripts/jest --ci --verbose --coverage diff --git a/.teamcity/src/Extensions.kt b/.teamcity/src/Extensions.kt index 0a8abf4a149cf..ce99c9c49e198 100644 --- a/.teamcity/src/Extensions.kt +++ b/.teamcity/src/Extensions.kt @@ -20,20 +20,21 @@ fun BuildType.kibanaAgent(size: Int) { } val testArtifactRules = """ + target/junit/**/* target/kibana-* - target/test-metrics/* + target/kibana-coverage/**/* target/kibana-security-solution/**/*.png - target/junit/**/* + target/test-metrics/* target/test-suites-ci-plan.json - test/**/screenshots/session/*.png - test/**/screenshots/failure/*.png test/**/screenshots/diff/*.png + test/**/screenshots/failure/*.png + test/**/screenshots/session/*.png test/functional/failure_debug/html/*.html - 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/**/screenshots/failure/*.png + x-pack/test/**/screenshots/session/*.png x-pack/test/functional/apps/reporting/reports/session/*.pdf + x-pack/test/functional/failure_debug/html/*.html """.trimIndent() fun BuildType.addTestSettings() { diff --git a/.teamcity/src/builds/test/AllTests.kt b/.teamcity/src/builds/test/AllTests.kt index 9506d98cbe50e..a49d5f2b07f4c 100644 --- a/.teamcity/src/builds/test/AllTests.kt +++ b/.teamcity/src/builds/test/AllTests.kt @@ -9,5 +9,5 @@ object AllTests : BuildType({ description = "All Non-Functional Tests" type = Type.COMPOSITE - dependsOn(QuickTests, Jest, XPackJest, JestIntegration, OssApiServerIntegration) + dependsOn(QuickTests, Jest, JestIntegration, OssApiServerIntegration) }) diff --git a/.teamcity/src/builds/test/Jest.kt b/.teamcity/src/builds/test/Jest.kt index c33c9c2678ca4..c9d170b5e5c3d 100644 --- a/.teamcity/src/builds/test/Jest.kt +++ b/.teamcity/src/builds/test/Jest.kt @@ -12,7 +12,7 @@ object Jest : BuildType({ kibanaAgent(8) steps { - runbld("Jest Unit", "./.ci/teamcity/oss/jest.sh") + runbld("Jest Unit", "./.ci/teamcity/tests/jest.sh") } addTestSettings() diff --git a/.teamcity/src/builds/test/XPackJest.kt b/.teamcity/src/builds/test/XPackJest.kt deleted file mode 100644 index 8246b60823ff9..0000000000000 --- a/.teamcity/src/builds/test/XPackJest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package builds.test - -import addTestSettings -import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType -import kibanaAgent -import runbld - -object XPackJest : BuildType({ - name = "X-Pack Jest Unit" - description = "Executes X-Pack Jest Unit Tests" - - kibanaAgent(16) - - steps { - runbld("X-Pack Jest Unit", "./.ci/teamcity/default/jest.sh") - } - - addTestSettings() -}) diff --git a/.teamcity/src/projects/Kibana.kt b/.teamcity/src/projects/Kibana.kt index 5cddcf18e067f..c84b65027dee6 100644 --- a/.teamcity/src/projects/Kibana.kt +++ b/.teamcity/src/projects/Kibana.kt @@ -77,7 +77,6 @@ fun Kibana(config: KibanaConfiguration = KibanaConfiguration()) : Project { name = "Jest" buildType(Jest) - buildType(XPackJest) buildType(JestIntegration) } diff --git a/jest.config.integration.js b/jest.config.integration.js index 2064abb7e36a1..99728c5471dfb 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -17,6 +17,7 @@ module.exports = { testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), + setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], reporters: [ 'default', [ @@ -24,5 +25,7 @@ module.exports = { { reportName: 'Jest Integration Tests' }, ], ], - setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], + coverageReporters: !!process.env.CI + ? [['json', { file: 'jest-integration.json' }]] + : ['html', 'text'], }; diff --git a/jest.config.js b/jest.config.js index f1833772c82a1..9ac5e57254e5a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,14 @@ */ module.exports = { + preset: '@kbn/test', rootDir: '.', - projects: [...require('./jest.config.oss').projects, ...require('./x-pack/jest.config').projects], + projects: [ + '/packages/*/jest.config.js', + '/src/*/jest.config.js', + '/src/legacy/*/jest.config.js', + '/src/plugins/*/jest.config.js', + '/test/*/jest.config.js', + '/x-pack/plugins/*/jest.config.js', + ], }; diff --git a/jest.config.oss.js b/jest.config.oss.js deleted file mode 100644 index 1b478aa85bdba..0000000000000 --- a/jest.config.oss.js +++ /dev/null @@ -1,19 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '.', - projects: [ - '/packages/*/jest.config.js', - '/src/*/jest.config.js', - '/src/legacy/*/jest.config.js', - '/src/plugins/*/jest.config.js', - '/test/*/jest.config.js', - ], -}; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index ed88944ed862d..ebedb314f9594 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -19,7 +19,7 @@ module.exports = { coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'], // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], + coverageReporters: !!process.env.CI ? [['json', { file: 'jest.json' }]] : ['html', 'text'], // An array of file extensions your modules use moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], diff --git a/src/dev/code_coverage/shell_scripts/extract_archives.sh b/src/dev/code_coverage/shell_scripts/extract_archives.sh index 376467f9f2e55..14b35f8786d02 100644 --- a/src/dev/code_coverage/shell_scripts/extract_archives.sh +++ b/src/dev/code_coverage/shell_scripts/extract_archives.sh @@ -6,7 +6,7 @@ EXTRACT_DIR=/tmp/extracted_coverage mkdir -p $EXTRACT_DIR echo "### Extracting downloaded artifacts" -for x in kibana-intake x-pack-intake kibana-oss-tests kibana-xpack-tests; do +for x in kibana-intake kibana-oss-tests kibana-xpack-tests; do tar -xzf $DOWNLOAD_DIR/coverage/${x}/kibana-coverage.tar.gz -C $EXTRACT_DIR || echo "### Error 'tarring': ${x}" done diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 6e28f9c3ef56a..9e387f97a016e 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -2,12 +2,6 @@ source test/scripts/jenkins_test_setup.sh -rename_coverage_file() { - test -f target/kibana-coverage/jest/coverage-final.json \ - && mv target/kibana-coverage/jest/coverage-final.json \ - target/kibana-coverage/jest/$1-coverage-final.json -} - if [[ -z "$CODE_COVERAGE" ]] ; then # Lint ./test/scripts/lint/eslint.sh @@ -34,13 +28,8 @@ if [[ -z "$CODE_COVERAGE" ]] ; then ./test/scripts/checks/test_hardening.sh else echo " -> Running jest tests with coverage" - node scripts/jest --ci --verbose --coverage --config jest.config.oss.js || true; - rename_coverage_file "oss" - echo "" - echo "" + node scripts/jest --ci --verbose --maxWorkers=6 --coverage || true; + echo " -> Running jest integration tests with coverage" - node --max-old-space-size=8192 scripts/jest_integration --ci --verbose --coverage || true; - rename_coverage_file "oss-integration" - echo "" - echo "" + node scripts/jest_integration --ci --verbose --coverage || true; fi diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh deleted file mode 100755 index 66fb5ae5370bc..0000000000000 --- a/test/scripts/jenkins_xpack.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup.sh - -if [[ -z "$CODE_COVERAGE" ]] ; then - echo " -> Running jest tests" - - ./test/scripts/test/xpack_jest_unit.sh -else - echo " -> Build runtime for canvas" - # build runtime for canvas - echo "NODE_ENV=$NODE_ENV" - node ./x-pack/plugins/canvas/scripts/shareable_runtime - echo " -> Running jest tests with coverage" - cd x-pack - node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=5 --coverage --config jest.config.js || true; - # rename file in order to be unique one - test -f ../target/kibana-coverage/jest/coverage-final.json \ - && mv ../target/kibana-coverage/jest/coverage-final.json \ - ../target/kibana-coverage/jest/xpack-coverage-final.json - echo "" - echo "" -fi diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh index 78ed804f88430..c48d9032466a3 100755 --- a/test/scripts/test/jest_integration.sh +++ b/test/scripts/test/jest_integration.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Integration Tests" \ - node scripts/jest_integration --ci --verbose + node scripts/jest_integration --ci --verbose --coverage diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index 88c0fe528b88c..14d7268c6f36d 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --config jest.config.oss.js --ci --verbose --maxWorkers=5 + node scripts/jest --ci --verbose --maxWorkers=10 --coverage diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh deleted file mode 100755 index 33b1c8a2b5183..0000000000000 --- a/test/scripts/test/xpack_jest_unit.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -checks-reporter-with-killswitch "X-Pack Jest" \ - node scripts/jest x-pack --ci --verbose --maxWorkers=5 diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 609d8f78aeb96..e393f3a5d2150 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -197,13 +197,6 @@ def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, teamAssignmen def runTests() { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() - } - }, 'kibana-oss-agent' : workers.functional( 'kibana-oss-tests', { kibanaPipeline.buildOss() }, diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 93cb7a719bbe8..e49692568cec8 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -179,20 +179,21 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ + 'target/junit/**/*', 'target/kibana-*', - 'target/test-metrics/*', + 'target/kibana-coverage/**/*', 'target/kibana-security-solution/**/*.png', - 'target/junit/**/*', + 'target/test-metrics/*', 'target/test-suites-ci-plan.json', - 'test/**/screenshots/session/*.png', - 'test/**/screenshots/failure/*.png', 'test/**/screenshots/diff/*.png', + 'test/**/screenshots/failure/*.png', + 'test/**/screenshots/session/*.png', 'test/functional/failure_debug/html/*.html', - '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/**/screenshots/failure/*.png', + 'x-pack/test/**/screenshots/session/*.png', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', + 'x-pack/test/functional/failure_debug/html/*.html', ] withEnv([ diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 3493a95f0bdce..74ad1267e9355 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -35,7 +35,6 @@ def test() { kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), - kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), ]) } diff --git a/x-pack/jest.config.js b/x-pack/jest.config.js deleted file mode 100644 index 8158987213cd2..0000000000000 --- a/x-pack/jest.config.js +++ /dev/null @@ -1,11 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '..', - projects: ['/x-pack/plugins/*/jest.config.js'], -}; From 82902e89189a0e74a94e206dc574033806675dae Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 27 Jan 2021 17:57:53 +0200 Subject: [PATCH 040/163] [Telemetry] Settings Collector: redact sensitive reported values (#88675) --- ...ana-plugin-core-public.uisettingsparams.md | 1 + ...-core-public.uisettingsparams.sensitive.md | 13 ++ ...re-server.iuisettingsclient.issensitive.md | 13 ++ ...na-plugin-core-server.iuisettingsclient.md | 1 + ...ana-plugin-core-server.uisettingsparams.md | 1 + ...-core-server.uisettingsparams.sensitive.md | 13 ++ .../src/tools/serializer.ts | 21 ++- src/core/public/public.api.md | 1 + src/core/server/server.api.md | 2 + .../ui_settings/settings/notifications.ts | 1 + src/core/server/ui_settings/types.ts | 4 + .../ui_settings/ui_settings_client.test.ts | 32 ++++ .../server/ui_settings/ui_settings_client.ts | 6 +- .../ui_settings/ui_settings_service.mock.ts | 1 + src/core/types/ui_settings.ts | 5 + src/plugins/kibana_usage_collection/README.md | 2 +- .../common/constants.ts | 4 + .../server/collectors/management/README.md | 51 +++++++ .../__snapshots__/index.test.ts.snap | 7 - .../collectors/management/index.test.ts | 61 -------- .../server/collectors/management/schema.ts | 51 +++++-- .../telemetry_management_collector.test.ts | 140 ++++++++++++++++++ .../telemetry_management_collector.ts | 19 +-- .../server/collectors/management/types.ts | 117 +++++++++++++++ src/plugins/telemetry/schema/oss_plugins.json | 83 ++++++++--- .../server/collector/collector.ts | 27 +++- .../vis_type_timelion/server/plugin.ts | 1 + .../dashboard_mode/server/ui_settings.ts | 1 + x-pack/plugins/reporting/server/plugin.ts | 1 + .../security_solution/server/ui_settings.ts | 4 + x-pack/test/usage_collection/config.ts | 5 +- .../stack_management_usage_test/kibana.json | 9 ++ .../public/index.ts | 11 ++ .../public/plugin.ts | 17 +++ .../public/types.ts | 14 ++ .../stack_management_usage_test/tsconfig.json | 12 ++ .../stack_management_usage/index.ts | 60 ++++++++ 37 files changed, 694 insertions(+), 118 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.uisettingsparams.sensitive.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.issensitive.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.uisettingsparams.sensitive.md create mode 100644 src/plugins/kibana_usage_collection/server/collectors/management/README.md delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/management/__snapshots__/index.test.ts.snap delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/management/types.ts create mode 100644 x-pack/test/usage_collection/plugins/stack_management_usage_test/kibana.json create mode 100644 x-pack/test/usage_collection/plugins/stack_management_usage_test/public/index.ts create mode 100644 x-pack/test/usage_collection/plugins/stack_management_usage_test/public/plugin.ts create mode 100644 x-pack/test/usage_collection/plugins/stack_management_usage_test/public/types.ts create mode 100644 x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json create mode 100644 x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md index 2cc149e2e2a79..0b7e6467667cb 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md @@ -26,6 +26,7 @@ export interface UiSettingsParams | [readonly](./kibana-plugin-core-public.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-core-public.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | | [schema](./kibana-plugin-core-public.uisettingsparams.schema.md) | Type<T> | | +| [sensitive](./kibana-plugin-core-public.uisettingsparams.sensitive.md) | boolean | a flag indicating that value might contain user sensitive data. used by telemetry to mask the value of the setting when sent. | | [type](./kibana-plugin-core-public.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-core-public.uisettingstype.md) | | [validation](./kibana-plugin-core-public.uisettingsparams.validation.md) | ImageValidation | StringValidation | | | [value](./kibana-plugin-core-public.uisettingsparams.value.md) | T | default value to fall back to if a user doesn't provide any | diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.sensitive.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.sensitive.md new file mode 100644 index 0000000000000..e12f3c5649f17 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.sensitive.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) > [sensitive](./kibana-plugin-core-public.uisettingsparams.sensitive.md) + +## UiSettingsParams.sensitive property + +a flag indicating that value might contain user sensitive data. used by telemetry to mask the value of the setting when sent. + +Signature: + +```typescript +sensitive?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.issensitive.md b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.issensitive.md new file mode 100644 index 0000000000000..a6f263e0b0f55 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.issensitive.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) > [isSensitive](./kibana-plugin-core-server.iuisettingsclient.issensitive.md) + +## IUiSettingsClient.isSensitive property + +Shows whether the uiSetting is a sensitive value. Used by telemetry to not send sensitive values. + +Signature: + +```typescript +isSensitive: (key: string) => boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md index af99b5e5bb215..dd4a69c13a2d9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md @@ -21,6 +21,7 @@ export interface IUiSettingsClient | [getRegistered](./kibana-plugin-core-server.iuisettingsclient.getregistered.md) | () => Readonly<Record<string, PublicUiSettingsParams>> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | | [getUserProvided](./kibana-plugin-core-server.iuisettingsclient.getuserprovided.md) | <T = any>() => Promise<Record<string, UserProvidedValues<T>>> | Retrieves a set of all uiSettings values set by the user. | | [isOverridden](./kibana-plugin-core-server.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | +| [isSensitive](./kibana-plugin-core-server.iuisettingsclient.issensitive.md) | (key: string) => boolean | Shows whether the uiSetting is a sensitive value. Used by telemetry to not send sensitive values. | | [remove](./kibana-plugin-core-server.iuisettingsclient.remove.md) | (key: string) => Promise<void> | Removes uiSettings value by key. | | [removeMany](./kibana-plugin-core-server.iuisettingsclient.removemany.md) | (keys: string[]) => Promise<void> | Removes multiple uiSettings values by keys. | | [set](./kibana-plugin-core-server.iuisettingsclient.set.md) | (key: string, value: any) => Promise<void> | Writes uiSettings value and marks it as set by the user. | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md index 4dfde5200e7e9..d35afc4a149d1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md @@ -26,6 +26,7 @@ export interface UiSettingsParams | [readonly](./kibana-plugin-core-server.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-core-server.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | | [schema](./kibana-plugin-core-server.uisettingsparams.schema.md) | Type<T> | | +| [sensitive](./kibana-plugin-core-server.uisettingsparams.sensitive.md) | boolean | a flag indicating that value might contain user sensitive data. used by telemetry to mask the value of the setting when sent. | | [type](./kibana-plugin-core-server.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-core-server.uisettingstype.md) | | [validation](./kibana-plugin-core-server.uisettingsparams.validation.md) | ImageValidation | StringValidation | | | [value](./kibana-plugin-core-server.uisettingsparams.value.md) | T | default value to fall back to if a user doesn't provide any | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.sensitive.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.sensitive.md new file mode 100644 index 0000000000000..f2c7de19dde1a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.sensitive.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) > [sensitive](./kibana-plugin-core-server.uisettingsparams.sensitive.md) + +## UiSettingsParams.sensitive property + +a flag indicating that value might contain user sensitive data. used by telemetry to mask the value of the setting when sent. + +Signature: + +```typescript +sensitive?: boolean; +``` diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index a941fa2a9d01f..bad40b5388407 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -19,7 +19,7 @@ export enum TelemetryKinds { Date = 10001, } -interface DescriptorValue { +export interface DescriptorValue { kind: ts.SyntaxKind | TelemetryKinds; type: keyof typeof ts.SyntaxKind | keyof typeof TelemetryKinds; } @@ -42,6 +42,13 @@ export function isObjectDescriptor(value: any) { return false; } +export function descriptorToObject(descriptor: Descriptor | DescriptorValue) { + return Object.entries(descriptor).reduce((acc, [key, value]) => { + acc[key] = value.kind ? kindToDescriptorName(value.kind) : descriptorToObject(value); + return acc; + }, {} as Record); +} + export function kindToDescriptorName(kind: number) { switch (kind) { case ts.SyntaxKind.StringKeyword: @@ -158,6 +165,16 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | if (symbolName === 'Date') { return { kind: TelemetryKinds.Date, type: 'Date' }; } + + // Support Array + if (symbolName === 'Array') { + if (node.typeArguments?.length !== 1) { + throw Error('Array type only supports 1 type parameter Array'); + } + const typeArgument = node.typeArguments[0]; + return { items: getDescriptor(typeArgument, program) }; + } + // Support `Record` if (symbolName === 'Record') { const descriptor = getDescriptor(node.typeArguments![1], program); @@ -243,7 +260,7 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | case ts.SyntaxKind.UnionType: case ts.SyntaxKind.AnyKeyword: default: - throw new Error(`Unknown type ${ts.SyntaxKind[node.kind]}; ${node.getText()}`); + throw new Error(`Unknown type ${ts.SyntaxKind[node.kind]}`); } } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d72b2aa5afd1e..0097127924a5c 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1507,6 +1507,7 @@ export interface UiSettingsParams { requiresPageReload?: boolean; // (undocumented) schema: Type; + sensitive?: boolean; type?: UiSettingsType; // (undocumented) validation?: ImageValidation | StringValidation; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 0b058011267eb..fc90284ffe5b2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1195,6 +1195,7 @@ export interface IUiSettingsClient { getRegistered: () => Readonly>; getUserProvided: () => Promise>>; isOverridden: (key: string) => boolean; + isSensitive: (key: string) => boolean; remove: (key: string) => Promise; removeMany: (keys: string[]) => Promise; set: (key: string, value: any) => Promise; @@ -3100,6 +3101,7 @@ export interface UiSettingsParams { requiresPageReload?: boolean; // (undocumented) schema: Type; + sensitive?: boolean; type?: UiSettingsType; // (undocumented) validation?: ImageValidation | StringValidation; diff --git a/src/core/server/ui_settings/settings/notifications.ts b/src/core/server/ui_settings/settings/notifications.ts index a60732ecb807a..cedd52289a68c 100644 --- a/src/core/server/ui_settings/settings/notifications.ts +++ b/src/core/server/ui_settings/settings/notifications.ts @@ -35,6 +35,7 @@ export const getNotificationsSettings = (): Record => }, }), category: ['notifications'], + sensitive: true, schema: schema.string(), }, 'notifications:lifetime:banner': { diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts index eb2d8b00dc488..73f46e4db3b2c 100644 --- a/src/core/server/ui_settings/types.ts +++ b/src/core/server/ui_settings/types.ts @@ -65,6 +65,10 @@ export interface IUiSettingsClient { * Shows whether the uiSettings value set by the user. */ isOverridden: (key: string) => boolean; + /** + * Shows whether the uiSetting is a sensitive value. Used by telemetry to not send sensitive values. + */ + isSensitive: (key: string) => boolean; } /** @internal */ diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 7fa5a85e5154e..26c4d9e8e6dd9 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -644,6 +644,38 @@ describe('ui settings', () => { }); }); + describe('#isSensitive()', () => { + it('returns false if sensitive config is not set', () => { + const defaults = { + foo: { + schema: schema.string(), + value: '1', + }, + }; + + const { uiSettings } = setup({ defaults }); + expect(uiSettings.isSensitive('foo')).toBe(false); + }); + + it('returns false if key is not in the settings', () => { + const { uiSettings } = setup(); + expect(uiSettings.isSensitive('baz')).toBe(false); + }); + + it('returns true if overrides defined and key is overridden', () => { + const defaults = { + foo: { + schema: schema.string(), + sensitive: true, + value: '1', + }, + }; + + const { uiSettings } = setup({ defaults }); + expect(uiSettings.isSensitive('foo')).toBe(true); + }); + }); + describe('#isOverridden()', () => { it('returns false if no overrides defined', () => { const { uiSettings } = setup(); diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 2ba8caaba5170..b8a46a2f994aa 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -52,7 +52,6 @@ export class UiSettingsClient implements IUiSettingsClient { constructor(options: UiSettingsServiceOptions) { const { type, id, buildNum, savedObjectsClient, log, defaults = {}, overrides = {} } = options; - this.type = type; this.id = id; this.buildNum = buildNum; @@ -132,6 +131,11 @@ export class UiSettingsClient implements IUiSettingsClient { return this.overrides.hasOwnProperty(key); } + isSensitive(key: string): boolean { + const definition = this.defaults[key]; + return !!definition?.sensitive; + } + private assertUpdateAllowed(key: string) { if (this.isOverridden(key)) { throw new CannotOverrideError(`Unable to update "${key}" because it is overridden`); diff --git a/src/core/server/ui_settings/ui_settings_service.mock.ts b/src/core/server/ui_settings/ui_settings_service.mock.ts index a03412e37f551..771b9d243656a 100644 --- a/src/core/server/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -25,6 +25,7 @@ const createClientMock = () => { remove: jest.fn(), removeMany: jest.fn(), isOverridden: jest.fn(), + isSensitive: jest.fn(), }; mocked.get.mockResolvedValue(false); mocked.getAll.mockResolvedValue({}); diff --git a/src/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index 24dfbbeea6726..92e8f6ef2f41e 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -56,6 +56,11 @@ export interface UiSettingsParams { requiresPageReload?: boolean; /** a flag indicating that value cannot be changed */ readonly?: boolean; + /** + * a flag indicating that value might contain user sensitive data. + * used by telemetry to mask the value of the setting when sent. + */ + sensitive?: boolean; /** defines a type of UI element {@link UiSettingsType} */ type?: UiSettingsType; /** optional deprecation information. Used to generate a deprecation warning. */ diff --git a/src/plugins/kibana_usage_collection/README.md b/src/plugins/kibana_usage_collection/README.md index 69711d30cdc74..85d362cf0a9b1 100644 --- a/src/plugins/kibana_usage_collection/README.md +++ b/src/plugins/kibana_usage_collection/README.md @@ -6,6 +6,6 @@ This plugin registers the basic usage collectors from Kibana: - UI Metrics - Ops stats - Number of Saved Objects per type -- Non-default UI Settings +- [User-changed UI Settings](./server/collectors/management/README.md) - CSP configuration - Core Metrics diff --git a/src/plugins/kibana_usage_collection/common/constants.ts b/src/plugins/kibana_usage_collection/common/constants.ts index 4505c59e0f630..052367765a6ec 100644 --- a/src/plugins/kibana_usage_collection/common/constants.ts +++ b/src/plugins/kibana_usage_collection/common/constants.ts @@ -13,3 +13,7 @@ export const PLUGIN_NAME = 'kibana_usage_collection'; * The type name used to publish Kibana usage stats in the formatted as bulk. */ export const KIBANA_STATS_TYPE = 'kibana_stats'; +/** + * Redacted keyword; used as a value for sensitive ui settings + */ +export const REDACTED_KEYWORD = '[REDACTED]'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/README.md b/src/plugins/kibana_usage_collection/server/collectors/management/README.md new file mode 100644 index 0000000000000..b539136d57b89 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/management/README.md @@ -0,0 +1,51 @@ +# User-changed UI Settings - Management Collector + +The Usage Collector `stack_management` reports user changed settings. +All user changed UI Settings are automatically collected. + +After adding a new setting you will be required to do the following steps: + +1. Update the [schema](./schema.ts) to include the setting name and schema type. +``` +export const stackManagementSchema: MakeSchemaFrom = { + 'MY_UI_SETTING': { type: 'keyword' }, +} +``` + +2. Update the [UsageStats interface](./types.ts) with the setting name and typescript type. +``` +export interface UsageStats { + 'MY_UI_SETTING': string; +} +``` +3. Run the telemetry checker with `--fix` flag to automatically fix the mappings + +``` +node scripts/telemetry_check --fix +``` + +If you forget any of the steps our telemetry tools and tests will help you through the process! + +## Sensitive fields + +If the configured UI setting might contain user sensitive information simply add the property `sensitive: true` to the ui setting registration config. + +``` +uiSettings.register({ + [NEWS_FEED_URL_SETTING]: { + name: i18n.translate('xpack.securitySolution.uiSettings.newsFeedUrl', { + defaultMessage: 'News feed URL', + }), + value: NEWS_FEED_URL_SETTING_DEFAULT, + sensitive: true, + description: i18n.translate('xpack.securitySolution.uiSettings.newsFeedUrlDescription', { + defaultMessage: '

News feed content will be retrieved from this URL

', + }), + category: [APP_ID], + requiresPageReload: true, + schema: schema.string(), + }, +}), +``` + +The value of any UI setting marked as `sensitive` will be reported as a keyword `[REDACTED]` instead of the actual value. This hides the actual sensitive information while giving us some intelligence over which fields the users are interactive with the most. diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/collectors/management/__snapshots__/index.test.ts.snap deleted file mode 100644 index def230dea8d70..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/management/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`telemetry_application_usage_collector fetch() 1`] = ` -Object { - "my-key": "my-value", -} -`; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts deleted file mode 100644 index 38baf02d6fe1b..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts +++ /dev/null @@ -1,61 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { loggingSystemMock, uiSettingsServiceMock } from '../../../../../core/server/mocks'; -import { - Collector, - createUsageCollectionSetupMock, - createCollectorFetchContextMock, -} from '../../../../usage_collection/server/usage_collection.mock'; - -import { registerManagementUsageCollector } from './'; - -const logger = loggingSystemMock.createLogger(); - -describe('telemetry_application_usage_collector', () => { - let collector: Collector; - - const usageCollectionMock = createUsageCollectionSetupMock(); - usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = new Collector(logger, config); - return createUsageCollectionSetupMock().makeUsageCollector(config); - }); - - const uiSettingsClient = uiSettingsServiceMock.createClient(); - const getUiSettingsClient = jest.fn(() => uiSettingsClient); - const mockedFetchContext = createCollectorFetchContextMock(); - - beforeAll(() => { - registerManagementUsageCollector(usageCollectionMock, getUiSettingsClient); - }); - - test('registered collector is set', () => { - expect(collector).not.toBeUndefined(); - }); - - test('isReady() => false if no client', () => { - getUiSettingsClient.mockImplementationOnce(() => undefined as any); - expect(collector.isReady()).toBe(false); - }); - - test('isReady() => true', () => { - expect(collector.isReady()).toBe(true); - }); - - test('fetch()', async () => { - uiSettingsClient.getUserProvided.mockImplementationOnce(async () => ({ - 'my-key': { userValue: 'my-value' }, - })); - await expect(collector.fetch(mockedFetchContext)).resolves.toMatchSnapshot(); - }); - - test('fetch() should not fail if invoked when not ready', async () => { - getUiSettingsClient.mockImplementationOnce(() => undefined as any); - await expect(collector.fetch(mockedFetchContext)).resolves.toBe(undefined); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 28eeb461f7a86..b644f282c1f36 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -7,18 +7,25 @@ */ import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; -import { UsageStats } from './telemetry_management_collector'; +import { UsageStats } from './types'; -// Retrieved by changing all the current settings in Kibana (we'll need to revisit it in the future). -// I would suggest we use flattened type for the mappings of this collector. export const stackManagementSchema: MakeSchemaFrom = { + // sensitive + 'timelion:quandl.key': { type: 'keyword' }, + 'securitySolution:defaultIndex': { type: 'keyword' }, + 'securitySolution:newsFeedUrl': { type: 'keyword' }, + 'xpackReporting:customPdfLogo': { type: 'keyword' }, + 'notifications:banner': { type: 'keyword' }, + 'timelion:graphite.url': { type: 'keyword' }, + 'xpackDashboardMode:roles': { type: 'keyword' }, + 'securitySolution:ipReputationLinks': { type: 'keyword' }, + // non-sensitive 'visualize:enableLabs': { type: 'boolean' }, 'visualization:heatmap:maxBuckets': { type: 'long' }, 'visualization:colorMapping': { type: 'text' }, 'visualization:regionmap:showWarnings': { type: 'boolean' }, 'visualization:dimmingOpacity': { type: 'float' }, 'visualization:tileMap:maxPrecision': { type: 'long' }, - 'securitySolution:ipReputationLinks': { type: 'text' }, 'csv:separator': { type: 'keyword' }, 'visualization:tileMap:WMSdefaults': { type: 'text' }, 'timelion:target_buckets': { type: 'long' }, @@ -27,14 +34,11 @@ export const stackManagementSchema: MakeSchemaFrom = { 'timelion:min_interval': { type: 'keyword' }, 'timelion:default_rows': { type: 'long' }, 'timelion:default_columns': { type: 'long' }, - 'timelion:quandl.key': { type: 'keyword' }, 'timelion:es.default_index': { type: 'keyword' }, 'timelion:showTutorial': { type: 'boolean' }, 'securitySolution:timeDefaults': { type: 'keyword' }, 'securitySolution:defaultAnomalyScore': { type: 'long' }, - 'securitySolution:defaultIndex': { type: 'keyword' }, // it's an array 'securitySolution:refreshIntervalDefaults': { type: 'keyword' }, - 'securitySolution:newsFeedUrl': { type: 'keyword' }, 'securitySolution:enableNewsFeed': { type: 'boolean' }, 'search:includeFrozen': { type: 'boolean' }, 'courier:maxConcurrentShardRequests': { type: 'long' }, @@ -43,21 +47,29 @@ export const stackManagementSchema: MakeSchemaFrom = { 'courier:customRequestPreference': { type: 'keyword' }, 'courier:ignoreFilterIfFieldNotInIndex': { type: 'boolean' }, 'rollups:enableIndexPatterns': { type: 'boolean' }, - 'xpackReporting:customPdfLogo': { type: 'text' }, 'notifications:lifetime:warning': { type: 'long' }, 'notifications:lifetime:banner': { type: 'long' }, 'notifications:lifetime:info': { type: 'long' }, - 'notifications:banner': { type: 'text' }, 'notifications:lifetime:error': { type: 'long' }, 'doc_table:highlight': { type: 'boolean' }, 'discover:searchOnPageLoad': { type: 'boolean' }, // eslint-disable-next-line @typescript-eslint/naming-convention 'doc_table:hideTimeColumn': { type: 'boolean' }, 'discover:sampleSize': { type: 'long' }, - defaultColumns: { type: 'keyword' }, // it's an array + defaultColumns: { + type: 'array', + items: { + type: 'keyword', + }, + }, 'context:defaultSize': { type: 'long' }, 'discover:aggs:terms:size': { type: 'long' }, - 'context:tieBreakerFields': { type: 'keyword' }, // it's an array + 'context:tieBreakerFields': { + type: 'array', + items: { + type: 'keyword', + }, + }, 'discover:sort:defaultOrder': { type: 'keyword' }, 'context:step': { type: 'long' }, 'accessibility:disableAnimations': { type: 'boolean' }, @@ -79,7 +91,12 @@ export const stackManagementSchema: MakeSchemaFrom = { 'query:queryString:options': { type: 'keyword' }, 'metrics:max_buckets': { type: 'long' }, 'query:allowLeadingWildcards': { type: 'boolean' }, - metaFields: { type: 'keyword' }, // it's an array + metaFields: { + type: 'array', + items: { + type: 'keyword', + }, + }, 'indexPattern:placeholder': { type: 'keyword' }, 'histogram:barTarget': { type: 'long' }, 'histogram:maxBars': { type: 'long' }, @@ -101,4 +118,14 @@ export const stackManagementSchema: MakeSchemaFrom = { 'csv:quoteValues': { type: 'boolean' }, 'dateFormat:dow': { type: 'keyword' }, dateFormat: { type: 'keyword' }, + 'autocomplete:useTimeRange': { type: 'boolean' }, + 'search:timeout': { type: 'long' }, + 'visualization:visualize:legacyChartsLibrary': { type: 'boolean' }, + 'doc_table:legacy': { type: 'boolean' }, + 'discover:modifyColumnsOnSwitch': { type: 'boolean' }, + 'discover:searchFieldsFromSource': { type: 'boolean' }, + 'securitySolution:rulesTableRefresh': { type: 'text' }, + 'apm:enableSignificantTerms': { type: 'boolean' }, + 'apm:enableServiceOverview': { type: 'boolean' }, + 'apm:enableCorrelations': { type: 'boolean' }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.test.ts new file mode 100644 index 0000000000000..4bcd98f894e2a --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { loggingSystemMock, uiSettingsServiceMock } from '../../../../../core/server/mocks'; +import { + Collector, + createUsageCollectionSetupMock, + createCollectorFetchContextMock, +} from '../../../../usage_collection/server/usage_collection.mock'; + +import { + registerManagementUsageCollector, + createCollectorFetch, +} from './telemetry_management_collector'; + +const logger = loggingSystemMock.createLogger(); + +describe('telemetry_application_usage_collector', () => { + let collector: Collector; + + const usageCollectionMock = createUsageCollectionSetupMock(); + usageCollectionMock.makeUsageCollector.mockImplementation((config) => { + collector = new Collector(logger, config); + return createUsageCollectionSetupMock().makeUsageCollector(config); + }); + + const uiSettingsClient = uiSettingsServiceMock.createClient(); + const getUiSettingsClient = jest.fn(() => uiSettingsClient); + const mockedFetchContext = createCollectorFetchContextMock(); + + beforeAll(() => { + registerManagementUsageCollector(usageCollectionMock, getUiSettingsClient); + }); + + test('registered collector is set', () => { + expect(collector).not.toBeUndefined(); + }); + + test('isReady() => false if no client', () => { + getUiSettingsClient.mockImplementationOnce(() => undefined as any); + expect(collector.isReady()).toBe(false); + }); + + test('isReady() => true', () => { + expect(collector.isReady()).toBe(true); + }); + + test('fetch()', async () => { + uiSettingsClient.getUserProvided.mockImplementationOnce(async () => ({ + 'visualization:colorMapping': { userValue: 'red' }, + })); + await expect(collector.fetch(mockedFetchContext)).resolves.toEqual({ + 'visualization:colorMapping': 'red', + }); + }); + + test('fetch() should not fail if invoked when not ready', async () => { + getUiSettingsClient.mockImplementationOnce(() => undefined as any); + await expect(collector.fetch(mockedFetchContext)).resolves.toBe(undefined); + }); +}); + +describe('createCollectorFetch', () => { + const mockUserSettings = { + item1: { userValue: 'test' }, + item2: { userValue: 123 }, + item3: { userValue: false }, + }; + + const mockIsSensitive = (key: string) => { + switch (key) { + case 'item1': + case 'item2': + return false; + case 'item3': + return true; + default: + throw new Error(`Unexpected ui setting: ${key}`); + } + }; + + it('returns #fetchUsageStats function', () => { + const getUiSettingsClient = jest.fn(() => undefined); + const fetchFunction = createCollectorFetch(getUiSettingsClient); + expect(typeof fetchFunction).toBe('function'); + }); + + describe('#fetchUsageStats', () => { + it('returns undefined if no uiSettingsClient returned from getUiSettingsClient', async () => { + const getUiSettingsClient = jest.fn(() => undefined); + const fetchFunction = createCollectorFetch(getUiSettingsClient); + const result = await fetchFunction(); + expect(result).toBe(undefined); + expect(getUiSettingsClient).toBeCalledTimes(1); + }); + + it('returns all user changed settings', async () => { + const uiSettingsClient = uiSettingsServiceMock.createClient(); + const getUiSettingsClient = jest.fn(() => uiSettingsClient); + uiSettingsClient.getUserProvided.mockResolvedValue(mockUserSettings); + uiSettingsClient.isSensitive.mockImplementation(mockIsSensitive); + const fetchFunction = createCollectorFetch(getUiSettingsClient); + const result = await fetchFunction(); + expect(typeof result).toBe('object'); + expect(Object.keys(result!)).toEqual(Object.keys(mockUserSettings)); + }); + + it('returns the actual values of non-sensitive settings', async () => { + const uiSettingsClient = uiSettingsServiceMock.createClient(); + const getUiSettingsClient = jest.fn(() => uiSettingsClient); + uiSettingsClient.getUserProvided.mockResolvedValue(mockUserSettings); + uiSettingsClient.isSensitive.mockImplementation(mockIsSensitive); + const fetchFunction = createCollectorFetch(getUiSettingsClient); + const result = await fetchFunction(); + expect(typeof result).toBe('object'); + expect(result!).toMatchObject({ + item1: 'test', + item2: 123, + }); + }); + + it('returns [REDACTED] as a value for sensitive settings', async () => { + const uiSettingsClient = uiSettingsServiceMock.createClient(); + const getUiSettingsClient = jest.fn(() => uiSettingsClient); + uiSettingsClient.getUserProvided.mockResolvedValue(mockUserSettings); + uiSettingsClient.isSensitive.mockImplementation(mockIsSensitive); + const fetchFunction = createCollectorFetch(getUiSettingsClient); + const result = await fetchFunction(); + expect(typeof result).toBe('object'); + expect(result!).toMatchObject({ + item3: '[REDACTED]', + }); + }); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts index c45f3d6139d95..651fbbd5a897a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts @@ -9,12 +9,8 @@ import { IUiSettingsClient } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { stackManagementSchema } from './schema'; - -export interface UsageStats extends Record { - // We don't support `type` yet. Only interfaces. So I added at least 1 known key to the generic - // Record extension to avoid eslint reverting it back to a `type` - 'visualize:enableLabs': boolean; -} +import { UsageStats } from './types'; +import { REDACTED_KEYWORD } from '../../../common/constants'; export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClient | undefined) { return async function fetchUsageStats(): Promise { @@ -23,11 +19,12 @@ export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClien return; } - const user = await uiSettingsClient.getUserProvided(); - const modifiedEntries = Object.keys(user) - .filter((key: string) => key !== 'buildNum') - .reduce((obj: any, key: string) => { - obj[key] = user[key].userValue; + const userProvided = await uiSettingsClient.getUserProvided(); + const modifiedEntries = Object.entries(userProvided) + .filter(([key]) => key !== 'buildNum') + .reduce((obj: any, [key, { userValue }]) => { + const sensitive = uiSettingsClient.isSensitive(key); + obj[key] = sensitive ? REDACTED_KEYWORD : userValue; return obj; }, {}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts new file mode 100644 index 0000000000000..417841ee89569 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export interface UsageStats { + /** + * sensitive settings + */ + 'timelion:quandl.key': string; + 'securitySolution:defaultIndex': string; + 'securitySolution:newsFeedUrl': string; + 'xpackReporting:customPdfLogo': string; + 'notifications:banner': string; + 'timelion:graphite.url': string; + 'xpackDashboardMode:roles': string; + 'securitySolution:ipReputationLinks': string; + /** + * non-sensitive settings + */ + 'autocomplete:useTimeRange': boolean; + 'search:timeout': number; + 'visualization:visualize:legacyChartsLibrary': boolean; + 'doc_table:legacy': boolean; + 'discover:modifyColumnsOnSwitch': boolean; + 'discover:searchFieldsFromSource': boolean; + 'securitySolution:rulesTableRefresh': string; + 'apm:enableSignificantTerms': boolean; + 'apm:enableServiceOverview': boolean; + 'apm:enableCorrelations': boolean; + 'visualize:enableLabs': boolean; + 'visualization:heatmap:maxBuckets': number; + 'visualization:colorMapping': string; + 'visualization:regionmap:showWarnings': boolean; + 'visualization:dimmingOpacity': number; + 'visualization:tileMap:maxPrecision': number; + 'csv:separator': string; + 'visualization:tileMap:WMSdefaults': string; + 'timelion:target_buckets': number; + 'timelion:max_buckets': number; + 'timelion:es.timefield': string; + 'timelion:min_interval': string; + 'timelion:default_rows': number; + 'timelion:default_columns': number; + 'timelion:es.default_index': string; + 'timelion:showTutorial': boolean; + 'securitySolution:timeDefaults': string; + 'securitySolution:defaultAnomalyScore': number; + 'securitySolution:refreshIntervalDefaults': string; + 'securitySolution:enableNewsFeed': boolean; + 'search:includeFrozen': boolean; + 'courier:maxConcurrentShardRequests': number; + 'courier:batchSearches': boolean; + 'courier:setRequestPreference': string; + 'courier:customRequestPreference': string; + 'courier:ignoreFilterIfFieldNotInIndex': boolean; + 'rollups:enableIndexPatterns': boolean; + 'notifications:lifetime:warning': number; + 'notifications:lifetime:banner': number; + 'notifications:lifetime:info': number; + 'notifications:lifetime:error': number; + 'doc_table:highlight': boolean; + 'discover:searchOnPageLoad': boolean; + // eslint-disable-next-line @typescript-eslint/naming-convention + 'doc_table:hideTimeColumn': boolean; + 'discover:sampleSize': number; + defaultColumns: string[]; + 'context:defaultSize': number; + 'discover:aggs:terms:size': number; + 'context:tieBreakerFields': string[]; + 'discover:sort:defaultOrder': string; + 'context:step': number; + 'accessibility:disableAnimations': boolean; + 'ml:fileDataVisualizerMaxFileSize': string; + 'ml:anomalyDetection:results:enableTimeDefaults': boolean; + 'ml:anomalyDetection:results:timeDefaults': string; + 'truncate:maxHeight': number; + 'timepicker:timeDefaults': string; + 'timepicker:refreshIntervalDefaults': string; + 'timepicker:quickRanges': string; + 'theme:version': string; + 'theme:darkMode': boolean; + 'state:storeInSessionStorage': boolean; + 'savedObjects:perPage': number; + 'search:queryLanguage': string; + 'shortDots:enable': boolean; + 'sort:options': string; + 'savedObjects:listingLimit': number; + 'query:queryString:options': string; + 'metrics:max_buckets': number; + 'query:allowLeadingWildcards': boolean; + metaFields: string[]; + 'indexPattern:placeholder': string; + 'histogram:barTarget': number; + 'histogram:maxBars': number; + 'format:number:defaultLocale': string; + 'format:percent:defaultPattern': string; + 'format:number:defaultPattern': string; + 'history:limit': number; + 'format:defaultTypeMap': string; + 'format:currency:defaultPattern': string; + defaultIndex: string; + 'format:bytes:defaultPattern': string; + 'filters:pinnedByDefault': boolean; + 'filterEditor:suggestValues': boolean; + 'fields:popularLimit': number; + dateNanosFormat: string; + defaultRoute: string; + 'dateFormat:tz': string; + 'dateFormat:scaled': string; + 'csv:quoteValues': boolean; + 'dateFormat:dow': string; + dateFormat: string; +} diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 7bac6a809eca3..950fdf9405b75 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -4076,6 +4076,27 @@ }, "stack_management": { "properties": { + "timelion:quandl.key": { + "type": "keyword" + }, + "securitySolution:defaultIndex": { + "type": "keyword" + }, + "securitySolution:newsFeedUrl": { + "type": "keyword" + }, + "xpackReporting:customPdfLogo": { + "type": "keyword" + }, + "notifications:banner": { + "type": "keyword" + }, + "timelion:graphite.url": { + "type": "keyword" + }, + "xpackDashboardMode:roles": { + "type": "keyword" + }, "visualize:enableLabs": { "type": "boolean" }, @@ -4095,7 +4116,7 @@ "type": "long" }, "securitySolution:ipReputationLinks": { - "type": "text" + "type": "keyword" }, "csv:separator": { "type": "keyword" @@ -4121,9 +4142,6 @@ "timelion:default_columns": { "type": "long" }, - "timelion:quandl.key": { - "type": "keyword" - }, "timelion:es.default_index": { "type": "keyword" }, @@ -4136,15 +4154,9 @@ "securitySolution:defaultAnomalyScore": { "type": "long" }, - "securitySolution:defaultIndex": { - "type": "keyword" - }, "securitySolution:refreshIntervalDefaults": { "type": "keyword" }, - "securitySolution:newsFeedUrl": { - "type": "keyword" - }, "securitySolution:enableNewsFeed": { "type": "boolean" }, @@ -4169,9 +4181,6 @@ "rollups:enableIndexPatterns": { "type": "boolean" }, - "xpackReporting:customPdfLogo": { - "type": "text" - }, "notifications:lifetime:warning": { "type": "long" }, @@ -4181,9 +4190,6 @@ "notifications:lifetime:info": { "type": "long" }, - "notifications:banner": { - "type": "text" - }, "notifications:lifetime:error": { "type": "long" }, @@ -4200,7 +4206,10 @@ "type": "long" }, "defaultColumns": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } }, "context:defaultSize": { "type": "long" @@ -4209,7 +4218,10 @@ "type": "long" }, "context:tieBreakerFields": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } }, "discover:sort:defaultOrder": { "type": "keyword" @@ -4275,7 +4287,10 @@ "type": "boolean" }, "metaFields": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } }, "indexPattern:placeholder": { "type": "keyword" @@ -4339,6 +4354,36 @@ }, "dateFormat": { "type": "keyword" + }, + "autocomplete:useTimeRange": { + "type": "boolean" + }, + "search:timeout": { + "type": "long" + }, + "visualization:visualize:legacyChartsLibrary": { + "type": "boolean" + }, + "doc_table:legacy": { + "type": "boolean" + }, + "discover:modifyColumnsOnSwitch": { + "type": "boolean" + }, + "discover:searchFieldsFromSource": { + "type": "boolean" + }, + "securitySolution:rulesTableRefresh": { + "type": "text" + }, + "apm:enableSignificantTerms": { + "type": "boolean" + }, + "apm:enableServiceOverview": { + "type": "boolean" + }, + "apm:enableCorrelations": { + "type": "boolean" } } }, diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index ccc17ea1c5967..8e8a74902d479 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -14,17 +14,38 @@ import { KibanaRequest, } from 'src/core/server'; -export type AllowedSchemaNumberTypes = 'long' | 'integer' | 'short' | 'byte' | 'double' | 'float'; +export type AllowedSchemaNumberTypes = + | 'long' + | 'integer' + | 'short' + | 'byte' + | 'double' + | 'float' + | 'date'; +export type AllowedSchemaStringTypes = 'keyword' | 'text' | 'date'; +export type AllowedSchemaBooleanTypes = 'boolean'; -export type AllowedSchemaTypes = AllowedSchemaNumberTypes | 'keyword' | 'text' | 'boolean' | 'date'; +export type AllowedSchemaTypes = + | AllowedSchemaNumberTypes + | AllowedSchemaStringTypes + | AllowedSchemaBooleanTypes; export interface SchemaField { type: string; } +export type PossibleSchemaTypes = U extends string + ? AllowedSchemaStringTypes + : U extends number + ? AllowedSchemaNumberTypes + : U extends boolean + ? AllowedSchemaBooleanTypes + : // allow any schema type from the union if typescript is unable to resolve the exact U type + AllowedSchemaTypes; + export type RecursiveMakeSchemaFrom = U extends object ? MakeSchemaFrom - : { type: AllowedSchemaTypes }; + : { type: PossibleSchemaTypes }; // Using Required to enforce all optional keys in the object export type MakeSchemaFrom = { diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index f999c1dfc773a..fca557efc01e3 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -173,6 +173,7 @@ export class Plugin { defaultMessage: '{experimentalLabel} Your API key from www.quandl.com', values: { experimentalLabel: `[${experimentalLabel}]` }, }), + sensitive: true, category: ['timelion'], schema: schema.string(), }, diff --git a/x-pack/plugins/dashboard_mode/server/ui_settings.ts b/x-pack/plugins/dashboard_mode/server/ui_settings.ts index f692ec8a33fc9..59de82cf7b3ab 100644 --- a/x-pack/plugins/dashboard_mode/server/ui_settings.ts +++ b/x-pack/plugins/dashboard_mode/server/ui_settings.ts @@ -22,6 +22,7 @@ export function getUiSettings(): Record> { }), value: [DASHBOARD_ONLY_USER_ROLE], category: ['dashboard'], + sensitive: true, deprecation: { message: i18n.translate('xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDeprecation', { defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.', diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 05556f050e213..35101dbaab246 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -52,6 +52,7 @@ export class ReportingPlugin description: i18n.translate('xpack.reporting.pdfFooterImageDescription', { defaultMessage: `Custom image to use in the PDF's footer`, }), + sensitive: true, type: 'image', schema: schema.nullable(schema.byteSize({ max: '200kb' })), category: [PLUGIN_ID], diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index 548f718e1bc80..0d679cdefb92c 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -78,6 +78,8 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { name: i18n.translate('xpack.securitySolution.uiSettings.defaultIndexLabel', { defaultMessage: 'Elasticsearch indices', }), + sensitive: true, + value: DEFAULT_INDEX_PATTERN, description: i18n.translate('xpack.securitySolution.uiSettings.defaultIndexDescription', { defaultMessage: @@ -147,6 +149,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { defaultMessage: 'News feed URL', }), value: NEWS_FEED_URL_SETTING_DEFAULT, + sensitive: true, description: i18n.translate('xpack.securitySolution.uiSettings.newsFeedUrlDescription', { defaultMessage: '

News feed content will be retrieved from this URL

', }), @@ -167,6 +170,7 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', } ), + sensitive: true, category: [APP_ID], requiresPageReload: true, schema: schema.arrayOf( diff --git a/x-pack/test/usage_collection/config.ts b/x-pack/test/usage_collection/config.ts index 27b12a1ff298c..d31ecc444d00d 100644 --- a/x-pack/test/usage_collection/config.ts +++ b/x-pack/test/usage_collection/config.ts @@ -24,7 +24,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...xpackFunctionalConfig.getAll(), // list paths to the files that contain your plugins tests - testFiles: [resolve(__dirname, './test_suites/application_usage')], + testFiles: [ + resolve(__dirname, './test_suites/application_usage'), + resolve(__dirname, './test_suites/stack_management_usage'), + ], services, pageObjects, diff --git a/x-pack/test/usage_collection/plugins/stack_management_usage_test/kibana.json b/x-pack/test/usage_collection/plugins/stack_management_usage_test/kibana.json new file mode 100644 index 0000000000000..b586de3fa4d79 --- /dev/null +++ b/x-pack/test/usage_collection/plugins/stack_management_usage_test/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "StackManagementUsageTest", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "StackManagementUsageTest"], + "requiredPlugins": [], + "server": false, + "ui": true +} diff --git a/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/index.ts b/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/index.ts new file mode 100644 index 0000000000000..82aae6988052a --- /dev/null +++ b/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/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 { StackManagementUsageTest } from './plugin'; + +export function plugin() { + return new StackManagementUsageTest(); +} diff --git a/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/plugin.ts b/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/plugin.ts new file mode 100644 index 0000000000000..3cd10a1d4c178 --- /dev/null +++ b/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/plugin.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, CoreStart } from 'kibana/public'; +import './types'; + +export class StackManagementUsageTest implements Plugin { + public setup(core: CoreSetup) {} + + public start(core: CoreStart) { + const allUiSettings = core.uiSettings.getAll(); + window.__registeredUiSettings__ = allUiSettings; + } +} diff --git a/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/types.ts b/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/types.ts new file mode 100644 index 0000000000000..c49ec89d94b9f --- /dev/null +++ b/x-pack/test/usage_collection/plugins/stack_management_usage_test/public/types.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. + */ + +import { IUiSettingsClient } from 'src/core/public'; +export {}; // Hack to declare this file as a module so TS allows us to extend the Global Window interface + +declare global { + interface Window { + __registeredUiSettings__: ReturnType; + } +} diff --git a/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json b/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json new file mode 100644 index 0000000000000..f1bf94a38de8f --- /dev/null +++ b/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "public/**/*.ts", + "public/**/*.tsx", + ], + "exclude": [] +} diff --git a/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts b/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts new file mode 100644 index 0000000000000..b8f0cf522605e --- /dev/null +++ b/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { stackManagementSchema } from '../../../../../src/plugins/kibana_usage_collection/server/collectors/management/schema'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + describe('Stack Management', function () { + this.tags('ciGroup1'); + const { common } = getPageObjects(['common']); + const browser = getService('browser'); + + let registeredSettings: Record; + + before(async () => { + await common.navigateToApp('home'); // Navigate to Home to make sure all the appIds are loaded + registeredSettings = await browser.execute(() => window.__registeredUiSettings__); + }); + + it('registers all UI Settings in the UsageStats interface', () => { + const unreportedUISettings = Object.keys(registeredSettings) + .filter((key) => key !== 'buildNum') + .filter((key) => typeof _.get(stackManagementSchema, key) === 'undefined'); + + if (unreportedUISettings.length) { + throw new Error( + `Detected the following unregistered UI Settings in the stack management collector: + ${JSON.stringify(unreportedUISettings, null)} + Update the management collector schema and its UsageStats interface. + Refer to src/plugins/kibana_usage_collection/server/collectors/management/README.md for additional information. + ` + ); + } + }); + + it('registers all sensitive UI settings as keyword type', async () => { + const sensitiveSettings = Object.entries(registeredSettings) + .filter(([, config]) => config.sensitive) + .map(([key]) => key); + + const nonBooleanSensitiveProps = sensitiveSettings + .map((key) => ({ key, ..._.get(stackManagementSchema, key) })) + .filter((keyDescriptor) => keyDescriptor.type !== 'keyword'); + + if (nonBooleanSensitiveProps.length) { + throw new Error( + `Detected the following sensitive UI Settings in the stack management collector not having a 'keyword' type: + ${JSON.stringify(nonBooleanSensitiveProps, null)} + Update each setting in the management collector schema with ({ type: 'keyword' }). + Refer to src/plugins/kibana_usage_collection/server/collectors/management/README.md for additional information. + ` + ); + } + }); + }); +} From 289ca57bb5f90183857d3c78d79673687a659db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Wed, 27 Jan 2021 17:27:34 +0100 Subject: [PATCH 041/163] [Metrics UI] Fix Host Overview boxes in Host Detail page (#89299) * change from type:gauge to type:top_n in inventory models * Add test for hostSystemOverview metric * fix lint errors Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../host/metrics/tsvb/host_docker_overview.ts | 2 +- .../host/metrics/tsvb/host_k8s_overview.ts | 2 +- .../host/metrics/tsvb/host_system_overview.ts | 2 +- .../shared/metrics/tsvb/aws_overview.ts | 2 +- .../apis/metrics_ui/metrics.ts | 25 +++++++++++++++++++ 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_overview.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_overview.ts index d7026d7648d37..b8cf094d7bd4c 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_overview.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_overview.ts @@ -15,7 +15,7 @@ export const hostDockerOverview: TSVBMetricModelCreator = ( index_pattern: indexPattern, interval, time_field: timeField, - type: 'gauge', + type: 'top_n', series: [ { id: 'total', diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_overview.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_overview.ts index 86d615231f070..1488fe5504c0b 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_overview.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_overview.ts @@ -16,7 +16,7 @@ export const hostK8sOverview: TSVBMetricModelCreator = ( index_pattern: indexPattern, interval, time_field: timeField, - type: 'gauge', + type: 'top_n', series: [ { id: 'cpucap', diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts index 953c14ab2a9ce..cbd76dd8e9637 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts @@ -16,7 +16,7 @@ export const hostSystemOverview: TSVBMetricModelCreator = ( index_pattern: indexPattern, interval, time_field: timeField, - type: 'gauge', + type: 'top_n', series: [ { id: 'cpu', diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_overview.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_overview.ts index 5ba61d1f92517..28e1b7860aab4 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_overview.ts +++ b/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_overview.ts @@ -14,7 +14,7 @@ export const awsOverview: TSVBMetricModelCreator = (timeField, indexPattern): TS id_type: 'cloud', interval: '>=5m', time_field: timeField, - type: 'gauge', + type: 'top_n', series: [ { id: 'cpu-util', diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics.ts index 23b0a96ecd401..b9cbc58bbd6f7 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics.ts @@ -93,5 +93,30 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.metrics.length).to.equal(2); }); }); + + it('should return multiple values for hostSystemOverview metric', () => { + const data = fetchNodeDetails({ + sourceId: 'default', + metrics: ['hostSystemOverview'], + timerange: { + to: max, + from: min, + interval: '>=1m', + }, + nodeId: 'demo-stack-mysql-01', + nodeType: 'host' as InfraNodeType, + }); + return data.then((resp) => { + if (!resp) { + return; + } + + const hostSystemOverviewMetric = resp.metrics.find( + (metric) => metric.id === 'hostSystemOverview' + ); + + expect(hostSystemOverviewMetric?.series.length).to.be.greaterThan(1); + }); + }); }); } From 7de33830d6a65166528adb51627d92ecdc2c153b Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Wed, 27 Jan 2021 16:57:05 +0000 Subject: [PATCH 042/163] [Discover] Grouping multifields in a doc table (#88560) * [Discover] Grouping multifields in a doc table * Fixing scss selector * Remove unnecessary comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/doc_viewer/doc_viewer.scss | 8 + .../components/table/table.test.tsx | 193 +++++++++++++++--- .../application/components/table/table.tsx | 107 ++++++++-- .../components/table/table_row.tsx | 25 ++- 4 files changed, 280 insertions(+), 53 deletions(-) diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss index 5bae3d64a6b69..95a50b54b5364 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss @@ -19,6 +19,14 @@ padding-top: $euiSizeS; } + .kbnDocViewer__multifield_row:hover td { + background-color: transparent; + } + + .kbnDocViewer__multifield_title { + font-family: $euiFontFamily; + } + .dscFieldName { color: $euiColorDarkShade; } diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 1ffc0e5af95ac..ac074cf229c77 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -167,30 +167,6 @@ describe('DocViewTable at Discover', () => { }); }); -describe('DocViewTable at Discover Doc', () => { - const hit = { - _index: 'logstash-2014.09.09', - _score: 1, - _type: 'doc', - _id: 'id123', - _source: { - extension: 'html', - not_mapped: 'yes', - }, - }; - // here no action buttons are rendered - const props = { - hit, - indexPattern, - }; - const component = mount(); - const foundLength = findTestSubject(component, 'addInclusiveFilterButton').length; - - it(`renders no action buttons`, () => { - expect(foundLength).toBe(0); - }); -}); - describe('DocViewTable at Discover Context', () => { // here no toggleColumnButtons are rendered const hit = { @@ -243,3 +219,172 @@ describe('DocViewTable at Discover Context', () => { expect(component.html() !== html).toBeTruthy(); }); }); + +describe('DocViewTable at Discover Doc', () => { + const hit = { + _index: 'logstash-2014.09.09', + _score: 1, + _type: 'doc', + _id: 'id123', + _source: { + extension: 'html', + not_mapped: 'yes', + }, + }; + // here no action buttons are rendered + const props = { + hit, + indexPattern, + }; + const component = mount(); + const foundLength = findTestSubject(component, 'addInclusiveFilterButton').length; + + it(`renders no action buttons`, () => { + expect(foundLength).toBe(0); + }); +}); + +describe('DocViewTable at Discover Doc with Fields API', () => { + const indexPatterneCommerce = ({ + fields: { + getAll: () => [ + { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'category', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'category.keyword', + displayName: 'category.keyword', + type: 'string', + scripted: false, + filterable: true, + spec: { + subType: { + multi: { + parent: 'category', + }, + }, + }, + }, + { + name: 'customer_first_name', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'customer_first_name.keyword', + displayName: 'customer_first_name.keyword', + type: 'string', + scripted: false, + filterable: false, + spec: { + subType: { + multi: { + parent: 'customer_first_name', + }, + }, + }, + }, + { + name: 'customer_first_name.nickname', + displayName: 'customer_first_name.nickname', + type: 'string', + scripted: false, + filterable: false, + spec: { + subType: { + multi: { + parent: 'customer_first_name', + }, + }, + }, + }, + ], + }, + metaFields: ['_index', '_type', '_score', '_id'], + flattenHit: jest.fn((hit) => { + const result = {} as Record; + Object.keys(hit).forEach((key) => { + if (key !== 'fields') { + result[key] = hit[key]; + } else { + Object.keys(hit.fields).forEach((field) => { + result[field] = hit.fields[field]; + }); + } + }); + return result; + }), + formatHit: jest.fn((hit) => { + const result = {} as Record; + Object.keys(hit).forEach((key) => { + if (key !== 'fields') { + result[key] = hit[key]; + } else { + Object.keys(hit.fields).forEach((field) => { + result[field] = hit.fields[field]; + }); + } + }); + return result; + }), + } as unknown) as IndexPattern; + + indexPatterneCommerce.fields.getByName = (name: string) => { + return indexPatterneCommerce.fields.getAll().find((field) => field.name === name); + }; + + const fieldsHit = { + _index: 'logstash-2014.09.09', + _type: 'doc', + _id: 'id123', + _score: null, + fields: { + category: "Women's Clothing", + 'category.keyword': "Women's Clothing", + customer_first_name: 'Betty', + 'customer_first_name.keyword': 'Betty', + 'customer_first_name.nickname': 'Betsy', + }, + }; + const props = { + hit: fieldsHit, + columns: ['Document'], + indexPattern: indexPatterneCommerce, + filter: jest.fn(), + onAddColumn: jest.fn(), + onRemoveColumn: jest.fn(), + }; + // @ts-ignore + const component = mount(); + it('renders multifield rows', () => { + const categoryMultifieldRow = findTestSubject( + component, + 'tableDocViewRow-multifieldsTitle-category' + ); + expect(categoryMultifieldRow.length).toBe(1); + const categoryKeywordRow = findTestSubject(component, 'tableDocViewRow-category.keyword'); + expect(categoryKeywordRow.length).toBe(1); + + const customerNameMultiFieldRow = findTestSubject( + component, + 'tableDocViewRow-multifieldsTitle-customer_first_name' + ); + expect(customerNameMultiFieldRow.length).toBe(1); + expect(findTestSubject(component, 'tableDocViewRow-customer_first_name.keyword').length).toBe( + 1 + ); + expect(findTestSubject(component, 'tableDocViewRow-customer_first_name.nickname').length).toBe( + 1 + ); + }); +}); diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 090df8baba409..7528828a06f97 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -5,8 +5,8 @@ * compliance with, at your election, the Elastic License or the Server Side * Public License, v 1. */ - -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; import { DocViewTableRow } from './table_row'; import { trimAngularSpan } from './table_helper'; import { isNestedFieldParent } from '../../helpers/nested_fields'; @@ -23,6 +23,36 @@ export function DocViewTable({ onRemoveColumn, }: DocViewRenderProps) { const [fieldRowOpen, setFieldRowOpen] = useState({} as Record); + const [multiFields, setMultiFields] = useState({} as Record); + const [fieldsWithParents, setFieldsWithParents] = useState([] as string[]); + + useEffect(() => { + if (!indexPattern) { + return; + } + const mapping = indexPattern.fields.getByName; + const flattened = indexPattern.flattenHit(hit); + const map: Record = {}; + const arr: string[] = []; + + Object.keys(flattened).forEach((key) => { + const field = mapping(key); + + if (field && field.spec?.subType?.multi?.parent) { + const parent = field.spec.subType.multi.parent; + if (!map[parent]) { + map[parent] = [] as string[]; + } + const value = map[parent]; + value.push(key); + map[parent] = value; + arr.push(key); + } + }); + setMultiFields(map); + setFieldsWithParents(arr); + }, [indexPattern, hit]); + if (!indexPattern) { return null; } @@ -34,11 +64,13 @@ export function DocViewTable({ fieldRowOpen[field] = !fieldRowOpen[field]; setFieldRowOpen({ ...fieldRowOpen }); } - return ( {Object.keys(flattened) + .filter((field) => { + return !fieldsWithParents.includes(field); + }) .sort((fieldA, fieldB) => { const mappingA = mapping(fieldA); const mappingB = mapping(fieldB); @@ -67,23 +99,60 @@ export function DocViewTable({ const fieldType = isNestedFieldParent(field, indexPattern) ? 'nested' : indexPattern.fields.getByName(field)?.type; - return ( - toggleValueCollapse(field)} - onToggleColumn={toggleColumn} - value={value} - valueRaw={valueRaw} - /> + + toggleValueCollapse(field)} + onToggleColumn={toggleColumn} + value={value} + valueRaw={valueRaw} + /> + {multiFields[field] ? ( + + + + + ) : null} + {multiFields[field] + ? multiFields[field].map((multiField) => { + return ( + toggleValueCollapse(field)} + onToggleColumn={toggleColumn} + value={value} + valueRaw={valueRaw} + /> + ); + }) + : null} + ); })} diff --git a/src/plugins/discover/public/application/components/table/table_row.tsx b/src/plugins/discover/public/application/components/table/table_row.tsx index 61f9fa50091c1..2b91a757e9bc2 100644 --- a/src/plugins/discover/public/application/components/table/table_row.tsx +++ b/src/plugins/discover/public/application/components/table/table_row.tsx @@ -18,7 +18,7 @@ import { DocViewTableRowIconUnderscore } from './table_row_icon_underscore'; import { FieldName } from '../field_name/field_name'; export interface Props { - field: string; + field?: string; fieldMapping?: FieldMapping; fieldType: string; displayUnderscoreWarning: boolean; @@ -51,25 +51,30 @@ export function DocViewTableRow({ kbnDocViewer__value: true, 'truncate-by-height': isCollapsible && isCollapsed, }); - + const key = field ? field : fieldMapping?.displayName; return ( - +
  + + {i18n.translate('discover.fieldChooser.discoverField.multiFields', { + defaultMessage: 'Multi fields', + })} + +
- + {field ? ( + + ) : ( +   + )} {isCollapsible && ( )} {displayUnderscoreWarning && } + {field ? null :
{key}: 
}
Date: Wed, 27 Jan 2021 17:58:42 +0100 Subject: [PATCH 043/163] [Discover] Merge discover.tsx and discover_legacy.tsx (#88465) --- .../public/application/angular/discover.js | 5 +- .../application/angular/discover_legacy.html | 4 +- .../doc_table/create_doc_table_react.tsx | 33 +- .../components/create_discover_directive.ts | 5 +- .../create_discover_legacy_directive.ts | 45 -- ...over_legacy.test.tsx => discover.test.tsx} | 16 +- .../application/components/discover.tsx | 125 +++-- .../discover_grid/discover_grid.tsx | 8 +- .../discover_grid/discover_grid_columns.tsx | 7 +- .../components/discover_legacy.tsx | 517 ------------------ .../public/application/components/types.ts | 179 ++++++ .../application/helpers/columns.test.ts | 46 ++ .../public/application/helpers/columns.ts | 22 + .../discover/public/get_inner_angular.ts | 2 - test/functional/apps/discover/_data_grid.ts | 4 +- .../apps/discover/_data_grid_field_data.ts | 2 +- 16 files changed, 387 insertions(+), 633 deletions(-) delete mode 100644 src/plugins/discover/public/application/components/create_discover_legacy_directive.ts rename src/plugins/discover/public/application/components/{discover_legacy.test.tsx => discover.test.tsx} (89%) delete mode 100644 src/plugins/discover/public/application/components/discover_legacy.tsx create mode 100644 src/plugins/discover/public/application/components/types.ts create mode 100644 src/plugins/discover/public/application/helpers/columns.test.ts create mode 100644 src/plugins/discover/public/application/helpers/columns.ts diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 5c26680c7cc45..41c80a717ce75 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -24,7 +24,6 @@ import { import { getSortArray } from './doc_table'; import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; -import indexTemplateGrid from './discover_datagrid.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; import { discoverResponseHandler } from './response_handler'; import { @@ -116,9 +115,7 @@ app.config(($routeProvider) => { }; const discoverRoute = { ...defaults, - template: getServices().uiSettings.get('doc_table:legacy', true) - ? indexTemplateLegacy - : indexTemplateGrid, + template: indexTemplateLegacy, reloadOnSearch: false, resolve: { savedObjects: function ($route, Promise) { diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 9383980fd9fd6..76e5c568ffde6 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -1,5 +1,5 @@ - - + diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index 06b6e504832e4..cbd93feb835a0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -9,8 +9,11 @@ import angular, { auto, ICompileService, IScope } from 'angular'; import { render } from 'react-dom'; import React, { useRef, useEffect } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { getServices, IIndexPattern } from '../../../kibana_services'; import { IndexPatternField } from '../../../../../data/common/index_patterns'; + export type AngularScope = IScope; export interface AngularDirective { @@ -83,9 +86,11 @@ export interface DocTableLegacyProps { indexPattern: IIndexPattern; minimumVisibleRows: number; onAddColumn?: (column: string) => void; + onBackToTop: () => void; onSort?: (sort: string[][]) => void; onMoveColumn?: (columns: string, newIdx: number) => void; onRemoveColumn?: (column: string) => void; + sampleSize: number; sort?: string[][]; useNewFieldsApi?: boolean; } @@ -120,5 +125,31 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) { return renderFn(ref.current, renderProps); } }, [renderFn, renderProps]); - return
; + return ( +
+
+ {renderProps.rows.length === renderProps.sampleSize ? ( +
+ + + + +
+ ) : ( + + ​ + + )} +
+ ); } diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index 42b99b635a791..10439488f4bc7 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -17,18 +17,21 @@ export function createDiscoverDirective(reactDirective: any) { ['histogramData', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], + ['minimumVisibleRows', { watchDepth: 'reference' }], ['onAddColumn', { watchDepth: 'reference' }], ['onAddFilter', { watchDepth: 'reference' }], ['onChangeInterval', { watchDepth: 'reference' }], + ['onMoveColumn', { watchDepth: 'reference' }], ['onRemoveColumn', { watchDepth: 'reference' }], ['onSetColumns', { watchDepth: 'reference' }], + ['onSkipBottomButtonClick', { watchDepth: 'reference' }], ['onSort', { watchDepth: 'reference' }], ['opts', { watchDepth: 'reference' }], ['resetQuery', { watchDepth: 'reference' }], ['resultState', { watchDepth: 'reference' }], ['rows', { watchDepth: 'reference' }], + ['savedSearch', { watchDepth: 'reference' }], ['searchSource', { watchDepth: 'reference' }], - ['setColumns', { watchDepth: 'reference' }], ['setIndexPattern', { watchDepth: 'reference' }], ['showSaveQuery', { watchDepth: 'reference' }], ['state', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts deleted file mode 100644 index b2b9fd38f73b1..0000000000000 --- a/src/plugins/discover/public/application/components/create_discover_legacy_directive.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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { DiscoverLegacy } from './discover_legacy'; - -export function createDiscoverLegacyDirective(reactDirective: any) { - return reactDirective(DiscoverLegacy, [ - ['fetch', { watchDepth: 'reference' }], - ['fetchCounter', { watchDepth: 'reference' }], - ['fetchError', { watchDepth: 'reference' }], - ['fieldCounts', { watchDepth: 'reference' }], - ['histogramData', { watchDepth: 'reference' }], - ['hits', { watchDepth: 'reference' }], - ['indexPattern', { watchDepth: 'reference' }], - ['minimumVisibleRows', { watchDepth: 'reference' }], - ['onAddColumn', { watchDepth: 'reference' }], - ['onAddFilter', { watchDepth: 'reference' }], - ['onChangeInterval', { watchDepth: 'reference' }], - ['onMoveColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ['onSetColumns', { watchDepth: 'reference' }], - ['onSkipBottomButtonClick', { watchDepth: 'reference' }], - ['onSort', { watchDepth: 'reference' }], - ['opts', { watchDepth: 'reference' }], - ['resetQuery', { watchDepth: 'reference' }], - ['resultState', { watchDepth: 'reference' }], - ['rows', { watchDepth: 'reference' }], - ['savedSearch', { watchDepth: 'reference' }], - ['searchSource', { watchDepth: 'reference' }], - ['setIndexPattern', { watchDepth: 'reference' }], - ['showSaveQuery', { watchDepth: 'reference' }], - ['state', { watchDepth: 'reference' }], - ['timefilterUpdateHandler', { watchDepth: 'reference' }], - ['timeRange', { watchDepth: 'reference' }], - ['topNavMenu', { watchDepth: 'reference' }], - ['updateQuery', { watchDepth: 'reference' }], - ['updateSavedQueryId', { watchDepth: 'reference' }], - ['useNewFieldsApi', { watchDepth: 'reference' }], - ]); -} diff --git a/src/plugins/discover/public/application/components/discover_legacy.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx similarity index 89% rename from src/plugins/discover/public/application/components/discover_legacy.test.tsx rename to src/plugins/discover/public/application/components/discover.test.tsx index 04f294912d49e..3088ca45f7941 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/discover.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallowWithIntl } from '@kbn/test/jest'; -import { DiscoverLegacy } from './discover_legacy'; +import { Discover } from './discover'; import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { esHits } from '../../__mocks__/es_hits'; import { indexPatternMock } from '../../__mocks__/index_pattern'; @@ -19,7 +19,7 @@ import { savedSearchMock } from '../../__mocks__/saved_search'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; import { dataPluginMock } from '../../../../data/public/mocks'; import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; -import { uiSettingsMock } from '../../__mocks__/ui_settings'; +import { uiSettingsMock as mockUiSettings } from '../../__mocks__/ui_settings'; import { IndexPattern, IndexPatternAttributes } from '../../../../data/common/index_patterns'; import { SavedObject } from '../../../../../core/types'; import { navigationPluginMock } from '../../../../navigation/public/mocks'; @@ -40,6 +40,7 @@ jest.mock('../../kibana_services', () => { }, }, navigation: mockNavigation, + uiSettings: mockUiSettings, }), }; }); @@ -53,6 +54,7 @@ function getProps(indexPattern: IndexPattern) { save: true, }, }, + uiSettings: mockUiSettings, } as unknown) as DiscoverServices; return { @@ -72,7 +74,7 @@ function getProps(indexPattern: IndexPattern) { onSkipBottomButtonClick: jest.fn(), onSort: jest.fn(), opts: { - config: uiSettingsMock, + config: mockUiSettings, data: dataPluginMock.createStartContract(), fixedScroll: jest.fn(), filterManager: createFilterManagerMock(), @@ -105,15 +107,13 @@ function getProps(indexPattern: IndexPattern) { }; } -describe('Descover legacy component', () => { +describe('Discover component', () => { test('selected index pattern without time field displays no chart toggle', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component.find('[data-test-subj="discoverChartToggle"]').length).toBe(0); }); test('selected index pattern with time field displays chart toggle', () => { - const component = shallowWithIntl( - - ); + const component = shallowWithIntl(); expect(component.find('[data-test-subj="discoverChartToggle"]').length).toBe(1); }); }); diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 704e7a9c02e1b..5653ef4f57435 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -26,25 +26,30 @@ import classNames from 'classnames'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; import { getServices } from '../../kibana_services'; -import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives'; +import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives'; import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; +import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react'; +import { SkipBottomButton } from './skip_bottom_button'; import { search } from '../../../../data/public'; import { DiscoverSidebarResponsive, DiscoverSidebarResponsiveProps, } from './sidebar/discover_sidebar_responsive'; -import { DiscoverProps } from './discover_legacy'; +import { DiscoverProps } from './types'; +import { getDisplayedColumns } from '../helpers/columns'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; -export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( +const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( + +)); +const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( )); -export const DataGridMemoized = React.memo((props: DiscoverGridProps) => ( - -)); +const DataGridMemoized = React.memo((props: DiscoverGridProps) => ); export function Discover({ fetch, @@ -54,11 +59,14 @@ export function Discover({ histogramData, hits, indexPattern, + minimumVisibleRows, onAddColumn, onAddFilter, onChangeInterval, + onMoveColumn, onRemoveColumn, onSetColumns, + onSkipBottomButtonClick, onSort, opts, resetQuery, @@ -66,7 +74,6 @@ export function Discover({ rows, searchSource, setIndexPattern, - showSaveQuery, state, timefilterUpdateHandler, timeRange, @@ -76,6 +83,11 @@ export function Discover({ }: DiscoverProps) { const scrollableDesktop = useRef(null); const collapseIcon = useRef(null); + const isMobile = () => { + // collapse icon isn't displayed in mobile view, use it to detect which view is displayed + return collapseIcon && !collapseIcon.current; + }; + const [toggleOn, toggleChart] = useState(true); const [isSidebarClosed, setIsSidebarClosed] = useState(false); const services = getServices(); @@ -88,18 +100,8 @@ export function Discover({ ? bucketAggConfig.buckets?.getInterval() : undefined; const contentCentered = resultState === 'uninitialized'; - const showTimeCol = !config.get('doc_table:hideTimeColumn', false) && indexPattern.timeFieldName; - const columns = - state.columns && - state.columns.length > 0 && - // check if all columns where removed except the configured timeField (this can't be removed) - !(state.columns.length === 1 && state.columns[0] === indexPattern.timeFieldName) - ? state.columns - : ['_source']; - // if columns include _source this is considered as default view, so you can't remove columns - // until you add a column using Discover's sidebar - const defaultColumns = columns.includes('_source'); - + const isLegacy = services.uiSettings.get('doc_table:legacy'); + const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); return ( @@ -114,7 +116,7 @@ export function Discover({ savedQueryId={state.savedQuery} screenTitle={savedSearch.title} showDatePicker={indexPattern.isTimeBased()} - showSaveQuery={showSaveQuery} + showSaveQuery={!!services.capabilities.discover.saveQuery} showSearchBar={true} useDefaultBehaviors={true} /> @@ -137,6 +139,7 @@ export function Discover({ setIndexPattern={setIndexPattern} isClosed={isSidebarClosed} trackUiMetric={trackUiMetric} + useNewFieldsApi={useNewFieldsApi} /> @@ -207,24 +210,28 @@ export function Discover({ /> )} - - { - toggleChart(!toggleOn); - }} - > - {toggleOn - ? i18n.translate('discover.hideChart', { - defaultMessage: 'Hide chart', - }) - : i18n.translate('discover.showChart', { - defaultMessage: 'Show chart', - })} - - + {opts.timefield && ( + + { + toggleChart(!toggleOn); + }} + data-test-subj="discoverChartToggle" + > + {toggleOn + ? i18n.translate('discover.hideChart', { + defaultMessage: 'Hide chart', + }) + : i18n.translate('discover.showChart', { + defaultMessage: 'Show chart', + })} + + + )} + {isLegacy && } {toggleOn && opts.timefield && ( @@ -238,7 +245,10 @@ export function Discover({ className="dscTimechart" > {opts.chartAggConfigs && histogramData && rows.length !== 0 && ( -
+
- {rows && rows.length && ( + {isLegacy && rows && rows.length && ( + { + if (scrollableDesktop && scrollableDesktop.current) { + scrollableDesktop.current.focus(); + } + // Only the desktop one needs to target a specific container + if (!isMobile() && scrollableDesktop.current) { + scrollableDesktop.current.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }} + onFilter={onAddFilter} + onMoveColumn={onMoveColumn} + onRemoveColumn={onRemoveColumn} + onSort={onSort} + sampleSize={opts.sampleSize} + useNewFieldsApi={useNewFieldsApi} + /> + )} + {!isLegacy && rows && rows.length && (
{ export const DiscoverGrid = ({ ariaLabelledBy, columns, - defaultColumns, indexPattern, onAddColumn, onFilter, @@ -144,6 +137,7 @@ export const DiscoverGrid = ({ sort, }: DiscoverGridProps) => { const [expanded, setExpanded] = useState(undefined); + const defaultColumns = columns.includes('_source'); /** * Pagination diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx index 481d308cf88a9..6a247ad951c9b 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -48,7 +48,12 @@ export function buildEuiGridColumn( id: columnName, schema: getSchemaByKbnType(indexPatternField?.type), isSortable: indexPatternField?.sortable, - display: indexPatternField?.displayName, + display: + columnName === '_source' + ? i18n.translate('discover.grid.documentHeader', { + defaultMessage: 'Document', + }) + : indexPatternField?.displayName, actions: { showHide: defaultColumns || columnName === indexPattern.timeFieldName diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx deleted file mode 100644 index 1b90b845a8fff..0000000000000 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ /dev/null @@ -1,517 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import './discover.scss'; - -import React, { useState, useRef } from 'react'; -import { - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiHideFor, - EuiPage, - EuiPageBody, - EuiPageContent, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { IUiSettingsClient, MountPoint } from 'kibana/public'; -import classNames from 'classnames'; -import { HitsCounter } from './hits_counter'; -import { TimechartHeader } from './timechart_header'; -import { getServices, IndexPattern } from '../../kibana_services'; -import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives'; -import { DiscoverNoResults } from './no_results'; -import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react'; -import { SkipBottomButton } from './skip_bottom_button'; -import { - search, - ISearchSource, - TimeRange, - Query, - IndexPatternAttributes, - DataPublicPluginStart, - AggConfigs, - FilterManager, -} from '../../../../data/public'; -import { Chart } from '../angular/helpers/point_series'; -import { AppState } from '../angular/discover_state'; -import { SavedSearch } from '../../saved_searches'; -import { SavedObject } from '../../../../../core/types'; -import { TopNavMenuData } from '../../../../navigation/public'; -import { - DiscoverSidebarResponsive, - DiscoverSidebarResponsiveProps, -} from './sidebar/discover_sidebar_responsive'; -import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; - -export interface DiscoverProps { - /** - * Function to fetch documents from Elasticsearch - */ - fetch: () => void; - /** - * Counter how often data was fetched (used for testing) - */ - fetchCounter: number; - /** - * Error in case of a failing document fetch - */ - fetchError?: Error; - /** - * Statistics by fields calculated using the fetched documents - */ - fieldCounts: Record; - /** - * Histogram aggregation data - */ - histogramData?: Chart; - /** - * Number of documents found by recent fetch - */ - hits: number; - /** - * Current IndexPattern - */ - indexPattern: IndexPattern; - /** - * Value needed for legacy "infinite" loading functionality - * Determins how much records are rendered using the legacy table - * Increased when scrolling down - */ - minimumVisibleRows: number; - /** - * Function to add a column to state - */ - onAddColumn: (column: string) => void; - /** - * Function to add a filter to state - */ - onAddFilter: DocViewFilterFn; - /** - * Function to change the used time interval of the date histogram - */ - onChangeInterval: (interval: string) => void; - /** - * Function to move a given column to a given index, used in legacy table - */ - onMoveColumn: (columns: string, newIdx: number) => void; - /** - * Function to remove a given column from state - */ - onRemoveColumn: (column: string) => void; - /** - * Function to replace columns in state - */ - onSetColumns: (columns: string[]) => void; - /** - * Function to scroll down the legacy table to the bottom - */ - onSkipBottomButtonClick: () => void; - /** - * Function to change sorting of the table, triggers a fetch - */ - onSort: (sort: string[][]) => void; - opts: { - /** - * Date histogram aggregation config - */ - chartAggConfigs?: AggConfigs; - /** - * Client of uiSettings - */ - config: IUiSettingsClient; - /** - * Data plugin - */ - data: DataPublicPluginStart; - /** - * Data plugin filter manager - */ - filterManager: FilterManager; - /** - * List of available index patterns - */ - indexPatternList: Array>; - /** - * The number of documents that can be displayed in the table/grid - */ - sampleSize: number; - /** - * Current instance of SavedSearch - */ - savedSearch: SavedSearch; - /** - * Function to set the header menu - */ - setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; - /** - * Timefield of the currently used index pattern - */ - timefield: string; - /** - * Function to set the current state - */ - setAppState: (state: Partial) => void; - }; - /** - * Function to reset the current query - */ - resetQuery: () => void; - /** - * Current state of the actual query, one of 'uninitialized', 'loading' ,'ready', 'none' - */ - resultState: string; - /** - * Array of document of the recent successful search request - */ - rows: ElasticSearchHit[]; - /** - * Instance of SearchSource, the high level search API - */ - searchSource: ISearchSource; - /** - * Function to change the current index pattern - */ - setIndexPattern: (id: string) => void; - /** - * Determines whether the user should be able to use the save query feature - */ - showSaveQuery: boolean; - /** - * Current app state of URL - */ - state: AppState; - /** - * Function to update the time filter - */ - timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; - /** - * Currently selected time range - */ - timeRange?: { from: string; to: string }; - /** - * Menu data of top navigation (New, save ...) - */ - topNavMenu: TopNavMenuData[]; - /** - * Function to update the actual query - */ - updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; - /** - * Function to update the actual savedQuery id - */ - updateSavedQueryId: (savedQueryId?: string) => void; - useNewFieldsApi?: boolean; -} - -export const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( - -)); -export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( - -)); - -export function DiscoverLegacy({ - fetch, - fetchCounter, - fieldCounts, - fetchError, - histogramData, - hits, - indexPattern, - minimumVisibleRows, - onAddColumn, - onAddFilter, - onChangeInterval, - onMoveColumn, - onRemoveColumn, - onSkipBottomButtonClick, - onSort, - opts, - resetQuery, - resultState, - rows, - searchSource, - setIndexPattern, - showSaveQuery, - state, - timefilterUpdateHandler, - timeRange, - topNavMenu, - updateQuery, - updateSavedQueryId, - useNewFieldsApi, -}: DiscoverProps) { - const scrollableDesktop = useRef(null); - const collapseIcon = useRef(null); - const isMobile = () => { - // collapse icon isn't displayed in mobile view, use it to detect which view is displayed - return collapseIcon && !collapseIcon.current; - }; - - const [toggleOn, toggleChart] = useState(true); - const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const services = getServices(); - const { TopNavMenu } = services.navigation.ui; - const { trackUiMetric } = services; - const { savedSearch, indexPatternList } = opts; - const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; - const bucketInterval = - bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) - ? bucketAggConfig.buckets?.getInterval() - : undefined; - const contentCentered = resultState === 'uninitialized'; - - const getDisplayColumns = () => { - if (!state.columns) { - return []; - } - const columns = [...state.columns]; - if (useNewFieldsApi) { - return columns.filter((column) => column !== '_source'); - } - return columns.length === 0 ? ['_source'] : columns; - }; - - return ( - - - - -

- {savedSearch.title} -

- - - - - - - setIsSidebarClosed(!isSidebarClosed)} - data-test-subj="collapseSideBarButton" - aria-controls="discover-sidebar" - aria-expanded={isSidebarClosed ? 'false' : 'true'} - aria-label="Toggle sidebar" - buttonRef={collapseIcon} - /> - - - - - {resultState === 'none' && ( - - )} - {resultState === 'uninitialized' && } - {resultState === 'loading' && } - {resultState === 'ready' && ( - - - - - 0 ? hits : 0} - showResetButton={!!(savedSearch && savedSearch.id)} - onResetQuery={resetQuery} - /> - - {toggleOn && ( - - - - )} - {opts.timefield && ( - - { - toggleChart(!toggleOn); - }} - data-test-subj="discoverChartToggle" - > - {toggleOn - ? i18n.translate('discover.hideChart', { - defaultMessage: 'Hide chart', - }) - : i18n.translate('discover.showChart', { - defaultMessage: 'Show chart', - })} - - - )} - - - - {toggleOn && opts.timefield && ( - -
- {opts.chartAggConfigs && rows.length !== 0 && histogramData && ( -
- -
- )} -
-
- )} - - -
-

- -

- {rows && rows.length && ( -
- - {rows.length === opts.sampleSize ? ( -
- - - { - if (scrollableDesktop && scrollableDesktop.current) { - scrollableDesktop.current.focus(); - } - // Only the desktop one needs to target a specific container - if (!isMobile() && scrollableDesktop.current) { - scrollableDesktop.current.scrollTo(0, 0); - } else if (window) { - window.scrollTo(0, 0); - } - }} - > - - -
- ) : ( - - ​ - - )} -
- )} -
-
-
- )} -
-
-
-
-
-
- ); -} diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts new file mode 100644 index 0000000000000..fe0d40f222f23 --- /dev/null +++ b/src/plugins/discover/public/application/components/types.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +import { IUiSettingsClient, MountPoint, SavedObject } from 'kibana/public'; +import { Chart } from '../angular/helpers/point_series'; +import { IndexPattern } from '../../../../data/common/index_patterns/index_patterns'; +import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; +import { AggConfigs } from '../../../../data/common/search/aggs'; + +import { + DataPublicPluginStart, + FilterManager, + IndexPatternAttributes, + ISearchSource, + Query, + TimeRange, +} from '../../../../data/public'; +import { SavedSearch } from '../../saved_searches'; +import { AppState } from '../angular/discover_state'; +import { TopNavMenuData } from '../../../../navigation/public'; + +export interface DiscoverProps { + /** + * Function to fetch documents from Elasticsearch + */ + fetch: () => void; + /** + * Counter how often data was fetched (used for testing) + */ + fetchCounter: number; + /** + * Error in case of a failing document fetch + */ + fetchError?: Error; + /** + * Statistics by fields calculated using the fetched documents + */ + fieldCounts: Record; + /** + * Histogram aggregation data + */ + histogramData?: Chart; + /** + * Number of documents found by recent fetch + */ + hits: number; + /** + * Current IndexPattern + */ + indexPattern: IndexPattern; + /** + * Value needed for legacy "infinite" loading functionality + * Determins how much records are rendered using the legacy table + * Increased when scrolling down + */ + minimumVisibleRows: number; + /** + * Function to add a column to state + */ + onAddColumn: (column: string) => void; + /** + * Function to add a filter to state + */ + onAddFilter: DocViewFilterFn; + /** + * Function to change the used time interval of the date histogram + */ + onChangeInterval: (interval: string) => void; + /** + * Function to move a given column to a given index, used in legacy table + */ + onMoveColumn: (columns: string, newIdx: number) => void; + /** + * Function to remove a given column from state + */ + onRemoveColumn: (column: string) => void; + /** + * Function to replace columns in state + */ + onSetColumns: (columns: string[]) => void; + /** + * Function to scroll down the legacy table to the bottom + */ + onSkipBottomButtonClick: () => void; + /** + * Function to change sorting of the table, triggers a fetch + */ + onSort: (sort: string[][]) => void; + opts: { + /** + * Date histogram aggregation config + */ + chartAggConfigs?: AggConfigs; + /** + * Client of uiSettings + */ + config: IUiSettingsClient; + /** + * Data plugin + */ + data: DataPublicPluginStart; + /** + * Data plugin filter manager + */ + filterManager: FilterManager; + /** + * List of available index patterns + */ + indexPatternList: Array>; + /** + * The number of documents that can be displayed in the table/grid + */ + sampleSize: number; + /** + * Current instance of SavedSearch + */ + savedSearch: SavedSearch; + /** + * Function to set the header menu + */ + setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + /** + * Timefield of the currently used index pattern + */ + timefield: string; + /** + * Function to set the current state + */ + setAppState: (state: Partial) => void; + }; + /** + * Function to reset the current query + */ + resetQuery: () => void; + /** + * Current state of the actual query, one of 'uninitialized', 'loading' ,'ready', 'none' + */ + resultState: string; + /** + * Array of document of the recent successful search request + */ + rows: ElasticSearchHit[]; + /** + * Instance of SearchSource, the high level search API + */ + searchSource: ISearchSource; + /** + * Function to change the current index pattern + */ + setIndexPattern: (id: string) => void; + /** + * Current app state of URL + */ + state: AppState; + /** + * Function to update the time filter + */ + timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; + /** + * Currently selected time range + */ + timeRange?: { from: string; to: string }; + /** + * Menu data of top navigation (New, save ...) + */ + topNavMenu: TopNavMenuData[]; + /** + * Function to update the actual query + */ + updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + /** + * Function to update the actual savedQuery id + */ + updateSavedQueryId: (savedQueryId?: string) => void; +} diff --git a/src/plugins/discover/public/application/helpers/columns.test.ts b/src/plugins/discover/public/application/helpers/columns.test.ts new file mode 100644 index 0000000000000..d455fd1f42c6d --- /dev/null +++ b/src/plugins/discover/public/application/helpers/columns.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { getDisplayedColumns } from './columns'; +import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; + +describe('getDisplayedColumns', () => { + test('returns default columns given a index pattern without timefield', async () => { + const result = getDisplayedColumns([], indexPatternMock); + expect(result).toMatchInlineSnapshot(` + Array [ + "_source", + ] + `); + }); + test('returns default columns given a index pattern with timefield', async () => { + const result = getDisplayedColumns([], indexPatternWithTimefieldMock); + expect(result).toMatchInlineSnapshot(` + Array [ + "_source", + ] + `); + }); + test('returns default columns when just timefield is in state', async () => { + const result = getDisplayedColumns(['timestamp'], indexPatternWithTimefieldMock); + expect(result).toMatchInlineSnapshot(` + Array [ + "_source", + ] + `); + }); + test('returns columns given by argument, no fallback ', async () => { + const result = getDisplayedColumns(['test'], indexPatternWithTimefieldMock); + expect(result).toMatchInlineSnapshot(` + Array [ + "test", + ] + `); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/columns.ts b/src/plugins/discover/public/application/helpers/columns.ts new file mode 100644 index 0000000000000..d2d47c932b7bd --- /dev/null +++ b/src/plugins/discover/public/application/helpers/columns.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +import { IndexPattern } from '../../../../data/common'; + +/** + * Function to provide fallback when + * 1) no columns are given + * 2) Just one column is given, which is the configured timefields + */ +export function getDisplayedColumns(stateColumns: string[] = [], indexPattern: IndexPattern) { + return stateColumns && + stateColumns.length > 0 && + // check if all columns where removed except the configured timeField (this can't be removed) + !(stateColumns.length === 1 && stateColumns[0] === indexPattern.timeFieldName) + ? stateColumns + : ['_source']; +} diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index b27426a6c0621..4eda742d967f4 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -42,7 +42,6 @@ import { } from '../../kibana_legacy/public'; import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; -import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive'; import { createDiscoverDirective } from './application/components/create_discover_directive'; /** @@ -124,7 +123,6 @@ export function initializeInnerAngularModule( .config(watchMultiDecorator) .run(registerListenEventListener) .directive('renderComplete', createRenderCompleteDirective) - .directive('discoverLegacy', createDiscoverLegacyDirective) .directive('discover', createDiscoverDirective); } diff --git a/test/functional/apps/discover/_data_grid.ts b/test/functional/apps/discover/_data_grid.ts index 3bac05c5b18fc..1329e7657ad9c 100644 --- a/test/functional/apps/discover/_data_grid.ts +++ b/test/functional/apps/discover/_data_grid.ts @@ -38,7 +38,7 @@ export default function ({ const getTitles = async () => (await testSubjects.getVisibleText('dataGridHeader')).replace(/\s|\r?\n|\r/g, ' '); - expect(await getTitles()).to.be('Time (@timestamp) _source'); + expect(await getTitles()).to.be('Time (@timestamp) Document'); await PageObjects.discover.clickFieldListItemAdd('bytes'); expect(await getTitles()).to.be('Time (@timestamp) bytes'); @@ -50,7 +50,7 @@ export default function ({ expect(await getTitles()).to.be('Time (@timestamp) agent'); await PageObjects.discover.clickFieldListItemAdd('agent'); - expect(await getTitles()).to.be('Time (@timestamp) _source'); + expect(await getTitles()).to.be('Time (@timestamp) Document'); }); }); } diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index bdbaacc33c1bd..3eec84ad3d7c0 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -57,7 +57,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('doc view should show Time and _source columns', async function () { - const expectedHeader = 'Time (@timestamp) _source'; + const expectedHeader = 'Time (@timestamp) Document'; const DocHeader = await dataGrid.getHeaderFields(); expect(DocHeader.join(' ')).to.be(expectedHeader); }); From d931ed61e4c989f44b1340f88848a2e4828f9727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Wed, 27 Jan 2021 17:59:44 +0100 Subject: [PATCH 044/163] [ILM] Policy phases redesign (#88671) * Phases redesign * Title and name field layout * Active highlight wip * Copy comments * Updated data allocation dropdown * Min age error message * Fixed tests * Fixed edit policy integration tests * Fixed more tests * Cleaned up test files * Use hotProperty instead of a string * Clean up in phase component * Fixed i18n files * Updated optional fields * Updated aria attributes after running axe tests * Added review suggestions * Reversed data allocation field changes * Fixed type error * Reversed on/off label and prepend input label * Deleted property consts from phases components * Removed not needed i18n consts and added i18n where missing * Fixed merge conflicts with master Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../client_integration/app/app.test.ts | 39 +- .../edit_policy/edit_policy.helpers.tsx | 46 +- .../edit_policy/edit_policy.test.ts | 38 +- .../__jest__/components/edit_policy.test.tsx | 14 +- .../public/application/constants/policy.ts | 27 +- .../active_highlight/active_highlight.scss | 16 + .../active_highlight/active_highlight.tsx | 17 + .../index.ts | 2 +- .../sections/edit_policy/components/index.ts | 1 + .../phases/cold_phase/cold_phase.tsx | 199 ++------ .../phases/delete_phase/delete_phase.tsx | 4 +- .../components/phases/hot_phase/hot_phase.tsx | 426 ++++++++---------- .../edit_policy/components/phases/index.ts | 2 + .../edit_policy/components/phases/phase.tsx | 119 +++++ .../phases/shared_fields/forcemerge_field.tsx | 58 +-- .../components/phases/shared_fields/index.ts | 8 +- ...put_field.tsx => index_priority_field.tsx} | 49 +- .../shared_fields/min_age_field/index.ts | 7 + .../min_age_field/min_age_field.tsx | 158 +++++++ .../util.ts | 0 .../min_age_input_field.tsx | 205 --------- .../phases/shared_fields/replicas_field.tsx | 59 +++ .../phases/shared_fields/shrink_field.tsx | 36 +- .../phases/warm_phase/warm_phase.tsx | 170 +------ .../sections/edit_policy/edit_policy.tsx | 362 +++++++-------- .../form/deserializer_and_serializer.test.ts | 9 - .../sections/edit_policy/form/schema.ts | 57 ++- .../edit_policy/form/serializer/serializer.ts | 28 +- .../sections/edit_policy/i18n_texts.ts | 29 +- .../lib/absolute_timing_to_relative_timing.ts | 7 +- .../application/services/ui_metric.test.ts | 10 +- .../public/application/services/ui_metric.ts | 9 +- .../public/shared_imports.ts | 1 + .../translations/translations/ja-JP.json | 33 -- .../translations/translations/zh-CN.json | 33 -- 35 files changed, 1030 insertions(+), 1248 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{phases/shared_fields/min_age_input_field => active_highlight}/index.ts (80%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/{set_priority_input_field.tsx => index_priority_field.tsx} (50%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/{min_age_input_field => min_age_field}/util.ts (100%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/min_age_input_field.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts index 1ffbae39d3705..1b49416ebbbe9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts @@ -21,6 +21,9 @@ const PERCENT_SIGN_NAME = 'test%'; const PERCENT_SIGN_WITH_OTHER_CHARS_NAME = 'test%#'; const PERCENT_SIGN_25_SEQUENCE = 'test%25'; +const createPolicyTitle = 'Create Policy'; +const editPolicyTitle = 'Edit Policy'; + window.scrollTo = jest.fn(); jest.mock('@elastic/eui', () => { @@ -52,7 +55,7 @@ describe('', () => { await actions.clickCreatePolicyButton(); component.update(); - expect(testBed.find('policyTitle').text()).toBe(`Create an index lifecycle policy`); + expect(testBed.find('policyTitle').text()).toBe(createPolicyTitle); expect(testBed.find('policyNameField').props().value).toBe(''); }); @@ -68,7 +71,7 @@ describe('', () => { await actions.clickCreatePolicyButton(); component.update(); - expect(testBed.find('policyTitle').text()).toBe(`Create an index lifecycle policy`); + expect(testBed.find('policyTitle').text()).toBe(createPolicyTitle); expect(testBed.find('policyNameField').props().value).toBe(''); }); }); @@ -89,9 +92,7 @@ describe('', () => { await actions.clickPolicyNameLink(); component.update(); - expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${SPECIAL_CHARS_NAME}` - ); + expect(testBed.find('policyTitle').text()).toBe(`${editPolicyTitle} ${SPECIAL_CHARS_NAME}`); }); test('loading edit policy page url works', async () => { @@ -102,9 +103,7 @@ describe('', () => { const { component } = testBed; component.update(); - expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${SPECIAL_CHARS_NAME}` - ); + expect(testBed.find('policyTitle').text()).toBe(`${editPolicyTitle} ${SPECIAL_CHARS_NAME}`); }); // using double encoding to counteract react-router's v5 internal decodeURI call @@ -117,9 +116,7 @@ describe('', () => { const { component } = testBed; component.update(); - expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${SPECIAL_CHARS_NAME}` - ); + expect(testBed.find('policyTitle').text()).toBe(`${editPolicyTitle} ${SPECIAL_CHARS_NAME}`); }); }); @@ -136,9 +133,7 @@ describe('', () => { const { component } = testBed; component.update(); - expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${PERCENT_SIGN_NAME}` - ); + expect(testBed.find('policyTitle').text()).toBe(`${editPolicyTitle} ${PERCENT_SIGN_NAME}`); }); test('loading edit policy page url with double encoding works', async () => { @@ -149,9 +144,7 @@ describe('', () => { const { component } = testBed; component.update(); - expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${PERCENT_SIGN_NAME}` - ); + expect(testBed.find('policyTitle').text()).toBe(`${editPolicyTitle} ${PERCENT_SIGN_NAME}`); }); }); @@ -174,7 +167,7 @@ describe('', () => { component.update(); expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${PERCENT_SIGN_WITH_OTHER_CHARS_NAME}` + `${editPolicyTitle} ${PERCENT_SIGN_WITH_OTHER_CHARS_NAME}` ); }); @@ -188,7 +181,7 @@ describe('', () => { // known issue https://github.com/elastic/kibana/issues/82440 expect(testBed.find('policyTitle').text()).not.toBe( - `Edit index lifecycle policy ${PERCENT_SIGN_WITH_OTHER_CHARS_NAME}` + `${editPolicyTitle} ${PERCENT_SIGN_WITH_OTHER_CHARS_NAME}` ); }); @@ -203,7 +196,7 @@ describe('', () => { component.update(); expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${PERCENT_SIGN_WITH_OTHER_CHARS_NAME}` + `${editPolicyTitle} ${PERCENT_SIGN_WITH_OTHER_CHARS_NAME}` ); }); }); @@ -225,7 +218,7 @@ describe('', () => { component.update(); expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${PERCENT_SIGN_25_SEQUENCE}` + `${editPolicyTitle} ${PERCENT_SIGN_25_SEQUENCE}` ); }); @@ -239,7 +232,7 @@ describe('', () => { // known issue https://github.com/elastic/kibana/issues/82440 expect(testBed.find('policyTitle').text()).not.toBe( - `Edit index lifecycle policy ${PERCENT_SIGN_25_SEQUENCE}` + `${editPolicyTitle} ${PERCENT_SIGN_25_SEQUENCE}` ); }); @@ -254,7 +247,7 @@ describe('', () => { component.update(); expect(testBed.find('policyTitle').text()).toBe( - `Edit index lifecycle policy ${PERCENT_SIGN_25_SEQUENCE}` + `${editPolicyTitle} ${PERCENT_SIGN_25_SEQUENCE}` ); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 72a0372628a22..64b654b030236 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -89,6 +89,13 @@ export const setup = async (arg?: { appServicesContext: Partial async (checked: boolean) => { + await act(async () => { + form.selectCheckBox(dataTestSubject, checked); + }); + component.update(); + }; + function createFormSetValueAction(dataTestSubject: string) { return async (value: V) => { await act(async () => { @@ -146,17 +153,21 @@ export const setup = async (arg?: { appServicesContext: Partial exists(toggleSelector), toggleForceMerge: createFormToggleAction(toggleSelector), setForcemergeSegmentsCount: createFormSetValueAction(`${phase}-selectedForceMergeSegments`), - setBestCompression: createFormToggleAction(`${phase}-bestCompression`), + setBestCompression: createFormCheckboxAction(`${phase}-bestCompression`), }; }; - const setIndexPriority = (phase: Phases) => - createFormSetValueAction(`${phase}-phaseIndexPriority`); + const createIndexPriorityActions = (phase: Phases) => { + const toggleSelector = `${phase}-indexPrioritySwitch`; + return { + indexPriorityExists: () => exists(toggleSelector), + toggleIndexPriority: createFormToggleAction(toggleSelector), + setIndexPriority: createFormSetValueAction(`${phase}-indexPriority`), + }; + }; const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`); - const warmPhaseOnRollover = createFormToggleAction(`warm-warmPhaseOnRollover`); - const setMinAgeValue = (phase: Phases) => createFormSetValueAction(`${phase}-selectedMinimumAge`); const setMinAgeUnits = (phase: Phases) => @@ -190,13 +201,15 @@ export const setup = async (arg?: { appServicesContext: Partial async (value: string) => { - await createFormToggleAction(`${phase}-shrinkSwitch`)(true); - await createFormSetValueAction(`${phase}-selectedPrimaryShardCount`)(value); + const createShrinkActions = (phase: Phases) => { + const toggleSelector = `${phase}-shrinkSwitch`; + return { + shrinkExists: () => exists(toggleSelector), + toggleShrink: createFormToggleAction(toggleSelector), + setShrink: createFormSetValueAction(`${phase}-primaryShardCount`), + }; }; - const shrinkExists = (phase: Phases) => () => exists(`${phase}-shrinkSwitch`); - const setFreeze = createFormToggleAction('freezeSwitch'); const freezeExists = () => exists('freezeSwitch'); @@ -250,25 +263,22 @@ export const setup = async (arg?: { appServicesContext: Partial', () => { max_size: '50gb', unknown_setting: 123, // Made up setting that should stay preserved }, - set_priority: { - priority: 100, - }, }, min_age: '0ms', }, @@ -126,8 +123,10 @@ describe('', () => { await actions.hot.toggleForceMerge(true); await actions.hot.setForcemergeSegmentsCount('123'); await actions.hot.setBestCompression(true); + await actions.hot.toggleShrink(true); await actions.hot.setShrink('2'); await actions.hot.setReadonly(true); + await actions.hot.toggleIndexPriority(true); await actions.hot.setIndexPriority('123'); await actions.savePolicy(); @@ -186,13 +185,7 @@ describe('', () => { const hotActions = policy.phases.hot.actions; const rolloverAction = hotActions.rollover; expect(rolloverAction).toBe(undefined); - expect(hotActions).toMatchInlineSnapshot(` - Object { - "set_priority": Object { - "priority": 100, - }, - } - `); + expect(hotActions).toMatchInlineSnapshot(`Object {}`); }); test('enabling searchable snapshot should hide force merge, freeze and shrink in subsequent phases', async () => { @@ -260,6 +253,7 @@ describe('', () => { "priority": 50, }, }, + "min_age": "0ms", } `); }); @@ -270,6 +264,7 @@ describe('', () => { await actions.warm.setDataAllocation('node_attrs'); await actions.warm.setSelectedNodeAttribute('test:123'); await actions.warm.setReplicas('123'); + await actions.warm.toggleShrink(true); await actions.warm.setShrink('123'); await actions.warm.toggleForceMerge(true); await actions.warm.setForcemergeSegmentsCount('123'); @@ -290,9 +285,6 @@ describe('', () => { "max_age": "30d", "max_size": "50gb", }, - "set_priority": Object { - "priority": 100, - }, }, "min_age": "0ms", }, @@ -316,24 +308,12 @@ describe('', () => { "number_of_shards": 123, }, }, + "min_age": "0ms", }, }, } `); }); - - test('setting warm phase on rollover to "false"', async () => { - const { actions } = testBed; - await actions.warm.enable(true); - await actions.warm.warmPhaseOnRollover(false); - await actions.warm.setMinAgeValue('123'); - await actions.warm.setMinAgeUnits('d'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const warmPhaseMinAge = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm - .min_age; - expect(warmPhaseMinAge).toBe('123d'); - }); }); describe('policy with include and exclude', () => { @@ -458,9 +438,6 @@ describe('', () => { "max_age": "30d", "max_size": "50gb", }, - "set_priority": Object { - "priority": 100, - }, }, "min_age": "0ms", }, @@ -662,9 +639,6 @@ describe('', () => { "allocate": Object { "number_of_replicas": 123, }, - "set_priority": Object { - "priority": 50, - }, } `); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index 6aa6c3177ca5d..d847c3a7f9766 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -157,7 +157,7 @@ const setPhaseIndexPriority = async ( phase: string, priority: string | number ) => { - const priorityInput = findTestSubject(rendered, `${phase}-phaseIndexPriority`); + const priorityInput = findTestSubject(rendered, `${phase}-indexPriority`); await act(async () => { priorityInput.simulate('change', { target: { value: priority } }); }); @@ -324,9 +324,6 @@ describe('edit policy', () => { max_age: '30d', max_size: '50gb', }, - set_priority: { - priority: 100, - }, }, min_age: '0ms', }, @@ -451,6 +448,7 @@ describe('edit policy', () => { const rendered = mountWithIntl(component); await noRollover(rendered); await setPolicyName(rendered, 'mypolicy'); + await setPhaseIndexPriority(rendered, 'hot', '-1'); waitForFormLibValidation(rendered); expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); @@ -512,7 +510,7 @@ describe('edit policy', () => { }); rendered.update(); await setPhaseAfter(rendered, 'warm', '1'); - const shrinkInput = findTestSubject(rendered, 'warm-selectedPrimaryShardCount'); + const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); await act(async () => { shrinkInput.simulate('change', { target: { value: '0' } }); }); @@ -529,7 +527,7 @@ describe('edit policy', () => { findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); }); rendered.update(); - const shrinkInput = findTestSubject(rendered, 'warm-selectedPrimaryShardCount'); + const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); await act(async () => { shrinkInput.simulate('change', { target: { value: '-1' } }); }); @@ -845,7 +843,7 @@ describe('edit policy', () => { await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - // Assert that only the custom and off options exist + // Assert that default, custom and 'none' options exist findTestSubject(rendered, 'dataTierSelect').simulate('click'); expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); @@ -885,7 +883,7 @@ describe('edit policy', () => { await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - // Assert that only the custom and off options exist + // Assert that default, custom and 'none' options exist findTestSubject(rendered, 'dataTierSelect').simulate('click'); expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeFalsy(); expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index a892a7a031a87..6eae59ec4e6ea 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -4,16 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - SerializedPhase, - DeletePhase, - SerializedPolicy, - RolloverAction, -} from '../../../common/types'; +import { SerializedPolicy, RolloverAction } from '../../../common/types'; -export const defaultSetPriority: string = '100'; - -export const defaultPhaseIndexPriority: string = '50'; +export const defaultIndexPriority = { + hot: '100', + warm: '50', + cold: '0', +}; export const defaultRolloverAction: RolloverAction = { max_age: '30d', @@ -30,15 +27,3 @@ export const defaultPolicy: SerializedPolicy = { }, }, }; - -export const defaultNewDeletePhase: DeletePhase = { - phaseEnabled: false, - selectedMinimumAge: '0', - selectedMinimumAgeUnits: 'd', - waitForSnapshotPolicy: '', -}; - -export const serializedPhaseInitialization: SerializedPhase = { - min_age: '0ms', - actions: {}, -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss new file mode 100644 index 0000000000000..96ca0c3a61067 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss @@ -0,0 +1,16 @@ +.ilmActivePhaseHighlight { + border-left: $euiBorderWidthThin solid $euiColorLightShade; + height: 100%; + + &.hotPhase.active { + border-left-color: $euiColorVis9_behindText; + } + + &.warmPhase.active { + border-left-color: $euiColorVis5_behindText; + } + + &.coldPhase.active { + border-left-color: $euiColorVis1_behindText; + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx new file mode 100644 index 0000000000000..64db9e1ec5481 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +import './active_highlight.scss'; + +interface Props { + phase: 'hot' | 'warm' | 'cold'; + enabled: boolean; +} +export const ActiveHighlight: FunctionComponent = ({ phase, enabled }) => { + return
; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts similarity index 80% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts index 0228a524f8129..a1db0c3997edb 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MinAgeInputField } from './min_age_input_field'; +export { ActiveHighlight } from './active_highlight'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index d22206d7ae4de..960b632d70bd4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -11,6 +11,7 @@ export { OptionalLabel } from './optional_label'; export { PolicyJsonFlyout } from './policy_json_flyout'; export { DescribedFormRow, ToggleFieldWithDescribedFormRow } from './described_form_row'; export { FieldLoadingError } from './field_loading_error'; +export { ActiveHighlight } from './active_highlight'; export { Timeline } from './timeline'; export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 4e1ec76c52a77..976f584ef4d3a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -9,28 +9,21 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { EuiDescribedFormGroup, EuiTextColor, EuiAccordion } from '@elastic/eui'; +import { EuiTextColor } from '@elastic/eui'; -import { Phases } from '../../../../../../../common/types'; +import { useFormData } from '../../../../../../shared_imports'; -import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports'; - -import { useEditPolicyContext } from '../../../edit_policy_context'; import { useConfigurationIssues } from '../../../form'; -import { - LearnMoreLink, - ActiveBadge, - DescribedFormRow, - ToggleFieldWithDescribedFormRow, -} from '../../'; +import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../'; import { - MinAgeInputField, DataTierAllocationField, - SetPriorityInputField, SearchableSnapshotField, + IndexPriorityField, + ReplicasField, } from '../shared_fields'; +import { Phase } from '../phase'; const i18nTexts = { dataTierAllocation: { @@ -41,166 +34,64 @@ const i18nTexts = { }, }; -const coldProperty: keyof Phases = 'cold'; - const formFieldPaths = { enabled: '_meta.cold.enabled', searchableSnapshot: 'phases.cold.actions.searchable_snapshot.snapshot_repository', }; export const ColdPhase: FunctionComponent = () => { - const { policy } = useEditPolicyContext(); const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); const [formData] = useFormData({ - watch: [formFieldPaths.enabled, formFieldPaths.searchableSnapshot], + watch: [formFieldPaths.searchableSnapshot], }); - const enabled = get(formData, formFieldPaths.enabled); const showReplicasField = get(formData, formFieldPaths.searchableSnapshot) == null; return ( -
- <> - {/* Section title group; containing min age */} - + + + {showReplicasField && } + + {/* Freeze section */} + {!isUsingSearchableSnapshotInHotPhase && ( + -

- -

{' '} - {enabled && } -
+

+ +

} - titleSize="s" description={ - <> -

- -

- - + + {' '} + + } fullWidth + titleSize="xs" + switchProps={{ + 'data-test-subj': 'freezeSwitch', + path: '_meta.cold.freezeEnabled', + }} > - {enabled && } - - {enabled && ( - <> - - - - { - /* Replicas section */ - showReplicasField && ( - - {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { - defaultMessage: 'Replicas', - })} - - } - description={i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', - { - defaultMessage: - 'Set the number of replicas. Remains the same as the previous phase by default.', - } - )} - switchProps={{ - 'data-test-subj': 'cold-setReplicasSwitch', - label: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', - { defaultMessage: 'Set replicas' } - ), - initialValue: - policy.phases.cold?.actions?.allocate?.number_of_replicas != null, - }} - fullWidth - > - - - ) - } - - {/* Freeze section */} - {!isUsingSearchableSnapshotInHotPhase && ( - - - - } - description={ - - {' '} - - - } - fullWidth - titleSize="xs" - switchProps={{ - 'data-test-subj': 'freezeSwitch', - path: '_meta.cold.freezeEnabled', - }} - > -
- - )} - {/* Data tier allocation section */} - - - - - )} - -
+
+ + )} + + {/* Data tier allocation section */} + + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx index 37323b97edc92..5c43bb413eb5e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx @@ -13,7 +13,7 @@ import { useFormData, UseField, ToggleField } from '../../../../../../shared_imp import { ActiveBadge, LearnMoreLink, OptionalLabel } from '../../index'; -import { MinAgeInputField, SnapshotPoliciesField } from '../shared_fields'; +import { MinAgeField, SnapshotPoliciesField } from '../shared_fields'; const formFieldPaths = { enabled: '_meta.delete.enabled', @@ -63,7 +63,7 @@ export const DeletePhase: FunctionComponent = () => { } fullWidth > - {enabled && } + {enabled && } {enabled ? ( { const { license } = useEditPolicyContext(); const [formData] = useFormData({ @@ -56,245 +51,210 @@ export const HotPhase: FunctionComponent = () => { const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); return ( - <> - + -

- -

{' '} - -
+

+ {i18n.translate('xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle', { + defaultMessage: 'Rollover', + })} +

} description={ -

- -

+ <> + +

+ {' '} + + } + docPath="ilm-rollover.html" + /> +

+
+ + path={isUsingDefaultRolloverPath}> + {(field) => ( + <> + field.setValue(e.target.checked)} + data-test-subj="useDefaultRolloverSwitch" + /> +   + + } + /> + + )} +
+ } + fullWidth > -
- - - - - {i18n.translate('xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle', { - defaultMessage: 'Rollover', - })} - - } - description={ - <> - -

- {' '} - + path="_meta.hot.customRollover.enabled"> + {(field) => ( + <> + field.setValue(e.target.checked)} + data-test-subj="rolloverSwitch" + /> +   + } - docPath="ilm-rollover.html" /> -

-
- - path={isUsingDefaultRolloverPath}> - {(field) => ( + + )} + + {isUsingRollover && ( + <> + + {showEmptyRolloverFieldsError && ( <> - field.setValue(e.target.checked)} - data-test-subj="useDefaultRolloverSwitch" - /> -   - - } - /> + +
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
+
+ )} - - - } - fullWidth - > - {isUsingDefaultRollover === false ? ( -
- path="_meta.hot.customRollover.enabled"> - {(field) => ( - <> - field.setValue(e.target.checked)} - data-test-subj="rolloverSwitch" + + + + {(field) => { + const showErrorCallout = field.errors.some( + (e) => e.code === ROLLOVER_EMPTY_VALIDATION + ); + if (showErrorCallout !== showEmptyRolloverFieldsError) { + setShowEmptyRolloverFieldsError(showErrorCallout); + } + return ( + + ); + }} + + + + -   - - } + + + + + + - - )} - - {isUsingRollover && ( - <> - - {showEmptyRolloverFieldsError && ( - <> - -
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
-
- - - )} - - - - {(field) => { - const showErrorCallout = field.errors.some( - (e) => e.code === ROLLOVER_EMPTY_VALIDATION - ); - if (showErrorCallout !== showEmptyRolloverFieldsError) { - setShowEmptyRolloverFieldsError(showErrorCallout); - } - return ( - - ); - }} - - - - - - - - - - - - - - - - - - - - - - - )} -
- ) : ( -
- )} - - {isUsingRollover && ( - <> - {} - - {license.canUseSearchableSnapshot() && } - - + + + + + + + + + + + + + )} +
+ ) : ( +
)} - - - + + {isUsingRollover && ( + <> + {} + + {license.canUseSearchableSnapshot() && } + + + )} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts index 076c16e87e8d6..c2c7e6a769071 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts @@ -11,3 +11,5 @@ export { WarmPhase } from './warm_phase'; export { ColdPhase } from './cold_phase'; export { DeletePhase } from './delete_phase'; + +export { Phase } from './phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase.tsx new file mode 100644 index 0000000000000..6de18f1c1d3cb --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase.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, { FunctionComponent, useState } from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiButtonEmpty, +} from '@elastic/eui'; +import { get } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { ToggleField, UseField, useFormData } from '../../../../../shared_imports'; +import { i18nTexts } from '../../i18n_texts'; + +import { ActiveHighlight } from '../active_highlight'; +import { MinAgeField } from './shared_fields'; + +interface Props { + phase: 'hot' | 'warm' | 'cold'; +} + +export const Phase: FunctionComponent = ({ children, phase }) => { + const enabledPath = `_meta.${phase}.enabled`; + const [formData] = useFormData({ + watch: [enabledPath], + }); + + // hot phase is always enabled + const enabled = get(formData, enabledPath) || phase === 'hot'; + + const [isShowingSettings, setShowingSettings] = useState(false); + return ( + + + + + + + + + + {phase !== 'hot' && ( + + + + )} + + +

{i18nTexts.editPolicy.titles[phase]}

+
+
+
+
+ {enabled && ( + + + + {phase !== 'hot' && } + + + { + setShowingSettings(!isShowingSettings); + }} + size="xs" + iconType="controlsVertical" + iconSide="left" + aria-controls={`${phase}-phaseContent`} + > + + + + + + )} +
+ + + {i18nTexts.editPolicy.descriptions[phase]} + + + {enabled && ( +
+ + {children} +
+ )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index 8776dbbbc7553..8d6807c90dae8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -6,9 +6,8 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiTextColor } from '@elastic/eui'; -import { UseField, ToggleField, NumericField } from '../../../../../../shared_imports'; +import { UseField, CheckBoxField, NumericField } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; @@ -38,50 +37,43 @@ export const ForcemergeField: React.FunctionComponent = ({ phase }) => { } description={ - + <> {' '} - + } titleSize="xs" fullWidth switchProps={{ - 'aria-label': i18nTexts.editPolicy.forceMergeEnabledFieldLabel, - 'data-test-subj': `${phase}-forceMergeSwitch`, - 'aria-controls': 'forcemergeContent', label: i18nTexts.editPolicy.forceMergeEnabledFieldLabel, + 'data-test-subj': `${phase}-forceMergeSwitch`, initialValue: initialToggleValue, }} > - -
- - -
+ + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts index 15167672265fd..710df7e95f8fc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts @@ -8,9 +8,7 @@ export { DataTierAllocationField } from './data_tier_allocation_field'; export { ForcemergeField } from './forcemerge_field'; -export { SetPriorityInputField } from './set_priority_input_field'; - -export { MinAgeInputField } from './min_age_input_field'; +export { MinAgeField } from './min_age_field'; export { SnapshotPoliciesField } from './snapshot_policies_field'; @@ -19,3 +17,7 @@ export { ShrinkField } from './shrink_field'; export { SearchableSnapshotField } from './searchable_snapshot_field'; export { ReadonlyField } from './readonly_field'; + +export { ReplicasField } from './replicas_field'; + +export { IndexPriorityField } from './index_priority_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx similarity index 50% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx index 328587a379b76..570033812c247 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx @@ -4,23 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; - -import { Phases } from '../../../../../../../common/types'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiTextColor } from '@elastic/eui'; import { UseField, NumericField } from '../../../../../../shared_imports'; - -import { LearnMoreLink } from '../..'; +import { LearnMoreLink, DescribedFormRow } from '../..'; +import { useEditPolicyContext } from '../../../edit_policy_context'; interface Props { - phase: keyof Phases & string; + phase: 'hot' | 'warm' | 'cold'; } -export const SetPriorityInputField: FunctionComponent = ({ phase }) => { +export const IndexPriorityField: FunctionComponent = ({ phase }) => { + const { policy, isNewPolicy } = useEditPolicyContext(); + + const initialToggleValue = useMemo(() => { + return ( + isNewPolicy || // enable index priority for new policies + !policy.phases[phase]?.actions || // enable index priority for new phases + policy.phases[phase]?.actions?.set_priority != null // enable index priority if it's set + ); + }, [isNewPolicy, policy.phases, phase]); + return ( - = ({ phase }) => { } titleSize="xs" fullWidth + switchProps={{ + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.indexPriority.indexPriorityEnabledFieldLabel', + { + defaultMessage: 'Set index priority', + } + ), + 'data-test-subj': `${phase}-indexPrioritySwitch`, + initialValue: initialToggleValue, + }} > + - + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/index.ts new file mode 100644 index 0000000000000..43ef0cf5d9b71 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/index.ts @@ -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 { MinAgeField } from './min_age_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx new file mode 100644 index 0000000000000..8a84b7fa0e762 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx @@ -0,0 +1,158 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiFieldNumber, + EuiFieldNumberProps, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiText, +} from '@elastic/eui'; + +import { UseField, getFieldValidityAndErrorMessage } from '../../../../../../../shared_imports'; + +import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util'; + +type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete'; + +const i18nTexts = { + daysOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.daysOptionLabel', { + defaultMessage: 'days', + }), + + hoursOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hoursOptionLabel', { + defaultMessage: 'hours', + }), + minutesOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.minutesOptionLabel', { + defaultMessage: 'minutes', + }), + + secondsOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.secondsOptionLabel', { + defaultMessage: 'seconds', + }), + millisecondsOptionLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.milliSecondsOptionLabel', + { + defaultMessage: 'milliseconds', + } + ), + + microsecondsOptionLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.microSecondsOptionLabel', + { + defaultMessage: 'microseconds', + } + ), + + nanosecondsOptionLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.nanoSecondsOptionLabel', + { + defaultMessage: 'nanoseconds', + } + ), +}; + +interface Props { + phase: PhaseWithMinAgeAction; +} + +export const MinAgeField: FunctionComponent = ({ phase }): React.ReactElement => { + return ( + + {(field) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + return ( + + + + + + + + + + + + + + + {(unitField) => { + const { isInvalid: isUnitFieldInvalid } = getFieldValidityAndErrorMessage( + unitField + ); + return ( + { + unitField.setValue(e.target.value); + }} + isInvalid={isUnitFieldInvalid} + append={'old'} + data-test-subj={`${phase}-selectedMinimumAgeUnits`} + aria-label={getUnitsAriaLabelForPhase(phase)} + options={[ + { + value: 'd', + text: i18nTexts.daysOptionLabel, + }, + { + value: 'h', + text: i18nTexts.hoursOptionLabel, + }, + { + value: 'm', + text: i18nTexts.minutesOptionLabel, + }, + { + value: 's', + text: i18nTexts.secondsOptionLabel, + }, + { + value: 'ms', + text: i18nTexts.millisecondsOptionLabel, + }, + { + value: 'micros', + text: i18nTexts.microsecondsOptionLabel, + }, + { + value: 'nanos', + text: i18nTexts.nanosecondsOptionLabel, + }, + ]} + /> + ); + }} + + + + + + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/util.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/util.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/min_age_input_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/min_age_input_field.tsx deleted file mode 100644 index 59086ce572252..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/min_age_input_field.tsx +++ /dev/null @@ -1,205 +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, { FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -import { UseField, NumericField, SelectField } from '../../../../../../../shared_imports'; - -import { LearnMoreLink } from '../../../learn_more_link'; -import { useConfigurationIssues } from '../../../../form'; - -import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util'; - -type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete'; - -interface Props { - phase: PhaseWithMinAgeAction; -} - -export const MinAgeInputField: FunctionComponent = ({ phase }): React.ReactElement => { - const { isUsingRollover: rolloverEnabled } = useConfigurationIssues(); - - let daysOptionLabel; - let hoursOptionLabel; - let minutesOptionLabel; - let secondsOptionLabel; - let millisecondsOptionLabel; - let microsecondsOptionLabel; - let nanosecondsOptionLabel; - - if (rolloverEnabled) { - daysOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel', - { - defaultMessage: 'days from rollover', - } - ); - - hoursOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel', - { - defaultMessage: 'hours from rollover', - } - ); - minutesOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.rolloverMinutesOptionLabel', - { - defaultMessage: 'minutes from rollover', - } - ); - - secondsOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.rolloverSecondsOptionLabel', - { - defaultMessage: 'seconds from rollover', - } - ); - millisecondsOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.rolloverMilliSecondsOptionLabel', - { - defaultMessage: 'milliseconds from rollover', - } - ); - - microsecondsOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel', - { - defaultMessage: 'microseconds from rollover', - } - ); - - nanosecondsOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.rolloverNanoSecondsOptionLabel', - { - defaultMessage: 'nanoseconds from rollover', - } - ); - } else { - daysOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel', - { - defaultMessage: 'days from index creation', - } - ); - - hoursOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.creationHoursOptionLabel', - { - defaultMessage: 'hours from index creation', - } - ); - - minutesOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel', - { - defaultMessage: 'minutes from index creation', - } - ); - - secondsOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel', - { - defaultMessage: 'seconds from index creation', - } - ); - - millisecondsOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel', - { - defaultMessage: 'milliseconds from index creation', - } - ); - - microsecondsOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel', - { - defaultMessage: 'microseconds from index creation', - } - ); - - nanosecondsOptionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel', - { - defaultMessage: 'nanoseconds from index creation', - } - ); - } - - return ( - - - - } - /> - ), - euiFieldProps: { - 'data-test-subj': `${phase}-selectedMinimumAge`, - min: 0, - }, - }} - /> - - - - - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx new file mode 100644 index 0000000000000..6d8e019ff8a0c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx @@ -0,0 +1,59 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { UseField, NumericField } from '../../../../../../shared_imports'; +import { useEditPolicyContext } from '../../../edit_policy_context'; +import { DescribedFormRow } from '../../described_form_row'; + +interface Props { + phase: 'warm' | 'cold'; +} + +export const ReplicasField: FunctionComponent = ({ phase }) => { + const { policy } = useEditPolicyContext(); + const initialValue = policy.phases[phase]?.actions?.allocate?.number_of_replicas != null; + return ( + + {i18n.translate('xpack.indexLifecycleMgmt.numberOfReplicas.formRowTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.numberOfReplicas.formRowDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + 'data-test-subj': `${phase}-setReplicasSwitch`, + label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.numberOfReplicas.switchLabel', { + defaultMessage: 'Set replicas', + }), + initialValue, + }} + fullWidth + > + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx index c5fc31d9839bd..da200e9e68d17 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx @@ -42,32 +42,28 @@ export const ShrinkField: FunctionComponent = ({ phase }) => { } titleSize="xs" switchProps={{ - 'aria-controls': 'shrinkContent', 'data-test-subj': `${phase}-shrinkSwitch`, label: i18nTexts.editPolicy.shrinkLabel, - 'aria-label': i18nTexts.editPolicy.shrinkLabel, initialValue: Boolean(policy.phases[phase]?.actions?.shrink), }} fullWidth > -
- - - - - - -
+ + + + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx index 77078e94d7e98..47255da08df72 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -5,30 +5,21 @@ */ import React, { FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { EuiSpacer, EuiDescribedFormGroup, EuiAccordion } from '@elastic/eui'; - -import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports'; - -import { Phases } from '../../../../../../../common/types'; - -import { useEditPolicyContext } from '../../../edit_policy_context'; import { useConfigurationIssues } from '../../../form'; -import { ActiveBadge, DescribedFormRow } from '../../'; - import { - MinAgeInputField, ForcemergeField, - SetPriorityInputField, + IndexPriorityField, DataTierAllocationField, ShrinkField, ReadonlyField, + ReplicasField, } from '../shared_fields'; +import { Phase } from '../phase'; + const i18nTexts = { dataTierAllocation: { description: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.description', { @@ -37,153 +28,26 @@ const i18nTexts = { }, }; -const warmProperty: keyof Phases = 'warm'; - -const formFieldPaths = { - enabled: '_meta.warm.enabled', - warmPhaseOnRollover: '_meta.warm.warmPhaseOnRollover', -}; - export const WarmPhase: FunctionComponent = () => { - const { policy } = useEditPolicyContext(); - const { isUsingSearchableSnapshotInHotPhase, isUsingRollover } = useConfigurationIssues(); - const [formData] = useFormData({ - watch: [formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover], - }); - - const enabled = get(formData, formFieldPaths.enabled); - const warmPhaseOnRollover = get(formData, formFieldPaths.warmPhaseOnRollover); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); return ( -
- <> - -

- -

{' '} - {enabled && } -
- } - titleSize="s" - description={ - <> -

- -

- - - } - fullWidth - > - <> - {enabled && ( - <> - {isUsingRollover && ( - - )} - {(!warmPhaseOnRollover || !isUsingRollover) && ( - <> - - - - )} - - )} - - + + - {enabled && ( - - - {i18n.translate('xpack.indexLifecycleMgmt.warmPhase.replicasTitle', { - defaultMessage: 'Replicas', - })} - - } - description={i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription', - { - defaultMessage: - 'Set the number of replicas. Remains the same as the previous phase by default.', - } - )} - switchProps={{ - 'data-test-subj': 'warm-setReplicasSwitch', - label: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel', - { defaultMessage: 'Set replicas' } - ), - initialValue: policy.phases.warm?.actions?.allocate?.number_of_replicas != null, - }} - fullWidth - > - - + {!isUsingSearchableSnapshotInHotPhase && } - {!isUsingSearchableSnapshotInHotPhase && } + {!isUsingSearchableSnapshotInHotPhase && } - {!isUsingSearchableSnapshotInHotPhase && } + - + {/* Data tier allocation section */} + - {/* Data tier allocation section */} - - - - )} - -
+ + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 228a0f9fdb942..b1cf41773de3c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect, useState, useMemo } from 'react'; +import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { get } from 'lodash'; import { RouteComponentProps } from 'react-router-dom'; @@ -16,7 +16,6 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, EuiButtonEmpty, - EuiDescribedFormGroup, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -24,33 +23,35 @@ import { EuiPage, EuiPageBody, EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, EuiSpacer, EuiSwitch, EuiText, EuiTitle, } from '@elastic/eui'; -import { useForm, UseField, TextField, useFormData } from '../../../shared_imports'; +import { TextField, UseField, useForm, useFormData } from '../../../shared_imports'; import { toasts } from '../../services/notification'; import { savePolicy } from './save_policy'; import { - LearnMoreLink, - PolicyJsonFlyout, ColdPhase, DeletePhase, HotPhase, + PolicyJsonFlyout, WarmPhase, Timeline, } from './components'; -import { schema, deserializer, createSerializer, createPolicyNameValidations, Form } from './form'; +import { createPolicyNameValidations, createSerializer, deserializer, Form, schema } from './form'; import { useEditPolicyContext } from './edit_policy_context'; import { FormInternal } from './types'; +import { createDocLink } from '../../services/documentation'; export interface Props { history: RouteComponentProps['history']; @@ -75,7 +76,7 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { return createSerializer(isNewPolicy ? undefined : currentPolicy); }, [isNewPolicy, currentPolicy]); - const [saveAsNew, setSaveAsNew] = useState(isNewPolicy); + const [saveAsNew, setSaveAsNew] = useState(false); const originalPolicyName: string = isNewPolicy ? '' : policyName!; const { form } = useForm({ @@ -116,7 +117,7 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { ); } else { const success = await savePolicy( - { ...policy, name: saveAsNew ? currentPolicyName : originalPolicyName }, + { ...policy, name: saveAsNew || isNewPolicy ? currentPolicyName : originalPolicyName }, isNewPolicy || saveAsNew ); if (success) { @@ -132,216 +133,187 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { return ( - - -

- {isNewPolicy - ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { - defaultMessage: 'Create an index lifecycle policy', - }) - : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { - defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', - values: { originalPolicyName }, - })} -

-
- -
-
- - -

- {' '} - + + + +

+ {isNewPolicy + ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { + defaultMessage: 'Create Policy', + }) + : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { + defaultMessage: 'Edit Policy {originalPolicyName}', + values: { originalPolicyName }, + })} +

+ + + + + + + + + + {isNewPolicy ? null : ( + + +

+ - } - /> -

-
- - - - {isNewPolicy ? null : ( - - -

- - - - .{' '} - -

-
- - - - { - setSaveAsNew(e.target.checked); - }} - label={ - - - - } /> - -
- )} - - {saveAsNew || isNewPolicy ? ( - - +

+
+ + + + { + setSaveAsNew(e.target.checked); + }} + label={ + -
- } - titleSize="s" - fullWidth - > - - path={policyNamePath} - config={{ - label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', { - defaultMessage: 'Policy name', - }), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage', - { - defaultMessage: - 'A policy name cannot start with an underscore and cannot contain a comma or a space.', - } - ), - validations: policyNameValidations, - }} - component={TextField} - componentProps={{ - fullWidth: false, - euiFieldProps: { - 'data-test-subj': 'policyNameField', - }, - }} + } /> - - ) : null} - - + + + )} + + {saveAsNew || isNewPolicy ? ( + + path={policyNamePath} + config={{ + label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', { + defaultMessage: 'Policy name', + }), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage', + { + defaultMessage: + 'A policy name cannot start with an underscore and cannot contain a comma or a space.', + } + ), + validations: policyNameValidations, + }} + component={TextField} + componentProps={{ + fullWidth: false, + euiFieldProps: { + 'data-test-subj': 'policyNameField', + }, + }} + /> + ) : null} - + - + - + - + - + - + - + - + - + - + - - - - - - {saveAsNew ? ( - - ) : ( - - )} - - + - - - - - - - - - - - {isShowingPolicyJsonFlyout ? ( - - ) : ( + + + + {isShowingPolicyJsonFlyout ? ( + + ) : ( + + )} + + + + + + + - )} - - - - - {isShowingPolicyJsonFlyout ? ( - setIsShowingPolicyJsonFlyout(false)} - /> - ) : null} - -
+ + + + + + {saveAsNew ? ( + + ) : ( + + )} + + + + + + + {isShowingPolicyJsonFlyout ? ( + setIsShowingPolicyJsonFlyout(false)} + /> + ) : null} + diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index b5abf51c29028..44512c2a511ec 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -260,15 +260,6 @@ describe('deserializer and serializer', () => { expect(result.phases.hot!.actions.readonly).toBeUndefined(); }); - it('removes min_age from warm when rollover is enabled', () => { - formInternal._meta.hot.customRollover.enabled = true; - formInternal._meta.warm.warmPhaseOnRollover = true; - - const result = serializer(formInternal); - - expect(result.phases.warm!.min_age).toBeUndefined(); - }); - it('adds default rollover configuration when enabled, but previously not configured', () => { delete formInternal.phases.hot!.actions.rollover; formInternal._meta.hot.isUsingDefaultRollover = true; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 4bdf902d27b6d..9883c2de0685d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import { FormSchema, fieldValidators } from '../../../../shared_imports'; -import { defaultSetPriority, defaultPhaseIndexPriority } from '../../../constants'; +import { defaultIndexPriority } from '../../../constants'; import { ROLLOVER_FORM_PATHS } from '../constants'; -const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS); - import { FormInternal } from '../types'; +const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS); + import { ifExistsNumberGreaterThanZero, ifExistsNumberNonNegative, @@ -54,7 +54,6 @@ export const schema: FormSchema = { }, bestCompression: { label: i18nTexts.editPolicy.bestCompressionFieldLabel, - helpText: i18nTexts.editPolicy.bestCompressionFieldHelpText, }, readonlyEnabled: { defaultValue: false, @@ -69,18 +68,11 @@ export const schema: FormSchema = { { defaultMessage: 'Activate warm phase' } ), }, - warmPhaseOnRollover: { - defaultValue: true, - label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', { - defaultMessage: 'Move to warm phase on rollover', - }), - }, minAgeUnit: { defaultValue: 'ms', }, bestCompression: { label: i18nTexts.editPolicy.bestCompressionFieldLabel, - helpText: i18nTexts.editPolicy.bestCompressionFieldHelpText, }, dataTierAllocationType: { label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel, @@ -218,9 +210,14 @@ export const schema: FormSchema = { }, set_priority: { priority: { - defaultValue: defaultSetPriority as any, - label: i18nTexts.editPolicy.setPriorityFieldLabel, - validations: [{ validator: ifExistsNumberNonNegative }], + defaultValue: defaultIndexPriority.hot as any, + label: i18nTexts.editPolicy.indexPriorityFieldLabel, + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { validator: ifExistsNumberNonNegative }, + ], serializer: serializers.stringToNumber, }, }, @@ -239,9 +236,12 @@ export const schema: FormSchema = { allocate: { number_of_replicas: { label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel', { - defaultMessage: 'Number of replicas (optional)', + defaultMessage: 'Number of replicas', }), validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, { validator: ifExistsNumberNonNegative, }, @@ -289,9 +289,14 @@ export const schema: FormSchema = { }, set_priority: { priority: { - defaultValue: defaultPhaseIndexPriority as any, - label: i18nTexts.editPolicy.setPriorityFieldLabel, - validations: [{ validator: ifExistsNumberNonNegative }], + defaultValue: defaultIndexPriority.warm as any, + label: i18nTexts.editPolicy.indexPriorityFieldLabel, + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { validator: ifExistsNumberNonNegative }, + ], serializer: serializers.stringToNumber, }, }, @@ -310,9 +315,12 @@ export const schema: FormSchema = { allocate: { number_of_replicas: { label: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel', { - defaultMessage: 'Number of replicas (optional)', + defaultMessage: 'Number of replicas', }), validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, { validator: ifExistsNumberNonNegative, }, @@ -322,9 +330,14 @@ export const schema: FormSchema = { }, set_priority: { priority: { - defaultValue: '0' as any, - label: i18nTexts.editPolicy.setPriorityFieldLabel, - validations: [{ validator: ifExistsNumberNonNegative }], + defaultValue: defaultIndexPriority.cold as any, + label: i18nTexts.editPolicy.indexPriorityFieldLabel, + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { validator: ifExistsNumberNonNegative }, + ], serializer: serializers.stringToNumber, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index f718073afa352..f7cdecdc352fb 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -20,9 +20,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( ): SerializedPolicy => { const { _meta, ...updatedPolicy } = data; - if (!updatedPolicy.phases || !updatedPolicy.phases.hot) { - updatedPolicy.phases = { hot: { actions: {} } }; - } + updatedPolicy.phases = { hot: { actions: {} }, ...updatedPolicy.phases }; return produce(originalPolicy ?? defaultPolicy, (draft) => { // Copy over all updated fields @@ -32,7 +30,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( * Important shared values for serialization */ const isUsingRollover = Boolean( - _meta.hot.isUsingDefaultRollover || _meta.hot.customRollover.enabled + _meta.hot?.isUsingDefaultRollover || _meta.hot?.customRollover.enabled ); // Next copy over all meta fields and delete any fields that have been removed @@ -53,7 +51,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( * HOT PHASE ROLLOVER */ if (isUsingRollover) { - if (_meta.hot.isUsingDefaultRollover) { + if (_meta.hot?.isUsingDefaultRollover) { hotPhaseActions.rollover = cloneDeep(defaultRolloverAction); } else { // Rollover may not exist if editing an existing policy with initially no rollover configured @@ -63,7 +61,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( // We are using user-defined, custom rollover settings. if (updatedPolicy.phases.hot!.actions.rollover?.max_age) { - hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.customRollover.maxAgeUnit}`; + hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot?.customRollover.maxAgeUnit}`; } else { delete hotPhaseActions.rollover.max_age; } @@ -73,7 +71,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( } if (updatedPolicy.phases.hot!.actions.rollover?.max_size) { - hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot.customRollover.maxStorageSizeUnit}`; + hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot?.customRollover.maxStorageSizeUnit}`; } else { delete hotPhaseActions.rollover.max_size; } @@ -84,20 +82,20 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( */ if (!updatedPolicy.phases.hot!.actions?.forcemerge) { delete hotPhaseActions.forcemerge; - } else if (_meta.hot.bestCompression) { + } else if (_meta.hot?.bestCompression) { hotPhaseActions.forcemerge!.index_codec = 'best_compression'; } else { delete hotPhaseActions.forcemerge!.index_codec; } - if (_meta.hot.bestCompression && hotPhaseActions.forcemerge) { + if (_meta.hot?.bestCompression && hotPhaseActions.forcemerge) { hotPhaseActions.forcemerge.index_codec = 'best_compression'; } /** * HOT PHASE READ-ONLY */ - if (_meta.hot.readonlyEnabled) { + if (_meta.hot?.readonlyEnabled) { hotPhaseActions.readonly = hotPhaseActions.readonly ?? {}; } else { delete hotPhaseActions.readonly; @@ -140,17 +138,9 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( /** * WARM PHASE MIN AGE * - * If warm phase on rollover is enabled, delete min age field - * An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - * They are mutually exclusive */ - if ( - (!isUsingRollover || !_meta.warm.warmPhaseOnRollover) && - updatedPolicy.phases.warm?.min_age - ) { + if (updatedPolicy.phases.warm?.min_age) { warmPhase.min_age = `${updatedPolicy.phases.warm!.min_age}${_meta.warm.minAgeUnit}`; - } else { - delete warmPhase.min_age; } /** diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index f30a40fdd2bb9..71085a6d7a2b8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -40,10 +40,10 @@ export const i18nTexts = { defaultMessage: 'Number of segments', } ), - setPriorityFieldLabel: i18n.translate( + indexPriorityFieldLabel: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel', { - defaultMessage: 'Index priority (optional)', + defaultMessage: 'Index priority', } ), bestCompressionFieldLabel: i18n.translate( @@ -170,5 +170,30 @@ export const i18nTexts = { } ), }, + titles: { + hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseTitle', { + defaultMessage: 'Hot phase', + }), + warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseTitle', { + defaultMessage: 'Warm phase', + }), + cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle', { + defaultMessage: 'Cold phase', + }), + }, + descriptions: { + hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescription', { + defaultMessage: + 'This phase is required. You are actively querying and writing to your index. For faster updates, you can roll over the index when it gets too big or too old.', + }), + warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescription', { + defaultMessage: + 'You are still querying your index, but it is read-only. You can allocate shards to less performant hardware. For faster searches, you can reduce the number of shards and force merge segments.', + }), + cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription', { + defaultMessage: + 'You are querying your index less frequently, so you can allocate shards on significantly less performant hardware. Because your queries are slower, you can reduce the number of replicas.', + }), + }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index c77b171a56bed..2f37608b2d7ae 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -70,9 +70,10 @@ export interface PhaseAgeInMilliseconds { const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete']; const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ - min_age: formData.phases[phase]?.min_age - ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit - : '0ms', + min_age: + formData.phases && formData.phases[phase]?.min_age + ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit + : '0ms', }); const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index 2f1c7798e7a4d..4b22d1c8448b5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -9,7 +9,7 @@ import { UIM_CONFIG_WARM_PHASE, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, - defaultPhaseIndexPriority, + defaultIndexPriority, } from '../constants/'; import { getUiMetricsForPhases } from './ui_metric'; @@ -22,7 +22,7 @@ describe('getUiMetricsForPhases', () => { min_age: '0ms', actions: { set_priority: { - priority: parseInt(defaultPhaseIndexPriority, 10), + priority: parseInt(defaultIndexPriority.cold, 10), }, }, }, @@ -37,7 +37,7 @@ describe('getUiMetricsForPhases', () => { min_age: '0ms', actions: { set_priority: { - priority: parseInt(defaultPhaseIndexPriority, 10), + priority: parseInt(defaultIndexPriority.warm, 10), }, }, }, @@ -52,7 +52,7 @@ describe('getUiMetricsForPhases', () => { min_age: '0ms', actions: { set_priority: { - priority: parseInt(defaultPhaseIndexPriority, 10) + 1, + priority: parseInt(defaultIndexPriority.warm, 10) + 1, }, }, }, @@ -68,7 +68,7 @@ describe('getUiMetricsForPhases', () => { actions: { freeze: {}, set_priority: { - priority: parseInt(defaultPhaseIndexPriority, 10), + priority: parseInt(defaultIndexPriority.cold, 10), }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index bcf4b6cf1da0d..ffdcf927a5b67 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -19,8 +19,7 @@ import { UIM_CONFIG_FREEZE_INDEX, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_WARM_PHASE, - defaultSetPriority, - defaultPhaseIndexPriority, + defaultIndexPriority, } from '../constants'; import { Phases } from '../../../common/types'; @@ -50,17 +49,17 @@ export function getUiMetricsForPhases(phases: Phases): string[] { const isHotPhasePriorityChanged = phases.hot && phases.hot.actions.set_priority && - phases.hot.actions.set_priority.priority !== parseInt(defaultSetPriority, 10); + phases.hot.actions.set_priority.priority !== parseInt(defaultIndexPriority.hot, 10); const isWarmPhasePriorityChanged = phases.warm && phases.warm.actions.set_priority && - phases.warm.actions.set_priority.priority !== parseInt(defaultPhaseIndexPriority, 10); + phases.warm.actions.set_priority.priority !== parseInt(defaultIndexPriority.warm, 10); const isColdPhasePriorityChanged = phases.cold && phases.cold.actions.set_priority && - phases.cold.actions.set_priority.priority !== parseInt(defaultPhaseIndexPriority, 10); + phases.cold.actions.set_priority.priority !== parseInt(defaultIndexPriority.cold, 10); // If the priority is different than the default, we'll consider it a user interaction, // even if the user has set it to undefined. return ( diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts index 4cb5d95239408..fdb25dec6f1fd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -31,6 +31,7 @@ export { SuperSelectField, ComboBoxField, TextField, + CheckBoxField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { attemptToURIDecode } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index da9228335a3f3..e2506cb6ca8f0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9416,9 +9416,7 @@ "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "コールドティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。安価なハードウェアのコールドフェーズにデータを格納します。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結", - "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription": "レプリカの数を設定します。デフォルトでは前のフェーズと同じです。", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "レプリカ数(任意)", - "xpack.indexLifecycleMgmt.coldPhase.replicasTitle": "レプリカ", "xpack.indexLifecycleMgmt.common.dataTier.title": "データ割り当て", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "キャンセル", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "削除", @@ -9431,11 +9429,8 @@ "xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title": "コールドティアを作成", "xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription": "属性に基づく割り当てを使用するには、elasticsearch.yml でカスタムノード属性を定義します。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "コールドフェーズを有効にする", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "インデックスへのクエリの頻度を減らすことで、大幅に性能が低いハードウェアにシャードを割り当てることができます。クエリが遅いため、複製の数を減らすことができます。", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "コールドフェーズ", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "インデックスを読み取り専用にし、メモリー消費量を最小化します。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "凍結", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel": "レプリカを設定", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "ノード属性に基づいてデータを移動します。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "カスタム", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "コールドティアのノードにデータを移動します。", @@ -9452,13 +9447,6 @@ "xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage": "インデックスライフサイクルポリシーを作成します", "xpack.indexLifecycleMgmt.editPolicy.createSearchableSnapshotLink": "スナップショットリポジドリを作成", "xpack.indexLifecycleMgmt.editPolicy.createSnapshotRepositoryLink": "新しいスナップショットリポジドリを作成", - "xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel": "インデックスの作成からの経過日数", - "xpack.indexLifecycleMgmt.editPolicy.creationHoursOptionLabel": "インデックスの作成からの経過時間数", - "xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel": "インデックスの作成からの経過時間(マイクロ秒)", - "xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel": "インデックスの作成からの経過時間(ミリ秒)", - "xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel": "インデックスの作成からの経過時間(分)", - "xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel": "インデックスの作成からの経過時間(ナノ秒)", - "xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel": "インデックスの作成からの経過時間(秒)", "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.allocationFieldLabel": "データティアオプション", "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.nodeAllocationFieldLabel": "ノード属性を選択", "xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel": "コールド", @@ -9499,23 +9487,17 @@ "xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage": "このページのエラーを修正してください。", "xpack.indexLifecycleMgmt.editPolicy.hidePolicyJsonButto": "リクエストを非表示", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.enableRolloverTipContent": "現在のインデックスが定義された条件のいずれかを満たすときに、新しいインデックスにロールオーバーします。", - "xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescriptionMessage": "このフェーズは必須です。アクティブにクエリを実行しインデックスに書き込んでいます。 更新を高速化するため、大きくなりすぎたり古くなりすぎたりした際にインデックスをロールオーバーできます。", - "xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseLabel": "ホットフェーズ", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.learnAboutRolloverLinkText": "詳細", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDefaultsTipContent": "インデックスが 30 日経過するか、50 GB に達したらロールオーバーします。", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDescriptionMessage": "効率的なストレージと高いパフォーマンスのための時系列データの自動ロールアウト。", "xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel": "インデックス優先度(任意)", "xpack.indexLifecycleMgmt.editPolicy.indexPriorityText": "インデックスの優先順位", - "xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexLifecycleManagementLinkText": "インデックスライフサイクルの詳細をご覧ください。", "xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexTemplatesLink": "インデックステンプレートの詳細をご覧ください", "xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink": "シャード割り当ての詳細をご覧ください", - "xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText": "タイミングの詳細をご覧ください", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesLoadingFailedTitle": "既存のライフサイクルポリシーを読み込めません", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesReloadButton": "再試行", - "xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText": "インデックスへのアクティブな書き込みから削除までの、インデックスライフサイクルの 4 つのフェーズを自動化するには、インデックスポリシーを使用します。", "xpack.indexLifecycleMgmt.editPolicy.loadSnapshotRepositoriesErrorBody": "このフィールドを更新し、既存のスナップショットリポジトリの名前を入力します。", "xpack.indexLifecycleMgmt.editPolicy.loadSnapshotRepositoriesErrorTitle": "スナップショットリポジトリを読み込めません", - "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名前", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "ノード属性を使用して、シャード割り当てを制御します。{learnMoreLink}。", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption": "割り当て構成を修正しない", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle": "ノードデータを読み込めません", @@ -9542,13 +9524,6 @@ "xpack.indexLifecycleMgmt.editPolicy.readonlyDescription": "有効にすると、インデックスおよびインデックスメタデータを読み取り専用にします。無効にすると、書き込みとメタデータの変更を許可します。", "xpack.indexLifecycleMgmt.editPolicy.readonlyTitle": "読み取り専用", "xpack.indexLifecycleMgmt.editPolicy.reloadSnapshotRepositoriesLabel": "スナップショットリポジドリの再読み込み", - "xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel": "ロールオーバーからの経過日数", - "xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel": "ロールオーバーからの経過時間数", - "xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel": "ロールオーバーからの経過時間(マイクロ秒)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverMilliSecondsOptionLabel": "ロールオーバーからの経過時間(ミリ秒)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverMinutesOptionLabel": "ロールオーバーからの経過時間数(分)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverNanoSecondsOptionLabel": "ロールオーバーからの経過時間(ナノ秒)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverSecondsOptionLabel": "ロールオーバーからの経過時間(秒)", "xpack.indexLifecycleMgmt.editPolicy.saveAsNewButton": "新規ポリシーとして保存します", "xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage": "新規ポリシーとして保存します", "xpack.indexLifecycleMgmt.editPolicy.saveButton": "ポリシーを保存", @@ -9573,15 +9548,11 @@ "xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription": "属性に基づく割り当てを使用するには、elasticsearch.yml でカスタムノード属性を定義します。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel": "ウォームフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText": "ノードの再起動後にインデックスを復元する優先順位を設定します。優先順位の高いインデックスは優先順位の低いインデックスよりも先に復元されます。", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel": "レプリカを設定", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescriptionMessage": "まだインデックスにクエリを実行中ですが、読み取り専用です。性能の低いハードウェアにシャードを割り当てることができます。検索を高速化するために、シャードの数を減らしセグメントを結合することができます。", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseLabel": "ウォームフェーズ", "xpack.indexLifecycleMgmt.featureCatalogueDescription": "ライフサイクルポリシーを定義し、インデックス年齢として自動的に処理を実行します。", "xpack.indexLifecycleMgmt.featureCatalogueTitle": "インデックスライフサイクルを管理", "xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel": "格納されたフィールドを圧縮", "xpack.indexLifecycleMgmt.forcemerge.enableLabel": "データを強制結合", "xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel": "セグメントの数", - "xpack.indexLifecycleMgmt.hotPhase.advancedSettingsButton": "高度な設定", "xpack.indexLifecycleMgmt.hotPhase.bytesLabel": "バイト", "xpack.indexLifecycleMgmt.hotPhase.daysLabel": "日", "xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel": "ロールオーバーを有効にする", @@ -9705,7 +9676,6 @@ "xpack.indexLifecycleMgmt.shrink.indexFieldLabel": "インデックスを縮小", "xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel": "プライマリシャードの数", "xpack.indexLifecycleMgmt.templateNotFoundMessage": "テンプレート{name}が見つかりません。", - "xpack.indexLifecycleMgmt.warmPhase.advancedSettingsButton": "高度な設定", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody": "ロールに基づく割り当てを使用するには、1つ以上のノードを、ウォームまたはホットティアに割り当てます。使用可能なノードがない場合、ポリシーは割り当てを完了できません。", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle": "ウォームティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold": "このポリシーはコールドフェーズのデータを{tier}ティアノードに移動します。", @@ -9713,10 +9683,7 @@ "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "このポリシーはウォームフェーズのデータを{tier}ティアノードに移動します。", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "ウォームティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。", - "xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel": "ロールオーバー時にウォームフェーズに変更", - "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription": "レプリカの数を設定します。デフォルトでは前のフェーズと同じです。", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカ数(任意)", - "xpack.indexLifecycleMgmt.warmPhase.replicasTitle": "レプリカ", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", "xpack.infra.alerting.alertsButton": "アラート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 679edaf9e0cdd..7b8ea35baf0ff 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9440,9 +9440,7 @@ "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到冷层的节点", "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。将处于冷阶段的数据存储在成本较低的硬件上。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引", - "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription": "设置副本数目。默认情况下仍与上一阶段相同。", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数(可选)", - "xpack.indexLifecycleMgmt.coldPhase.replicasTitle": "副本分片", "xpack.indexLifecycleMgmt.common.dataTier.title": "数据分配", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "取消", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "删除", @@ -9455,11 +9453,8 @@ "xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title": "创建冷层", "xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription": "在 elasticsearch.yml 中定义定制节点属性,以使用基于属性的分配。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "激活冷阶段", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "您查询自己索引的频率较低,因此您可以在效率较低的硬件上分配分片。因为您的查询较为缓慢,所以您可以减少副本分片数目。", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "使索引只读,并最大限度减小其内存占用。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "冻结", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel": "设置副本", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "根据节点属性移动数据。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "定制", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "将数据移到冷层中的节点。", @@ -9476,13 +9471,6 @@ "xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage": "创建索引生命周期策略", "xpack.indexLifecycleMgmt.editPolicy.createSearchableSnapshotLink": "创建快照库", "xpack.indexLifecycleMgmt.editPolicy.createSnapshotRepositoryLink": "创建新的快照库", - "xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel": "天(自索引创建)", - "xpack.indexLifecycleMgmt.editPolicy.creationHoursOptionLabel": "小时(自索引创建)", - "xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel": "微秒(自索引创建)", - "xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel": "毫秒(自索引创建)", - "xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel": "分钟(自索引创建)", - "xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel": "纳秒(自索引创建)", - "xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel": "秒(自索引创建)", "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.allocationFieldLabel": "数据层选项", "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.nodeAllocationFieldLabel": "选择节点属性", "xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel": "冷", @@ -9523,23 +9511,17 @@ "xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage": "请修复此页面上的错误。", "xpack.indexLifecycleMgmt.editPolicy.hidePolicyJsonButto": "隐藏请求", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.enableRolloverTipContent": "在当前索引满足定义的条件之一时,滚动更新到新索引。", - "xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescriptionMessage": "此阶段为必需。您正频繁地查询并写到您的索引。 为了获取更快的更新,在索引变得过大或过旧时,您可以滚动更新索引。", - "xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseLabel": "热阶段", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.learnAboutRolloverLinkText": "了解详情", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDefaultsTipContent": "当索引已存在 30 天或达到 50 GB 时滚动更新。", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDescriptionMessage": "自动滚动更新时间序列数据,以实现高效存储和更高性能。", "xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel": "索引优先级(可选)", "xpack.indexLifecycleMgmt.editPolicy.indexPriorityText": "索引优先级", - "xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexLifecycleManagementLinkText": "了解索引生命周期。", "xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexTemplatesLink": "了解索引模板", "xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink": "了解分片分配", - "xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText": "了解计时", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesLoadingFailedTitle": "无法加载现有生命周期策略", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesReloadButton": "重试", - "xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText": "使用索引策略自动化索引生命周期的四个阶段,从频繁地写入到索引到删除索引。", "xpack.indexLifecycleMgmt.editPolicy.loadSnapshotRepositoriesErrorBody": "刷新此字段并输入现有快照储存库的名称。", "xpack.indexLifecycleMgmt.editPolicy.loadSnapshotRepositoriesErrorTitle": "无法加载快照存储库", - "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名称", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "使用节点属性控制分片分配。{learnMoreLink}。", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption": "切勿修改分配配置", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle": "无法加载节点数据", @@ -9566,13 +9548,6 @@ "xpack.indexLifecycleMgmt.editPolicy.readonlyDescription": "启用以使索引及索引元数据只读;禁用以允许写入和元数据更改。", "xpack.indexLifecycleMgmt.editPolicy.readonlyTitle": "只读", "xpack.indexLifecycleMgmt.editPolicy.reloadSnapshotRepositoriesLabel": "重新加载快照存储库", - "xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel": "天(自滚动更新)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel": "小时(自滚动更新)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel": "微秒(自滚动更新)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverMilliSecondsOptionLabel": "毫秒(自滚动更新)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverMinutesOptionLabel": "分钟(自滚动更新)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverNanoSecondsOptionLabel": "纳秒(自滚动更新)", - "xpack.indexLifecycleMgmt.editPolicy.rolloverSecondsOptionLabel": "秒(自滚动更新)", "xpack.indexLifecycleMgmt.editPolicy.saveAsNewButton": "另存为新策略", "xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage": "另存为新策略", "xpack.indexLifecycleMgmt.editPolicy.saveButton": "保存策略", @@ -9597,15 +9572,11 @@ "xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription": "在 elasticsearch.yml 中定义定制节点属性,以使用基于属性的分配。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel": "激活温阶段", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText": "设置在节点重新启动后恢复索引的优先级。较高优先级的索引会在较低优先级的索引之前恢复。", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel": "设置副本", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescriptionMessage": "您仍在查询自己的索引,但其为只读。您可以将分片分配给效率较低的硬件。为了获取更快的搜索,您可以减少分片数目并强制合并段。", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseLabel": "温阶段", "xpack.indexLifecycleMgmt.featureCatalogueDescription": "定义生命周期策略,以随着索引老化自动执行操作。", "xpack.indexLifecycleMgmt.featureCatalogueTitle": "管理索引生命周期", "xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel": "压缩已存储字段", "xpack.indexLifecycleMgmt.forcemerge.enableLabel": "强制合并数据", "xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel": "分段数目", - "xpack.indexLifecycleMgmt.hotPhase.advancedSettingsButton": "高级设置", "xpack.indexLifecycleMgmt.hotPhase.bytesLabel": "字节", "xpack.indexLifecycleMgmt.hotPhase.daysLabel": "天", "xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel": "启用滚动更新", @@ -9731,7 +9702,6 @@ "xpack.indexLifecycleMgmt.shrink.indexFieldLabel": "缩小索引", "xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel": "主分片数目", "xpack.indexLifecycleMgmt.templateNotFoundMessage": "找不到模板 {name}。", - "xpack.indexLifecycleMgmt.warmPhase.advancedSettingsButton": "高级设置", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody": "至少将一个节点分配到温层或冷层,以使用基于角色的分配。如果没有可用节点,则策略无法完成分配。", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到温层的节点", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold": "此策略会改为将冷阶段的数据移到{tier}层节点。", @@ -9739,10 +9709,7 @@ "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "此策略会改为将温阶段的数据移到{tier}层节点。", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "没有分配到温层的节点", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。", - "xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel": "滚动更新时移到温阶段", - "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription": "设置副本数目。默认情况下仍与上一阶段相同。", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数(可选)", - "xpack.indexLifecycleMgmt.warmPhase.replicasTitle": "副本分片", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", "xpack.infra.alerting.alertsButton": "告警", From 466334529c2fa981e83babdc6bcc01b039aee038 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 27 Jan 2021 09:00:50 -0800 Subject: [PATCH 045/163] [Alerts][Actions][Telemetry] Fix mappings for Kibana actions and alert types telemetry. (#88532) * [Alerts][Actions][Telemetry] Fix mappings for Kibana actions and alert types telemetry. * fixed count_active_by_type for actions * fixed tests * Fixed due to comments. * Fixed due to comments. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/usage/actions_telemetry.test.ts | 90 ++++++++++++++++++- .../actions/server/usage/actions_telemetry.ts | 48 +++++++++- x-pack/plugins/actions/server/usage/task.ts | 26 +++++- .../server/usage/alerts_telemetry.test.ts | 8 +- .../alerts/server/usage/alerts_telemetry.ts | 13 ++- 5 files changed, 169 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts index 1b0fe03633531..08a3fd007554e 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getTotalCount } from './actions_telemetry'; +import { getInUseTotalCount, getTotalCount } from './actions_telemetry'; describe('actions telemetry', () => { - test('getTotalCount should replace action types names with . to __', async () => { + test('getTotalCount should replace first symbol . to __ for action types names', async () => { const mockEsClient = jest.fn(); mockEsClient.mockReturnValue({ aggregations: { byActionTypeId: { value: { - types: { '.index': 1, '.server-log': 1 }, + types: { '.index': 1, '.server-log': 1, 'some.type': 1, 'another.type.': 1 }, }, }, }, @@ -56,6 +56,38 @@ describe('actions telemetry', () => { updated_at: '2020-03-26T18:46:44.449Z', }, }, + { + _id: 'action:00000000-1', + _index: '.kibana_1', + _score: 0, + _source: { + action: { + actionTypeId: 'some.type', + config: {}, + name: 'test type', + secrets: {}, + }, + references: [], + type: 'action', + updated_at: '2020-03-26T18:46:44.449Z', + }, + }, + { + _id: 'action:00000000-2', + _index: '.kibana_1', + _score: 0, + _source: { + action: { + actionTypeId: 'another.type.', + config: {}, + name: 'test another type', + secrets: {}, + }, + references: [], + type: 'action', + updated_at: '2020-03-26T18:46:44.449Z', + }, + }, ], }, }); @@ -69,6 +101,58 @@ Object { "countByType": Object { "__index": 1, "__server-log": 1, + "another.type__": 1, + "some.type": 1, + }, + "countTotal": 4, +} +`); + }); + + test('getInUseTotalCount', async () => { + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue({ + aggregations: { + refs: { + actionRefIds: { + value: { + connectorIds: { '1': 'action-0', '123': 'action-0' }, + total: 2, + }, + }, + }, + hits: { + hits: [], + }, + }, + }); + const actionsBulkGet = jest.fn(); + actionsBulkGet.mockReturnValue({ + saved_objects: [ + { + id: '1', + attributes: { + actionTypeId: '.server-log', + }, + }, + { + id: '123', + attributes: { + actionTypeId: '.slack', + }, + }, + ], + }); + const telemetry = await getInUseTotalCount(mockEsClient, actionsBulkGet, 'test'); + + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(actionsBulkGet).toHaveBeenCalledTimes(1); + + expect(telemetry).toMatchInlineSnapshot(` +Object { + "countByType": Object { + "__server-log": 1, + "__slack": 1, }, "countTotal": 2, } diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts index e3ff2552fed9c..cc49232150eee 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { + LegacyAPICaller, + SavedObjectsBaseOptions, + SavedObjectsBulkGetObject, + SavedObjectsBulkResponse, +} from 'kibana/server'; +import { ActionResult } from '../types'; export async function getTotalCount(callCluster: LegacyAPICaller, kibanaIndex: string) { const scriptedMetric = { @@ -58,14 +64,23 @@ export async function getTotalCount(callCluster: LegacyAPICaller, kibanaIndex: s // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, - [key.replace('.', '__')]: searchResult.aggregations.byActionTypeId.value.types[key], + [replaceFirstAndLastDotSymbols(key)]: searchResult.aggregations.byActionTypeId.value.types[ + key + ], }), {} ), }; } -export async function getInUseTotalCount(callCluster: LegacyAPICaller, kibanaIndex: string) { +export async function getInUseTotalCount( + callCluster: LegacyAPICaller, + actionsBulkGet: ( + objects?: SavedObjectsBulkGetObject[] | undefined, + options?: SavedObjectsBaseOptions | undefined + ) => Promise>>>, + kibanaIndex: string +): Promise<{ countTotal: number; countByType: Record }> { const scriptedMetric = { scripted_metric: { init_script: 'state.connectorIds = new HashMap(); state.total = 0;', @@ -145,7 +160,32 @@ export async function getInUseTotalCount(callCluster: LegacyAPICaller, kibanaInd }, }); - return actionResults.aggregations.refs.actionRefIds.value.total; + const bulkFilter = Object.entries( + actionResults.aggregations.refs.actionRefIds.value.connectorIds + ).map(([key]) => ({ + id: key, + type: 'action', + fields: ['id', 'actionTypeId'], + })); + const actions = await actionsBulkGet(bulkFilter); + const countByType = actions.saved_objects.reduce( + (actionTypeCount: Record, action) => { + const alertTypeId = replaceFirstAndLastDotSymbols(action.attributes.actionTypeId); + const currentCount = + actionTypeCount[alertTypeId] !== undefined ? actionTypeCount[alertTypeId] : 0; + actionTypeCount[alertTypeId] = currentCount + 1; + return actionTypeCount; + }, + {} + ); + return { countTotal: actionResults.aggregations.refs.actionRefIds.value.total, countByType }; +} + +function replaceFirstAndLastDotSymbols(strToReplace: string) { + const hasFirstSymbolDot = strToReplace.startsWith('.'); + const appliedString = hasFirstSymbolDot ? strToReplace.replace('.', '__') : strToReplace; + const hasLastSymbolDot = strToReplace.endsWith('.'); + return hasLastSymbolDot ? `${appliedString.slice(0, -1)}__` : appliedString; } // TODO: Implement executions count telemetry with eventLog, when it will write to index diff --git a/x-pack/plugins/actions/server/usage/task.ts b/x-pack/plugins/actions/server/usage/task.ts index 176ba29ef748a..001db21ffebcc 100644 --- a/x-pack/plugins/actions/server/usage/task.ts +++ b/x-pack/plugins/actions/server/usage/task.ts @@ -4,13 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, CoreSetup, LegacyAPICaller } from 'kibana/server'; +import { + Logger, + CoreSetup, + LegacyAPICaller, + SavedObjectsBulkGetObject, + SavedObjectsBaseOptions, +} from 'kibana/server'; import moment from 'moment'; import { RunContext, TaskManagerSetupContract, TaskManagerStartContract, } from '../../../task_manager/server'; +import { ActionResult } from '../types'; import { getTotalCount, getInUseTotalCount } from './actions_telemetry'; export const TELEMETRY_TASK_TYPE = 'actions_telemetry'; @@ -66,19 +73,30 @@ export function telemetryTaskRunner(logger: Logger, core: CoreSetup, kibanaIndex client.callAsInternalUser(...args) ); }; + const actionsBulkGet = ( + objects?: SavedObjectsBulkGetObject[], + options?: SavedObjectsBaseOptions + ) => { + return core + .getStartServices() + .then(([{ savedObjects }]) => + savedObjects.createInternalRepository(['action']).bulkGet(objects, options) + ); + }; return { async run() { return Promise.all([ getTotalCount(callCluster, kibanaIndex), - getInUseTotalCount(callCluster, kibanaIndex), + getInUseTotalCount(callCluster, actionsBulkGet, kibanaIndex), ]) - .then(([totalAggegations, countActiveTotal]) => { + .then(([totalAggegations, totalInUse]) => { return { state: { runs: (state.runs || 0) + 1, count_total: totalAggegations.countTotal, count_by_type: totalAggegations.countByType, - count_active_total: countActiveTotal, + count_active_total: totalInUse.countTotal, + count_active_by_type: totalInUse.countByType, }, runAt: getNextMidnight(), }; diff --git a/x-pack/plugins/alerts/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerts/server/usage/alerts_telemetry.test.ts index 171f80cf11cb8..843100194e4b6 100644 --- a/x-pack/plugins/alerts/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerts/server/usage/alerts_telemetry.test.ts @@ -7,13 +7,13 @@ import { getTotalCountInUse } from './alerts_telemetry'; describe('alerts telemetry', () => { - test('getTotalCountInUse should replace action types names with . to __', async () => { + test('getTotalCountInUse should replace first "." symbol to "__" in alert types names', async () => { const mockEsClient = jest.fn(); mockEsClient.mockReturnValue({ aggregations: { byAlertTypeId: { value: { - types: { '.index-threshold': 2 }, + types: { '.index-threshold': 2, 'logs.alert.document.count': 1, 'document.test.': 1 }, }, }, }, @@ -30,8 +30,10 @@ describe('alerts telemetry', () => { Object { "countByType": Object { "__index-threshold": 2, + "document.test__": 1, + "logs.alert.document.count": 1, }, - "countTotal": 2, + "countTotal": 4, } `); }); diff --git a/x-pack/plugins/alerts/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerts/server/usage/alerts_telemetry.ts index 6edebb1decb61..72b189aa67f4b 100644 --- a/x-pack/plugins/alerts/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerts/server/usage/alerts_telemetry.ts @@ -250,7 +250,7 @@ export async function getTotalCountAggregations(callCluster: LegacyAPICaller, ki // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, - [key.replace('.', '__')]: results.aggregations.byAlertTypeId.value.types[key], + [replaceFirstAndLastDotSymbols(key)]: results.aggregations.byAlertTypeId.value.types[key], }), {} ), @@ -310,11 +310,20 @@ export async function getTotalCountInUse(callCluster: LegacyAPICaller, kibanaIne // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, - [key.replace('.', '__')]: searchResult.aggregations.byAlertTypeId.value.types[key], + [replaceFirstAndLastDotSymbols(key)]: searchResult.aggregations.byAlertTypeId.value.types[ + key + ], }), {} ), }; } +function replaceFirstAndLastDotSymbols(strToReplace: string) { + const hasFirstSymbolDot = strToReplace.startsWith('.'); + const appliedString = hasFirstSymbolDot ? strToReplace.replace('.', '__') : strToReplace; + const hasLastSymbolDot = strToReplace.endsWith('.'); + return hasLastSymbolDot ? `${appliedString.slice(0, -1)}__` : appliedString; +} + // TODO: Implement executions count telemetry with eventLog, when it will write to index From 723dd32693addda90c73cafe041b9bf5ce4b3ea3 Mon Sep 17 00:00:00 2001 From: Kent Marten <65553677+kmartastic@users.noreply.github.com> Date: Wed, 27 Jan 2021 09:34:03 -0800 Subject: [PATCH 046/163] [maps] Update point to point docs with use cases (#89246) Co-authored-by: Kent Marten --- docs/maps/maps-aggregations.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index f09d99250c22d..3c66e187bf59c 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -98,6 +98,13 @@ Point to point uses an {es} {ref}/search-aggregations-bucket-terms-aggregation.h Then, a nested {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] groups sources for each destination into grids. A line connects each source grid centroid to each destination. +Point-to-point layers are used in several common use cases: + +* Source-destination maps for network traffic +* Origin-destination maps for flight data +* Origin-destination flows for import/export/migration +* Origin-destination for pick-up/drop-off data + image::maps/images/point_to_point.png[] [role="xpack"] From da9ad2ade4e80961c30ef4df27f4d1467a34c4ab Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 27 Jan 2021 11:39:50 -0600 Subject: [PATCH 047/163] [ML] Add embedded map to geo_point fields for Data Visualizer (#88880) --- .../maps/public/embeddable/map_embeddable.tsx | 2 +- x-pack/plugins/ml/kibana.json | 6 +- .../plugins/ml/public/application/_index.scss | 1 + x-pack/plugins/ml/public/application/app.tsx | 3 + .../components/ml_embedded_map/_index.scss | 1 + .../ml_embedded_map/_ml_embedded_map.scss | 8 + .../components/ml_embedded_map/index.ts | 7 + .../ml_embedded_map/ml_embedded_map.tsx | 160 ++++++++++++++++++ .../contexts/kibana/kibana_context.ts | 4 + .../expanded_row/file_based_expanded_row.tsx | 2 +- .../geo_point_content/format_utils.ts | 76 +++++++++ .../geo_point_content/geo_point_content.tsx | 79 +++++++++ .../expanded_row/geo_point_content/index.ts | 7 + .../file_datavisualizer_view/_index.scss | 2 +- .../components/expanded_row/expanded_row.tsx | 28 ++- .../expanded_row/geo_point_content.tsx | 78 +++++++++ .../examples_list/examples_list.tsx | 34 ++-- .../field_data_row/top_values/top_values.tsx | 108 +++++++----- .../datavisualizer/index_based/page.tsx | 35 ++-- .../stats_table/_field_data_row.scss | 15 ++ .../datavisualizer/stats_table/_index.scss | 10 +- .../field_data_expanded_row/_index.scss | 6 + .../boolean_content.tsx | 7 +- .../field_data_expanded_row/date_content.tsx | 7 +- .../document_stats.tsx | 3 +- .../expanded_row_content.tsx | 24 +++ .../geo_point_content.tsx | 35 ---- .../field_data_expanded_row/index.ts | 2 +- .../field_data_expanded_row/ip_content.tsx | 20 +-- .../keyword_content.tsx | 19 +-- .../number_content.tsx | 26 +-- .../field_data_expanded_row/other_content.tsx | 6 +- .../field_data_expanded_row/text_content.tsx | 7 +- .../components/field_data_row/_index.scss | 3 + .../field_data_row/distinct_values.tsx | 2 +- .../field_data_row/document_stats.tsx | 2 +- .../field_data_row/number_content_preview.tsx | 7 +- .../application/util/dependency_cache.ts | 3 + x-pack/plugins/ml/public/plugin.ts | 8 +- .../models/data_visualizer/data_visualizer.ts | 21 +-- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../data_visualizer/file_data_visualizer.ts | 41 +++++ .../files_to_import/geo_file.csv | 14 ++ .../data_visualizer/index_data_visualizer.ts | 79 ++++++++- .../services/ml/data_visualizer_table.ts | 51 +++++- 46 files changed, 842 insertions(+), 223 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/ml_embedded_map/_index.scss create mode 100644 x-pack/plugins/ml/public/application/components/ml_embedded_map/_ml_embedded_map.scss create mode 100644 x-pack/plugins/ml/public/application/components/ml_embedded_map/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/format_utils.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/geo_point_content.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/expanded_row_content.tsx delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/geo_point_content.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/_index.scss create mode 100644 x-pack/test/functional/apps/ml/data_visualizer/files_to_import/geo_file.csv diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 67981ab3aede5..bcdc23bddd2eb 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -68,7 +68,7 @@ import { MapEmbeddableInput, MapEmbeddableOutput, } from './types'; -export { MapEmbeddableInput }; +export { MapEmbeddableInput, MapEmbeddableOutput }; export class MapEmbeddable extends Embeddable diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 8ec9b8ee976d4..1c47512e0b3de 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -24,7 +24,8 @@ "security", "spaces", "management", - "licenseManagement" + "licenseManagement", + "maps" ], "server": true, "ui": true, @@ -35,7 +36,8 @@ "dashboard", "savedObjects", "home", - "spaces" + "spaces", + "maps" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/ml/public/application/_index.scss b/x-pack/plugins/ml/public/application/_index.scss index 7512c180970ad..da1c226a665f6 100644 --- a/x-pack/plugins/ml/public/application/_index.scss +++ b/x-pack/plugins/ml/public/application/_index.scss @@ -30,6 +30,7 @@ @import 'components/navigation_menu/index'; @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly @import 'components/stats_bar/index'; + @import 'components/ml_embedded_map/index'; // Hacks are last so they can overwrite anything above if needed @import 'hacks'; diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index d3a055f957c3a..68ee32e24391c 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -77,6 +77,8 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { security: deps.security, licenseManagement: deps.licenseManagement, storage: localStorage, + embeddable: deps.embeddable, + maps: deps.maps, ...coreStart, }; @@ -118,6 +120,7 @@ export const renderApp = ( http: coreStart.http, security: deps.security, urlGenerators: deps.share.urlGenerators, + maps: deps.maps, }); appMountParams.onAppLeave((actions) => actions.default()); diff --git a/x-pack/plugins/ml/public/application/components/ml_embedded_map/_index.scss b/x-pack/plugins/ml/public/application/components/ml_embedded_map/_index.scss new file mode 100644 index 0000000000000..6d0d30dae670e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_embedded_map/_index.scss @@ -0,0 +1 @@ +@import 'ml_embedded_map'; diff --git a/x-pack/plugins/ml/public/application/components/ml_embedded_map/_ml_embedded_map.scss b/x-pack/plugins/ml/public/application/components/ml_embedded_map/_ml_embedded_map.scss new file mode 100644 index 0000000000000..495fc40ddb27c --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_embedded_map/_ml_embedded_map.scss @@ -0,0 +1,8 @@ +.mlEmbeddedMapContent { + width: 100%; + height: 100%; + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents +} diff --git a/x-pack/plugins/ml/public/application/components/ml_embedded_map/index.ts b/x-pack/plugins/ml/public/application/components/ml_embedded_map/index.ts new file mode 100644 index 0000000000000..ce5dfb5171a6d --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_embedded_map/index.ts @@ -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 { MlEmbeddedMapComponent } from './ml_embedded_map'; diff --git a/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx b/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx new file mode 100644 index 0000000000000..d5fdc9d52a102 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx @@ -0,0 +1,160 @@ +/* + * 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, { useEffect, useRef, useState } from 'react'; + +import { htmlIdGenerator } from '@elastic/eui'; +import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; +import { + MapEmbeddable, + MapEmbeddableInput, + MapEmbeddableOutput, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../maps/public/embeddable'; +import { MAP_SAVED_OBJECT_TYPE, RenderTooltipContentParams } from '../../../../../maps/public'; +import { + EmbeddableFactory, + ErrorEmbeddable, + isErrorEmbeddable, + ViewMode, +} from '../../../../../../../src/plugins/embeddable/public'; +import { useMlKibana } from '../../contexts/kibana'; + +export function MlEmbeddedMapComponent({ + layerList, + mapEmbeddableInput, + renderTooltipContent, +}: { + layerList: LayerDescriptor[]; + mapEmbeddableInput?: MapEmbeddableInput; + renderTooltipContent?: (params: RenderTooltipContentParams) => JSX.Element; +}) { + const [embeddable, setEmbeddable] = useState(); + + const embeddableRoot: React.RefObject = useRef(null); + const baseLayers = useRef(); + + const { + services: { embeddable: embeddablePlugin, maps: mapsPlugin }, + } = useMlKibana(); + + const factory: + | EmbeddableFactory + | undefined = embeddablePlugin + ? embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE) + : undefined; + + // Update the layer list with updated geo points upon refresh + useEffect(() => { + async function updateIndexPatternSearchLayer() { + if ( + embeddable && + !isErrorEmbeddable(embeddable) && + Array.isArray(layerList) && + Array.isArray(baseLayers.current) + ) { + embeddable.setLayerList([...baseLayers.current, ...layerList]); + } + } + updateIndexPatternSearchLayer(); + }, [embeddable, layerList]); + + useEffect(() => { + async function setupEmbeddable() { + if (!factory) { + // eslint-disable-next-line no-console + console.error('Map embeddable not found.'); + return; + } + const input: MapEmbeddableInput = { + id: htmlIdGenerator()(), + attributes: { title: '' }, + filters: [], + hidePanelTitles: true, + refreshConfig: { + value: 0, + pause: false, + }, + viewMode: ViewMode.VIEW, + isLayerTOCOpen: false, + hideFilterActions: true, + // Zoom Lat/Lon values are set to make sure map is in center in the panel + // It will also omit Greenland/Antarctica etc. NOTE: Can be removed when initialLocation is set + mapCenter: { + lon: 11, + lat: 20, + zoom: 1, + }, + // can use mapSettings to center map on anomalies + mapSettings: { + disableInteractive: false, + hideToolbarOverlay: false, + hideLayerControl: false, + hideViewControl: false, + // Doesn't currently work with GEO_JSON. Will uncomment when https://github.com/elastic/kibana/pull/88294 is in + // initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS, // this will startup based on data-extent + autoFitToDataBounds: true, // this will auto-fit when there are changes to the filter and/or query + }, + }; + + const embeddableObject = await factory.create(input); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + const basemapLayerDescriptor = mapsPlugin + ? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor() + : null; + + if (basemapLayerDescriptor) { + baseLayers.current = [basemapLayerDescriptor]; + await embeddableObject.setLayerList(baseLayers.current); + } + } + + setEmbeddable(embeddableObject); + } + + setupEmbeddable(); + // we want this effect to execute exactly once after the component mounts + }, []); + + useEffect(() => { + if (embeddable && !isErrorEmbeddable(embeddable) && mapEmbeddableInput !== undefined) { + embeddable.updateInput(mapEmbeddableInput); + } + }, [embeddable, mapEmbeddableInput]); + + useEffect(() => { + if (embeddable && !isErrorEmbeddable(embeddable) && renderTooltipContent !== undefined) { + embeddable.setRenderTooltipContent(renderTooltipContent); + } + }, [embeddable, renderTooltipContent]); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot]); + + if (!embeddablePlugin) { + // eslint-disable-next-line no-console + console.error('Embeddable start plugin not found'); + return null; + } + if (!mapsPlugin) { + // eslint-disable-next-line no-console + console.error('Maps start plugin not found'); + return null; + } + + return ( +
+ ); +} diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 4a7550788db56..a97fd513c9c99 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -15,12 +15,16 @@ import { LicenseManagementUIPluginSetup } from '../../../../../license_managemen import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; import { MlServicesContext } from '../../app'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; +import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; +import { MapsStartApi } from '../../../../../maps/public'; interface StartPlugins { data: DataPublicPluginStart; security?: SecurityPluginSetup; licenseManagement?: LicenseManagementUIPluginSetup; share: SharePluginStart; + embeddable: EmbeddableStart; + maps?: MapsStartApi; } export type StartServices = CoreStart & StartPlugins & { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/file_based_expanded_row.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/file_based_expanded_row.tsx index 77f31ae9c2322..a8d8eb18776ec 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/file_based_expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/file_based_expanded_row.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { BooleanContent, DateContent, - GeoPointContent, IpContent, KeywordContent, OtherContent, TextContent, NumberContent, } from '../../../stats_table/components/field_data_expanded_row'; +import { GeoPointContent } from './geo_point_content/geo_point_content'; import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import type { FileBasedFieldVisConfig } from '../../../stats_table/types/field_vis_config'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/format_utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/format_utils.ts new file mode 100644 index 0000000000000..8a442e345014d --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/format_utils.ts @@ -0,0 +1,76 @@ +/* + * 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 { Feature, Point } from 'geojson'; +import { euiPaletteColorBlind } from '@elastic/eui'; +import { DEFAULT_GEO_REGEX } from './geo_point_content'; +import { SOURCE_TYPES } from '../../../../../../../../maps/common/constants'; + +export const convertWKTGeoToLonLat = ( + value: string | number +): { lat: number; lon: number } | undefined => { + if (typeof value === 'string') { + const trimmedValue = value.trim().replace('POINT (', '').replace(')', ''); + const regExpSerializer = DEFAULT_GEO_REGEX; + const parsed = regExpSerializer.exec(trimmedValue.trim()); + + if (parsed?.groups?.lat != null && parsed?.groups?.lon != null) { + return { + lat: parseFloat(parsed.groups.lat.trim()), + lon: parseFloat(parsed.groups.lon.trim()), + }; + } + } +}; + +export const DEFAULT_POINT_COLOR = euiPaletteColorBlind()[0]; +export const getGeoPointsLayer = ( + features: Array>, + pointColor: string = DEFAULT_POINT_COLOR +) => { + return { + id: 'geo_points', + label: 'Geo points', + sourceDescriptor: { + type: SOURCE_TYPES.GEOJSON_FILE, + __featureCollection: { + features, + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: pointColor, + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/geo_point_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/geo_point_content.tsx new file mode 100644 index 0000000000000..a498b786229bb --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/geo_point_content.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useMemo } from 'react'; + +import { EuiFlexItem } from '@elastic/eui'; +import { Feature, Point } from 'geojson'; +import type { FieldDataRowProps } from '../../../../stats_table/types/field_data_row'; +import { DocumentStatsTable } from '../../../../stats_table/components/field_data_expanded_row/document_stats'; +import { MlEmbeddedMapComponent } from '../../../../../components/ml_embedded_map'; +import { convertWKTGeoToLonLat, getGeoPointsLayer } from './format_utils'; +import { ExpandedRowContent } from '../../../../stats_table/components/field_data_expanded_row/expanded_row_content'; +import { ExamplesList } from '../../../../index_based/components/field_data_row/examples_list'; + +export const DEFAULT_GEO_REGEX = RegExp('(?.+) (?.+)'); + +export const GeoPointContent: FC = ({ config }) => { + const formattedResults = useMemo(() => { + const { stats } = config; + + if (stats === undefined || stats.topValues === undefined) return null; + if (Array.isArray(stats.topValues)) { + const geoPointsFeatures: Array> = []; + + // reformatting the top values from POINT (-2.359207 51.37837) to (-2.359207, 51.37837) + const formattedExamples = []; + + for (let i = 0; i < stats.topValues.length; i++) { + const value = stats.topValues[i]; + const coordinates = convertWKTGeoToLonLat(value.key); + if (coordinates) { + const formattedGeoPoint = `(${coordinates.lat}, ${coordinates.lon})`; + formattedExamples.push(coordinates); + + geoPointsFeatures.push({ + type: 'Feature', + id: `ml-${config.fieldName}-${i}`, + geometry: { + type: 'Point', + coordinates: [coordinates.lat, coordinates.lon], + }, + properties: { + value: formattedGeoPoint, + count: value.doc_count, + }, + }); + } + } + + if (geoPointsFeatures.length > 0) { + return { + examples: formattedExamples, + layerList: [getGeoPointsLayer(geoPointsFeatures)], + }; + } + } + }, [config]); + return ( + + + {formattedResults && Array.isArray(formattedResults.examples) && ( + + + + )} + {formattedResults && Array.isArray(formattedResults.layerList) && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/index.ts new file mode 100644 index 0000000000000..d3a50db45ec57 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/geo_point_content/index.ts @@ -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 { GeoPointContent } from './geo_point_content'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss index 957ca0eec24d3..c1191ad270e4c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss @@ -1 +1 @@ -@import 'file_datavisualizer_view' +@import 'file_datavisualizer_view'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx index 7018f73ff6c32..6d58efee36f36 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx @@ -6,23 +6,31 @@ import React from 'react'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import { LoadingIndicator } from '../field_data_row/loading_indicator'; +import { NotInDocsContent } from '../field_data_row/content_types'; import { FieldVisConfig } from '../../../stats_table/types'; import { BooleanContent, DateContent, - GeoPointContent, IpContent, KeywordContent, NumberContent, OtherContent, TextContent, } from '../../../stats_table/components/field_data_expanded_row'; +import { CombinedQuery, GeoPointContent } from './geo_point_content'; +import { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; -import { LoadingIndicator } from '../field_data_row/loading_indicator'; -import { NotInDocsContent } from '../field_data_row/content_types'; - -export const IndexBasedDataVisualizerExpandedRow = ({ item }: { item: FieldVisConfig }) => { +export const IndexBasedDataVisualizerExpandedRow = ({ + item, + indexPattern, + combinedQuery, +}: { + item: FieldVisConfig; + indexPattern: IndexPattern | undefined; + combinedQuery: CombinedQuery; +}) => { const config = item; const { loading, type, existsInDocs, fieldName } = config; @@ -42,7 +50,13 @@ export const IndexBasedDataVisualizerExpandedRow = ({ item }: { item: FieldVisCo return ; case ML_JOB_FIELD_TYPES.GEO_POINT: - return ; + return ( + + ); case ML_JOB_FIELD_TYPES.IP: return ; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx new file mode 100644 index 0000000000000..94db5d1ba686c --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.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, useEffect, useState } from 'react'; + +import { EuiFlexItem } from '@elastic/eui'; +import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; +import { FieldVisConfig } from '../../../stats_table/types'; +import { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { MlEmbeddedMapComponent } from '../../../../components/ml_embedded_map'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import { ES_GEO_FIELD_TYPE } from '../../../../../../../maps/common/constants'; +import { LayerDescriptor } from '../../../../../../../maps/common/descriptor_types'; +import { useMlKibana } from '../../../../contexts/kibana'; +import { DocumentStatsTable } from '../../../stats_table/components/field_data_expanded_row/document_stats'; +import { ExpandedRowContent } from '../../../stats_table/components/field_data_expanded_row/expanded_row_content'; + +export interface CombinedQuery { + searchString: string | { [key: string]: any }; + searchQueryLanguage: string; +} +export const GeoPointContent: FC<{ + config: FieldVisConfig; + indexPattern: IndexPattern | undefined; + combinedQuery: CombinedQuery; +}> = ({ config, indexPattern, combinedQuery }) => { + const { stats } = config; + const [layerList, setLayerList] = useState([]); + const { + services: { maps: mapsPlugin }, + } = useMlKibana(); + + // Update the layer list with updated geo points upon refresh + useEffect(() => { + async function updateIndexPatternSearchLayer() { + if ( + indexPattern?.id !== undefined && + config !== undefined && + config.fieldName !== undefined && + config.type === ML_JOB_FIELD_TYPES.GEO_POINT + ) { + const params = { + indexPatternId: indexPattern.id, + geoFieldName: config.fieldName, + geoFieldType: config.type as ES_GEO_FIELD_TYPE.GEO_POINT, + query: { + query: combinedQuery.searchString, + language: combinedQuery.searchQueryLanguage, + }, + }; + const searchLayerDescriptor = mapsPlugin + ? await mapsPlugin.createLayerDescriptors.createESSearchSourceLayerDescriptor(params) + : null; + if (searchLayerDescriptor) { + setLayerList([...layerList, searchLayerDescriptor]); + } + } + } + updateIndexPatternSearchLayer(); + }, [indexPattern, config.fieldName, combinedQuery]); + + if (stats?.examples === undefined) return null; + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/examples_list.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/examples_list.tsx index 1e8f7586258d5..a59c6e0fe4183 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/examples_list.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/examples_list.tsx @@ -15,25 +15,29 @@ interface Props { } export const ExamplesList: FC = ({ examples }) => { - if ( - examples === undefined || - examples === null || - !Array.isArray(examples) || - examples.length === 0 - ) { + if (examples === undefined || examples === null || !Array.isArray(examples)) { return null; } - - const examplesContent = examples.map((example, i) => { - return ( - ); - }); + } else { + examplesContent = examples.map((example, i) => { + return ( + + ); + }); + } return (
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/top_values.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/top_values.tsx index bd83aaa1f6149..8ad48a3e60d3a 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/top_values.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/top_values.tsx @@ -19,9 +19,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import classNames from 'classnames'; import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format'; import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place'; +import { ExpandedRowFieldHeader } from '../../../../stats_table/components/expanded_row_field_header'; +import { FieldVisStats } from '../../../../stats_table/types'; interface Props { - stats: any; + stats: FieldVisStats | undefined; fieldFormat?: any; barColor?: 'primary' | 'secondary' | 'danger' | 'subdued' | 'accent'; compressed?: boolean; @@ -37,6 +39,7 @@ function getPercentLabel(docCount: number, topValuesSampleSize: number): string } export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed }) => { + if (stats === undefined) return null; const { topValues, topValuesSampleSize, @@ -46,51 +49,64 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed } = stats; const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count; return ( -
- {Array.isArray(topValues) && - topValues.map((value: any) => ( - - + + + + +
+ {Array.isArray(topValues) && + topValues.map((value) => ( + + + + + {kibanaFieldFormat(value.key, fieldFormat)} + + + + + + + {progressBarMax !== undefined && ( + + + {getPercentLabel(value.doc_count, progressBarMax)} + + )} - > - - - {kibanaFieldFormat(value.key, fieldFormat)} - - - - - - - - - {getPercentLabel(value.doc_count, progressBarMax)} - - - - ))} - {isTopValuesSampled === true && ( - - - - - - - )} -
+
+ ))} + {isTopValuesSampled === true && ( + + + + + + + )} +
+ ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index e5b243d524034..8cf5e28ad3b5b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useEffect, useMemo, useState } from 'react'; +import React, { FC, Fragment, useEffect, useMemo, useState, useCallback } from 'react'; import { merge } from 'rxjs'; import { EuiFlexGroup, @@ -112,19 +112,6 @@ export const getDefaultDataVisualizerListState = (): Required { - const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName); - if (item !== undefined) { - m[fieldName] = ; - } - return m; - }, {} as ItemIdToExpandedRowMap); -} - export const Page: FC = () => { const mlContext = useMlContext(); const restorableDefaults = getDefaultDataVisualizerListState(); @@ -678,6 +665,26 @@ export const Page: FC = () => { } return { visibleFieldsCount: _visibleFieldsCount, totalFieldsCount: _totalFieldsCount }; }, [overallStats, showEmptyFields]); + + const getItemIdToExpandedRowMap = useCallback( + function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap { + return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => { + const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName); + if (item !== undefined) { + m[fieldName] = ( + + ); + } + return m; + }, {} as ItemIdToExpandedRowMap); + }, + [currentIndexPattern, searchQuery] + ); + const { services: { docLinks }, } = useMlKibana(); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_field_data_row.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_field_data_row.scss index 2ab26f1564a1f..1832b0f78b895 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_field_data_row.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_field_data_row.scss @@ -60,6 +60,21 @@ @include euiCodeFont; } + .mlFieldDataCard__geoContent { + z-index: auto; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + .embPanel__content { + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents + } + } + .mlFieldDataCard__stats { padding: $euiSizeS $euiSizeS 0 $euiSizeS; text-align: center; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_index.scss index 6e7e66db9e03a..9e838c180713f 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_index.scss @@ -1,5 +1,6 @@ -@import 'components/field_data_expanded_row/number_content'; +@import 'components/field_data_expanded_row/index'; @import 'components/field_count_stats/index'; +@import 'components/field_data_row/index'; .mlDataVisualizerFieldExpandedRow { padding-left: $euiSize * 4; @@ -37,6 +38,7 @@ } .mlDataVisualizerSummaryTable { max-width: 350px; + min-width: 250px; .euiTableRow > .euiTableRowCell { border-bottom: 0; } @@ -45,6 +47,10 @@ } } .mlDataVisualizerSummaryTableWrapper { - max-width: 350px; + max-width: 300px; + } + .mlDataVisualizerMapWrapper { + min-height: 300px; + min-width: 600px; } } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/_index.scss index fdc591a140fea..799beec093cca 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/_index.scss @@ -1 +1,7 @@ @import 'number_content'; + +.mlDataVisualizerExpandedRow { + @include euiBreakpoint('xs', 's', 'm') { + flex-direction: column; + } +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/boolean_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/boolean_content.tsx index a75920dd09b34..70e98153fd55c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/boolean_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/boolean_content.tsx @@ -5,7 +5,7 @@ */ import React, { FC, ReactNode, useMemo } from 'react'; -import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiBasicTable, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { Axis, BarSeries, Chart, Settings } from '@elastic/charts'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -16,6 +16,7 @@ import { getTFPercentage } from '../../utils'; import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; import { useDataVizChartTheme } from '../../hooks'; import { DocumentStatsTable } from './document_stats'; +import { ExpandedRowContent } from './expanded_row_content'; function getPercentLabel(value: number): string { if (value === 0) { @@ -85,7 +86,7 @@ export const BooleanContent: FC = ({ config }) => { ); return ( - + @@ -138,6 +139,6 @@ export const BooleanContent: FC = ({ config }) => { /> - + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/date_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/date_content.tsx index 8d122df628381..a6e7901df4e8e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/date_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/date_content.tsx @@ -5,7 +5,7 @@ */ import React, { FC, ReactNode } from 'react'; -import { EuiBasicTable, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiBasicTable, EuiFlexItem } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n'; import type { FieldDataRowProps } from '../../types/field_data_row'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { DocumentStatsTable } from './document_stats'; +import { ExpandedRowContent } from './expanded_row_content'; const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS'; interface SummaryTableItem { function: string; @@ -66,7 +67,7 @@ export const DateContent: FC = ({ config }) => { ]; return ( - + {summaryTableTitle} @@ -80,6 +81,6 @@ export const DateContent: FC = ({ config }) => { tableLayout="auto" /> - + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/document_stats.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/document_stats.tsx index 177ac722166f7..fd23e9d51bf4e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/document_stats.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/document_stats.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiFlexItem } from '@elastic/eui'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { FieldDataRowProps } from '../../types'; +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; const metaTableColumns = [ { @@ -59,7 +60,7 @@ export const DocumentStatsTable: FC = ({ config }) => { defaultMessage="percentage" /> ), - value: `${(count / sampleCount) * 100}%`, + value: `${roundToDecimalPlace((count / sampleCount) * 100)}%`, }, { function: 'distinctValues', diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/expanded_row_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/expanded_row_content.tsx new file mode 100644 index 0000000000000..6c14bd7a64a94 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/expanded_row_content.tsx @@ -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 React, { FC, ReactNode } from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; + +interface Props { + children: ReactNode; + dataTestSubj: string; +} +export const ExpandedRowContent: FC = ({ children, dataTestSubj }) => { + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/geo_point_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/geo_point_content.tsx deleted file mode 100644 index 993c7a94f5e06..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/geo_point_content.tsx +++ /dev/null @@ -1,35 +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, { FC } from 'react'; - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { FieldDataRowProps } from '../../types/field_data_row'; -import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; -import { DocumentStatsTable } from './document_stats'; -import { TopValues } from '../../../index_based/components/field_data_row/top_values'; - -export const GeoPointContent: FC = ({ config }) => { - const { stats } = config; - if (stats === undefined || (stats?.examples === undefined && stats?.topValues === undefined)) - return null; - - return ( - - - {Array.isArray(stats.examples) && ( - - - - )} - {Array.isArray(stats.topValues) && ( - - - - )} - - ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/index.ts index c6cd50f6bc2e9..b9b1e181343b7 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/index.ts @@ -6,7 +6,7 @@ export { BooleanContent } from './boolean_content'; export { DateContent } from './date_content'; -export { GeoPointContent } from './geo_point_content'; +export { GeoPointContent } from '../../../file_based/components/expanded_row/geo_point_content/geo_point_content'; export { KeywordContent } from './keyword_content'; export { IpContent } from './ip_content'; export { NumberContent } from './number_content'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/ip_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/ip_content.tsx index 79492bb44a2dc..183268d6a7ef8 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/ip_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/ip_content.tsx @@ -5,14 +5,10 @@ */ import React, { FC } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - import type { FieldDataRowProps } from '../../types/field_data_row'; import { TopValues } from '../../../index_based/components/field_data_row/top_values'; -import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { DocumentStatsTable } from './document_stats'; +import { ExpandedRowContent } from './expanded_row_content'; export const IpContent: FC = ({ config }) => { const { stats } = config; @@ -22,17 +18,9 @@ export const IpContent: FC = ({ config }) => { const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; return ( - + - - - - - - - + + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx index 634f5b55513a3..d11ecc7d8804b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx @@ -5,31 +5,20 @@ */ import React, { FC } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import type { FieldDataRowProps } from '../../types/field_data_row'; import { TopValues } from '../../../index_based/components/field_data_row/top_values'; -import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { DocumentStatsTable } from './document_stats'; +import { ExpandedRowContent } from './expanded_row_content'; export const KeywordContent: FC = ({ config }) => { const { stats } = config; const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; return ( - + - - - - - - - - + + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/number_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/number_content.tsx index d05de26d3c5d4..56811e61ad891 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/number_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/number_content.tsx @@ -5,7 +5,7 @@ */ import React, { FC, ReactNode, useEffect, useState } from 'react'; -import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiBasicTable, EuiFlexItem, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -20,6 +20,7 @@ import { import { TopValues } from '../../../index_based/components/field_data_row/top_values'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { DocumentStatsTable } from './document_stats'; +import { ExpandedRowContent } from './expanded_row_content'; const METRIC_DISTRIBUTION_CHART_WIDTH = 325; const METRIC_DISTRIBUTION_CHART_HEIGHT = 200; @@ -97,7 +98,7 @@ export const NumberContent: FC = ({ config }) => { } ); return ( - + {summaryTableTitle} @@ -112,24 +113,7 @@ export const NumberContent: FC = ({ config }) => { {stats && ( - - - - - - - - - - + )} {distribution && ( @@ -164,6 +148,6 @@ export const NumberContent: FC = ({ config }) => { )} - + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/other_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/other_content.tsx index a6d7398990cd3..08884619db2d6 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/other_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/other_content.tsx @@ -5,18 +5,18 @@ */ import React, { FC } from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; import type { FieldDataRowProps } from '../../types/field_data_row'; import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; import { DocumentStatsTable } from './document_stats'; +import { ExpandedRowContent } from './expanded_row_content'; export const OtherContent: FC = ({ config }) => { const { stats } = config; if (stats === undefined) return null; return ( - + {Array.isArray(stats.examples) && } - + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/text_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/text_content.tsx index 55639ecc5761f..c0a5ca18d03b5 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/text_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/text_content.tsx @@ -5,13 +5,14 @@ */ import React, { FC, Fragment } from 'react'; -import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import type { FieldDataRowProps } from '../../types/field_data_row'; import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; +import { ExpandedRowContent } from './expanded_row_content'; export const TextContent: FC = ({ config }) => { const { stats } = config; @@ -23,7 +24,7 @@ export const TextContent: FC = ({ config }) => { const numExamples = examples.length; return ( - + {numExamples > 0 && } {numExamples === 0 && ( @@ -59,6 +60,6 @@ export const TextContent: FC = ({ config }) => { )} - + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/_index.scss new file mode 100644 index 0000000000000..27483feb573b8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/_index.scss @@ -0,0 +1,3 @@ +.mlDataVisualizerColumnHeaderIcon { + max-width: $euiSizeM; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/distinct_values.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/distinct_values.tsx index 819d5278c0d78..44c028d1ba8b5 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/distinct_values.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/distinct_values.tsx @@ -12,7 +12,7 @@ export const DistinctValues = ({ cardinality }: { cardinality?: number }) => { if (cardinality === undefined) return null; return ( - + diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/document_stats.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/document_stats.tsx index 9c89d74fa751b..b223fd5d98c1a 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/document_stats.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/document_stats.tsx @@ -21,7 +21,7 @@ export const DocumentStat = ({ config }: FieldDataRowProps) => { return ( - + diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/number_content_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/number_content_preview.tsx index 3a84ae644cb4e..996fffd225f96 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/number_content_preview.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/number_content_preview.tsx @@ -14,6 +14,7 @@ import { } from '../metric_distribution_chart'; import { formatSingleValue } from '../../../../formatters/format_value'; import { FieldVisConfig } from '../../types'; +import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format'; const METRIC_DISTRIBUTION_CHART_WIDTH = 150; const METRIC_DISTRIBUTION_CHART_HEIGHT = 80; @@ -59,14 +60,16 @@ export const IndexBasedNumberContentPreview: FC = ({ <> - {legendText.min} + + {kibanaFieldFormat(legendText.min, fieldFormat)} + - {legendText.max} + {kibanaFieldFormat(legendText.max, fieldFormat)} diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index d7ca0203ab69e..9aa16f521554c 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -21,6 +21,7 @@ import type { import type { IndexPatternsContract, DataPublicPluginStart } from 'src/plugins/data/public'; import type { SharePluginStart } from 'src/plugins/share/public'; import type { SecurityPluginSetup } from '../../../../security/public'; +import type { MapsStartApi } from '../../../../maps/public'; export interface DependencyCache { timefilter: DataPublicPluginSetup['query']['timefilter'] | null; @@ -40,6 +41,7 @@ export interface DependencyCache { security: SecurityPluginSetup | undefined | null; i18n: I18nStart | null; urlGenerators: SharePluginStart['urlGenerators'] | null; + maps: MapsStartApi | null; } const cache: DependencyCache = { @@ -60,6 +62,7 @@ const cache: DependencyCache = { security: null, i18n: null, urlGenerators: null, + maps: null, }; export function setDependencyCache(deps: Partial) { diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 7c32671be93c4..3ba79e0eb9187 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -25,7 +25,7 @@ import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; import type { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; -import type { EmbeddableSetup } from 'src/plugins/embeddable/public'; +import type { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; import type { SpacesPluginStart } from '../../spaces/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; @@ -45,6 +45,7 @@ import { setDependencyCache } from './application/util/dependency_cache'; import { registerFeature } from './register_feature'; // Not importing from `ml_url_generator/index` here to avoid importing unnecessary code import { registerUrlGenerator } from './ml_url_generator/ml_url_generator'; +import type { MapsStartApi } from '../../maps/public'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -52,6 +53,8 @@ export interface MlStartDependencies { kibanaLegacy: KibanaLegacyStart; uiActions: UiActionsStart; spaces?: SpacesPluginStart; + embeddable: EmbeddableStart; + maps?: MapsStartApi; } export interface MlSetupDependencies { security?: SecurityPluginSetup; @@ -102,7 +105,8 @@ export class MlPlugin implements Plugin { usageCollection: pluginsSetup.usageCollection, licenseManagement: pluginsSetup.licenseManagement, home: pluginsSetup.home, - embeddable: pluginsSetup.embeddable, + embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable }, + maps: pluginsStart.maps, uiActions: pluginsStart.uiActions, kibanaVersion, }, diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 7b3f97b684edc..22e75ec694733 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -1189,7 +1189,8 @@ export class DataVisualizer { }); const searchBody = { - _source: field, + fields: [field], + _source: false, query: { bool: { filter: filterCriteria, @@ -1209,16 +1210,16 @@ export class DataVisualizer { if (body.hits.total.value > 0) { const hits = body.hits.hits; for (let i = 0; i < hits.length; i++) { - // Look in the _source for the field value. - // If the field is not in the _source (as will happen if the - // field is populated using copy_to in the index mapping), - // there will be no example to add. // Use lodash get() to support field names containing dots. - const example: any = get(hits[i]._source, field); - if (example !== undefined && stats.examples.indexOf(example) === -1) { - stats.examples.push(example); - if (stats.examples.length === maxExamples) { - break; + const doc: object[] | undefined = get(hits[i].fields, field); + // the results from fields query is always an array + if (Array.isArray(doc) && doc.length > 0) { + const example = doc[0]; + if (example !== undefined && stats.examples.indexOf(example) === -1) { + stats.examples.push(example); + if (stats.examples.length === maxExamples) { + break; + } } } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e2506cb6ca8f0..d798bc9b42c95 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13073,8 +13073,6 @@ "xpack.ml.featureRegistry.mlFeatureName": "機械学習", "xpack.ml.fieldDataCard.cardBoolean.valuesLabel": "値", "xpack.ml.fieldDataCard.cardDate.summaryTableTitle": "まとめ", - "xpack.ml.fieldDataCard.cardIp.topValuesLabel": "トップの値", - "xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "トップの値", "xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "たとえば、ドキュメントマッピングで {copyToParam} パラメーターを使ったり、{includesParam} と {excludesParam} パラメーターを使用してインデックスした後に {sourceParam} フィールドから切り取ったりして入力される場合があります。", "xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "このフィールドはクエリが実行されたドキュメントの {sourceParam} フィールドにありませんでした。", "xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "このフィールドの例が取得されませんでした", @@ -13092,7 +13090,6 @@ "xpack.ml.fieldDataCardExpandedRow.numberContent.medianLabel": "中間", "xpack.ml.fieldDataCardExpandedRow.numberContent.minLabel": "分", "xpack.ml.fieldDataCardExpandedRow.numberContent.summaryTableTitle": "まとめ", - "xpack.ml.fieldDataCardExpandedRow.numberContent.topValuesTitle": "トップの値", "xpack.ml.fieldTitleBar.documentCountLabel": "ドキュメントカウント", "xpack.ml.fieldTypeIcon.booleanTypeAriaLabel": "ブールタイプ", "xpack.ml.fieldTypeIcon.dateTypeAriaLabel": "日付タイプ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7b8ea35baf0ff..c6f2965803813 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13104,8 +13104,6 @@ "xpack.ml.featureRegistry.mlFeatureName": "Machine Learning", "xpack.ml.fieldDataCard.cardBoolean.valuesLabel": "值", "xpack.ml.fieldDataCard.cardDate.summaryTableTitle": "摘要", - "xpack.ml.fieldDataCard.cardIp.topValuesLabel": "排名最前值", - "xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "排名最前值", "xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "例如,可以使用文档映射中的 {copyToParam} 参数进行填充,也可以在索引后通过使用 {includesParam} 和 {excludesParam} 参数从 {sourceParam} 字段中修剪。", "xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "查询的文档的 {sourceParam} 字段中不存在此字段。", "xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "没有获取此字段的示例", @@ -13123,7 +13121,6 @@ "xpack.ml.fieldDataCardExpandedRow.numberContent.medianLabel": "中值", "xpack.ml.fieldDataCardExpandedRow.numberContent.minLabel": "最小值", "xpack.ml.fieldDataCardExpandedRow.numberContent.summaryTableTitle": "摘要", - "xpack.ml.fieldDataCardExpandedRow.numberContent.topValuesTitle": "排名最前值", "xpack.ml.fieldTitleBar.documentCountLabel": "文档计数", "xpack.ml.fieldTypeIcon.booleanTypeAriaLabel": "布尔类型", "xpack.ml.fieldTypeIcon.dateTypeAriaLabel": "日期类型", diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index 531eba54f931d..10926a831d36b 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -112,6 +112,47 @@ export default function ({ getService }: FtrProviderContext) { fieldNameFiltersResultCount: 1, }, }, + { + suiteSuffix: 'with a file containing geo field', + filePath: path.join(__dirname, 'files_to_import', 'geo_file.csv'), + indexName: 'user-import_2', + createIndexPattern: false, + fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT], + fieldNameFilters: ['Coordinates'], + expected: { + results: { + title: 'geo_file.csv', + numberOfFields: 3, + }, + metricFields: [], + nonMetricFields: [ + { + fieldName: 'Context', + type: ML_JOB_FIELD_TYPES.UNKNOWN, + docCountFormatted: '0 (0%)', + exampleCount: 0, + }, + { + fieldName: 'Coordinates', + type: ML_JOB_FIELD_TYPES.GEO_POINT, + docCountFormatted: '13 (100%)', + exampleCount: 7, + }, + { + fieldName: 'Location', + type: ML_JOB_FIELD_TYPES.KEYWORD, + docCountFormatted: '13 (100%)', + exampleCount: 7, + }, + ], + visibleMetricFieldsCount: 0, + totalMetricFieldsCount: 0, + populatedFieldsCount: 3, + totalFieldsCount: 3, + fieldTypeFiltersResultCount: 1, + fieldNameFiltersResultCount: 1, + }, + }, ]; const testDataListNegative = [ diff --git a/x-pack/test/functional/apps/ml/data_visualizer/files_to_import/geo_file.csv b/x-pack/test/functional/apps/ml/data_visualizer/files_to_import/geo_file.csv new file mode 100644 index 0000000000000..df7417f474d83 --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_visualizer/files_to_import/geo_file.csv @@ -0,0 +1,14 @@ +Coordinates,Location,Context +POINT (-2.516919 51.423683),On or near A4175, +POINT (-2.515072 51.419357),On or near Stockwood Hill, +POINT (-2.509126 51.416137),On or near St Francis Road, +POINT (-2.509384 51.40959),On or near Barnard Walk, +POINT (-2.509126 51.416137),On or near St Francis Road, +POINT (-2.516919 51.423683),On or near A4175, +POINT (-2.511571 51.414895),On or near Orchard Close, +POINT (-2.534338 51.417697),On or near Scotland Lane, +POINT (-2.509384 51.40959),On or near Barnard Walk, +POINT (-2.495055 51.422132),On or near Cross Street, +POINT (-2.509384 51.40959),On or near Barnard Walk, +POINT (-2.495055 51.422132),On or near Cross Street, +POINT (-2.509126 51.416137),On or near St Francis Road, \ No newline at end of file diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index 0833f84960ea6..01d7ca6af4cc3 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -24,6 +24,11 @@ interface TestData { sourceIndexOrSavedSearch: string; fieldNameFilters: string[]; fieldTypeFilters: string[]; + rowsPerPage?: 10 | 25 | 50; + sampleSizeValidations: Array<{ + size: number; + expected: { field: string; docCountFormatted: string }; + }>; expected: { totalDocCountFormatted: string; metricFields?: MetricFieldVisConfig[]; @@ -47,6 +52,10 @@ export default function ({ getService }: FtrProviderContext) { sourceIndexOrSavedSearch: 'ft_farequote', fieldNameFilters: ['airline', '@timestamp'], fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD], + sampleSizeValidations: [ + { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, + ], expected: { totalDocCountFormatted: '86,274', metricFields: [ @@ -132,6 +141,10 @@ export default function ({ getService }: FtrProviderContext) { sourceIndexOrSavedSearch: 'ft_farequote_kuery', fieldNameFilters: ['@version'], fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT], + sampleSizeValidations: [ + { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, + ], expected: { totalDocCountFormatted: '34,415', metricFields: [ @@ -217,6 +230,10 @@ export default function ({ getService }: FtrProviderContext) { sourceIndexOrSavedSearch: 'ft_farequote_lucene', fieldNameFilters: ['@version.keyword', 'type'], fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER], + sampleSizeValidations: [ + { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, + ], expected: { totalDocCountFormatted: '34,416', metricFields: [ @@ -297,6 +314,41 @@ export default function ({ getService }: FtrProviderContext) { }, }; + const sampleLogTestData: TestData = { + suiteTitle: 'geo point field', + sourceIndexOrSavedSearch: 'ft_module_sample_logs', + fieldNameFilters: ['geo.coordinates'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT], + rowsPerPage: 50, + expected: { + totalDocCountFormatted: '408', + metricFields: [], + // only testing the geo_point fields + nonMetricFields: [ + { + fieldName: 'geo.coordinates', + type: ML_JOB_FIELD_TYPES.GEO_POINT, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '408 (100%)', + exampleCount: 10, + }, + ], + emptyFields: [], + visibleMetricFieldsCount: 4, + totalMetricFieldsCount: 5, + populatedFieldsCount: 35, + totalFieldsCount: 36, + fieldNameFiltersResultCount: 1, + fieldTypeFiltersResultCount: 1, + }, + sampleSizeValidations: [ + { size: 1000, expected: { field: 'geo.coordinates', docCountFormatted: '408 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '408 (100%)' } }, + ], + }; + function runTests(testData: TestData) { it(`${testData.suiteTitle} loads the source data in the data visualizer`, async () => { await ml.testExecution.logTestStep( @@ -332,6 +384,10 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); + if (testData.rowsPerPage) { + await ml.dataVisualizerTable.ensureNumRowsPerPage(testData.rowsPerPage); + } + await ml.dataVisualizerTable.assertSearchPanelExist(); await ml.dataVisualizerTable.assertSampleSizeInputExists(); await ml.dataVisualizerTable.assertFieldTypeInputExists(); @@ -376,8 +432,14 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( `${testData.suiteTitle} sample size control changes non-metric fields` ); - await ml.dataVisualizerTable.setSampleSizeInputValue(1000, 'airline', '1000 (100%)'); - await ml.dataVisualizerTable.setSampleSizeInputValue(5000, '@timestamp', '5000 (100%)'); + for (const sampleSizeCase of testData.sampleSizeValidations) { + const { size, expected } = sampleSizeCase; + await ml.dataVisualizerTable.setSampleSizeInputValue( + size, + expected.field, + expected.docCountFormatted + ); + } await ml.testExecution.logTestStep('sets and resets field type filter correctly'); await ml.dataVisualizerTable.setFieldTypeFilter( @@ -411,7 +473,10 @@ export default function ({ getService }: FtrProviderContext) { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); + await esArchiver.loadIfNeeded('ml/module_sample_logs'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_module_sample_logs', '@timestamp'); await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded(); await ml.testResources.createSavedSearchFarequoteKueryIfNeeded(); await ml.testResources.setKibanaTimeZoneToUTC(); @@ -447,5 +512,15 @@ export default function ({ getService }: FtrProviderContext) { runTests(farequoteLuceneSearchTestData); }); + + describe('with module_sample_logs ', function () { + // Run tests on full farequote index. + it(`${sampleLogTestData.suiteTitle} loads the data visualizer selector page`, async () => { + // Start navigation from the base of the ML app. + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataVisualizer(); + }); + runTests(sampleLogTestData); + }); }); } diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index ad4625ed4dcb4..4772b3c894471 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -288,6 +288,16 @@ export function MachineLearningDataVisualizerTableProvider( await this.ensureDetailsClosed(fieldName); } + public async assertExamplesList(fieldName: string, expectedExamplesCount: number) { + const examplesList = await testSubjects.find( + this.detailsSelector(fieldName, 'mlFieldDataExamplesList') + ); + const examplesListItems = await examplesList.findAllByTagName('li'); + expect(examplesListItems).to.have.length( + expectedExamplesCount, + `Expected example list item count for field '${fieldName}' to be '${expectedExamplesCount}' (got '${examplesListItems.length}')` + ); + } public async assertTextFieldContents( fieldName: string, docCountFormatted: string, @@ -297,14 +307,33 @@ export function MachineLearningDataVisualizerTableProvider( await this.assertFieldDocCount(fieldName, docCountFormatted); await this.ensureDetailsOpen(fieldName); - const examplesList = await testSubjects.find( - this.detailsSelector(fieldName, 'mlFieldDataExamplesList') - ); - const examplesListItems = await examplesList.findAllByTagName('li'); - expect(examplesListItems).to.have.length( - expectedExamplesCount, - `Expected example list item count for field '${fieldName}' to be '${expectedExamplesCount}' (got '${examplesListItems.length}')` - ); + await this.assertExamplesList(fieldName, expectedExamplesCount); + await this.ensureDetailsClosed(fieldName); + } + + public async assertGeoPointFieldContents( + fieldName: string, + docCountFormatted: string, + expectedExamplesCount: number + ) { + await this.assertRowExists(fieldName); + await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); + + await this.assertExamplesList(fieldName, expectedExamplesCount); + + await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlEmbeddedMapContent')); + + await this.ensureDetailsClosed(fieldName); + } + + public async assertUnknownFieldContents(fieldName: string, docCountFormatted: string) { + await this.assertRowExists(fieldName); + await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); + + await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlDVDocumentStatsContent')); + await this.ensureDetailsClosed(fieldName); } @@ -321,10 +350,14 @@ export function MachineLearningDataVisualizerTableProvider( await this.assertKeywordFieldContents(fieldName, docCountFormatted, exampleCount); } else if (fieldType === ML_JOB_FIELD_TYPES.TEXT) { await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount); + } else if (fieldType === ML_JOB_FIELD_TYPES.GEO_POINT) { + await this.assertGeoPointFieldContents(fieldName, docCountFormatted, exampleCount); + } else if (fieldType === ML_JOB_FIELD_TYPES.UNKNOWN) { + await this.assertUnknownFieldContents(fieldName, docCountFormatted); } } - public async ensureNumRowsPerPage(n: 10 | 25 | 100) { + public async ensureNumRowsPerPage(n: 10 | 25 | 50) { const paginationButton = 'mlDataVisualizerTable > tablePaginationPopoverButton'; await retry.tryForTime(10000, async () => { await testSubjects.existOrFail(paginationButton); From 42a9490e7bb967649043fe59af5048bae3842505 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Wed, 27 Jan 2021 17:58:06 +0000 Subject: [PATCH 048/163] [Logs UI] Display category in anomalies table (#88677) * Add category pattern to anomalies table --- .../results/log_entry_anomalies.ts | 86 ++----------------- .../results/log_entry_categories.ts | 54 +----------- .../results/log_entry_category_examples.ts | 2 +- .../results/log_entry_examples.ts | 12 +-- .../common/http_api/log_entries/entries.ts | 51 +---------- .../common/http_api/log_entries/highlights.ts | 3 +- .../common/http_api/shared/time_range.ts | 9 +- .../infra/common/log_analysis/index.ts | 2 + .../log_analysis/log_analysis_results.ts | 43 ++++++++++ .../log_analysis/log_entry_anomalies.ts | 59 +++++++++++++ .../log_entry_categories_analysis.ts | 42 +++++++++ .../common/log_analysis/log_entry_examples.ts | 17 ++++ .../infra/common/log_entry/log_entry.ts | 54 +++++++++++- x-pack/plugins/infra/common/time/index.ts | 1 + .../plugins/infra/common/time/time_range.ts | 14 +++ .../category_expression.tsx | 2 +- .../logging/log_text_stream/item.ts | 2 +- .../log_entry_field_column.test.tsx | 2 +- .../log_entry_field_column.tsx | 2 +- .../log_entry_message_column.test.tsx | 2 +- .../log_entry_message_column.tsx | 2 +- .../logging/log_text_stream/log_entry_row.tsx | 2 +- .../scrollable_log_text_stream_view.tsx | 2 +- .../containers/logs/log_entries/index.ts | 2 +- .../log_highlights/log_entry_highlights.tsx | 3 +- .../containers/logs/log_stream/index.ts | 3 +- .../view_log_in_context.ts | 2 +- .../containers/logs/with_stream_items.ts | 2 +- .../page_results_content.tsx | 2 +- .../analyze_dataset_in_ml_action.tsx | 2 +- .../anomaly_severity_indicator_list.tsx | 2 +- .../top_categories/category_details_row.tsx | 2 +- .../category_example_message.tsx | 4 +- .../top_categories/datasets_action_list.tsx | 4 +- .../sections/top_categories/datasets_list.tsx | 2 +- .../log_entry_count_sparkline.tsx | 4 +- .../single_metric_sparkline.tsx | 2 +- .../top_categories/top_categories_section.tsx | 4 +- .../top_categories/top_categories_table.tsx | 6 +- .../get_top_log_entry_categories.ts | 4 +- .../use_log_entry_categories_results.ts | 6 +- .../log_entry_rate/page_results_content.tsx | 2 +- .../sections/anomalies/chart.tsx | 2 +- .../sections/anomalies/expanded_row.tsx | 6 +- .../sections/anomalies/index.tsx | 2 +- .../sections/anomalies/log_entry_example.tsx | 8 +- .../sections/anomalies/table.tsx | 41 +++++---- .../service_calls/get_log_entry_anomalies.ts | 4 +- .../use_log_entry_anomalies_results.ts | 18 ++-- .../log_entry_rate/use_log_entry_examples.ts | 2 +- .../logs/stream/page_view_log_in_context.tsx | 2 +- .../infra/public/test_utils/entries.ts | 2 +- .../infra/public/utils/log_entry/log_entry.ts | 2 +- .../utils/log_entry/log_entry_highlight.ts | 2 +- .../log_entries_domain/log_entries_domain.ts | 3 +- .../lib/domains/log_entries_domain/message.ts | 2 +- .../lib/log_analysis/log_entry_anomalies.ts | 52 ++++++++--- .../log_entry_categories_analysis.ts | 8 +- .../queries/log_entry_anomalies.ts | 12 +-- .../queries/top_log_entry_categories.ts | 6 +- .../results/log_entry_anomalies.ts | 5 +- .../apis/metrics_ui/log_entries.ts | 5 +- 62 files changed, 394 insertions(+), 313 deletions(-) create mode 100644 x-pack/plugins/infra/common/log_analysis/log_entry_anomalies.ts create mode 100644 x-pack/plugins/infra/common/log_analysis/log_entry_examples.ts create mode 100644 x-pack/plugins/infra/common/time/time_range.ts rename x-pack/plugins/infra/public/{pages/logs/log_entry_categories/sections/top_categories => components/logging/log_analysis_results}/category_expression.tsx (95%) diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_anomalies.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_anomalies.ts index 62b76a0ae475e..614684d29ae76 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_anomalies.ts @@ -7,48 +7,17 @@ import * as rt from 'io-ts'; import { timeRangeRT, routeTimingMetadataRT } from '../../shared'; +import { + logEntryAnomalyRT, + logEntryAnomalyDatasetsRT, + anomaliesSortRT, + paginationRT, + paginationCursorRT, +} from '../../../log_analysis'; export const LOG_ANALYSIS_GET_LOG_ENTRY_ANOMALIES_PATH = '/api/infra/log_analysis/results/log_entry_anomalies'; -// [Sort field value, tiebreaker value] -const paginationCursorRT = rt.tuple([ - rt.union([rt.string, rt.number]), - rt.union([rt.string, rt.number]), -]); - -export type PaginationCursor = rt.TypeOf; - -export const anomalyTypeRT = rt.keyof({ - logRate: null, - logCategory: null, -}); - -export type AnomalyType = rt.TypeOf; - -const logEntryAnomalyCommonFieldsRT = rt.type({ - id: rt.string, - anomalyScore: rt.number, - dataset: rt.string, - typical: rt.number, - actual: rt.number, - type: anomalyTypeRT, - duration: rt.number, - startTime: rt.number, - jobId: rt.string, -}); -const logEntrylogRateAnomalyRT = logEntryAnomalyCommonFieldsRT; -const logEntrylogCategoryAnomalyRT = rt.partial({ - categoryId: rt.string, -}); -const logEntryAnomalyRT = rt.intersection([ - logEntryAnomalyCommonFieldsRT, - logEntrylogRateAnomalyRT, - logEntrylogCategoryAnomalyRT, -]); - -export type LogEntryAnomaly = rt.TypeOf; - export const getLogEntryAnomaliesSuccessReponsePayloadRT = rt.intersection([ rt.type({ data: rt.intersection([ @@ -78,43 +47,6 @@ export type GetLogEntryAnomaliesSuccessResponsePayload = rt.TypeOf< typeof getLogEntryAnomaliesSuccessReponsePayloadRT >; -const sortOptionsRT = rt.keyof({ - anomalyScore: null, - dataset: null, - startTime: null, -}); - -const sortDirectionsRT = rt.keyof({ - asc: null, - desc: null, -}); - -const paginationPreviousPageCursorRT = rt.type({ - searchBefore: paginationCursorRT, -}); - -const paginationNextPageCursorRT = rt.type({ - searchAfter: paginationCursorRT, -}); - -const paginationRT = rt.intersection([ - rt.type({ - pageSize: rt.number, - }), - rt.partial({ - cursor: rt.union([paginationPreviousPageCursorRT, paginationNextPageCursorRT]), - }), -]); - -export type Pagination = rt.TypeOf; - -const sortRT = rt.type({ - field: sortOptionsRT, - direction: sortDirectionsRT, -}); - -export type Sort = rt.TypeOf; - export const getLogEntryAnomaliesRequestPayloadRT = rt.type({ data: rt.intersection([ rt.type({ @@ -127,9 +59,9 @@ export const getLogEntryAnomaliesRequestPayloadRT = rt.type({ // Pagination properties pagination: paginationRT, // Sort properties - sort: sortRT, + sort: anomaliesSortRT, // Dataset filters - datasets: rt.array(rt.string), + datasets: logEntryAnomalyDatasetsRT, }), ]), }); diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_categories.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_categories.ts index 0554192398fc5..019ae01c1437c 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_categories.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_categories.ts @@ -13,6 +13,8 @@ import { routeTimingMetadataRT, } from '../../shared'; +import { logEntryCategoryRT, categoriesSortRT } from '../../../log_analysis'; + export const LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH = '/api/infra/log_analysis/results/log_entry_categories'; @@ -30,23 +32,6 @@ export type LogEntryCategoriesHistogramParameters = rt.TypeOf< typeof logEntryCategoriesHistogramParametersRT >; -const sortOptionsRT = rt.keyof({ - maximumAnomalyScore: null, - logEntryCount: null, -}); - -const sortDirectionsRT = rt.keyof({ - asc: null, - desc: null, -}); - -const categorySortRT = rt.type({ - field: sortOptionsRT, - direction: sortDirectionsRT, -}); - -export type CategorySort = rt.TypeOf; - export const getLogEntryCategoriesRequestPayloadRT = rt.type({ data: rt.intersection([ rt.type({ @@ -59,7 +44,7 @@ export const getLogEntryCategoriesRequestPayloadRT = rt.type({ // a list of histograms to create histograms: rt.array(logEntryCategoriesHistogramParametersRT), // the criteria to the categories by - sort: categorySortRT, + sort: categoriesSortRT, }), rt.partial({ // the datasets to filter for (optional, unfiltered if not present) @@ -76,39 +61,6 @@ export type GetLogEntryCategoriesRequestPayload = rt.TypeOf< * response */ -export const logEntryCategoryHistogramBucketRT = rt.type({ - startTime: rt.number, - bucketDuration: rt.number, - logEntryCount: rt.number, -}); - -export type LogEntryCategoryHistogramBucket = rt.TypeOf; - -export const logEntryCategoryHistogramRT = rt.type({ - histogramId: rt.string, - buckets: rt.array(logEntryCategoryHistogramBucketRT), -}); - -export type LogEntryCategoryHistogram = rt.TypeOf; - -export const logEntryCategoryDatasetRT = rt.type({ - name: rt.string, - maximumAnomalyScore: rt.number, -}); - -export type LogEntryCategoryDataset = rt.TypeOf; - -export const logEntryCategoryRT = rt.type({ - categoryId: rt.number, - datasets: rt.array(logEntryCategoryDatasetRT), - histograms: rt.array(logEntryCategoryHistogramRT), - logEntryCount: rt.number, - maximumAnomalyScore: rt.number, - regularExpression: rt.string, -}); - -export type LogEntryCategory = rt.TypeOf; - export const getLogEntryCategoriesSuccessReponsePayloadRT = rt.intersection([ rt.type({ data: rt.type({ diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts index e9e3c6e0ca3f9..3166d40d70392 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts @@ -12,7 +12,7 @@ import { timeRangeRT, routeTimingMetadataRT, } from '../../shared'; -import { logEntryContextRT } from '../../log_entries'; +import { logEntryContextRT } from '../../../log_entry'; export const LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH = '/api/infra/log_analysis/results/log_entry_category_examples'; diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_examples.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_examples.ts index 1eed29cd37560..c061545ec09ed 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_examples.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_examples.ts @@ -5,7 +5,7 @@ */ import * as rt from 'io-ts'; - +import { logEntryExampleRT } from '../../../log_analysis'; import { badRequestErrorRT, forbiddenErrorRT, @@ -46,16 +46,6 @@ export type GetLogEntryExamplesRequestPayload = rt.TypeOf< * response */ -const logEntryExampleRT = rt.type({ - id: rt.string, - dataset: rt.string, - message: rt.string, - timestamp: rt.number, - tiebreaker: rt.number, -}); - -export type LogEntryExample = rt.TypeOf; - export const getLogEntryExamplesSuccessReponsePayloadRT = rt.intersection([ rt.type({ data: rt.type({ diff --git a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index 31bc62f48791a..b4d9a5744d5ac 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts @@ -5,8 +5,7 @@ */ import * as rt from 'io-ts'; -import { logEntryCursorRT } from '../../log_entry'; -import { jsonArrayRT } from '../../typed_json'; +import { logEntryCursorRT, logEntryRT } from '../../log_entry'; import { logSourceColumnConfigurationRT } from '../log_sources'; export const LOG_ENTRIES_PATH = '/api/log_entries/entries'; @@ -52,54 +51,6 @@ export type LogEntriesAfterRequest = rt.TypeOf; export type LogEntriesCenteredRequest = rt.TypeOf; export type LogEntriesRequest = rt.TypeOf; -export const logMessageConstantPartRT = rt.type({ - constant: rt.string, -}); -export const logMessageFieldPartRT = rt.type({ - field: rt.string, - value: jsonArrayRT, - highlights: rt.array(rt.string), -}); - -export const logMessagePartRT = rt.union([logMessageConstantPartRT, logMessageFieldPartRT]); - -export const logTimestampColumnRT = rt.type({ columnId: rt.string, timestamp: rt.number }); -export const logFieldColumnRT = rt.type({ - columnId: rt.string, - field: rt.string, - value: jsonArrayRT, - highlights: rt.array(rt.string), -}); -export const logMessageColumnRT = rt.type({ - columnId: rt.string, - message: rt.array(logMessagePartRT), -}); - -export const logColumnRT = rt.union([logTimestampColumnRT, logFieldColumnRT, logMessageColumnRT]); - -export const logEntryContextRT = rt.union([ - rt.type({}), - rt.type({ 'container.id': rt.string }), - rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }), -]); - -export const logEntryRT = rt.type({ - id: rt.string, - cursor: logEntryCursorRT, - columns: rt.array(logColumnRT), - context: logEntryContextRT, -}); - -export type LogMessageConstantPart = rt.TypeOf; -export type LogMessageFieldPart = rt.TypeOf; -export type LogMessagePart = rt.TypeOf; -export type LogTimestampColumn = rt.TypeOf; -export type LogFieldColumn = rt.TypeOf; -export type LogMessageColumn = rt.TypeOf; -export type LogColumn = rt.TypeOf; -export type LogEntryContext = rt.TypeOf; -export type LogEntry = rt.TypeOf; - export const logEntriesResponseRT = rt.type({ data: rt.intersection([ rt.type({ diff --git a/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts b/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts index 648da43134a27..96bf8beb29021 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts @@ -5,13 +5,12 @@ */ import * as rt from 'io-ts'; -import { logEntryCursorRT } from '../../log_entry'; +import { logEntryCursorRT, logEntryRT } from '../../log_entry'; import { logEntriesBaseRequestRT, logEntriesBeforeRequestRT, logEntriesAfterRequestRT, logEntriesCenteredRequestRT, - logEntryRT, } from './entries'; export const LOG_ENTRIES_HIGHLIGHTS_PATH = '/api/log_entries/highlights'; diff --git a/x-pack/plugins/infra/common/http_api/shared/time_range.ts b/x-pack/plugins/infra/common/http_api/shared/time_range.ts index efda07423748b..07317092cdedb 100644 --- a/x-pack/plugins/infra/common/http_api/shared/time_range.ts +++ b/x-pack/plugins/infra/common/http_api/shared/time_range.ts @@ -4,11 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as rt from 'io-ts'; - -export const timeRangeRT = rt.type({ - startTime: rt.number, - endTime: rt.number, -}); - -export type TimeRange = rt.TypeOf; +export * from '../../time/time_range'; diff --git a/x-pack/plugins/infra/common/log_analysis/index.ts b/x-pack/plugins/infra/common/log_analysis/index.ts index 0b4fa374a5da9..f055f642c8d1b 100644 --- a/x-pack/plugins/infra/common/log_analysis/index.ts +++ b/x-pack/plugins/infra/common/log_analysis/index.ts @@ -10,3 +10,5 @@ export * from './log_analysis_results'; export * from './log_entry_rate_analysis'; export * from './log_entry_categories_analysis'; export * from './job_parameters'; +export * from './log_entry_anomalies'; +export * from './log_entry_examples'; diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts index f4497dbba5056..897a5a4bb84df 100644 --- a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts +++ b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as rt from 'io-ts'; + export const ML_SEVERITY_SCORES = { warning: 3, minor: 25, @@ -55,3 +57,44 @@ export const compareDatasetsByMaximumAnomalyScore = < firstDataset: Dataset, secondDataset: Dataset ) => firstDataset.maximumAnomalyScore - secondDataset.maximumAnomalyScore; + +// Generic Sort + +const sortDirectionsRT = rt.keyof({ + asc: null, + desc: null, +}); + +export const sortRT = (fields: Fields) => + rt.type({ + field: fields, + direction: sortDirectionsRT, + }); + +// Pagination +// [Sort field value, tiebreaker value] +export const paginationCursorRT = rt.tuple([ + rt.union([rt.string, rt.number]), + rt.union([rt.string, rt.number]), +]); + +export type PaginationCursor = rt.TypeOf; + +const paginationPreviousPageCursorRT = rt.type({ + searchBefore: paginationCursorRT, +}); + +const paginationNextPageCursorRT = rt.type({ + searchAfter: paginationCursorRT, +}); + +export const paginationRT = rt.intersection([ + rt.type({ + pageSize: rt.number, + }), + rt.partial({ + cursor: rt.union([paginationPreviousPageCursorRT, paginationNextPageCursorRT]), + }), +]); + +export type Pagination = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/common/log_analysis/log_entry_anomalies.ts new file mode 100644 index 0000000000000..c426646e8e847 --- /dev/null +++ b/x-pack/plugins/infra/common/log_analysis/log_entry_anomalies.ts @@ -0,0 +1,59 @@ +/* + * 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 rt from 'io-ts'; +import { sortRT } from './log_analysis_results'; + +export const anomalyTypeRT = rt.keyof({ + logRate: null, + logCategory: null, +}); + +export type AnomalyType = rt.TypeOf; + +export const logEntryAnomalyCommonFieldsRT = rt.type({ + id: rt.string, + anomalyScore: rt.number, + dataset: rt.string, + typical: rt.number, + actual: rt.number, + type: anomalyTypeRT, + duration: rt.number, + startTime: rt.number, + jobId: rt.string, +}); +export const logEntrylogRateAnomalyRT = logEntryAnomalyCommonFieldsRT; +export type RateAnomaly = rt.TypeOf; + +export const logEntrylogCategoryAnomalyRT = rt.intersection([ + logEntryAnomalyCommonFieldsRT, + rt.type({ + categoryId: rt.string, + categoryRegex: rt.string, + categoryTerms: rt.string, + }), +]); +export type CategoryAnomaly = rt.TypeOf; + +export const logEntryAnomalyRT = rt.union([logEntrylogRateAnomalyRT, logEntrylogCategoryAnomalyRT]); + +export type LogEntryAnomaly = rt.TypeOf; + +export const logEntryAnomalyDatasetsRT = rt.array(rt.string); +export type LogEntryAnomalyDatasets = rt.TypeOf; + +export const isCategoryAnomaly = (anomaly: LogEntryAnomaly): anomaly is CategoryAnomaly => { + return anomaly.type === 'logCategory'; +}; + +const sortOptionsRT = rt.keyof({ + anomalyScore: null, + dataset: null, + startTime: null, +}); + +export const anomaliesSortRT = sortRT(sortOptionsRT); +export type AnomaliesSort = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/common/log_analysis/log_entry_categories_analysis.ts index 0957126ee52e3..4292eaeb5f98c 100644 --- a/x-pack/plugins/infra/common/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/common/log_analysis/log_entry_categories_analysis.ts @@ -5,6 +5,7 @@ */ import * as rt from 'io-ts'; +import { sortRT } from './log_analysis_results'; export const logEntryCategoriesJobTypeRT = rt.keyof({ 'log-entry-categories-count': null, @@ -15,3 +16,44 @@ export type LogEntryCategoriesJobType = rt.TypeOf; + +export const logEntryCategoryHistogramBucketRT = rt.type({ + startTime: rt.number, + bucketDuration: rt.number, + logEntryCount: rt.number, +}); + +export type LogEntryCategoryHistogramBucket = rt.TypeOf; + +export const logEntryCategoryHistogramRT = rt.type({ + histogramId: rt.string, + buckets: rt.array(logEntryCategoryHistogramBucketRT), +}); + +export type LogEntryCategoryHistogram = rt.TypeOf; + +export const logEntryCategoryRT = rt.type({ + categoryId: rt.number, + datasets: rt.array(logEntryCategoryDatasetRT), + histograms: rt.array(logEntryCategoryHistogramRT), + logEntryCount: rt.number, + maximumAnomalyScore: rt.number, + regularExpression: rt.string, +}); + +export type LogEntryCategory = rt.TypeOf; + +const sortOptionsRT = rt.keyof({ + maximumAnomalyScore: null, + logEntryCount: null, +}); + +export const categoriesSortRT = sortRT(sortOptionsRT); +export type CategoriesSort = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/log_analysis/log_entry_examples.ts b/x-pack/plugins/infra/common/log_analysis/log_entry_examples.ts new file mode 100644 index 0000000000000..78d230e35dc74 --- /dev/null +++ b/x-pack/plugins/infra/common/log_analysis/log_entry_examples.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const logEntryExampleRT = rt.type({ + id: rt.string, + dataset: rt.string, + message: rt.string, + timestamp: rt.number, + tiebreaker: rt.number, +}); + +export type LogEntryExample = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/log_entry/log_entry.ts b/x-pack/plugins/infra/common/log_entry/log_entry.ts index e02acebe27711..eec1fb59f3091 100644 --- a/x-pack/plugins/infra/common/log_entry/log_entry.ts +++ b/x-pack/plugins/infra/common/log_entry/log_entry.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as rt from 'io-ts'; import { TimeKey } from '../time'; -import { InfraLogEntry } from '../graphql/types'; - -export type LogEntry = InfraLogEntry; +import { logEntryCursorRT } from './log_entry_cursor'; +import { jsonArrayRT } from '../typed_json'; export interface LogEntryOrigin { id: string; @@ -42,3 +42,51 @@ export function isLessOrEqual(time1: LogEntryTime, time2: LogEntryTime) { export function isBetween(min: LogEntryTime, max: LogEntryTime, operand: LogEntryTime) { return isLessOrEqual(min, operand) && isLessOrEqual(operand, max); } + +export const logMessageConstantPartRT = rt.type({ + constant: rt.string, +}); +export const logMessageFieldPartRT = rt.type({ + field: rt.string, + value: jsonArrayRT, + highlights: rt.array(rt.string), +}); + +export const logMessagePartRT = rt.union([logMessageConstantPartRT, logMessageFieldPartRT]); + +export const logTimestampColumnRT = rt.type({ columnId: rt.string, timestamp: rt.number }); +export const logFieldColumnRT = rt.type({ + columnId: rt.string, + field: rt.string, + value: jsonArrayRT, + highlights: rt.array(rt.string), +}); +export const logMessageColumnRT = rt.type({ + columnId: rt.string, + message: rt.array(logMessagePartRT), +}); + +export const logColumnRT = rt.union([logTimestampColumnRT, logFieldColumnRT, logMessageColumnRT]); + +export const logEntryContextRT = rt.union([ + rt.type({}), + rt.type({ 'container.id': rt.string }), + rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }), +]); + +export const logEntryRT = rt.type({ + id: rt.string, + cursor: logEntryCursorRT, + columns: rt.array(logColumnRT), + context: logEntryContextRT, +}); + +export type LogMessageConstantPart = rt.TypeOf; +export type LogMessageFieldPart = rt.TypeOf; +export type LogMessagePart = rt.TypeOf; +export type LogEntryContext = rt.TypeOf; +export type LogEntry = rt.TypeOf; +export type LogTimestampColumn = rt.TypeOf; +export type LogFieldColumn = rt.TypeOf; +export type LogMessageColumn = rt.TypeOf; +export type LogColumn = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/time/index.ts b/x-pack/plugins/infra/common/time/index.ts index f49d46fa4920f..63bba2fa807ac 100644 --- a/x-pack/plugins/infra/common/time/index.ts +++ b/x-pack/plugins/infra/common/time/index.ts @@ -7,3 +7,4 @@ export * from './time_unit'; export * from './time_scale'; export * from './time_key'; +export * from './time_range'; diff --git a/x-pack/plugins/infra/common/time/time_range.ts b/x-pack/plugins/infra/common/time/time_range.ts new file mode 100644 index 0000000000000..efda07423748b --- /dev/null +++ b/x-pack/plugins/infra/common/time/time_range.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. + */ + +import * as rt from 'io-ts'; + +export const timeRangeRT = rt.type({ + startTime: rt.number, + endTime: rt.number, +}); + +export type TimeRange = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_expression.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_results/category_expression.tsx similarity index 95% rename from x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_expression.tsx rename to x-pack/plugins/infra/public/components/logging/log_analysis_results/category_expression.tsx index d5480977e7f9e..9684777ac9216 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_expression.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_results/category_expression.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { memo } from 'react'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; export const RegularExpressionRepresentation: React.FunctionComponent<{ maximumSegmentCount?: number; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item.ts b/x-pack/plugins/infra/public/components/logging/log_text_stream/item.ts index 19e8108ee50e8..b0ff36574bede 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/item.ts +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item.ts @@ -7,7 +7,7 @@ import { bisector } from 'd3-array'; import { compareToTimeKey, TimeKey } from '../../../../common/time'; -import { LogEntry } from '../../../../common/http_api'; +import { LogEntry } from '../../../../common/log_entry'; export type StreamItem = LogEntryStreamItem; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx index 8de9e565b00be..2b30d43f8c38d 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx @@ -7,7 +7,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; -import { LogFieldColumn } from '../../../../common/http_api'; +import { LogFieldColumn } from '../../../../common/log_entry'; import { LogEntryFieldColumn } from './log_entry_field_column'; describe('LogEntryFieldColumn', () => { diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx index 4a9b0d0906a76..0d295b4df5566 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { LogColumn } from '../../../../common/http_api'; +import { LogColumn } from '../../../../common/log_entry'; import { isFieldColumn, isHighlightFieldColumn } from '../../../utils/log_entry'; import { FieldValue } from './field_value'; import { LogEntryColumnContent } from './log_entry_column'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx index 5d36e5cd47c59..00281c2df3133 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx @@ -7,7 +7,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; -import { LogMessageColumn } from '../../../../common/http_api'; +import { LogMessageColumn } from '../../../../common/log_entry'; import { LogEntryMessageColumn } from './log_entry_message_column'; describe('LogEntryMessageColumn', () => { diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx index bfc160ada2e6a..92214dee9de22 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx @@ -6,7 +6,7 @@ import React, { memo, useMemo } from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { LogColumn, LogMessagePart } from '../../../../common/http_api'; +import { LogColumn, LogMessagePart } from '../../../../common/log_entry'; import { isConstantSegment, isFieldSegment, diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index 93c657fbdda97..1a472df2b5c90 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -17,7 +17,7 @@ import { LogEntryFieldColumn } from './log_entry_field_column'; import { LogEntryMessageColumn } from './log_entry_message_column'; import { LogEntryTimestampColumn } from './log_entry_timestamp_column'; import { monospaceTextStyle, hoveredContentStyle, highlightedContentStyle } from './text_styles'; -import { LogEntry, LogColumn } from '../../../../common/http_api'; +import { LogEntry, LogColumn } from '../../../../common/log_entry'; import { LogEntryContextMenu } from './log_entry_context_menu'; import { LogColumnRenderConfiguration, diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index d399e47a73562..8fb63533cf61b 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -25,7 +25,7 @@ import { MeasurableItemView } from './measurable_item_view'; import { VerticalScrollPanel } from './vertical_scroll_panel'; import { useColumnWidths, LogEntryColumnWidths } from './log_entry_column'; import { LogDateRow } from './log_date_row'; -import { LogEntry } from '../../../../common/http_api'; +import { LogEntry } from '../../../../common/log_entry'; import { LogColumnRenderConfiguration } from '../../../utils/log_column_render_configuration'; interface ScrollableLogTextStreamViewProps { diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts index bf4c5fbe0b13b..f1b820857e340 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts @@ -10,10 +10,10 @@ import { pick, throttle } from 'lodash'; import { TimeKey, timeKeyIsBetween } from '../../../../common/time'; import { LogEntriesResponse, - LogEntry, LogEntriesRequest, LogEntriesBaseRequest, } from '../../../../common/http_api'; +import { LogEntry } from '../../../../common/log_entry'; import { fetchLogEntries } from './api/fetch_log_entries'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx b/x-pack/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx index b4edebe8f8207..fb72874df5409 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx @@ -9,7 +9,8 @@ import { useEffect, useMemo, useState } from 'react'; import { TimeKey } from '../../../../common/time'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { fetchLogEntriesHighlights } from './api/fetch_log_entries_highlights'; -import { LogEntry, LogEntriesHighlightsResponse } from '../../../../common/http_api'; +import { LogEntriesHighlightsResponse } from '../../../../common/http_api'; +import { LogEntry } from '../../../../common/log_entry'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; export const useLogEntryHighlights = ( diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index ff30e993aa3a9..da7176125dae4 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -10,8 +10,7 @@ import usePrevious from 'react-use/lib/usePrevious'; import { esKuery } from '../../../../../../../src/plugins/data/public'; import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { LogEntry } from '../../../../common/http_api'; -import { LogEntryCursor } from '../../../../common/log_entry'; +import { LogEntryCursor, LogEntry } from '../../../../common/log_entry'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { LogSourceConfigurationProperties } from '../log_source'; diff --git a/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts index 61e1ea353880a..2888e5a2b3ac5 100644 --- a/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts +++ b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts @@ -5,7 +5,7 @@ */ import { useState } from 'react'; import createContainer from 'constate'; -import { LogEntry } from '../../../../common/http_api'; +import { LogEntry } from '../../../../common/log_entry'; interface ViewLogInContextProps { sourceId: string; diff --git a/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts b/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts index 2b8986820d5a4..89b5d993fa01e 100644 --- a/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts +++ b/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts @@ -11,7 +11,7 @@ import { RendererFunction } from '../../utils/typed_react'; import { LogHighlightsState } from './log_highlights/log_highlights'; import { LogEntriesState, LogEntriesStateParams, LogEntriesCallbacks } from './log_entries'; import { UniqueTimeKey } from '../../../common/time'; -import { LogEntry } from '../../../common/http_api'; +import { LogEntry } from '../../../common/log_entry'; export const WithStreamItems: React.FunctionComponent<{ children: RendererFunction< diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index ecddd8a9aa5be..4445b735bedc9 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -12,7 +12,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTrackPageview } from '../../../../../observability/public'; -import { TimeRange } from '../../../../common/http_api/shared/time_range'; +import { TimeRange } from '../../../../common/time/time_range'; import { CategoryJobNoticesSection } from '../../../components/logging/log_analysis_job_status'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/analyze_dataset_in_ml_action.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/analyze_dataset_in_ml_action.tsx index 3e1398c804686..8fe87c14c1a7c 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/analyze_dataset_in_ml_action.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/analyze_dataset_in_ml_action.tsx @@ -8,7 +8,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { TimeRange } from '../../../../../../common/http_api/shared'; +import { TimeRange } from '../../../../../../common/time/time_range'; import { getEntitySpecificSingleMetricViewerLink } from '../../../../../components/logging/log_analysis_results'; import { useLinkProps } from '../../../../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator_list.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator_list.tsx index 47bb31ab4ae3e..20f0ee00bd505 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator_list.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator_list.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { LogEntryCategoryDataset } from '../../../../../../common/http_api/log_analysis'; +import { LogEntryCategoryDataset } from '../../../../../../common/log_analysis'; import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis'; import { AnomalySeverityIndicator } from '../../../../../components/logging/log_analysis_results/anomaly_severity_indicator'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx index de07f3eb02029..8b4f075b782a9 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx @@ -7,7 +7,7 @@ import React, { useEffect } from 'react'; import { useLogEntryCategoryExamples } from '../../use_log_entry_category_examples'; import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples'; -import { TimeRange } from '../../../../../../common/http_api/shared'; +import { TimeRange } from '../../../../../../common/time/time_range'; import { CategoryExampleMessage } from './category_example_message'; const exampleCount = 5; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx index 84d7e198636e9..e24fdd06bc6d9 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { encode } from 'rison-node'; import moment from 'moment'; -import { LogEntry, LogEntryContext } from '../../../../../../common/http_api'; -import { TimeRange } from '../../../../../../common/http_api/shared'; +import { LogEntry, LogEntryContext } from '../../../../../../common/log_entry'; +import { TimeRange } from '../../../../../../common/time'; import { getFriendlyNameForPartitionId, partitionField, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_action_list.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_action_list.tsx index 2321dafaead1c..6bbc640b5b007 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_action_list.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_action_list.tsx @@ -6,8 +6,8 @@ import React from 'react'; -import { LogEntryCategoryDataset } from '../../../../../../common/http_api/log_analysis'; -import { TimeRange } from '../../../../../../common/http_api/shared'; +import { LogEntryCategoryDataset } from '../../../../../../common/log_analysis'; +import { TimeRange } from '../../../../../../common/time'; import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis'; import { AnalyzeCategoryDatasetInMlAction } from './analyze_dataset_in_ml_action'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_list.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_list.tsx index 779ac3e8c3a07..78690285180d7 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_list.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_list.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { LogEntryCategoryDataset } from '../../../../../../common/http_api/log_analysis'; +import { LogEntryCategoryDataset } from '../../../../../../common/log_analysis'; import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis'; export const DatasetsList: React.FunctionComponent<{ diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/log_entry_count_sparkline.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/log_entry_count_sparkline.tsx index 42d6509802ed4..d94dbb9d33556 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/log_entry_count_sparkline.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/log_entry_count_sparkline.tsx @@ -6,8 +6,8 @@ import React, { useMemo } from 'react'; -import { LogEntryCategoryHistogram } from '../../../../../../common/http_api/log_analysis'; -import { TimeRange } from '../../../../../../common/http_api/shared'; +import { LogEntryCategoryHistogram } from '../../../../../../common/log_analysis'; +import { TimeRange } from '../../../../../../common/time'; import { SingleMetricComparison } from './single_metric_comparison'; import { SingleMetricSparkline } from './single_metric_sparkline'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx index 5fb8e3380f23f..c8453bdcdefbd 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx @@ -13,7 +13,7 @@ import { } from '@elastic/eui/dist/eui_charts_theme'; import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting'; -import { TimeRange } from '../../../../../../common/http_api/shared'; +import { TimeRange } from '../../../../../../common/time'; interface TimeSeriesPoint { timestamp: number; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx index c7a6c89012a3a..f810a675a18d1 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx @@ -8,8 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, EuiTitle } fro import { i18n } from '@kbn/i18n'; import React from 'react'; -import { LogEntryCategory } from '../../../../../../common/http_api/log_analysis'; -import { TimeRange } from '../../../../../../common/http_api/shared'; +import { LogEntryCategory } from '../../../../../../common/log_analysis'; +import { TimeRange } from '../../../../../../common/time'; import { BetaBadge } from '../../../../../components/beta_badge'; import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; import { RecreateJobButton } from '../../../../../components/logging/log_analysis_setup/create_job_button'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx index 954b6a9ab3ed3..834c99502a590 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx @@ -15,12 +15,12 @@ import { LogEntryCategory, LogEntryCategoryDataset, LogEntryCategoryHistogram, -} from '../../../../../../common/http_api/log_analysis'; -import { TimeRange } from '../../../../../../common/http_api/shared'; +} from '../../../../../../common/log_analysis'; +import { TimeRange } from '../../../../../../common/time'; import { RowExpansionButton } from '../../../../../components/basic_table'; import { AnomalySeverityIndicatorList } from './anomaly_severity_indicator_list'; import { CategoryDetailsRow } from './category_details_row'; -import { RegularExpressionRepresentation } from './category_expression'; +import { RegularExpressionRepresentation } from '../../../../../components/logging/log_analysis_results/category_expression'; import { DatasetActionsList } from './datasets_action_list'; import { DatasetsList } from './datasets_list'; import { LogEntryCountSparkline } from './log_entry_count_sparkline'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/service_calls/get_top_log_entry_categories.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/service_calls/get_top_log_entry_categories.ts index a0eaecf04fa4b..b25b6cbe6f631 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/service_calls/get_top_log_entry_categories.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/service_calls/get_top_log_entry_categories.ts @@ -10,8 +10,8 @@ import { getLogEntryCategoriesRequestPayloadRT, getLogEntryCategoriesSuccessReponsePayloadRT, LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, - CategorySort, } from '../../../../../common/http_api/log_analysis'; +import { CategoriesSort } from '../../../../../common/log_analysis'; import { decodeOrThrow } from '../../../../../common/runtime_types'; interface RequestArgs { @@ -20,7 +20,7 @@ interface RequestArgs { endTime: number; categoryCount: number; datasets?: string[]; - sort: CategorySort; + sort: CategoriesSort; } export const callGetTopLogEntryCategoriesAPI = async ( diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts index a64b73dea25e6..e3fba92610955 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts @@ -9,8 +9,8 @@ import { useMemo, useState } from 'react'; import { GetLogEntryCategoriesSuccessResponsePayload, GetLogEntryCategoryDatasetsSuccessResponsePayload, - CategorySort, } from '../../../../common/http_api/log_analysis'; +import { CategoriesSort } from '../../../../common/log_analysis'; import { useTrackedPromise, CanceledPromiseError } from '../../../utils/use_tracked_promise'; import { callGetTopLogEntryCategoriesAPI } from './service_calls/get_top_log_entry_categories'; import { callGetLogEntryCategoryDatasetsAPI } from './service_calls/get_log_entry_category_datasets'; @@ -19,8 +19,8 @@ import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; type TopLogEntryCategories = GetLogEntryCategoriesSuccessResponsePayload['data']['categories']; type LogEntryCategoryDatasets = GetLogEntryCategoryDatasetsSuccessResponsePayload['data']['datasets']; -export type SortOptions = CategorySort; -export type ChangeSortOptions = (sortOptions: CategorySort) => void; +export type SortOptions = CategoriesSort; +export type ChangeSortOptions = (sortOptions: CategoriesSort) => void; export const useLogEntryCategoriesResults = ({ categoriesCount, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 09d3746c6ace6..f5007a1d48c4a 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -13,7 +13,7 @@ import { encode, RisonValue } from 'rison-node'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTrackPageview } from '../../../../../observability/public'; -import { TimeRange } from '../../../../common/http_api/shared/time_range'; +import { TimeRange } from '../../../../common/time/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; import { TimeKey } from '../../../../common/time'; import { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx index ae5c3b5b93b47..503d383201592 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx @@ -23,7 +23,7 @@ import moment from 'moment'; import React, { useCallback, useMemo } from 'react'; import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; -import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { TimeRange } from '../../../../../../common/time/time_range'; import { MLSeverityScoreCategories, ML_SEVERITY_COLORS, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx index 37032a95e9640..39fb1a5e6ae19 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import useMount from 'react-use/lib/useMount'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { LogEntryAnomaly } from '../../../../../../common/http_api'; -import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { LogEntryAnomaly, isCategoryAnomaly } from '../../../../../../common/log_analysis'; +import { TimeRange } from '../../../../../../common/time/time_range'; import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples'; import { useLogSourceContext } from '../../../../../containers/logs/log_source'; import { useLogEntryExamples } from '../../use_log_entry_examples'; @@ -40,7 +40,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ exampleCount: EXAMPLE_COUNT, sourceId, startTime: anomaly.startTime, - categoryId: anomaly.categoryId, + categoryId: isCategoryAnomaly(anomaly) ? anomaly.categoryId : undefined, }); useMount(() => { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx index c89f0329e9f2e..780e8c7ec5ec9 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { LogEntryRateResults } from '../../use_log_entry_rate_results'; -import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { TimeRange } from '../../../../../../common/time/time_range'; import { getAnnotationsForAll, getLogEntryRateCombinedSeries } from '../helpers/data_formatters'; import { AnomaliesChart } from './chart'; import { AnomaliesTable } from './table'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx index ab3476cd78eb3..7446b3c348606 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -25,10 +25,10 @@ import { LogColumnHeader, } from '../../../../../components/logging/log_text_stream/column_headers'; import { useLinkProps } from '../../../../../hooks/use_link_props'; -import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { TimeRange } from '../../../../../../common/time/time_range'; import { partitionField } from '../../../../../../common/log_analysis/job_parameters'; import { getEntitySpecificSingleMetricViewerLink } from '../../../../../components/logging/log_analysis_results/analyze_in_ml_button'; -import { LogEntryExample } from '../../../../../../common/http_api/log_analysis/results'; +import { LogEntryExample, isCategoryAnomaly } from '../../../../../../common/log_analysis'; import { LogColumnConfiguration, isTimestampLogColumnConfiguration, @@ -36,7 +36,7 @@ import { isMessageLogColumnConfiguration, } from '../../../../../utils/source_configuration'; import { localizedDate } from '../../../../../../common/formatters/datetime'; -import { LogEntryAnomaly } from '../../../../../../common/http_api'; +import { LogEntryAnomaly } from '../../../../../../common/log_analysis'; import { useLogEntryFlyoutContext } from '../../../../../containers/logs/log_flyout'; export const exampleMessageScale = 'medium' as const; @@ -116,7 +116,7 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ const viewAnomalyInMachineLearningLinkProps = useLinkProps( getEntitySpecificSingleMetricViewerLink(anomaly.jobId, timeRange, { [partitionField]: dataset, - ...(anomaly.categoryId ? { mlcategory: anomaly.categoryId } : {}), + ...(isCategoryAnomaly(anomaly) ? { mlcategory: anomaly.categoryId } : {}), }) ); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index 855113d66f510..4b8c2b02bb8af 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -18,16 +18,18 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo } from 'react'; import useSet from 'react-use/lib/useSet'; -import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { TimeRange } from '../../../../../../common/time/time_range'; import { + AnomalyType, formatAnomalyScore, getFriendlyNameForPartitionId, formatOneDecimalPlace, + isCategoryAnomaly, } from '../../../../../../common/log_analysis'; -import { AnomalyType } from '../../../../../../common/http_api/log_analysis'; import { RowExpansionButton } from '../../../../../components/basic_table'; import { AnomaliesTableExpandedRow } from './expanded_row'; import { AnomalySeverityIndicator } from '../../../../../components/logging/log_analysis_results/anomaly_severity_indicator'; +import { RegularExpressionRepresentation } from '../../../../../components/logging/log_analysis_results/category_expression'; import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting'; import { Page, @@ -50,6 +52,7 @@ interface TableItem { typical: number; actual: number; type: AnomalyType; + categoryRegex?: string; } const anomalyScoreColumnName = i18n.translate( @@ -124,6 +127,7 @@ export const AnomaliesTable: React.FunctionComponent<{ type: anomaly.type, typical: anomaly.typical, actual: anomaly.actual, + categoryRegex: isCategoryAnomaly(anomaly) ? anomaly.categoryRegex : undefined, }; }); }, [results]); @@ -166,9 +170,7 @@ export const AnomaliesTable: React.FunctionComponent<{ { name: anomalyMessageColumnName, truncateText: true, - render: (item: TableItem) => ( - - ), + render: (item: TableItem) => , }, { field: 'startTime', @@ -226,15 +228,9 @@ export const AnomaliesTable: React.FunctionComponent<{ ); }; -const AnomalyMessage = ({ - actual, - typical, - type, -}: { - actual: number; - typical: number; - type: AnomalyType; -}) => { +const AnomalyMessage = ({ anomaly }: { anomaly: TableItem }) => { + const { type, actual, typical } = anomaly; + const moreThanExpectedAnomalyMessage = i18n.translate( 'xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage', { @@ -262,9 +258,20 @@ const AnomalyMessage = ({ const ratioMessage = useRatio ? `${formatOneDecimalPlace(ratio)}x` : ''; return ( - - {`${ratioMessage} ${message}`} - + + + + + + {`${ratioMessage} ${message}`} + {anomaly.categoryRegex && ( + <> + {': '} + + + )} + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_anomalies.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_anomalies.ts index 7f90604bfefdd..f915b0d78c43d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_anomalies.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_anomalies.ts @@ -11,13 +11,13 @@ import { LOG_ANALYSIS_GET_LOG_ENTRY_ANOMALIES_PATH, } from '../../../../../common/http_api/log_analysis'; import { decodeOrThrow } from '../../../../../common/runtime_types'; -import { Sort, Pagination } from '../../../../../common/http_api/log_analysis'; +import { AnomaliesSort, Pagination } from '../../../../../common/log_analysis'; interface RequestArgs { sourceId: string; startTime: number; endTime: number; - sort: Sort; + sort: AnomaliesSort; pagination: Pagination; datasets?: string[]; } diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts index 396c1ad3e1857..fbfe76f1473f5 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts @@ -9,21 +9,21 @@ import useMount from 'react-use/lib/useMount'; import { useTrackedPromise, CanceledPromiseError } from '../../../utils/use_tracked_promise'; import { callGetLogEntryAnomaliesAPI } from './service_calls/get_log_entry_anomalies'; import { callGetLogEntryAnomaliesDatasetsAPI } from './service_calls/get_log_entry_anomalies_datasets'; +import { GetLogEntryAnomaliesDatasetsSuccessResponsePayload } from '../../../../common/http_api/log_analysis'; import { - Sort, + AnomaliesSort, Pagination, PaginationCursor, - GetLogEntryAnomaliesDatasetsSuccessResponsePayload, LogEntryAnomaly, -} from '../../../../common/http_api/log_analysis'; +} from '../../../../common/log_analysis'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -export type SortOptions = Sort; +export type SortOptions = AnomaliesSort; export type PaginationOptions = Pick; export type Page = number; export type FetchNextPage = () => void; export type FetchPreviousPage = () => void; -export type ChangeSortOptions = (sortOptions: Sort) => void; +export type ChangeSortOptions = (sortOptions: AnomaliesSort) => void; export type ChangePaginationOptions = (paginationOptions: PaginationOptions) => void; export type LogEntryAnomalies = LogEntryAnomaly[]; type LogEntryAnomaliesDatasets = GetLogEntryAnomaliesDatasetsSuccessResponsePayload['data']['datasets']; @@ -38,7 +38,7 @@ interface ReducerState { paginationCursor: Pagination['cursor'] | undefined; hasNextPage: boolean; paginationOptions: PaginationOptions; - sortOptions: Sort; + sortOptions: AnomaliesSort; timeRange: { start: number; end: number; @@ -53,7 +53,7 @@ type ReducerStateDefaults = Pick< type ReducerAction = | { type: 'changePaginationOptions'; payload: { paginationOptions: PaginationOptions } } - | { type: 'changeSortOptions'; payload: { sortOptions: Sort } } + | { type: 'changeSortOptions'; payload: { sortOptions: AnomaliesSort } } | { type: 'fetchNextPage' } | { type: 'fetchPreviousPage' } | { type: 'changeHasNextPage'; payload: { hasNextPage: boolean } } @@ -144,7 +144,7 @@ export const useLogEntryAnomaliesResults = ({ endTime: number; startTime: number; sourceId: string; - defaultSortOptions: Sort; + defaultSortOptions: AnomaliesSort; defaultPaginationOptions: Pick; onGetLogEntryAnomaliesDatasetsError?: (error: Error) => void; filteredDatasets?: string[]; @@ -225,7 +225,7 @@ export const useLogEntryAnomaliesResults = ({ ); const changeSortOptions = useCallback( - (nextSortOptions: Sort) => { + (nextSortOptions: AnomaliesSort) => { dispatch({ type: 'changeSortOptions', payload: { sortOptions: nextSortOptions } }); }, [dispatch] diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_examples.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_examples.ts index e809ab9cd5a6f..90b8b03a81602 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_examples.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_examples.ts @@ -6,7 +6,7 @@ import { useMemo, useState } from 'react'; -import { LogEntryExample } from '../../../../common/http_api'; +import { LogEntryExample } from '../../../../common/log_analysis'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { callGetLogEntryExamplesAPI } from './service_calls/get_log_entry_examples'; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx index 3fa89da5b5e51..011653fd6eb47 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx @@ -16,7 +16,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { isEmpty } from 'lodash'; import React, { useCallback, useContext, useMemo } from 'react'; -import { LogEntry } from '../../../../common/http_api'; +import { LogEntry } from '../../../../common/log_entry'; import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; import { useViewportDimensions } from '../../../utils/use_viewport_dimensions'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; diff --git a/x-pack/plugins/infra/public/test_utils/entries.ts b/x-pack/plugins/infra/public/test_utils/entries.ts index 04c87d5f73902..96737fb175365 100644 --- a/x-pack/plugins/infra/public/test_utils/entries.ts +++ b/x-pack/plugins/infra/public/test_utils/entries.ts @@ -5,7 +5,7 @@ */ import faker from 'faker'; -import { LogEntry } from '../../common/http_api'; +import { LogEntry } from '../../common/log_entry'; import { LogSourceConfiguration } from '../containers/logs/log_source'; export const ENTRIES_EMPTY = { diff --git a/x-pack/plugins/infra/public/utils/log_entry/log_entry.ts b/x-pack/plugins/infra/public/utils/log_entry/log_entry.ts index bb528ee5b18c5..c69104ad6177e 100644 --- a/x-pack/plugins/infra/public/utils/log_entry/log_entry.ts +++ b/x-pack/plugins/infra/public/utils/log_entry/log_entry.ts @@ -17,7 +17,7 @@ import { LogMessagePart, LogMessageFieldPart, LogMessageConstantPart, -} from '../../../common/http_api'; +} from '../../../common/log_entry'; export type LogEntryMessageSegment = InfraLogEntryFields.Message; export type LogEntryConstantMessageSegment = InfraLogEntryFields.InfraLogMessageConstantSegmentInlineFragment; diff --git a/x-pack/plugins/infra/public/utils/log_entry/log_entry_highlight.ts b/x-pack/plugins/infra/public/utils/log_entry/log_entry_highlight.ts index abb004911214b..208316c693d4d 100644 --- a/x-pack/plugins/infra/public/utils/log_entry/log_entry_highlight.ts +++ b/x-pack/plugins/infra/public/utils/log_entry/log_entry_highlight.ts @@ -12,7 +12,7 @@ import { LogFieldColumn, LogMessagePart, LogMessageFieldPart, -} from '../../../common/http_api'; +} from '../../../common/log_entry'; export type LogEntryHighlightColumn = InfraLogEntryHighlightFields.Columns; export type LogEntryHighlightMessageColumn = InfraLogEntryHighlightFields.InfraLogEntryMessageColumnInlineFragment; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 0b1df3abd465a..4c5debe58ed26 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -10,10 +10,9 @@ import type { InfraPluginRequestHandlerContext } from '../../../types'; import { LogEntriesSummaryBucket, LogEntriesSummaryHighlightsBucket, - LogEntry, - LogColumn, LogEntriesRequest, } from '../../../../common/http_api'; +import { LogEntry, LogColumn } from '../../../../common/log_entry'; import { InfraSourceConfiguration, InfraSources, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts index 19ab82c9c5ac1..d04e036b33b21 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LogMessagePart } from '../../../../common/http_api/log_entries'; +import { LogMessagePart } from '../../../../common/log_entry'; import { JsonArray, JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; import { LogMessageFormattingCondition, diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts index c6a4593912280..fbcc3671f08a2 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts @@ -12,12 +12,11 @@ import { logEntryCategoriesJobTypes, logEntryRateJobTypes, jobCustomSettingsRT, -} from '../../../common/log_analysis'; -import { - Sort, + LogEntryAnomalyDatasets, + AnomaliesSort, Pagination, - GetLogEntryAnomaliesRequestPayload, -} from '../../../common/http_api/log_analysis'; + isCategoryAnomaly, +} from '../../../common/log_analysis'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { createLogEntryAnomaliesQuery, logEntryAnomaliesResponseRT } from './queries'; import { @@ -95,9 +94,9 @@ export async function getLogEntryAnomalies( sourceId: string, startTime: number, endTime: number, - sort: Sort, + sort: AnomaliesSort, pagination: Pagination, - datasets: GetLogEntryAnomaliesRequestPayload['data']['datasets'] + datasets?: LogEntryAnomalyDatasets ) { const finalizeLogEntryAnomaliesSpan = startTracingSpan('get log entry anomalies'); @@ -131,7 +130,7 @@ export async function getLogEntryAnomalies( datasets ); - const data = anomalies.map((anomaly) => { + const parsedAnomalies = anomalies.map((anomaly) => { const { jobId } = anomaly; if (!anomaly.categoryId) { @@ -141,10 +140,41 @@ export async function getLogEntryAnomalies( } }); + const categoryIds = parsedAnomalies.reduce((acc, anomaly) => { + return isCategoryAnomaly(anomaly) ? [...acc, parseInt(anomaly.categoryId, 10)] : acc; + }, []); + + const logEntryCategoriesCountJobId = getJobId( + context.infra.spaceId, + sourceId, + logEntryCategoriesJobTypes[0] + ); + + const { logEntryCategoriesById } = await fetchLogEntryCategories( + context, + logEntryCategoriesCountJobId, + categoryIds + ); + + const parsedAnomaliesWithExpandedCategoryInformation = parsedAnomalies.map((anomaly) => { + if (isCategoryAnomaly(anomaly)) { + if (logEntryCategoriesById[parseInt(anomaly.categoryId, 10)]) { + const { + _source: { regex, terms }, + } = logEntryCategoriesById[parseInt(anomaly.categoryId, 10)]; + return { ...anomaly, ...{ categoryRegex: regex, categoryTerms: terms } }; + } else { + return { ...anomaly, ...{ categoryRegex: '', categoryTerms: '' } }; + } + } else { + return anomaly; + } + }); + const logEntryAnomaliesSpan = finalizeLogEntryAnomaliesSpan(); return { - data, + data: parsedAnomaliesWithExpandedCategoryInformation, paginationCursors, hasMoreEntries, timing: { @@ -208,9 +238,9 @@ async function fetchLogEntryAnomalies( jobIds: string[], startTime: number, endTime: number, - sort: Sort, + sort: AnomaliesSort, pagination: Pagination, - datasets: GetLogEntryAnomaliesRequestPayload['data']['datasets'] + datasets?: LogEntryAnomalyDatasets ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index 7dd5aae9784f5..071a8a94e009b 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -5,14 +5,14 @@ */ import type { ILegacyScopedClusterClient } from 'src/core/server'; -import { LogEntryContext } from '../../../common/http_api'; +import { LogEntryContext } from '../../../common/log_entry'; import { compareDatasetsByMaximumAnomalyScore, getJobId, jobCustomSettingsRT, logEntryCategoriesJobTypes, + CategoriesSort, } from '../../../common/log_analysis'; -import { CategorySort } from '../../../common/http_api/log_analysis'; import { startTracingSpan } from '../../../common/performance_tracing'; import { decodeOrThrow } from '../../../common/runtime_types'; import type { MlAnomalyDetectors, MlSystem } from '../../types'; @@ -51,7 +51,7 @@ export async function getTopLogEntryCategories( categoryCount: number, datasets: string[], histograms: HistogramParameters[], - sort: CategorySort + sort: CategoriesSort ) { const finalizeTopLogEntryCategoriesSpan = startTracingSpan('get top categories'); @@ -218,7 +218,7 @@ async function fetchTopLogEntryCategories( endTime: number, categoryCount: number, datasets: string[], - sort: CategorySort + sort: CategoriesSort ) { const finalizeEsSearchSpan = startTracingSpan('Fetch top categories from ES'); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts index e692ed019cf86..8e01cafcf62ae 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts @@ -14,10 +14,10 @@ import { createDatasetsFilters, } from './common'; import { - Sort, + AnomaliesSort, + LogEntryAnomalyDatasets, Pagination, - GetLogEntryAnomaliesRequestPayload, -} from '../../../../common/http_api/log_analysis'; +} from '../../../../common/log_analysis'; // TODO: Reassess validity of this against ML docs const TIEBREAKER_FIELD = '_doc'; @@ -32,9 +32,9 @@ export const createLogEntryAnomaliesQuery = ( jobIds: string[], startTime: number, endTime: number, - sort: Sort, + sort: AnomaliesSort, pagination: Pagination, - datasets: GetLogEntryAnomaliesRequestPayload['data']['datasets'] + datasets?: LogEntryAnomalyDatasets ) => { const { field } = sort; const { pageSize } = pagination; @@ -118,7 +118,7 @@ export const logEntryAnomaliesResponseRT = rt.intersection([ export type LogEntryAnomaliesResponseRT = rt.TypeOf; -const parsePaginationCursor = (sort: Sort, pagination: Pagination) => { +const parsePaginationCursor = (sort: AnomaliesSort, pagination: Pagination) => { const { cursor } = pagination; const { direction } = sort; diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts index 057054b427227..f1363900d3696 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts @@ -14,13 +14,13 @@ import { createDatasetsFilters, } from './common'; -import { CategorySort } from '../../../../common/http_api/log_analysis'; +import { CategoriesSort } from '../../../../common/log_analysis'; type CategoryAggregationOrder = | 'filter_record>maximum_record_score' | 'filter_model_plot>sum_actual'; const getAggregationOrderForSortField = ( - field: CategorySort['field'] + field: CategoriesSort['field'] ): CategoryAggregationOrder => { switch (field) { case 'maximumAnomalyScore': @@ -40,7 +40,7 @@ export const createTopLogEntryCategoriesQuery = ( endTime: number, size: number, datasets: string[], - sort: CategorySort + sort: CategoriesSort ) => ({ ...defaultRequestParameters, body: { diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies.ts index ec2bc6e5ed739..42d126d4ef036 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies.ts @@ -11,9 +11,8 @@ import { getLogEntryAnomaliesSuccessReponsePayloadRT, getLogEntryAnomaliesRequestPayloadRT, GetLogEntryAnomaliesRequestPayload, - Sort, - Pagination, } from '../../../../common/http_api/log_analysis'; +import { AnomaliesSort, Pagination } from '../../../../common/log_analysis'; import { createValidationFunction } from '../../../../common/runtime_types'; import { assertHasInfraMlPlugins } from '../../../utils/request_context'; import { getLogEntryAnomalies } from '../../../lib/log_analysis'; @@ -98,7 +97,7 @@ const getSortAndPagination = ( sort: Partial = {}, pagination: Partial = {} ): { - sort: Sort; + sort: AnomaliesSort; pagination: Pagination; } => { const sortDefaults = { diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_entries.ts b/x-pack/test/api_integration/apis/metrics_ui/log_entries.ts index 2d148f4c2c0f7..79d5e68344432 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/log_entries.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/log_entries.ts @@ -13,10 +13,13 @@ import { LOG_ENTRIES_PATH, logEntriesRequestRT, logEntriesResponseRT, +} from '../../../../plugins/infra/common/http_api'; + +import { LogTimestampColumn, LogFieldColumn, LogMessageColumn, -} from '../../../../plugins/infra/common/http_api'; +} from '../../../../plugins/infra/common/log_entry'; import { FtrProviderContext } from '../../ftr_provider_context'; From 96e4bdc8ae5e008796bb66254b4e9652e88b044e Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Wed, 27 Jan 2021 13:06:22 -0500 Subject: [PATCH 049/163] Dashboard - Hide Short URL Option (#89338) * fix typo from sub-feature privileges --- .../dashboard/public/application/top_nav/show_share_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx index 660e7635eb99d..ecebef2ec3c9c 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx @@ -94,7 +94,7 @@ export function ShowShareModal({ share.toggleShareContextMenu({ anchorElement, allowEmbed: true, - allowShortUrl: !dashboardCapabilities.hideWriteControls || dashboardCapabilities.createShortUrl, + allowShortUrl: dashboardCapabilities.createShortUrl, shareableUrl: setStateToKbnUrl( '_a', dashboardStateManager.getAppState(), From 3c604438b8fcb6588fcc4a77517dcc0392dfc5d6 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 27 Jan 2021 11:22:26 -0700 Subject: [PATCH 050/163] [kbn/pm] throw an error if package doesn't have a script (#89438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [kbn/pm] throw an error if package doesn't have a script * actually add the kbn/es build script 🤦‍♂️ Co-authored-by: spalger --- .ci/teamcity/tests/test_projects.sh | 2 +- packages/kbn-es/package.json | 1 + packages/kbn-pm/README.md | 4 ++-- packages/kbn-pm/dist/index.js | 26 ++++++++++++++++++-------- packages/kbn-pm/src/cli.ts | 5 ++++- packages/kbn-pm/src/commands/run.ts | 22 +++++++++++++++------- test/scripts/checks/test_projects.sh | 2 +- 7 files changed, 42 insertions(+), 20 deletions(-) diff --git a/.ci/teamcity/tests/test_projects.sh b/.ci/teamcity/tests/test_projects.sh index 2553650930392..06dd3607a6799 100755 --- a/.ci/teamcity/tests/test_projects.sh +++ b/.ci/teamcity/tests/test_projects.sh @@ -5,4 +5,4 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" checks-reporter-with-killswitch "Test Projects" \ - yarn kbn run test --exclude kibana --oss --skip-kibana-plugins + yarn kbn run test --exclude kibana --oss --skip-kibana-plugins --skip-missing diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index c8e25a95594c6..8ea83d744bcb9 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -8,6 +8,7 @@ "devOnly": true }, "scripts": { + "build": "node scripts/build", "kbn:bootstrap": "node scripts/build", "kbn:watch": "node scripts/build --watch" }, diff --git a/packages/kbn-pm/README.md b/packages/kbn-pm/README.md index c169b5c75e178..eb1ac6ffa92aa 100644 --- a/packages/kbn-pm/README.md +++ b/packages/kbn-pm/README.md @@ -150,14 +150,14 @@ e.g. `build` or `test`. Instead of jumping into each package and running `yarn build` you can run: ``` -yarn kbn run build +yarn kbn run build --skip-missing ``` And if needed, you can skip packages in the same way as for bootstrapping, e.g. with `--exclude` and `--skip-kibana-plugins`: ``` -yarn kbn run build --exclude kibana +yarn kbn run build --exclude kibana --skip-missing ``` ### Watching diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 09995a9be30a6..95ab46582723e 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -179,12 +179,15 @@ function help() { --debug Set log level to debug --quiet Set log level to error --silent Disable log output + + "run" options: + --skip-missing Ignore packages which don't have the requested script ` + '\n'); } async function run(argv) { _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].setLogLevel(Object(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__["pickLevelFromFlags"])(getopts__WEBPACK_IMPORTED_MODULE_1___default()(argv, { - boolean: ['verbose', 'debug', 'quiet', 'silent'] + boolean: ['verbose', 'debug', 'quiet', 'silent', 'skip-missing'] }))); // We can simplify this setup (and remove this extra handling) once Yarn // starts forwarding the `--` directly to this script, see // https://github.com/yarnpkg/yarn/blob/b2d3e1a8fe45ef376b716d597cc79b38702a9320/src/cli/index.js#L174-L182 @@ -52620,7 +52623,8 @@ const RunCommand = { name: 'run', async run(projects, projectGraph, { - extraArgs + extraArgs, + options }) { const batchedProjects = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_3__["topologicallyBatchProjects"])(projects, projectGraph); @@ -52631,13 +52635,19 @@ const RunCommand = { const scriptName = extraArgs[0]; const scriptArgs = extraArgs.slice(1); await Object(_utils_parallelize__WEBPACK_IMPORTED_MODULE_2__["parallelizeBatches"])(batchedProjects, async project => { - if (project.hasScript(scriptName)) { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].info(`[${project.name}] running "${scriptName}" script`); - await project.runScriptStreaming(scriptName, { - args: scriptArgs - }); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].success(`[${project.name}] complete`); + if (!project.hasScript(scriptName)) { + if (!!options['skip-missing']) { + return; + } + + throw new _utils_errors__WEBPACK_IMPORTED_MODULE_0__["CliError"](`[${project.name}] no "${scriptName}" script defined. To skip packages without the "${scriptName}" script pass --skip-missing`); } + + _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].info(`[${project.name}] running "${scriptName}" script`); + await project.runScriptStreaming(scriptName, { + args: scriptArgs + }); + _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].success(`[${project.name}] complete`); }); } diff --git a/packages/kbn-pm/src/cli.ts b/packages/kbn-pm/src/cli.ts index a27587085eab1..e6be8d1821d01 100644 --- a/packages/kbn-pm/src/cli.ts +++ b/packages/kbn-pm/src/cli.ts @@ -41,6 +41,9 @@ function help() { --debug Set log level to debug --quiet Set log level to error --silent Disable log output + + "run" options: + --skip-missing Ignore packages which don't have the requested script ` + '\n' ); } @@ -49,7 +52,7 @@ export async function run(argv: string[]) { log.setLogLevel( pickLevelFromFlags( getopts(argv, { - boolean: ['verbose', 'debug', 'quiet', 'silent'], + boolean: ['verbose', 'debug', 'quiet', 'silent', 'skip-missing'], }) ) ); diff --git a/packages/kbn-pm/src/commands/run.ts b/packages/kbn-pm/src/commands/run.ts index acbafe07b9a84..fb306f37082fe 100644 --- a/packages/kbn-pm/src/commands/run.ts +++ b/packages/kbn-pm/src/commands/run.ts @@ -16,7 +16,7 @@ export const RunCommand: ICommand = { description: 'Run script defined in package.json in each package that contains that script.', name: 'run', - async run(projects, projectGraph, { extraArgs }) { + async run(projects, projectGraph, { extraArgs, options }) { const batchedProjects = topologicallyBatchProjects(projects, projectGraph); if (extraArgs.length === 0) { @@ -27,13 +27,21 @@ export const RunCommand: ICommand = { const scriptArgs = extraArgs.slice(1); await parallelizeBatches(batchedProjects, async (project) => { - if (project.hasScript(scriptName)) { - log.info(`[${project.name}] running "${scriptName}" script`); - await project.runScriptStreaming(scriptName, { - args: scriptArgs, - }); - log.success(`[${project.name}] complete`); + if (!project.hasScript(scriptName)) { + if (!!options['skip-missing']) { + return; + } + + throw new CliError( + `[${project.name}] no "${scriptName}" script defined. To skip packages without the "${scriptName}" script pass --skip-missing` + ); } + + log.info(`[${project.name}] running "${scriptName}" script`); + await project.runScriptStreaming(scriptName, { + args: scriptArgs, + }); + log.success(`[${project.name}] complete`); }); }, }; diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh index 56f15f6839e9d..be3fe4c4be9d0 100755 --- a/test/scripts/checks/test_projects.sh +++ b/test/scripts/checks/test_projects.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Test Projects" \ - yarn kbn run test --exclude kibana --oss --skip-kibana-plugins + yarn kbn run test --exclude kibana --oss --skip-kibana-plugins --skip-missing From 740155e214f018818f0243641d58af9780d0e72e Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 27 Jan 2021 13:08:53 -0600 Subject: [PATCH 051/163] Address intermittent test failure (#89367) After observing the conditions when this test fails, it appears that some (but not all) signals are available. As these signals are generated by a rule via a bulk create, the odds of us retrieving signals in the middle of that bulk creation is very slim (but not impossible). The crux of the error here was: we wait for signals to be generated, but not the ones that we need. Specifically, we are waiting for a single signal to be available, but since we are asserting on sequences of signals, we need several to be available to us. While not perfect (because the signals we receive are not technically guaranteed to be sequence signals), increasing the number of signals that we wait for before proceeding should be sufficient to prevent this failure state. In debugging, it was observed that every test returning 9-10 signals succeeded, while it was possible for the test to return only one signal and fail. --- .../security_and_spaces/tests/generating_signals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index c3c7ecd0aba81..00a20abe367ae 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -265,7 +265,7 @@ export default ({ getService }: FtrProviderContext) => { }; const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByRuleIds(supertest, ['eql-rule']); const sequenceSignal = signalsOpen.hits.hits.find( (signal) => signal._source.signal.depth === 2 From c8ef36ab126a72813de0a400175e02c13aa5fd27 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 27 Jan 2021 13:03:00 -0700 Subject: [PATCH 052/163] skip flaky suite (#89475) --- test/functional/apps/management/_scripted_fields_preview.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.js index 104d41b7e2a75..46619b89dfc59 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.js @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const SCRIPTED_FIELD_NAME = 'myScriptedField'; - describe('scripted fields preview', () => { + // FLAKY: https://github.com/elastic/kibana/issues/89475 + describe.skip('scripted fields preview', () => { before(async function () { await browser.setWindowSize(1200, 800); await PageObjects.settings.createIndexPattern(); From 27d9a9ddaaf1a7d7767bf8accc887ffa7ab737c2 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 27 Jan 2021 13:04:11 -0700 Subject: [PATCH 053/163] skip flaky suite (#89477) --- test/functional/apps/discover/_saved_queries.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 6e6c53ec04985..ec6c455ecc979 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); - describe('saved queries saved objects', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/89477 + describe.skip('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); From 44b8333141e85ebc882f9ccfb98abaf6e523e3fa Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 27 Jan 2021 13:07:12 -0700 Subject: [PATCH 054/163] skip flaky suite (#89476) --- test/functional/apps/dashboard/dashboard_save.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/dashboard/dashboard_save.ts b/test/functional/apps/dashboard/dashboard_save.ts index 27cbba7db393d..e36136cd45141 100644 --- a/test/functional/apps/dashboard/dashboard_save.ts +++ b/test/functional/apps/dashboard/dashboard_save.ts @@ -12,7 +12,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header']); const listingTable = getService('listingTable'); - describe('dashboard save', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/89476 + describe.skip('dashboard save', function describeIndexTests() { this.tags('includeFirefox'); const dashboardName = 'Dashboard Save Test'; const dashboardNameEnterKey = 'Dashboard Save Test with Enter Key'; From 4499f62dcb7b8296d1cd37193530868ff432ffe9 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 27 Jan 2021 13:08:09 -0700 Subject: [PATCH 055/163] skip flaky suite (#89478) --- test/functional/apps/management/_import_objects.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 07811c9c68e45..754406938e47b 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -23,7 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); - describe('import objects', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/89478 + describe.skip('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { await kibanaServer.uiSettings.replace({}); From 1ff4256d64cdecca9a993ec0035ca176e918a73e Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 27 Jan 2021 13:38:16 -0700 Subject: [PATCH 056/163] [Maps] migrate maps, maps_file_upload, and maps_legacy_licensing to TS projects (#89439) * [Maps] migrate maps, maps_file_upload, and maps_legacy_licensing to TS projects * include types to avoid rison import errors * add mappings to tsconfig include --- .../add_layer_panel/view.tsx | 2 +- .../layer_panel/join_editor/join_editor.tsx | 2 +- .../layer_settings/layer_settings.tsx | 2 +- .../map_container/map_container.tsx | 2 +- .../map_settings_panel/map_settings_panel.tsx | 2 +- .../connected_components/mb_map/mb_map.tsx | 2 +- .../fit_to_data/fit_to_data.tsx | 2 +- .../tools_control/tools_control.tsx | 2 +- .../toc_entry_actions_popover.tsx | 2 +- .../routes/map_page/map_app/map_app.tsx | 4 +-- x-pack/plugins/maps/public/url_generator.ts | 1 + x-pack/plugins/maps/tsconfig.json | 25 +++++++++++++++++++ x-pack/plugins/maps_file_upload/tsconfig.json | 15 +++++++++++ .../maps_legacy_licensing/tsconfig.json | 14 +++++++++++ x-pack/tsconfig.json | 6 +++++ x-pack/tsconfig.refs.json | 3 +++ 16 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/maps/tsconfig.json create mode 100644 x-pack/plugins/maps_file_upload/tsconfig.json create mode 100644 x-pack/plugins/maps_legacy_licensing/tsconfig.json diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx index e2529fff66f3b..78a9f82bb698f 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx @@ -26,7 +26,7 @@ const ADD_LAYER_STEP_LABEL = i18n.translate('xpack.maps.addLayerPanel.addLayer', }); const SELECT_WIZARD_LABEL = ADD_LAYER_STEP_LABEL; -interface Props { +export interface Props { addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => void; closeFlyout: () => void; hasPreviewLayers: boolean; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx index 1bcee961db9e1..d47f130d4ede3 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx @@ -25,7 +25,7 @@ import { ILayer } from '../../../classes/layers/layer'; import { JoinDescriptor } from '../../../../common/descriptor_types'; import { IField } from '../../../classes/fields/field'; -interface Props { +export interface Props { joins: JoinDescriptor[]; layer: ILayer; layerDisplayName: string; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx index 33d684b320208..c0462f824cd06 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx @@ -21,7 +21,7 @@ import { AlphaSlider } from '../../../components/alpha_slider'; import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; import { ILayer } from '../../../classes/layers/layer'; -interface Props { +export interface Props { layer: ILayer; updateLabel: (layerId: string, label: string) => void; updateMinZoom: (layerId: string, minZoom: number) => void; diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 93476f6e14da5..36d07e3870818 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -35,7 +35,7 @@ import 'mapbox-gl/dist/mapbox-gl.css'; const RENDER_COMPLETE_EVENT = 'renderComplete'; -interface Props { +export interface Props { addFilters: ((filters: Filter[]) => Promise) | null; getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx index 726e2c3be7846..9cbbdec5e3d17 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx @@ -23,7 +23,7 @@ import { SpatialFiltersPanel } from './spatial_filters_panel'; import { DisplayPanel } from './display_panel'; import { MapCenter } from '../../../common/descriptor_types'; -interface Props { +export interface Props { cancelChanges: () => void; center: MapCenter; hasMapSettingsChanges: boolean; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 820453f166a46..21a8abcbaa4e9 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -49,7 +49,7 @@ import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; mapboxgl.workerUrl = mbWorkerUrl; mapboxgl.setRTLTextPlugin(mbRtlPlugin); -interface Props { +export interface Props { isMapReady: boolean; settings: MapSettings; layerList: ILayer[]; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx index 3f56d8d50b0f0..edf626612cb69 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx @@ -10,7 +10,7 @@ import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ILayer } from '../../../classes/layers/layer'; -interface Props { +export interface Props { layerList: ILayer[]; fitToBounds: () => void; } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx index ef320c73bce2f..a0f3aa40e75dd 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx @@ -50,7 +50,7 @@ const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( } ); -interface Props { +export interface Props { cancelDraw: () => void; geoFields: GeoFieldWithIndex[]; initiateDraw: (drawState: DrawState) => void; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index 4d669dfbe235e..fd0a0d55d2c1b 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { ILayer } from '../../../../../../classes/layers/layer'; import { TOCEntryButton } from '../toc_entry_button'; -interface Props { +export interface Props { cloneLayer: (layerId: string) => void; displayName: string; editLayer: () => void; diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 817fbf3656103..c0a378f38fc13 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -52,7 +52,7 @@ import { unsavedChangesWarning, } from '../saved_map'; -interface Props { +export interface Props { savedMap: SavedMap; // saveCounter used to trigger MapApp render after SaveMap.save saveCounter: number; @@ -83,7 +83,7 @@ interface Props { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } -interface State { +export interface State { initialized: boolean; indexPatterns: IndexPattern[]; savedQuery?: SavedQuery; diff --git a/x-pack/plugins/maps/public/url_generator.ts b/x-pack/plugins/maps/public/url_generator.ts index be6a7f5fe6fa7..7f4215f4b1275 100644 --- a/x-pack/plugins/maps/public/url_generator.ts +++ b/x-pack/plugins/maps/public/url_generator.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import rison from 'rison-node'; import { TimeRange, diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json new file mode 100644 index 0000000000000..b70459c690c07 --- /dev/null +++ b/x-pack/plugins/maps/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "config.ts", + "../../../typings/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/maps_legacy/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../maps_file_upload/tsconfig.json" }, + { "path": "../saved_objects_tagging/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/maps_file_upload/tsconfig.json b/x-pack/plugins/maps_file_upload/tsconfig.json new file mode 100644 index 0000000000000..f068d62b71739 --- /dev/null +++ b/x-pack/plugins/maps_file_upload/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*", "mappings.ts"], + "references": [ + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/maps_legacy_licensing/tsconfig.json b/x-pack/plugins/maps_legacy_licensing/tsconfig.json new file mode 100644 index 0000000000000..90e8265515a16 --- /dev/null +++ b/x-pack/plugins/maps_legacy_licensing/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*"], + "references": [ + { "path": "../licensing/tsconfig.json" }, + ] +} diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index a6eb098b5d678..4975dcfe885ab 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -19,6 +19,9 @@ "plugins/event_log/**/*", "plugins/licensing/**/*", "plugins/lens/**/*", + "plugins/maps/**/*", + "plugins/maps_file_upload/**/*", + "plugins/maps_legacy_licensing/**/*", "plugins/searchprofiler/**/*", "plugins/security_solution/cypress/**/*", "plugins/task_manager/**/*", @@ -86,6 +89,9 @@ { "path": "./plugins/event_log/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, + { "path": "./plugins/maps/tsconfig.json" }, + { "path": "./plugins/maps_file_upload/tsconfig.json" }, + { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./plugins/searchprofiler/tsconfig.json" }, { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 6a9e54e2e7adf..fcbc4d40530e1 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -15,6 +15,9 @@ { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "./plugins/maps/tsconfig.json" }, + { "path": "./plugins/maps_file_upload/tsconfig.json" }, + { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./plugins/searchprofiler/tsconfig.json" }, { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, From 8f3e1cf7fc2c7d10bed9e872ef6c2ed00773375f Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Wed, 27 Jan 2021 14:55:00 -0600 Subject: [PATCH 057/163] unskip getting_started/shakespeare test elasticsearch 64016 (#89346) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/getting_started/_shakespeare.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/getting_started/_shakespeare.ts b/test/functional/apps/getting_started/_shakespeare.ts index 95abbf9fa8a78..5a891af0de93d 100644 --- a/test/functional/apps/getting_started/_shakespeare.ts +++ b/test/functional/apps/getting_started/_shakespeare.ts @@ -30,8 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html - // Failing: See https://github.com/elastic/kibana/issues/82206 - describe.skip('Shakespeare', function describeIndexTests() { + describe('Shakespeare', function describeIndexTests() { // index starts on the first "count" metric at 1 // Each new metric or aggregation added to a visualization gets the next index. // So to modify a metric or aggregation tests need to keep track of the From 5ce916b3088bbf1c55f837ece25fd901177b91e0 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 27 Jan 2021 16:26:45 -0500 Subject: [PATCH 058/163] [kbn-es] Always use bundled JDK when starting Elasticsearch (#89437) --- packages/kbn-es/src/cli_commands/snapshot.js | 2 -- packages/kbn-es/src/cluster.js | 2 +- packages/kbn-es/src/install/archive.js | 16 +++------------- packages/kbn-es/src/install/snapshot.js | 2 -- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/kbn-es/src/cli_commands/snapshot.js b/packages/kbn-es/src/cli_commands/snapshot.js index d66c352a356aa..711992a5895ed 100644 --- a/packages/kbn-es/src/cli_commands/snapshot.js +++ b/packages/kbn-es/src/cli_commands/snapshot.js @@ -62,8 +62,6 @@ exports.run = async (defaults = {}) => { await cluster.extractDataDirectory(installPath, options.dataArchive); } - options.bundledJDK = true; - await cluster.run(installPath, options); } }; diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 60f8a327594d6..f554dd8a1b8e5 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -279,7 +279,7 @@ exports.Cluster = class Cluster { env: { ...(installPath ? { ES_TMPDIR: path.resolve(installPath, 'ES_TMPDIR') } : {}), ...process.env, - ...(options.bundledJDK ? { JAVA_HOME: '' } : {}), + JAVA_HOME: '', // By default, we want to always unset JAVA_HOME so that the bundled JDK will be used ...(options.esEnvVars || {}), }, stdio: ['ignore', 'pipe', 'pipe'], diff --git a/packages/kbn-es/src/install/archive.js b/packages/kbn-es/src/install/archive.js index 1e9e19f4533af..80ff4eb6f83b0 100644 --- a/packages/kbn-es/src/install/archive.js +++ b/packages/kbn-es/src/install/archive.js @@ -34,7 +34,6 @@ exports.installArchive = async function installArchive(archive, options = {}) { basePath = BASE_PATH, installPath = path.resolve(basePath, path.basename(archive, '.tar.gz')), log = defaultLog, - bundledJDK = false, esArgs = [], } = options; @@ -64,7 +63,7 @@ exports.installArchive = async function installArchive(archive, options = {}) { await appendToConfig(installPath, 'xpack.security.enabled', 'true'); await appendToConfig(installPath, 'xpack.license.self_generated.type', license); - await configureKeystore(installPath, log, bundledJDK, [ + await configureKeystore(installPath, log, [ ['bootstrap.password', password], ...parseSettings(esArgs, { filter: SettingsFilter.SecureOnly }), ]); @@ -89,20 +88,11 @@ async function appendToConfig(installPath, key, value) { * * @param {String} installPath * @param {ToolingLog} log - * @param {boolean} bundledJDK * @param {Array<[string, string]>} secureSettings List of custom Elasticsearch secure settings to * add into the keystore. */ -async function configureKeystore( - installPath, - log = defaultLog, - bundledJDK = false, - secureSettings -) { - const env = {}; - if (bundledJDK) { - env.JAVA_HOME = ''; - } +async function configureKeystore(installPath, log = defaultLog, secureSettings) { + const env = { JAVA_HOME: '' }; await execa(ES_KEYSTORE_BIN, ['create'], { cwd: installPath, env }); for (const [secureSettingName, secureSettingValue] of secureSettings) { diff --git a/packages/kbn-es/src/install/snapshot.js b/packages/kbn-es/src/install/snapshot.js index 55c0e41ea9640..b9562f20d81b7 100644 --- a/packages/kbn-es/src/install/snapshot.js +++ b/packages/kbn-es/src/install/snapshot.js @@ -61,7 +61,6 @@ exports.installSnapshot = async function installSnapshot({ basePath = BASE_PATH, installPath = path.resolve(basePath, version), log = defaultLog, - bundledJDK = true, esArgs, }) { const { downloadPath } = await exports.downloadSnapshot({ @@ -78,7 +77,6 @@ exports.installSnapshot = async function installSnapshot({ basePath, installPath, log, - bundledJDK, esArgs, }); }; From 445cb2ef87efec7ce0d06baebbe2eadc0d207af4 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 27 Jan 2021 18:44:17 -0500 Subject: [PATCH 059/163] [Lens] Fix crash in transition from unique count to last value (#88916) * [Lens] Fix transition from unique count to last value * Fix test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../operations/layer_helpers.test.ts | 8 +++---- .../operations/layer_helpers.ts | 7 +----- .../test/functional/apps/lens/smokescreen.ts | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 94cf13a5c50a4..63f8bfb97d8f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -2181,7 +2181,7 @@ describe('state_helpers', () => { expect(errors).toHaveLength(1); }); - it('should consider incompleteColumns before layer columns', () => { + it('should ignore incompleteColumns when checking for errors', () => { const savedRef = jest.fn().mockReturnValue(['error 1']); const incompleteRef = jest.fn(); operationDefinitionMap.testReference.getErrorMessage = savedRef; @@ -2206,9 +2206,9 @@ describe('state_helpers', () => { }, indexPattern ); - expect(savedRef).not.toHaveBeenCalled(); - expect(incompleteRef).toHaveBeenCalled(); - expect(errors).toBeUndefined(); + expect(savedRef).toHaveBeenCalled(); + expect(incompleteRef).not.toHaveBeenCalled(); + expect(errors).toHaveLength(1); delete operationDefinitionMap.testIncompleteReference; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 10618cc754556..7c0036de62124 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -870,12 +870,7 @@ export function getErrorMessages( ): string[] | undefined { const errors: string[] = Object.entries(layer.columns) .flatMap(([columnId, column]) => { - // If we're transitioning to another operation, check for "new" incompleteColumns rather - // than "old" saved operation on the layer - const columnFinalRef = - layer.incompleteColumns?.[columnId]?.operationType || column.operationType; - const def = operationDefinitionMap[columnFinalRef]; - + const def = operationDefinitionMap[column.operationType]; if (def.getErrorMessage) { return def.getErrorMessage(layer, columnId, indexPattern); } diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index f2d91c2ae577f..88682d475146f 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -514,6 +514,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); + it('should transition from unique count to last value', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'cardinality', + field: 'ip', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'last_value', + field: 'bytes', + isPreviousIncompatible: true, + }); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Last value of bytes' + ); + }); + it('should allow to change index pattern', async () => { await PageObjects.lens.switchFirstLayerIndexPattern('log*'); expect(await PageObjects.lens.getFirstLayerIndexPattern()).to.equal('log*'); From 511c9913b38bcc887513aa408765c925f95d43ab Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 27 Jan 2021 15:48:36 -0800 Subject: [PATCH 060/163] Adds migration settings to Docker (#89501) Signed-off-by: Tyler Smalley --- .../resources/bin/kibana-docker | 105 +++++++++--------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 30e3b60dcee83..1598f00354bf8 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -15,11 +15,17 @@ # --elasticsearch.logQueries=true kibana_vars=( + apm_oss.apmAgentConfigurationIndex + apm_oss.errorIndices + apm_oss.indexPattern + apm_oss.metricsIndices + apm_oss.onboardingIndices + apm_oss.sourcemapIndices + apm_oss.spanIndices + apm_oss.transactionIndices console.enabled console.proxyConfig console.proxyFilter - ops.cGroupOverrides.cpuPath - ops.cGroupOverrides.cpuAcctPath cpu.cgroup.path.override cpuacct.cgroup.path.override csp.rules @@ -41,10 +47,10 @@ kibana_vars=( elasticsearch.ssl.certificateAuthorities elasticsearch.ssl.key elasticsearch.ssl.keyPassphrase - elasticsearch.ssl.keystore.path elasticsearch.ssl.keystore.password - elasticsearch.ssl.truststore.path + elasticsearch.ssl.keystore.path elasticsearch.ssl.truststore.password + elasticsearch.ssl.truststore.path elasticsearch.ssl.verificationMode elasticsearch.username enterpriseSearch.accessCheckTimeout @@ -76,34 +82,42 @@ kibana_vars=( map.tilemap.options.minZoom map.tilemap.options.subdomains map.tilemap.url + migrations.batchSize + migrations.enableV2 + migrations.pollInterval + migrations.scrollDuration + migrations.skip monitoring.cluster_alerts.email_notifications.email_address monitoring.enabled monitoring.kibana.collection.enabled monitoring.kibana.collection.interval monitoring.ui.container.elasticsearch.enabled monitoring.ui.container.logstash.enabled - monitoring.ui.elasticsearch.password - monitoring.ui.elasticsearch.pingTimeout monitoring.ui.elasticsearch.hosts - monitoring.ui.elasticsearch.username monitoring.ui.elasticsearch.logFetchCount + monitoring.ui.elasticsearch.password + monitoring.ui.elasticsearch.pingTimeout monitoring.ui.elasticsearch.ssl.certificateAuthorities monitoring.ui.elasticsearch.ssl.verificationMode + monitoring.ui.elasticsearch.username monitoring.ui.enabled monitoring.ui.max_bucket_size monitoring.ui.min_interval_seconds newsfeed.enabled + ops.cGroupOverrides.cpuAcctPath + ops.cGroupOverrides.cpuPath ops.interval path.data pid.file regionmap security.showInsecureClusterWarning server.basePath - server.customResponseHeaders server.compression.enabled server.compression.referrerWhitelist server.cors server.cors.origin + server.customResponseHeaders + server.customResponseHeaders server.defaultRoute server.host server.keepAliveTimeout @@ -117,20 +131,24 @@ kibana_vars=( server.ssl.certificateAuthorities server.ssl.cipherSuites server.ssl.clientAuthentication - server.customResponseHeaders server.ssl.enabled server.ssl.key server.ssl.keyPassphrase - server.ssl.keystore.path server.ssl.keystore.password - server.ssl.truststore.path - server.ssl.truststore.password + server.ssl.keystore.path server.ssl.redirectHttpFromPort server.ssl.supportedProtocols + server.ssl.truststore.password + server.ssl.truststore.path server.xsrf.disableProtection server.xsrf.whitelist status.allowAnonymous status.v6ApiFormat + telemetry.allowChangingOptInStatus + telemetry.enabled + telemetry.optIn + telemetry.optInStatusUrl + telemetry.sendUsageFrom tilemap.options.attribution tilemap.options.maxZoom tilemap.options.minZoom @@ -142,9 +160,9 @@ kibana_vars=( xpack.actions.enabled xpack.actions.enabledActionTypes xpack.actions.preconfigured - xpack.actions.proxyUrl xpack.actions.proxyHeaders xpack.actions.proxyRejectUnauthorizedCertificates + xpack.actions.proxyUrl xpack.actions.rejectUnauthorized xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval @@ -154,37 +172,29 @@ kibana_vars=( xpack.apm.ui.enabled xpack.apm.ui.maxTraceItems xpack.apm.ui.transactionGroupBucketSize - apm_oss.apmAgentConfigurationIndex - apm_oss.indexPattern - apm_oss.errorIndices - apm_oss.onboardingIndices - apm_oss.spanIndices - apm_oss.sourcemapIndices - apm_oss.transactionIndices - apm_oss.metricsIndices xpack.canvas.enabled - xpack.code.ui.enabled xpack.code.disk.thresholdEnabled xpack.code.disk.watermarkLow - xpack.code.maxWorkspace xpack.code.indexRepoFrequencyMs - xpack.code.updateRepoFrequencyMs xpack.code.lsp.verbose - xpack.code.verbose + xpack.code.maxWorkspace xpack.code.security.enableGitCertCheck xpack.code.security.gitHostWhitelist xpack.code.security.gitProtocolWhitelist + xpack.code.ui.enabled + xpack.code.updateRepoFrequencyMs + xpack.code.verbose xpack.encryptedSavedObjects.encryptionKey xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys xpack.event_log.enabled - xpack.event_log.logEntries xpack.event_log.indexEntries + xpack.event_log.logEntries xpack.fleet.agents.elasticsearch.host xpack.fleet.agents.kibana.host xpack.fleet.agents.tlsCheckDisabled xpack.fleet.registryUrl - xpack.graph.enabled xpack.graph.canEditDrillDownUrls + xpack.graph.enabled xpack.graph.savePolicy xpack.grokdebugger.enabled xpack.infra.enabled @@ -208,28 +218,28 @@ kibana_vars=( xpack.reporting.capture.browser.chromium.disableSandbox xpack.reporting.capture.browser.chromium.inspect xpack.reporting.capture.browser.chromium.maxScreenshotDimension + xpack.reporting.capture.browser.chromium.proxy.bypass xpack.reporting.capture.browser.chromium.proxy.enabled xpack.reporting.capture.browser.chromium.proxy.server - xpack.reporting.capture.browser.chromium.proxy.bypass xpack.reporting.capture.browser.type xpack.reporting.capture.concurrency xpack.reporting.capture.loadDelay + xpack.reporting.capture.maxAttempts xpack.reporting.capture.settleTime xpack.reporting.capture.timeout + xpack.reporting.capture.timeouts.openUrl + xpack.reporting.capture.timeouts.renderComplete + xpack.reporting.capture.timeouts.waitForElements xpack.reporting.capture.viewport.height xpack.reporting.capture.viewport.width xpack.reporting.capture.zoom xpack.reporting.csv.checkForFormulas - xpack.reporting.csv.escapeFormulaValues xpack.reporting.csv.enablePanelActionDownload - xpack.reporting.csv.useByteOrderMarkEncoding + xpack.reporting.csv.escapeFormulaValues xpack.reporting.csv.maxSizeBytes xpack.reporting.csv.scroll.duration xpack.reporting.csv.scroll.size - xpack.reporting.capture.maxAttempts - xpack.reporting.capture.timeouts.openUrl - xpack.reporting.capture.timeouts.waitForElements - xpack.reporting.capture.timeouts.renderComplete + xpack.reporting.csv.useByteOrderMarkEncoding xpack.reporting.enabled xpack.reporting.encryptionKey xpack.reporting.index @@ -248,43 +258,38 @@ kibana_vars=( xpack.reporting.queue.timeout xpack.reporting.roles.allow xpack.rollup.enabled - xpack.security.audit.enabled xpack.searchprofiler.enabled - xpack.security.authc.providers + xpack.security.audit.enabled xpack.security.authc.oidc.realm - xpack.security.authc.saml.realm + xpack.security.authc.providers xpack.security.authc.saml.maxRedirectURLSize + xpack.security.authc.saml.realm xpack.security.authc.selector.enabled xpack.security.cookieName xpack.security.enabled xpack.security.encryptionKey + xpack.security.loginAssistanceMessage + xpack.security.loginHelp xpack.security.sameSiteCookies xpack.security.secureCookies - xpack.security.sessionTimeout + xpack.security.session.cleanupInterval xpack.security.session.idleTimeout xpack.security.session.lifespan - xpack.security.session.cleanupInterval - xpack.security.loginAssistanceMessage - xpack.security.loginHelp + xpack.security.sessionTimeout xpack.spaces.enabled xpack.spaces.maxSpaces xpack.task_manager.enabled + xpack.task_manager.index xpack.task_manager.max_attempts - xpack.task_manager.poll_interval xpack.task_manager.max_poll_inactivity_cycles - xpack.task_manager.request_capacity - xpack.task_manager.index xpack.task_manager.max_workers - xpack.task_manager.monitored_stats_required_freshness xpack.task_manager.monitored_aggregated_stats_refresh_rate + xpack.task_manager.monitored_stats_required_freshness xpack.task_manager.monitored_stats_running_average_window xpack.task_manager.monitored_task_execution_thresholds + xpack.task_manager.poll_interval + xpack.task_manager.request_capacity xpack.task_manager.version_conflict_threshold - telemetry.allowChangingOptInStatus - telemetry.enabled - telemetry.optIn - telemetry.optInStatusUrl - telemetry.sendUsageFrom ) longopts='' From f2aa5bcd9511b73a119e4a976bc13093a1c018fd Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 27 Jan 2021 16:54:03 -0700 Subject: [PATCH 061/163] [Maps] remove maps_oss TS project (#89502) --- src/plugins/maps_oss/tsconfig.json | 14 -------------- tsconfig.json | 2 -- tsconfig.refs.json | 1 - 3 files changed, 17 deletions(-) delete mode 100644 src/plugins/maps_oss/tsconfig.json diff --git a/src/plugins/maps_oss/tsconfig.json b/src/plugins/maps_oss/tsconfig.json deleted file mode 100644 index 03c30c3c49fd3..0000000000000 --- a/src/plugins/maps_oss/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts"], - "references": [ - { "path": "../visualizations/tsconfig.json" }, - ] -} diff --git a/tsconfig.json b/tsconfig.json index 334a3febfddda..bdd4ba296d1c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,7 +28,6 @@ "src/plugins/kibana_utils/**/*", "src/plugins/management/**/*", "src/plugins/maps_legacy/**/*", - "src/plugins/maps_oss/**/*", "src/plugins/navigation/**/*", "src/plugins/newsfeed/**/*", "src/plugins/region_map/**/*", @@ -86,7 +85,6 @@ { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/maps_legacy/tsconfig.json" }, - { "path": "./src/plugins/maps_oss/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/region_map/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index a8eecd278160c..211a50ec1a539 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -24,7 +24,6 @@ { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/maps_legacy/tsconfig.json" }, - { "path": "./src/plugins/maps_oss/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/region_map/tsconfig.json" }, From b2d441214608a71752b3e9ffdb29ccc5c10310bc Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 27 Jan 2021 16:15:28 -0800 Subject: [PATCH 062/163] [CI] Decrease number of Jest workers (#89504) Signed-off-by: Tyler Smalley --- .ci/teamcity/oss/jest.sh | 2 +- .ci/teamcity/tests/jest.sh | 2 +- test/scripts/test/jest_unit.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/teamcity/oss/jest.sh b/.ci/teamcity/oss/jest.sh index b323a88ef06bc..6d9396574c077 100755 --- a/.ci/teamcity/oss/jest.sh +++ b/.ci/teamcity/oss/jest.sh @@ -10,4 +10,4 @@ source "$(dirname "${0}")/../util.sh" export JOB=kibana-oss-jest checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose + node scripts/jest --ci --maxWorkers=5 --verbose diff --git a/.ci/teamcity/tests/jest.sh b/.ci/teamcity/tests/jest.sh index c8b9b075e0e61..3d60915c1b1b5 100755 --- a/.ci/teamcity/tests/jest.sh +++ b/.ci/teamcity/tests/jest.sh @@ -7,4 +7,4 @@ source "$(dirname "${0}")/../util.sh" export JOB=kibana-jest checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose --coverage + node scripts/jest --ci --maxWorkers=5 --verbose diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index 14d7268c6f36d..06c159c0a4ace 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose --maxWorkers=10 --coverage + node scripts/jest --ci --verbose --maxWorkers=6 --coverage From fd2e9d08212be0c4b43a99c62dd445405ff8092c Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 27 Jan 2021 16:21:30 -0800 Subject: [PATCH 063/163] Convert default_watch.json to a JS object in order to avoid TS complaints (#89488) * Remove unused defaultWatchJson static member from public JsonWatch model. --- .../application/models/watch/default_watch.js | 40 +++++++++++++++++++ .../models/watch/default_watch.json | 33 --------------- .../application/models/watch/index.d.ts | 1 + .../public/application/models/watch/index.js | 1 + .../application/models/watch/json_watch.js | 12 +++--- .../watch_create_json.test.ts | 15 +++---- .../watch_edit.test.ts | 11 ++--- 7 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/watcher/public/application/models/watch/default_watch.js delete mode 100644 x-pack/plugins/watcher/public/application/models/watch/default_watch.json diff --git a/x-pack/plugins/watcher/public/application/models/watch/default_watch.js b/x-pack/plugins/watcher/public/application/models/watch/default_watch.js new file mode 100644 index 0000000000000..51b28f8bd0332 --- /dev/null +++ b/x-pack/plugins/watcher/public/application/models/watch/default_watch.js @@ -0,0 +1,40 @@ +/* + * 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 defaultWatch = { + trigger: { + schedule: { + interval: '30m', + }, + }, + input: { + search: { + request: { + body: { + size: 0, + query: { + match_all: {}, + }, + }, + indices: ['*'], + }, + }, + }, + condition: { + compare: { + 'ctx.payload.hits.total': { + gte: 10, + }, + }, + }, + actions: { + 'my-logging-action': { + logging: { + text: 'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.', + }, + }, + }, +}; diff --git a/x-pack/plugins/watcher/public/application/models/watch/default_watch.json b/x-pack/plugins/watcher/public/application/models/watch/default_watch.json deleted file mode 100644 index 22c78660a0bb0..0000000000000 --- a/x-pack/plugins/watcher/public/application/models/watch/default_watch.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "trigger": { - "schedule": { - "interval": "30m" - } - }, - "input": { - "search": { - "request": { - "body": { - "size": 0, - "query" : { - "match_all": {} - } - }, - "indices": [ "*" ] - } - } - }, - "condition": { - "compare": { - "ctx.payload.hits.total": { - "gte": 10 - } - }}, - "actions": { - "my-logging-action": { - "logging": { - "text": "There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10." - } - } - } -} diff --git a/x-pack/plugins/watcher/public/application/models/watch/index.d.ts b/x-pack/plugins/watcher/public/application/models/watch/index.d.ts index 73ee2279d3912..b9158bdd9ed70 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/index.d.ts +++ b/x-pack/plugins/watcher/public/application/models/watch/index.d.ts @@ -5,3 +5,4 @@ */ export const Watch: any; +export const defaultWatch: any; diff --git a/x-pack/plugins/watcher/public/application/models/watch/index.js b/x-pack/plugins/watcher/public/application/models/watch/index.js index 9a74f6e548409..0741e5c5400df 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/index.js +++ b/x-pack/plugins/watcher/public/application/models/watch/index.js @@ -5,3 +5,4 @@ */ export { Watch } from './watch'; +export { defaultWatch } from './default_watch'; diff --git a/x-pack/plugins/watcher/public/application/models/watch/json_watch.js b/x-pack/plugins/watcher/public/application/models/watch/json_watch.js index 0e67c8b18ca5e..24378d42b7451 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/json_watch.js +++ b/x-pack/plugins/watcher/public/application/models/watch/json_watch.js @@ -6,11 +6,12 @@ import uuid from 'uuid'; import { get } from 'lodash'; -import { BaseWatch } from './base_watch'; -import { ACTION_TYPES, WATCH_TYPES } from '../../../../common/constants'; -import defaultWatchJson from './default_watch.json'; import { i18n } from '@kbn/i18n'; +import { ACTION_TYPES, WATCH_TYPES } from '../../../../common/constants'; +import { BaseWatch } from './base_watch'; +import { defaultWatch } from './default_watch'; + /** * {@code JsonWatch} allows a user to create a Watch by writing the raw JSON. */ @@ -20,11 +21,11 @@ export class JsonWatch extends BaseWatch { props.id = typeof props.id === 'undefined' ? uuid.v4() : props.id; super(props); const existingWatch = get(props, 'watch'); - this.watch = existingWatch ? existingWatch : defaultWatchJson; + this.watch = existingWatch ? existingWatch : defaultWatch; this.watchString = get( props, 'watchString', - JSON.stringify(existingWatch ? existingWatch : defaultWatchJson, null, 2) + JSON.stringify(existingWatch ? existingWatch : defaultWatch, null, 2) ); this.id = props.id; } @@ -113,7 +114,6 @@ export class JsonWatch extends BaseWatch { return new JsonWatch(upstreamWatch); } - static defaultWatchJson = defaultWatchJson; static typeName = i18n.translate('xpack.watcher.models.jsonWatch.typeName', { defaultMessage: 'Advanced Watch', }); diff --git a/x-pack/plugins/watcher/tests_client_integration/watch_create_json.test.ts b/x-pack/plugins/watcher/tests_client_integration/watch_create_json.test.ts index b3fbb8235f251..9bd8f8bbd7d57 100644 --- a/x-pack/plugins/watcher/tests_client_integration/watch_create_json.test.ts +++ b/x-pack/plugins/watcher/tests_client_integration/watch_create_json.test.ts @@ -5,11 +5,12 @@ */ import { act } from 'react-dom/test-utils'; + +import { getExecuteDetails } from '../__fixtures__'; +import { defaultWatch } from '../public/application/models/watch'; import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers'; import { WATCH } from './helpers/jest_constants'; -import defaultWatchJson from '../public/application/models/watch/default_watch.json'; -import { getExecuteDetails } from '../__fixtures__'; const { setup } = pageHelpers.watchCreateJson; @@ -117,7 +118,7 @@ describe(' create route', () => { }, }, ], - watch: defaultWatchJson, + watch: defaultWatch, }) ); }); @@ -172,7 +173,7 @@ describe(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; - const actionModes = Object.keys(defaultWatchJson.actions).reduce( + const actionModes = Object.keys(defaultWatch.actions).reduce( (actionAccum: any, action) => { actionAccum[action] = 'simulate'; return actionAccum; @@ -186,7 +187,7 @@ describe(' create route', () => { isNew: true, isActive: true, actions: [], - watch: defaultWatchJson, + watch: defaultWatch, }; expect(latestRequest.requestBody).toEqual( @@ -234,7 +235,7 @@ describe(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; - const actionModes = Object.keys(defaultWatchJson.actions).reduce( + const actionModes = Object.keys(defaultWatch.actions).reduce( (actionAccum: any, action) => { actionAccum[action] = ACTION_MODE; return actionAccum; @@ -248,7 +249,7 @@ describe(' create route', () => { isNew: true, isActive: true, actions: [], - watch: defaultWatchJson, + watch: defaultWatch, }; const triggeredTime = `now+${TRIGGERED_TIME}s`; diff --git a/x-pack/plugins/watcher/tests_client_integration/watch_edit.test.ts b/x-pack/plugins/watcher/tests_client_integration/watch_edit.test.ts index eefe9d03c05ef..c24d939c9237e 100644 --- a/x-pack/plugins/watcher/tests_client_integration/watch_edit.test.ts +++ b/x-pack/plugins/watcher/tests_client_integration/watch_edit.test.ts @@ -7,12 +7,13 @@ import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import axios from 'axios'; +import { getRandomString } from '@kbn/test/jest'; + +import { getWatch } from '../__fixtures__'; +import { defaultWatch } from '../public/application/models/watch'; import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchEditTestBed } from './helpers/watch_edit.helpers'; import { WATCH } from './helpers/jest_constants'; -import defaultWatchJson from '../public/application/models/watch/default_watch.json'; -import { getWatch } from '../__fixtures__'; -import { getRandomString } from '@kbn/test/jest'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); @@ -69,7 +70,7 @@ describe('', () => { expect(exists('jsonWatchForm')).toBe(true); expect(find('nameInput').props().value).toBe(watch.name); expect(find('idInput').props().value).toBe(watch.id); - expect(JSON.parse(codeEditor.props().value as string)).toEqual(defaultWatchJson); + expect(JSON.parse(codeEditor.props().value as string)).toEqual(defaultWatch); // ID should not be editable expect(find('idInput').props().readOnly).toEqual(true); @@ -112,7 +113,7 @@ describe('', () => { }, }, ], - watch: defaultWatchJson, + watch: defaultWatch, }) ); }); From 2572cd291b8e3e703680859bab6c024474608924 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 27 Jan 2021 18:25:46 -0600 Subject: [PATCH 064/163] [build/docker] Add support for centos ARM builds (#84831) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Tyler Smalley --- src/dev/build/args.test.ts | 68 +++++++++++++++---- src/dev/build/args.ts | 22 ++++-- src/dev/build/build_distributables.ts | 24 ++++--- src/dev/build/cli.ts | 8 ++- .../os_packages/create_os_package_tasks.ts | 54 ++++++++++++--- .../docker_generator/bundle_dockerfiles.ts | 1 - .../os_packages/docker_generator/index.ts | 2 +- .../tasks/os_packages/docker_generator/run.ts | 51 ++++++++------ .../docker_generator/template_context.ts | 2 + .../docker_generator/templates/Dockerfile | 21 ++++-- .../templates/build_docker_sh.template.ts | 6 +- .../templates/dockerfile.template.ts | 1 + 12 files changed, 191 insertions(+), 69 deletions(-) diff --git a/src/dev/build/args.test.ts b/src/dev/build/args.test.ts index 584f0dfe54b74..745b9d0b910c8 100644 --- a/src/dev/build/args.test.ts +++ b/src/dev/build/args.test.ts @@ -30,8 +30,9 @@ it('build default and oss dist for current platform, without packages, by defaul "buildOssDist": true, "createArchives": true, "createDebPackage": false, - "createDockerPackage": false, - "createDockerUbiPackage": false, + "createDockerCentOS": false, + "createDockerContexts": false, + "createDockerUBI": false, "createRpmPackage": false, "downloadFreshNode": true, "isRelease": false, @@ -53,8 +54,9 @@ it('builds packages if --all-platforms is passed', () => { "buildOssDist": true, "createArchives": true, "createDebPackage": true, - "createDockerPackage": true, - "createDockerUbiPackage": true, + "createDockerCentOS": true, + "createDockerContexts": true, + "createDockerUBI": true, "createRpmPackage": true, "downloadFreshNode": true, "isRelease": false, @@ -76,8 +78,9 @@ it('limits packages if --rpm passed with --all-platforms', () => { "buildOssDist": true, "createArchives": true, "createDebPackage": false, - "createDockerPackage": false, - "createDockerUbiPackage": false, + "createDockerCentOS": false, + "createDockerContexts": false, + "createDockerUBI": false, "createRpmPackage": true, "downloadFreshNode": true, "isRelease": false, @@ -99,8 +102,9 @@ it('limits packages if --deb passed with --all-platforms', () => { "buildOssDist": true, "createArchives": true, "createDebPackage": true, - "createDockerPackage": false, - "createDockerUbiPackage": false, + "createDockerCentOS": false, + "createDockerContexts": false, + "createDockerUBI": false, "createRpmPackage": false, "downloadFreshNode": true, "isRelease": false, @@ -115,7 +119,7 @@ it('limits packages if --deb passed with --all-platforms', () => { }); it('limits packages if --docker passed with --all-platforms', () => { - expect(readCliArgs(['node', 'scripts/build', '--all-platforms', '--docker'])) + expect(readCliArgs(['node', 'scripts/build', '--all-platforms', '--docker-images'])) .toMatchInlineSnapshot(` Object { "buildOptions": Object { @@ -123,8 +127,9 @@ it('limits packages if --docker passed with --all-platforms', () => { "buildOssDist": true, "createArchives": true, "createDebPackage": false, - "createDockerPackage": true, - "createDockerUbiPackage": true, + "createDockerCentOS": true, + "createDockerContexts": false, + "createDockerUBI": true, "createRpmPackage": false, "downloadFreshNode": true, "isRelease": false, @@ -139,16 +144,24 @@ it('limits packages if --docker passed with --all-platforms', () => { }); it('limits packages if --docker passed with --skip-docker-ubi and --all-platforms', () => { - expect(readCliArgs(['node', 'scripts/build', '--all-platforms', '--docker', '--skip-docker-ubi'])) - .toMatchInlineSnapshot(` + expect( + readCliArgs([ + 'node', + 'scripts/build', + '--all-platforms', + '--docker-images', + '--skip-docker-ubi', + ]) + ).toMatchInlineSnapshot(` Object { "buildOptions": Object { "buildDefaultDist": true, "buildOssDist": true, "createArchives": true, "createDebPackage": false, - "createDockerPackage": true, - "createDockerUbiPackage": false, + "createDockerCentOS": true, + "createDockerContexts": false, + "createDockerUBI": false, "createRpmPackage": false, "downloadFreshNode": true, "isRelease": false, @@ -161,3 +174,28 @@ it('limits packages if --docker passed with --skip-docker-ubi and --all-platform } `); }); + +it('limits packages if --all-platforms passed with --skip-docker-centos', () => { + expect(readCliArgs(['node', 'scripts/build', '--all-platforms', '--skip-docker-centos'])) + .toMatchInlineSnapshot(` + Object { + "buildOptions": Object { + "buildDefaultDist": true, + "buildOssDist": true, + "createArchives": true, + "createDebPackage": true, + "createDockerCentOS": false, + "createDockerContexts": true, + "createDockerUBI": true, + "createRpmPackage": true, + "downloadFreshNode": true, + "isRelease": false, + "targetAllPlatforms": true, + "versionQualifier": "", + }, + "log": , + "showHelp": false, + "unknownFlags": Array [], + } + `); +}); diff --git a/src/dev/build/args.ts b/src/dev/build/args.ts index 5070e325f40a4..2d26d7db3a5e3 100644 --- a/src/dev/build/args.ts +++ b/src/dev/build/args.ts @@ -21,8 +21,10 @@ export function readCliArgs(argv: string[]) { 'skip-os-packages', 'rpm', 'deb', - 'docker', + 'docker-images', + 'docker-contexts', 'skip-docker-ubi', + 'skip-docker-centos', 'release', 'skip-node-download', 'verbose', @@ -42,7 +44,8 @@ export function readCliArgs(argv: string[]) { debug: true, rpm: null, deb: null, - docker: null, + 'docker-images': null, + 'docker-contexts': null, oss: null, 'version-qualifier': '', }, @@ -69,7 +72,7 @@ export function readCliArgs(argv: string[]) { // In order to build a docker image we always need // to generate all the platforms - if (flags.docker) { + if (flags['docker-images'] || flags['docker-contexts']) { flags['all-platforms'] = true; } @@ -79,7 +82,12 @@ export function readCliArgs(argv: string[]) { } // build all if no flags specified - if (flags.rpm === null && flags.deb === null && flags.docker === null) { + if ( + flags.rpm === null && + flags.deb === null && + flags['docker-images'] === null && + flags['docker-contexts'] === null + ) { return true; } @@ -95,8 +103,10 @@ export function readCliArgs(argv: string[]) { createArchives: !Boolean(flags['skip-archives']), createRpmPackage: isOsPackageDesired('rpm'), createDebPackage: isOsPackageDesired('deb'), - createDockerPackage: isOsPackageDesired('docker'), - createDockerUbiPackage: isOsPackageDesired('docker') && !Boolean(flags['skip-docker-ubi']), + createDockerCentOS: + isOsPackageDesired('docker-images') && !Boolean(flags['skip-docker-centos']), + createDockerUBI: isOsPackageDesired('docker-images') && !Boolean(flags['skip-docker-ubi']), + createDockerContexts: isOsPackageDesired('docker-contexts'), targetAllPlatforms: Boolean(flags['all-platforms']), }; diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 6620673829711..df4ba45517cc1 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -19,8 +19,9 @@ export interface BuildOptions { createArchives: boolean; createRpmPackage: boolean; createDebPackage: boolean; - createDockerPackage: boolean; - createDockerUbiPackage: boolean; + createDockerUBI: boolean; + createDockerCentOS: boolean; + createDockerContexts: boolean; versionQualifier: string | undefined; targetAllPlatforms: boolean; } @@ -95,12 +96,19 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions // control w/ --rpm or --skip-os-packages await run(Tasks.CreateRpmPackage); } - if (options.createDockerPackage) { - // control w/ --docker or --skip-docker-ubi or --skip-os-packages - await run(Tasks.CreateDockerPackage); - if (options.createDockerUbiPackage) { - await run(Tasks.CreateDockerUbiPackage); - } + if (options.createDockerUBI) { + // control w/ --docker-images or --skip-docker-ubi or --skip-os-packages + await run(Tasks.CreateDockerUBI); + } + + if (options.createDockerCentOS) { + // control w/ --docker-images or --skip-docker-centos or --skip-os-packages + await run(Tasks.CreateDockerCentOS); + } + + if (options.createDockerContexts) { + // control w/ --docker-contexts or --skip-os-packages + await run(Tasks.CreateDockerContexts); } /** diff --git a/src/dev/build/cli.ts b/src/dev/build/cli.ts index ca9debfdc1ae1..3e3a0a493f2d1 100644 --- a/src/dev/build/cli.ts +++ b/src/dev/build/cli.ts @@ -38,10 +38,12 @@ if (showHelp) { --skip-archives {dim Don't produce tar/zip archives} --skip-os-packages {dim Don't produce rpm/deb/docker packages} --all-platforms {dim Produce archives for all platforms, not just this one} - --rpm {dim Only build the rpm package} - --deb {dim Only build the deb package} - --docker {dim Only build the docker image} + --rpm {dim Only build the rpm packages} + --deb {dim Only build the deb packages} + --docker-images {dim Only build the Docker images} + --docker-contexts {dim Only build the Docker build contexts} --skip-docker-ubi {dim Don't build the docker ubi image} + --skip-docker-centos {dim Don't build the docker centos image} --release {dim Produce a release-ready distributable} --version-qualifier {dim Suffix version with a qualifier} --skip-node-download {dim Reuse existing downloads of node.js} diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index ba57a5f3dbfc9..fd0224d3de13b 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -8,7 +8,7 @@ import { Task } from '../../lib'; import { runFpm } from './run_fpm'; -import { runDockerGenerator, runDockerGeneratorForUBI } from './docker_generator'; +import { runDockerGenerator } from './docker_generator'; export const CreateDebPackage: Task = { description: 'Creating deb package', @@ -49,20 +49,56 @@ export const CreateRpmPackage: Task = { }, }; -export const CreateDockerPackage: Task = { - description: 'Creating docker package', +export const CreateDockerCentOS: Task = { + description: 'Creating Docker CentOS image', async run(config, log, build) { - // Builds Docker targets for default and oss - await runDockerGenerator(config, log, build); + await runDockerGenerator(config, log, build, { + ubi: false, + context: false, + architecture: 'x64', + image: true, + }); + await runDockerGenerator(config, log, build, { + ubi: false, + context: false, + architecture: 'aarch64', + image: true, + }); }, }; -export const CreateDockerUbiPackage: Task = { - description: 'Creating docker ubi package', +export const CreateDockerUBI: Task = { + description: 'Creating Docker UBI image', async run(config, log, build) { - // Builds Docker target default with ubi7 base image - await runDockerGeneratorForUBI(config, log, build); + if (!build.isOss()) { + await runDockerGenerator(config, log, build, { + ubi: true, + context: false, + architecture: 'x64', + image: true, + }); + } + }, +}; + +export const CreateDockerContexts: Task = { + description: 'Creating Docker build contexts', + + async run(config, log, build) { + await runDockerGenerator(config, log, build, { + ubi: false, + context: true, + image: false, + }); + + if (!build.isOss()) { + await runDockerGenerator(config, log, build, { + ubi: true, + context: true, + image: false, + }); + } }, }; diff --git a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts index 07a86927d5a35..4780457fe8054 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts @@ -18,7 +18,6 @@ export async function bundleDockerFiles(config: Config, log: ToolingLog, scope: log.info( `Generating kibana${scope.imageFlavor}${scope.ubiImageFlavor} docker build context bundle` ); - const dockerFilesDirName = `kibana${scope.imageFlavor}${scope.ubiImageFlavor}-${scope.version}-docker-build-context`; const dockerFilesBuildDir = resolve(scope.dockerBuildDir, dockerFilesDirName); const dockerFilesOutputDir = config.resolveFromTarget(`${dockerFilesDirName}.tar.gz`); diff --git a/src/dev/build/tasks/os_packages/docker_generator/index.ts b/src/dev/build/tasks/os_packages/docker_generator/index.ts index 1e6e6156c3ed9..229bd5242228c 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/index.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/index.ts @@ -6,4 +6,4 @@ * Public License, v 1. */ -export { runDockerGenerator, runDockerGeneratorForUBI } from './run'; +export { runDockerGenerator } from './run'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 26a6a9d6e4a03..c92de567cb446 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -26,19 +26,26 @@ export async function runDockerGenerator( config: Config, log: ToolingLog, build: Build, - ubi: boolean = false + flags: { + architecture?: string; + context: boolean; + image: boolean; + ubi: boolean; + } ) { // UBI var config - const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8'; + const baseOSImage = flags.ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8'; const ubiVersionTag = 'ubi8'; - const ubiImageFlavor = ubi ? `-${ubiVersionTag}` : ''; + const ubiImageFlavor = flags.ubi ? `-${ubiVersionTag}` : ''; // General docker var config const license = build.isOss() ? 'ASL 2.0' : 'Elastic License'; const imageFlavor = build.isOss() ? '-oss' : ''; const imageTag = 'docker.elastic.co/kibana/kibana'; const version = config.getBuildVersion(); - const artifactTarball = `kibana${imageFlavor}-${version}-linux-x86_64.tar.gz`; + const artifactArchitecture = flags.architecture === 'aarch64' ? 'aarch64' : 'x86_64'; + const artifactPrefix = `kibana${imageFlavor}-${version}-linux`; + const artifactTarball = `${artifactPrefix}-${artifactArchitecture}.tar.gz`; const artifactsDir = config.resolveFromTarget('.'); const dockerBuildDate = new Date().toISOString(); // That would produce oss, default and default-ubi7 @@ -47,10 +54,12 @@ export async function runDockerGenerator( 'kibana-docker', build.isOss() ? `oss` : `default${ubiImageFlavor}` ); + const imageArchitecture = flags.architecture === 'aarch64' ? '-aarch64' : ''; const dockerTargetFilename = config.resolveFromTarget( - `kibana${imageFlavor}${ubiImageFlavor}-${version}-docker-image.tar.gz` + `kibana${imageFlavor}${ubiImageFlavor}-${version}-docker-image${imageArchitecture}.tar.gz` ); const scope: TemplateContext = { + artifactPrefix, artifactTarball, imageFlavor, version, @@ -62,7 +71,8 @@ export async function runDockerGenerator( baseOSImage, ubiImageFlavor, dockerBuildDate, - ubi, + ubi: flags.ubi, + architecture: flags.architecture, revision: config.getBuildSha(), }; @@ -106,20 +116,23 @@ export async function runDockerGenerator( // created from the templates/build_docker_sh.template.js // and we just run that bash script await chmodAsync(`${resolve(dockerBuildDir, 'build_docker.sh')}`, '755'); - await exec(log, `./build_docker.sh`, [], { - cwd: dockerBuildDir, - level: 'info', - }); - - // Pack Dockerfiles and create a target for them - await bundleDockerFiles(config, log, scope); -} -export async function runDockerGeneratorForUBI(config: Config, log: ToolingLog, build: Build) { - // Only run ubi docker image build for default distribution - if (build.isOss()) { - return; + // Only build images on native targets + type HostArchitectureToDocker = Record; + const hostTarget: HostArchitectureToDocker = { + x64: 'x64', + arm64: 'aarch64', + }; + const buildImage = hostTarget[process.arch] === flags.architecture && flags.image; + if (buildImage) { + await exec(log, `./build_docker.sh`, [], { + cwd: dockerBuildDir, + level: 'info', + }); } - await runDockerGenerator(config, log, build, true); + // Pack Dockerfiles and create a target for them + if (flags.context) { + await bundleDockerFiles(config, log, scope); + } } diff --git a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts index 4805ba0ba9bc0..8de2b5e9361e5 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts @@ -7,6 +7,7 @@ */ export interface TemplateContext { + artifactPrefix: string; artifactTarball: string; imageFlavor: string; version: string; @@ -21,4 +22,5 @@ export interface TemplateContext { usePublicArtifact?: boolean; ubi: boolean; revision: string; + architecture?: string; } diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index 1a0e325a2486a..eb4708b6ac555 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -17,17 +17,19 @@ RUN {{packageManager}} install -y findutils tar gzip {{#usePublicArtifact}} RUN cd /opt && \ - curl --retry 8 -s -L -O https://artifacts.elastic.co/downloads/kibana/{{artifactTarball}} && \ + curl --retry 8 -s -L \ + --output kibana.tar.gz \ + https://artifacts.elastic.co/downloads/kibana/{{artifactPrefix}}-$(arch).tar.gz && \ cd - {{/usePublicArtifact}} {{^usePublicArtifact}} -COPY {{artifactTarball}} /opt +COPY {{artifactTarball}} /opt/kibana.tar.gz {{/usePublicArtifact}} RUN mkdir /usr/share/kibana WORKDIR /usr/share/kibana -RUN tar --strip-components=1 -zxf /opt/{{artifactTarball}} +RUN tar --strip-components=1 -zxf /opt/kibana.tar.gz # Ensure that group permissions are the same as user permissions. # This will help when relying on GID-0 to run Kibana, rather than UID-1000. # OpenShift does this, for example. @@ -51,7 +53,7 @@ EXPOSE 5601 RUN for iter in {1..10}; do \ {{packageManager}} update --setopt=tsflags=nodocs -y && \ {{packageManager}} install --setopt=tsflags=nodocs -y \ - fontconfig freetype shadow-utils libnss3.so {{#ubi}}findutils{{/ubi}} && \ + fontconfig freetype shadow-utils nss {{#ubi}}findutils{{/ubi}} && \ {{packageManager}} clean all && exit_code=0 && break || exit_code=$? && echo "{{packageManager}} error: retry $iter in 10s" && \ sleep 10; \ done; \ @@ -59,8 +61,17 @@ RUN for iter in {1..10}; do \ # Add an init process, check the checksum to make sure it's a match RUN set -e ; \ + TINI_BIN="" ; \ + case "$(arch)" in \ + aarch64) \ + TINI_BIN='tini-arm64' ; \ + ;; \ + x86_64) \ + TINI_BIN='tini-amd64' ; \ + ;; \ + *) echo >&2 "Unsupported architecture $(arch)" ; exit 1 ;; \ + esac ; \ TINI_VERSION='v0.19.0' ; \ - TINI_BIN='tini-amd64' ; \ curl --retry 8 -S -L -O "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${TINI_BIN}" ; \ curl --retry 8 -S -L -O "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${TINI_BIN}.sha256sum" ; \ sha256sum -c "${TINI_BIN}.sha256sum" ; \ diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index 5e2a0b72769fe..93c5f82aa1e42 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -17,7 +17,9 @@ function generator({ dockerTargetFilename, baseOSImage, ubiImageFlavor, + architecture, }: TemplateContext) { + const fileArchitecture = architecture === 'aarch64' ? 'arm64' : 'amd64'; return dedent(` #!/usr/bin/env bash # @@ -54,9 +56,9 @@ function generator({ retry_docker_pull ${baseOSImage} echo "Building: kibana${imageFlavor}${ubiImageFlavor}-docker"; \\ - docker build -t ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} -f Dockerfile . || exit 1; + docker build -t ${imageTag}${imageFlavor}${ubiImageFlavor}:${version}-${fileArchitecture} -f Dockerfile . || exit 1; - docker save ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} | gzip -c > ${dockerTargetFilename} + docker save ${imageTag}${imageFlavor}${ubiImageFlavor}:${version}-${fileArchitecture} | gzip -c > ${dockerTargetFilename} exit 0 `); diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts index a5cc2d9527cbf..f4e9d11ca9c21 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts @@ -16,6 +16,7 @@ function generator(options: TemplateContext) { const template = readFileSync(resolve(__dirname, './Dockerfile')); return Mustache.render(template.toString(), { packageManager: options.ubiImageFlavor ? 'microdnf' : 'yum', + tiniBin: options.architecture === 'aarch64' ? 'tini-arm64' : 'tini-amd64', ...options, }); } From cf3c746eca9f1d7abe88502bed248a62da3b8175 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 28 Jan 2021 00:51:01 +0000 Subject: [PATCH 065/163] chore(NA): bazel machinery installation on kbn bootstrap (#89469) * chore(NA): bazel machinery installation on kbn bootstrap * refact(NA): simplify install logic * chore(NA): update kbn pm with last changes --- .bazeliskversion | 1 + .bazelversion | 1 + WORKSPACE.bazel | 3 + package.json | 1 + packages/kbn-pm/dist/index.js | 1863 +++++++++-------- packages/kbn-pm/src/commands/bootstrap.ts | 7 +- packages/kbn-pm/src/utils/bazel/index.ts | 9 + .../kbn-pm/src/utils/bazel/install_tools.ts | 53 + src/dev/precommit_hook/casing_check_config.js | 4 + yarn.lock | 5 + 10 files changed, 1056 insertions(+), 891 deletions(-) create mode 100644 .bazeliskversion create mode 100644 .bazelversion create mode 100644 WORKSPACE.bazel create mode 100644 packages/kbn-pm/src/utils/bazel/index.ts create mode 100644 packages/kbn-pm/src/utils/bazel/install_tools.ts diff --git a/.bazeliskversion b/.bazeliskversion new file mode 100644 index 0000000000000..661e7aeadf36f --- /dev/null +++ b/.bazeliskversion @@ -0,0 +1 @@ +1.7.3 diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000000000..fcdb2e109f68c --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +4.0.0 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel new file mode 100644 index 0000000000000..0828157524fa1 --- /dev/null +++ b/WORKSPACE.bazel @@ -0,0 +1,3 @@ +workspace( + name = "kibana", +) diff --git a/package.json b/package.json index 42949a7014131..a64995e57a1f7 100644 --- a/package.json +++ b/package.json @@ -346,6 +346,7 @@ "@babel/register": "^7.12.10", "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", + "@bazel/ibazel": "^0.14.0", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.5.0", "@elastic/apm-rum": "^5.6.1", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 95ab46582723e..df04965cd8c32 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(510); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(512); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); @@ -106,7 +106,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(251); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return _utils_package_json__WEBPACK_IMPORTED_MODULE_4__["transformDependencies"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(509); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(511); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -139,7 +139,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(128); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(503); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(505); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one @@ -8827,9 +8827,9 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(129); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(371); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(402); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(403); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(373); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(404); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(405); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -8865,6 +8865,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(368); /* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(365); /* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(369); +/* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(371); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -8881,19 +8882,23 @@ __webpack_require__.r(__webpack_exports__); + const BootstrapCommand = { description: 'Install dependencies and crosslink projects', name: 'bootstrap', async run(projects, projectGraph, { options, - kbn + kbn, + rootPath }) { var _projects$get; const batchedProjects = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_4__["topologicallyBatchProjects"])(projects, projectGraph); const kibanaProjectPath = (_projects$get = projects.get('kibana')) === null || _projects$get === void 0 ? void 0 : _projects$get.path; - const extraArgs = [...(options['frozen-lockfile'] === true ? ['--frozen-lockfile'] : []), ...(options['prefer-offline'] === true ? ['--prefer-offline'] : [])]; + const extraArgs = [...(options['frozen-lockfile'] === true ? ['--frozen-lockfile'] : []), ...(options['prefer-offline'] === true ? ['--prefer-offline'] : [])]; // Install bazel machinery tools if needed + + await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_9__["installBazelTools"])(rootPath); // Install monorepo npm dependencies for (const batch of batchedProjects) { for (const project of batch) { @@ -47935,12 +47940,90 @@ function addProjectToTree(tree, pathParts, project) { /* 371 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var _install_tools__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(372); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "installBazelTools", function() { return _install_tools__WEBPACK_IMPORTED_MODULE_0__["installBazelTools"]; }); + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + + +/***/ }), +/* 372 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "installBazelTools", function() { return installBazelTools; }); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(319); +/* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(131); +/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(246); +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + + + + + +async function readBazelToolsVersionFile(repoRootPath, versionFilename) { + const version = (await Object(_fs__WEBPACK_IMPORTED_MODULE_2__["readFile"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(repoRootPath, versionFilename))).toString().split('\n')[0]; + + if (!version) { + throw new Error(`[bazel_tools] Failed on reading bazel tools versions\n ${versionFilename} file do not contain any version set`); + } + + return version; +} + +async function installBazelTools(repoRootPath) { + _log__WEBPACK_IMPORTED_MODULE_3__["log"].debug(`[bazel_tools] reading bazel tools versions from version files`); + const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); + const bazelVersion = await readBazelToolsVersionFile(repoRootPath, '.bazelversion'); // Check what globals are installed + + _log__WEBPACK_IMPORTED_MODULE_3__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); + const { + stdout + } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_1__["spawn"])('yarn', ['global', 'list'], { + stdio: 'pipe' + }); // Install bazelisk if not installed + + if (!stdout.includes(`@bazel/bazelisk@${bazeliskVersion}`)) { + _log__WEBPACK_IMPORTED_MODULE_3__["log"].info(`[bazel_tools] installing Bazel tools`); + _log__WEBPACK_IMPORTED_MODULE_3__["log"].debug(`[bazel_tools] bazelisk is not installed. Installing @bazel/bazelisk@${bazeliskVersion} and bazel@${bazelVersion}`); + await Object(_child_process__WEBPACK_IMPORTED_MODULE_1__["spawn"])('yarn', ['global', 'add', `@bazel/bazelisk@${bazeliskVersion}`], { + env: { + USE_BAZEL_VERSION: bazelVersion + }, + stdio: 'pipe' + }); + } + + _log__WEBPACK_IMPORTED_MODULE_3__["log"].success(`[bazel_tools] all bazel tools are correctly installed`); +} + +/***/ }), +/* 373 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(372); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(374); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -48029,20 +48112,20 @@ const CleanCommand = { }; /***/ }), -/* 372 */ +/* 374 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readline = __webpack_require__(373); -const chalk = __webpack_require__(374); -const cliCursor = __webpack_require__(381); -const cliSpinners = __webpack_require__(383); -const logSymbols = __webpack_require__(385); -const stripAnsi = __webpack_require__(394); -const wcwidth = __webpack_require__(396); -const isInteractive = __webpack_require__(400); -const MuteStream = __webpack_require__(401); +const readline = __webpack_require__(375); +const chalk = __webpack_require__(376); +const cliCursor = __webpack_require__(383); +const cliSpinners = __webpack_require__(385); +const logSymbols = __webpack_require__(387); +const stripAnsi = __webpack_require__(396); +const wcwidth = __webpack_require__(398); +const isInteractive = __webpack_require__(402); +const MuteStream = __webpack_require__(403); const TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); @@ -48395,23 +48478,23 @@ module.exports.promise = (action, options) => { /***/ }), -/* 373 */ +/* 375 */ /***/ (function(module, exports) { module.exports = require("readline"); /***/ }), -/* 374 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiStyles = __webpack_require__(375); +const ansiStyles = __webpack_require__(377); const {stdout: stdoutColor, stderr: stderrColor} = __webpack_require__(120); const { stringReplaceAll, stringEncaseCRLFWithFirstIndex -} = __webpack_require__(379); +} = __webpack_require__(381); // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ @@ -48612,7 +48695,7 @@ const chalkTag = (chalk, ...strings) => { } if (template === undefined) { - template = __webpack_require__(380); + template = __webpack_require__(382); } return template(chalk, parts.join('')); @@ -48641,7 +48724,7 @@ module.exports = chalk; /***/ }), -/* 375 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48687,7 +48770,7 @@ const setLazyProperty = (object, property, get) => { let colorConvert; const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { if (colorConvert === undefined) { - colorConvert = __webpack_require__(376); + colorConvert = __webpack_require__(378); } const offset = isBackground ? 10 : 0; @@ -48812,11 +48895,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 376 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(377); -const route = __webpack_require__(378); +const conversions = __webpack_require__(379); +const route = __webpack_require__(380); const convert = {}; @@ -48899,7 +48982,7 @@ module.exports = convert; /***/ }), -/* 377 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ @@ -49744,10 +49827,10 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 378 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(377); +const conversions = __webpack_require__(379); /* This function routes a model to all other models. @@ -49847,7 +49930,7 @@ module.exports = function (fromModel) { /***/ }), -/* 379 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -49893,7 +49976,7 @@ module.exports = { /***/ }), -/* 380 */ +/* 382 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50034,12 +50117,12 @@ module.exports = (chalk, temporary) => { /***/ }), -/* 381 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(382); +const restoreCursor = __webpack_require__(384); let isHidden = false; @@ -50076,7 +50159,7 @@ exports.toggle = (force, writableStream) => { /***/ }), -/* 382 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50092,13 +50175,13 @@ module.exports = onetime(() => { /***/ }), -/* 383 */ +/* 385 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const spinners = Object.assign({}, __webpack_require__(384)); +const spinners = Object.assign({}, __webpack_require__(386)); const spinnersList = Object.keys(spinners); @@ -50116,18 +50199,18 @@ module.exports.default = spinners; /***/ }), -/* 384 */ +/* 386 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"dots8Bit\":{\"interval\":80,\"frames\":[\"⠀\",\"⠁\",\"⠂\",\"⠃\",\"⠄\",\"⠅\",\"⠆\",\"⠇\",\"⡀\",\"⡁\",\"⡂\",\"⡃\",\"⡄\",\"⡅\",\"⡆\",\"⡇\",\"⠈\",\"⠉\",\"⠊\",\"⠋\",\"⠌\",\"⠍\",\"⠎\",\"⠏\",\"⡈\",\"⡉\",\"⡊\",\"⡋\",\"⡌\",\"⡍\",\"⡎\",\"⡏\",\"⠐\",\"⠑\",\"⠒\",\"⠓\",\"⠔\",\"⠕\",\"⠖\",\"⠗\",\"⡐\",\"⡑\",\"⡒\",\"⡓\",\"⡔\",\"⡕\",\"⡖\",\"⡗\",\"⠘\",\"⠙\",\"⠚\",\"⠛\",\"⠜\",\"⠝\",\"⠞\",\"⠟\",\"⡘\",\"⡙\",\"⡚\",\"⡛\",\"⡜\",\"⡝\",\"⡞\",\"⡟\",\"⠠\",\"⠡\",\"⠢\",\"⠣\",\"⠤\",\"⠥\",\"⠦\",\"⠧\",\"⡠\",\"⡡\",\"⡢\",\"⡣\",\"⡤\",\"⡥\",\"⡦\",\"⡧\",\"⠨\",\"⠩\",\"⠪\",\"⠫\",\"⠬\",\"⠭\",\"⠮\",\"⠯\",\"⡨\",\"⡩\",\"⡪\",\"⡫\",\"⡬\",\"⡭\",\"⡮\",\"⡯\",\"⠰\",\"⠱\",\"⠲\",\"⠳\",\"⠴\",\"⠵\",\"⠶\",\"⠷\",\"⡰\",\"⡱\",\"⡲\",\"⡳\",\"⡴\",\"⡵\",\"⡶\",\"⡷\",\"⠸\",\"⠹\",\"⠺\",\"⠻\",\"⠼\",\"⠽\",\"⠾\",\"⠿\",\"⡸\",\"⡹\",\"⡺\",\"⡻\",\"⡼\",\"⡽\",\"⡾\",\"⡿\",\"⢀\",\"⢁\",\"⢂\",\"⢃\",\"⢄\",\"⢅\",\"⢆\",\"⢇\",\"⣀\",\"⣁\",\"⣂\",\"⣃\",\"⣄\",\"⣅\",\"⣆\",\"⣇\",\"⢈\",\"⢉\",\"⢊\",\"⢋\",\"⢌\",\"⢍\",\"⢎\",\"⢏\",\"⣈\",\"⣉\",\"⣊\",\"⣋\",\"⣌\",\"⣍\",\"⣎\",\"⣏\",\"⢐\",\"⢑\",\"⢒\",\"⢓\",\"⢔\",\"⢕\",\"⢖\",\"⢗\",\"⣐\",\"⣑\",\"⣒\",\"⣓\",\"⣔\",\"⣕\",\"⣖\",\"⣗\",\"⢘\",\"⢙\",\"⢚\",\"⢛\",\"⢜\",\"⢝\",\"⢞\",\"⢟\",\"⣘\",\"⣙\",\"⣚\",\"⣛\",\"⣜\",\"⣝\",\"⣞\",\"⣟\",\"⢠\",\"⢡\",\"⢢\",\"⢣\",\"⢤\",\"⢥\",\"⢦\",\"⢧\",\"⣠\",\"⣡\",\"⣢\",\"⣣\",\"⣤\",\"⣥\",\"⣦\",\"⣧\",\"⢨\",\"⢩\",\"⢪\",\"⢫\",\"⢬\",\"⢭\",\"⢮\",\"⢯\",\"⣨\",\"⣩\",\"⣪\",\"⣫\",\"⣬\",\"⣭\",\"⣮\",\"⣯\",\"⢰\",\"⢱\",\"⢲\",\"⢳\",\"⢴\",\"⢵\",\"⢶\",\"⢷\",\"⣰\",\"⣱\",\"⣲\",\"⣳\",\"⣴\",\"⣵\",\"⣶\",\"⣷\",\"⢸\",\"⢹\",\"⢺\",\"⢻\",\"⢼\",\"⢽\",\"⢾\",\"⢿\",\"⣸\",\"⣹\",\"⣺\",\"⣻\",\"⣼\",\"⣽\",\"⣾\",\"⣿\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕛 \",\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"material\":{\"interval\":17,\"frames\":[\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"██████████▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"█████████████▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁██████████████▁▁▁▁\",\"▁▁▁██████████████▁▁▁\",\"▁▁▁▁█████████████▁▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁▁█████████████▁▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁▁███████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁▁█████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]},\"grenade\":{\"interval\":80,\"frames\":[\"، \",\"′ \",\" ´ \",\" ‾ \",\" ⸌\",\" ⸊\",\" |\",\" ⁎\",\" ⁕\",\" ෴ \",\" ⁓\",\" \",\" \",\" \"]},\"point\":{\"interval\":125,\"frames\":[\"∙∙∙\",\"●∙∙\",\"∙●∙\",\"∙∙●\",\"∙∙∙\"]},\"layer\":{\"interval\":150,\"frames\":[\"-\",\"=\",\"≡\"]},\"betaWave\":{\"interval\":80,\"frames\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]},\"aesthetic\":{\"interval\":80,\"frames\":[\"▰▱▱▱▱▱▱\",\"▰▰▱▱▱▱▱\",\"▰▰▰▱▱▱▱\",\"▰▰▰▰▱▱▱\",\"▰▰▰▰▰▱▱\",\"▰▰▰▰▰▰▱\",\"▰▰▰▰▰▰▰\",\"▰▱▱▱▱▱▱\"]}}"); /***/ }), -/* 385 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(386); +const chalk = __webpack_require__(388); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -50149,16 +50232,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 386 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(265); -const ansiStyles = __webpack_require__(387); -const stdoutColor = __webpack_require__(392).stdout; +const ansiStyles = __webpack_require__(389); +const stdoutColor = __webpack_require__(394).stdout; -const template = __webpack_require__(393); +const template = __webpack_require__(395); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -50384,12 +50467,12 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 387 */ +/* 389 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(module) { -const colorConvert = __webpack_require__(388); +const colorConvert = __webpack_require__(390); const wrapAnsi16 = (fn, offset) => function () { const code = fn.apply(colorConvert, arguments); @@ -50557,11 +50640,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 388 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(389); -var route = __webpack_require__(391); +var conversions = __webpack_require__(391); +var route = __webpack_require__(393); var convert = {}; @@ -50641,11 +50724,11 @@ module.exports = convert; /***/ }), -/* 389 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ -var cssKeywords = __webpack_require__(390); +var cssKeywords = __webpack_require__(392); // NOTE: conversions should only return primitive values (i.e. arrays, or // values that give correct `typeof` results). @@ -51515,7 +51598,7 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 390 */ +/* 392 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51674,10 +51757,10 @@ module.exports = { /***/ }), -/* 391 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(389); +var conversions = __webpack_require__(391); /* this function routes a model to all other models. @@ -51777,7 +51860,7 @@ module.exports = function (fromModel) { /***/ }), -/* 392 */ +/* 394 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51915,7 +51998,7 @@ module.exports = { /***/ }), -/* 393 */ +/* 395 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52050,18 +52133,18 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 394 */ +/* 396 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(395); +const ansiRegex = __webpack_require__(397); module.exports = string => typeof string === 'string' ? string.replace(ansiRegex(), '') : string; /***/ }), -/* 395 */ +/* 397 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52078,14 +52161,14 @@ module.exports = ({onlyFirst = false} = {}) => { /***/ }), -/* 396 */ +/* 398 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(397) -var combining = __webpack_require__(399) +var defaults = __webpack_require__(399) +var combining = __webpack_require__(401) var DEFAULTS = { nul: 0, @@ -52184,10 +52267,10 @@ function bisearch(ucs) { /***/ }), -/* 397 */ +/* 399 */ /***/ (function(module, exports, __webpack_require__) { -var clone = __webpack_require__(398); +var clone = __webpack_require__(400); module.exports = function(options, defaults) { options = options || {}; @@ -52202,7 +52285,7 @@ module.exports = function(options, defaults) { }; /***/ }), -/* 398 */ +/* 400 */ /***/ (function(module, exports, __webpack_require__) { var clone = (function() { @@ -52374,7 +52457,7 @@ if ( true && module.exports) { /***/ }), -/* 399 */ +/* 401 */ /***/ (function(module, exports) { module.exports = [ @@ -52430,7 +52513,7 @@ module.exports = [ /***/ }), -/* 400 */ +/* 402 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52446,7 +52529,7 @@ module.exports = ({stream = process.stdout} = {}) => { /***/ }), -/* 401 */ +/* 403 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -52597,7 +52680,7 @@ MuteStream.prototype.close = proxy('close') /***/ }), -/* 402 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52654,7 +52737,7 @@ const RunCommand = { }; /***/ }), -/* 403 */ +/* 405 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52664,7 +52747,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(247); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(248); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(404); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(406); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -52739,14 +52822,14 @@ const WatchCommand = { }; /***/ }), -/* 404 */ +/* 406 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(405); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(407); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -52802,141 +52885,141 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 405 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(406); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(408); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(407); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(409); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(408); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(410); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(409); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(411); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(410); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(412); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(411); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(413); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(412); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(414); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(413); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(415); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(414); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(416); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(415); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(417); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(416); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(418); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); /* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(80); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(417); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(419); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(418); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(420); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(419); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(421); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(420); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(422); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(421); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(423); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(422); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(424); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(423); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(425); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(425); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(427); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(426); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(428); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(427); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(429); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(428); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(430); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(429); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(431); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(430); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(432); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(433); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(435); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(434); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(436); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(435); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(437); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(436); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(438); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(437); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(439); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); /* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(105); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(438); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(440); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(439); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(441); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(440); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(442); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(441); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(443); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); /* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(31); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(442); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(444); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(443); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(445); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(444); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(446); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); /* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(66); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(446); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(448); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(447); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(449); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(448); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(450); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(451); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(453); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); /* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(81); @@ -52947,175 +53030,175 @@ __webpack_require__.r(__webpack_exports__); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["flatMap"]; }); -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(452); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(454); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(453); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(455); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(454); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(456); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(455); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(457); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); /* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(41); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(456); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(458); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(457); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(459); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(458); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(460); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(459); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(461); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(460); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(462); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(461); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(463); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(462); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(464); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(463); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(465); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(464); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(466); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(449); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(451); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(465); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(467); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(466); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(468); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(467); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(469); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(468); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(470); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); /* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(30); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(469); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(471); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(470); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(472); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(450); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(452); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(471); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(473); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(472); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(474); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(473); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(475); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(474); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(476); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(475); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(477); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(476); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(478); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(477); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(479); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(478); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(480); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(479); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(481); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(480); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(482); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(482); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(484); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(483); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(485); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(484); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(486); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(432); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(434); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(445); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(447); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(485); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(487); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(486); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(488); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(487); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(489); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(488); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(490); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(489); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(491); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(431); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(433); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(490); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(492); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(491); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(493); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(492); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(494); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(493); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(495); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(494); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(496); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(495); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(497); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(496); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(498); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(497); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(499); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(498); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(500); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(499); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(501); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(500); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(502); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(501); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(503); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(502); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(504); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -53226,7 +53309,7 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 406 */ +/* 408 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53305,14 +53388,14 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 407 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(406); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(408); /* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(108); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -53328,7 +53411,7 @@ function auditTime(duration, scheduler) { /***/ }), -/* 408 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53375,7 +53458,7 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 409 */ +/* 411 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53476,7 +53559,7 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 410 */ +/* 412 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53637,7 +53720,7 @@ function dispatchBufferClose(arg) { /***/ }), -/* 411 */ +/* 413 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53756,7 +53839,7 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 412 */ +/* 414 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53849,7 +53932,7 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 413 */ +/* 415 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53909,7 +53992,7 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 414 */ +/* 416 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53925,7 +54008,7 @@ function combineAll(project) { /***/ }), -/* 415 */ +/* 417 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53957,7 +54040,7 @@ function combineLatest() { /***/ }), -/* 416 */ +/* 418 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53977,7 +54060,7 @@ function concat() { /***/ }), -/* 417 */ +/* 419 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53993,13 +54076,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 418 */ +/* 420 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(417); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(419); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -54009,7 +54092,7 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 419 */ +/* 421 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54074,7 +54157,7 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 420 */ +/* 422 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54159,7 +54242,7 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 421 */ +/* 423 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54235,7 +54318,7 @@ function dispatchNext(subscriber) { /***/ }), -/* 422 */ +/* 424 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54285,7 +54368,7 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 423 */ +/* 425 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54293,7 +54376,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(424); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(426); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11); /* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(42); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -54392,7 +54475,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 424 */ +/* 426 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54406,7 +54489,7 @@ function isDate(value) { /***/ }), -/* 425 */ +/* 427 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54552,7 +54635,7 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 426 */ +/* 428 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54590,7 +54673,7 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 427 */ +/* 429 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54666,7 +54749,7 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 428 */ +/* 430 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54737,13 +54820,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 429 */ +/* 431 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(428); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(430); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -54753,7 +54836,7 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 430 */ +/* 432 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54761,9 +54844,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); /* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(62); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(431); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(422); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(432); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(433); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(424); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(434); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -54785,7 +54868,7 @@ function elementAt(index, defaultValue) { /***/ }), -/* 431 */ +/* 433 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54851,7 +54934,7 @@ function defaultErrorFactory() { /***/ }), -/* 432 */ +/* 434 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54913,7 +54996,7 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 433 */ +/* 435 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54935,7 +55018,7 @@ function endWith() { /***/ }), -/* 434 */ +/* 436 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54997,7 +55080,7 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 435 */ +/* 437 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55051,7 +55134,7 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 436 */ +/* 438 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55145,7 +55228,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 437 */ +/* 439 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55257,7 +55340,7 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 438 */ +/* 440 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55295,7 +55378,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 439 */ +/* 441 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55367,13 +55450,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 440 */ +/* 442 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(439); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(441); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -55383,7 +55466,7 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 441 */ +/* 443 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55391,9 +55474,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(432); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(422); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(431); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(434); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(424); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(433); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -55410,7 +55493,7 @@ function first(predicate, defaultValue) { /***/ }), -/* 442 */ +/* 444 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55447,7 +55530,7 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 443 */ +/* 445 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55491,7 +55574,7 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 444 */ +/* 446 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55499,9 +55582,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(445); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(431); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(422); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(447); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(433); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(424); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -55518,7 +55601,7 @@ function last(predicate, defaultValue) { /***/ }), -/* 445 */ +/* 447 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55595,7 +55678,7 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 446 */ +/* 448 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55634,7 +55717,7 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 447 */ +/* 449 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55684,13 +55767,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 448 */ +/* 450 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(449); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(451); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -55703,15 +55786,15 @@ function max(comparer) { /***/ }), -/* 449 */ +/* 451 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(445); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(422); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(452); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(447); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(424); /* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -55732,7 +55815,7 @@ function reduce(accumulator, seed) { /***/ }), -/* 450 */ +/* 452 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55814,7 +55897,7 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 451 */ +/* 453 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55834,7 +55917,7 @@ function merge() { /***/ }), -/* 452 */ +/* 454 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55859,7 +55942,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 453 */ +/* 455 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55968,13 +56051,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 454 */ +/* 456 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(449); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(451); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -55987,7 +56070,7 @@ function min(comparer) { /***/ }), -/* 455 */ +/* 457 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56036,7 +56119,7 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 456 */ +/* 458 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56126,7 +56209,7 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 457 */ +/* 459 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56174,7 +56257,7 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 458 */ +/* 460 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56197,7 +56280,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 459 */ +/* 461 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56237,14 +56320,14 @@ function plucker(props, length) { /***/ }), -/* 460 */ +/* 462 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(455); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(457); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -56257,14 +56340,14 @@ function publish(selector) { /***/ }), -/* 461 */ +/* 463 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); /* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(32); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(455); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(457); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -56275,14 +56358,14 @@ function publishBehavior(value) { /***/ }), -/* 462 */ +/* 464 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); /* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(50); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(455); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(457); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -56293,14 +56376,14 @@ function publishLast() { /***/ }), -/* 463 */ +/* 465 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); /* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(33); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(455); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(457); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -56316,7 +56399,7 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 464 */ +/* 466 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56343,7 +56426,7 @@ function race() { /***/ }), -/* 465 */ +/* 467 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56408,7 +56491,7 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 466 */ +/* 468 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56502,7 +56585,7 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 467 */ +/* 469 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56555,7 +56638,7 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 468 */ +/* 470 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56641,7 +56724,7 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 469 */ +/* 471 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56696,7 +56779,7 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 470 */ +/* 472 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56756,7 +56839,7 @@ function dispatchNotification(state) { /***/ }), -/* 471 */ +/* 473 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56879,13 +56962,13 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 472 */ +/* 474 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(455); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(457); /* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27); /** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ @@ -56902,7 +56985,7 @@ function share() { /***/ }), -/* 473 */ +/* 475 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56971,7 +57054,7 @@ function shareReplayOperator(_a) { /***/ }), -/* 474 */ +/* 476 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57051,7 +57134,7 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 475 */ +/* 477 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57093,7 +57176,7 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 476 */ +/* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57155,7 +57238,7 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 477 */ +/* 479 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57212,7 +57295,7 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 478 */ +/* 480 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57268,7 +57351,7 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 479 */ +/* 481 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57297,13 +57380,13 @@ function startWith() { /***/ }), -/* 480 */ +/* 482 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(481); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(483); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -57328,7 +57411,7 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 481 */ +/* 483 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57392,13 +57475,13 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 482 */ +/* 484 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(483); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(485); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -57410,7 +57493,7 @@ function switchAll() { /***/ }), -/* 483 */ +/* 485 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57498,13 +57581,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 484 */ +/* 486 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(483); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(485); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -57514,7 +57597,7 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 485 */ +/* 487 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57562,7 +57645,7 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 486 */ +/* 488 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57630,7 +57713,7 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 487 */ +/* 489 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57718,7 +57801,7 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 488 */ +/* 490 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57820,7 +57903,7 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 489 */ +/* 491 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57829,7 +57912,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(488); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(490); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -57918,7 +58001,7 @@ function dispatchNext(arg) { /***/ }), -/* 490 */ +/* 492 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57926,7 +58009,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(450); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); /* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(91); /* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(66); /** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ @@ -57962,7 +58045,7 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 491 */ +/* 493 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57970,7 +58053,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); /* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(64); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(492); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(494); /* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(49); /** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ @@ -57987,7 +58070,7 @@ function timeout(due, scheduler) { /***/ }), -/* 492 */ +/* 494 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57995,7 +58078,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(424); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(426); /* harmony import */ var _innerSubscribe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(90); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_innerSubscribe PURE_IMPORTS_END */ @@ -58066,7 +58149,7 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 493 */ +/* 495 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58096,13 +58179,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 494 */ +/* 496 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(449); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(451); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -58119,7 +58202,7 @@ function toArray() { /***/ }), -/* 495 */ +/* 497 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58197,7 +58280,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 496 */ +/* 498 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58287,7 +58370,7 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 497 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58457,7 +58540,7 @@ function dispatchWindowClose(state) { /***/ }), -/* 498 */ +/* 500 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58600,7 +58683,7 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 499 */ +/* 501 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58697,7 +58780,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 500 */ +/* 502 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58792,7 +58875,7 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 501 */ +/* 503 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58814,7 +58897,7 @@ function zip() { /***/ }), -/* 502 */ +/* 504 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58830,7 +58913,7 @@ function zipAll(project) { /***/ }), -/* 503 */ +/* 505 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58840,7 +58923,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); /* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(370); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(504); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(506); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -58911,7 +58994,7 @@ function toArray(value) { } /***/ }), -/* 504 */ +/* 506 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58919,13 +59002,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(505); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(507); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(239); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(365); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(509); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(511); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -59076,15 +59159,15 @@ class Kibana { } /***/ }), -/* 505 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(150); -const arrayUnion = __webpack_require__(506); -const arrayDiffer = __webpack_require__(507); -const arrify = __webpack_require__(508); +const arrayUnion = __webpack_require__(508); +const arrayDiffer = __webpack_require__(509); +const arrify = __webpack_require__(510); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -59108,7 +59191,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 506 */ +/* 508 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59120,7 +59203,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 507 */ +/* 509 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59135,7 +59218,7 @@ module.exports = arrayDiffer; /***/ }), -/* 508 */ +/* 510 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59165,7 +59248,7 @@ module.exports = arrify; /***/ }), -/* 509 */ +/* 511 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59224,12 +59307,12 @@ function getProjectPaths({ } /***/ }), -/* 510 */ +/* 512 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(511); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(513); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); /* @@ -59242,19 +59325,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 511 */ +/* 513 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(512); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(514); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(509); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(511); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); @@ -59380,7 +59463,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 512 */ +/* 514 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59388,14 +59471,14 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(156); const path = __webpack_require__(4); const os = __webpack_require__(121); -const pMap = __webpack_require__(513); -const arrify = __webpack_require__(508); -const globby = __webpack_require__(514); -const hasGlob = __webpack_require__(710); -const cpFile = __webpack_require__(712); -const junk = __webpack_require__(722); -const pFilter = __webpack_require__(723); -const CpyError = __webpack_require__(725); +const pMap = __webpack_require__(515); +const arrify = __webpack_require__(510); +const globby = __webpack_require__(516); +const hasGlob = __webpack_require__(712); +const cpFile = __webpack_require__(714); +const junk = __webpack_require__(724); +const pFilter = __webpack_require__(725); +const CpyError = __webpack_require__(727); const defaultOptions = { ignoreJunk: true @@ -59546,7 +59629,7 @@ module.exports = (source, destination, { /***/ }), -/* 513 */ +/* 515 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59634,17 +59717,17 @@ module.exports = async ( /***/ }), -/* 514 */ +/* 516 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(515); +const arrayUnion = __webpack_require__(517); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(517); -const dirGlob = __webpack_require__(703); -const gitignore = __webpack_require__(706); +const fastGlob = __webpack_require__(519); +const dirGlob = __webpack_require__(705); +const gitignore = __webpack_require__(708); const DEFAULT_FILTER = () => false; @@ -59789,12 +59872,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 515 */ +/* 517 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(516); +var arrayUniq = __webpack_require__(518); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -59802,7 +59885,7 @@ module.exports = function () { /***/ }), -/* 516 */ +/* 518 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59871,10 +59954,10 @@ if ('Set' in global) { /***/ }), -/* 517 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(518); +const pkg = __webpack_require__(520); module.exports = pkg.async; module.exports.default = pkg.async; @@ -59887,19 +59970,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 518 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(519); -var taskManager = __webpack_require__(520); -var reader_async_1 = __webpack_require__(674); -var reader_stream_1 = __webpack_require__(698); -var reader_sync_1 = __webpack_require__(699); -var arrayUtils = __webpack_require__(701); -var streamUtils = __webpack_require__(702); +var optionsManager = __webpack_require__(521); +var taskManager = __webpack_require__(522); +var reader_async_1 = __webpack_require__(676); +var reader_stream_1 = __webpack_require__(700); +var reader_sync_1 = __webpack_require__(701); +var arrayUtils = __webpack_require__(703); +var streamUtils = __webpack_require__(704); /** * Synchronous API. */ @@ -59965,7 +60048,7 @@ function isString(source) { /***/ }), -/* 519 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60003,13 +60086,13 @@ exports.prepare = prepare; /***/ }), -/* 520 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(521); +var patternUtils = __webpack_require__(523); /** * Generate tasks based on parent directory of each pattern. */ @@ -60100,16 +60183,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 521 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(522); +var globParent = __webpack_require__(524); var isGlob = __webpack_require__(172); -var micromatch = __webpack_require__(525); +var micromatch = __webpack_require__(527); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -60255,15 +60338,15 @@ exports.matchAny = matchAny; /***/ }), -/* 522 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(523); -var pathDirname = __webpack_require__(524); +var isglob = __webpack_require__(525); +var pathDirname = __webpack_require__(526); var isWin32 = __webpack_require__(121).platform() === 'win32'; module.exports = function globParent(str) { @@ -60286,7 +60369,7 @@ module.exports = function globParent(str) { /***/ }), -/* 523 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -60317,7 +60400,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 524 */ +/* 526 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60467,7 +60550,7 @@ module.exports.win32 = win32; /***/ }), -/* 525 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60478,18 +60561,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(112); -var braces = __webpack_require__(526); -var toRegex = __webpack_require__(527); -var extend = __webpack_require__(640); +var braces = __webpack_require__(528); +var toRegex = __webpack_require__(529); +var extend = __webpack_require__(642); /** * Local dependencies */ -var compilers = __webpack_require__(642); -var parsers = __webpack_require__(669); -var cache = __webpack_require__(670); -var utils = __webpack_require__(671); +var compilers = __webpack_require__(644); +var parsers = __webpack_require__(671); +var cache = __webpack_require__(672); +var utils = __webpack_require__(673); var MAX_LENGTH = 1024 * 64; /** @@ -61351,7 +61434,7 @@ module.exports = micromatch; /***/ }), -/* 526 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61361,18 +61444,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(527); -var unique = __webpack_require__(549); -var extend = __webpack_require__(550); +var toRegex = __webpack_require__(529); +var unique = __webpack_require__(551); +var extend = __webpack_require__(552); /** * Local dependencies */ -var compilers = __webpack_require__(552); -var parsers = __webpack_require__(565); -var Braces = __webpack_require__(569); -var utils = __webpack_require__(553); +var compilers = __webpack_require__(554); +var parsers = __webpack_require__(567); +var Braces = __webpack_require__(571); +var utils = __webpack_require__(555); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -61676,16 +61759,16 @@ module.exports = braces; /***/ }), -/* 527 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(528); -var define = __webpack_require__(534); -var extend = __webpack_require__(542); -var not = __webpack_require__(546); +var safe = __webpack_require__(530); +var define = __webpack_require__(536); +var extend = __webpack_require__(544); +var not = __webpack_require__(548); var MAX_LENGTH = 1024 * 64; /** @@ -61838,10 +61921,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 528 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(529); +var parse = __webpack_require__(531); var types = parse.types; module.exports = function (re, opts) { @@ -61887,13 +61970,13 @@ function isRegExp (x) { /***/ }), -/* 529 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(530); -var types = __webpack_require__(531); -var sets = __webpack_require__(532); -var positions = __webpack_require__(533); +var util = __webpack_require__(532); +var types = __webpack_require__(533); +var sets = __webpack_require__(534); +var positions = __webpack_require__(535); module.exports = function(regexpStr) { @@ -62175,11 +62258,11 @@ module.exports.types = types; /***/ }), -/* 530 */ +/* 532 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(531); -var sets = __webpack_require__(532); +var types = __webpack_require__(533); +var sets = __webpack_require__(534); // All of these are private and only used by randexp. @@ -62292,7 +62375,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 531 */ +/* 533 */ /***/ (function(module, exports) { module.exports = { @@ -62308,10 +62391,10 @@ module.exports = { /***/ }), -/* 532 */ +/* 534 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(531); +var types = __webpack_require__(533); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -62396,10 +62479,10 @@ exports.anyChar = function() { /***/ }), -/* 533 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(531); +var types = __webpack_require__(533); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -62419,7 +62502,7 @@ exports.end = function() { /***/ }), -/* 534 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62432,8 +62515,8 @@ exports.end = function() { -var isobject = __webpack_require__(535); -var isDescriptor = __webpack_require__(536); +var isobject = __webpack_require__(537); +var isDescriptor = __webpack_require__(538); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -62464,7 +62547,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 535 */ +/* 537 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62483,7 +62566,7 @@ module.exports = function isObject(val) { /***/ }), -/* 536 */ +/* 538 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62496,9 +62579,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(537); -var isAccessor = __webpack_require__(538); -var isData = __webpack_require__(540); +var typeOf = __webpack_require__(539); +var isAccessor = __webpack_require__(540); +var isData = __webpack_require__(542); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -62512,7 +62595,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 537 */ +/* 539 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -62647,7 +62730,7 @@ function isBuffer(val) { /***/ }), -/* 538 */ +/* 540 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62660,7 +62743,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(539); +var typeOf = __webpack_require__(541); // accessor descriptor properties var accessor = { @@ -62723,7 +62806,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 539 */ +/* 541 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -62858,7 +62941,7 @@ function isBuffer(val) { /***/ }), -/* 540 */ +/* 542 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62871,7 +62954,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(541); +var typeOf = __webpack_require__(543); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -62914,7 +62997,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 541 */ +/* 543 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63049,14 +63132,14 @@ function isBuffer(val) { /***/ }), -/* 542 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(543); -var assignSymbols = __webpack_require__(545); +var isExtendable = __webpack_require__(545); +var assignSymbols = __webpack_require__(547); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63116,7 +63199,7 @@ function isEnum(obj, key) { /***/ }), -/* 543 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63129,7 +63212,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(544); +var isPlainObject = __webpack_require__(546); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63137,7 +63220,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 544 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63150,7 +63233,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(535); +var isObject = __webpack_require__(537); function isObjectObject(o) { return isObject(o) === true @@ -63181,7 +63264,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 545 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63228,14 +63311,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 546 */ +/* 548 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(547); -var safe = __webpack_require__(528); +var extend = __webpack_require__(549); +var safe = __webpack_require__(530); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -63307,14 +63390,14 @@ module.exports = toRegex; /***/ }), -/* 547 */ +/* 549 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(548); -var assignSymbols = __webpack_require__(545); +var isExtendable = __webpack_require__(550); +var assignSymbols = __webpack_require__(547); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63374,7 +63457,7 @@ function isEnum(obj, key) { /***/ }), -/* 548 */ +/* 550 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63387,7 +63470,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(544); +var isPlainObject = __webpack_require__(546); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63395,7 +63478,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 549 */ +/* 551 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63445,13 +63528,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 550 */ +/* 552 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(551); +var isObject = __webpack_require__(553); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -63485,7 +63568,7 @@ function hasOwn(obj, key) { /***/ }), -/* 551 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63505,13 +63588,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 552 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(553); +var utils = __webpack_require__(555); module.exports = function(braces, options) { braces.compiler @@ -63794,25 +63877,25 @@ function hasQueue(node) { /***/ }), -/* 553 */ +/* 555 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(554); +var splitString = __webpack_require__(556); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(550); -utils.flatten = __webpack_require__(557); -utils.isObject = __webpack_require__(535); -utils.fillRange = __webpack_require__(558); -utils.repeat = __webpack_require__(564); -utils.unique = __webpack_require__(549); +utils.extend = __webpack_require__(552); +utils.flatten = __webpack_require__(559); +utils.isObject = __webpack_require__(537); +utils.fillRange = __webpack_require__(560); +utils.repeat = __webpack_require__(566); +utils.unique = __webpack_require__(551); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -64144,7 +64227,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 554 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64157,7 +64240,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(555); +var extend = __webpack_require__(557); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -64322,14 +64405,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 555 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(556); -var assignSymbols = __webpack_require__(545); +var isExtendable = __webpack_require__(558); +var assignSymbols = __webpack_require__(547); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -64389,7 +64472,7 @@ function isEnum(obj, key) { /***/ }), -/* 556 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64402,7 +64485,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(544); +var isPlainObject = __webpack_require__(546); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -64410,7 +64493,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 557 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64439,7 +64522,7 @@ function flat(arr, res) { /***/ }), -/* 558 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64453,10 +64536,10 @@ function flat(arr, res) { var util = __webpack_require__(112); -var isNumber = __webpack_require__(559); -var extend = __webpack_require__(550); -var repeat = __webpack_require__(562); -var toRegex = __webpack_require__(563); +var isNumber = __webpack_require__(561); +var extend = __webpack_require__(552); +var repeat = __webpack_require__(564); +var toRegex = __webpack_require__(565); /** * Return a range of numbers or letters. @@ -64654,7 +64737,7 @@ module.exports = fillRange; /***/ }), -/* 559 */ +/* 561 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64667,7 +64750,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(560); +var typeOf = __webpack_require__(562); module.exports = function isNumber(num) { var type = typeOf(num); @@ -64683,10 +64766,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 560 */ +/* 562 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(561); +var isBuffer = __webpack_require__(563); var toString = Object.prototype.toString; /** @@ -64805,7 +64888,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 561 */ +/* 563 */ /***/ (function(module, exports) { /*! @@ -64832,7 +64915,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 562 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64909,7 +64992,7 @@ function repeat(str, num) { /***/ }), -/* 563 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64922,8 +65005,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(562); -var isNumber = __webpack_require__(559); +var repeat = __webpack_require__(564); +var isNumber = __webpack_require__(561); var cache = {}; function toRegexRange(min, max, options) { @@ -65210,7 +65293,7 @@ module.exports = toRegexRange; /***/ }), -/* 564 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65235,14 +65318,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 565 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(566); -var utils = __webpack_require__(553); +var Node = __webpack_require__(568); +var utils = __webpack_require__(555); /** * Braces parsers @@ -65602,15 +65685,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 566 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(535); -var define = __webpack_require__(567); -var utils = __webpack_require__(568); +var isObject = __webpack_require__(537); +var define = __webpack_require__(569); +var utils = __webpack_require__(570); var ownNames; /** @@ -66101,7 +66184,7 @@ exports = module.exports = Node; /***/ }), -/* 567 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66114,7 +66197,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(536); +var isDescriptor = __webpack_require__(538); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -66139,13 +66222,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 568 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(560); +var typeOf = __webpack_require__(562); var utils = module.exports; /** @@ -67165,17 +67248,17 @@ function assert(val, message) { /***/ }), -/* 569 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(550); -var Snapdragon = __webpack_require__(570); -var compilers = __webpack_require__(552); -var parsers = __webpack_require__(565); -var utils = __webpack_require__(553); +var extend = __webpack_require__(552); +var Snapdragon = __webpack_require__(572); +var compilers = __webpack_require__(554); +var parsers = __webpack_require__(567); +var utils = __webpack_require__(555); /** * Customize Snapdragon parser and renderer @@ -67276,17 +67359,17 @@ module.exports = Braces; /***/ }), -/* 570 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(571); -var define = __webpack_require__(598); -var Compiler = __webpack_require__(608); -var Parser = __webpack_require__(637); -var utils = __webpack_require__(617); +var Base = __webpack_require__(573); +var define = __webpack_require__(600); +var Compiler = __webpack_require__(610); +var Parser = __webpack_require__(639); +var utils = __webpack_require__(619); var regexCache = {}; var cache = {}; @@ -67457,20 +67540,20 @@ module.exports.Parser = Parser; /***/ }), -/* 571 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(572); -var CacheBase = __webpack_require__(573); -var Emitter = __webpack_require__(574); -var isObject = __webpack_require__(535); -var merge = __webpack_require__(592); -var pascal = __webpack_require__(595); -var cu = __webpack_require__(596); +var define = __webpack_require__(574); +var CacheBase = __webpack_require__(575); +var Emitter = __webpack_require__(576); +var isObject = __webpack_require__(537); +var merge = __webpack_require__(594); +var pascal = __webpack_require__(597); +var cu = __webpack_require__(598); /** * Optionally define a custom `cache` namespace to use. @@ -67899,7 +67982,7 @@ module.exports.namespace = namespace; /***/ }), -/* 572 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67912,7 +67995,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(536); +var isDescriptor = __webpack_require__(538); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -67937,21 +68020,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 573 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(535); -var Emitter = __webpack_require__(574); -var visit = __webpack_require__(575); -var toPath = __webpack_require__(578); -var union = __webpack_require__(579); -var del = __webpack_require__(583); -var get = __webpack_require__(581); -var has = __webpack_require__(588); -var set = __webpack_require__(591); +var isObject = __webpack_require__(537); +var Emitter = __webpack_require__(576); +var visit = __webpack_require__(577); +var toPath = __webpack_require__(580); +var union = __webpack_require__(581); +var del = __webpack_require__(585); +var get = __webpack_require__(583); +var has = __webpack_require__(590); +var set = __webpack_require__(593); /** * Create a `Cache` constructor that when instantiated will @@ -68205,7 +68288,7 @@ module.exports.namespace = namespace; /***/ }), -/* 574 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { @@ -68374,7 +68457,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 575 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68387,8 +68470,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(576); -var mapVisit = __webpack_require__(577); +var visit = __webpack_require__(578); +var mapVisit = __webpack_require__(579); module.exports = function(collection, method, val) { var result; @@ -68411,7 +68494,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 576 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68424,7 +68507,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(535); +var isObject = __webpack_require__(537); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -68451,14 +68534,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 577 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(576); +var visit = __webpack_require__(578); /** * Map `visit` over an array of objects. @@ -68495,7 +68578,7 @@ function isObject(val) { /***/ }), -/* 578 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68508,7 +68591,7 @@ function isObject(val) { -var typeOf = __webpack_require__(560); +var typeOf = __webpack_require__(562); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -68535,16 +68618,16 @@ function filter(arr) { /***/ }), -/* 579 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(551); -var union = __webpack_require__(580); -var get = __webpack_require__(581); -var set = __webpack_require__(582); +var isObject = __webpack_require__(553); +var union = __webpack_require__(582); +var get = __webpack_require__(583); +var set = __webpack_require__(584); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -68572,7 +68655,7 @@ function arrayify(val) { /***/ }), -/* 580 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68608,7 +68691,7 @@ module.exports = function union(init) { /***/ }), -/* 581 */ +/* 583 */ /***/ (function(module, exports) { /*! @@ -68664,7 +68747,7 @@ function toString(val) { /***/ }), -/* 582 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68677,10 +68760,10 @@ function toString(val) { -var split = __webpack_require__(554); -var extend = __webpack_require__(550); -var isPlainObject = __webpack_require__(544); -var isObject = __webpack_require__(551); +var split = __webpack_require__(556); +var extend = __webpack_require__(552); +var isPlainObject = __webpack_require__(546); +var isObject = __webpack_require__(553); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -68726,7 +68809,7 @@ function isValidKey(key) { /***/ }), -/* 583 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68739,8 +68822,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(535); -var has = __webpack_require__(584); +var isObject = __webpack_require__(537); +var has = __webpack_require__(586); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -68765,7 +68848,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 584 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68778,9 +68861,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(585); -var hasValues = __webpack_require__(587); -var get = __webpack_require__(581); +var isObject = __webpack_require__(587); +var hasValues = __webpack_require__(589); +var get = __webpack_require__(583); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -68791,7 +68874,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 585 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68804,7 +68887,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(586); +var isArray = __webpack_require__(588); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -68812,7 +68895,7 @@ module.exports = function isObject(val) { /***/ }), -/* 586 */ +/* 588 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -68823,7 +68906,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 587 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68866,7 +68949,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 588 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68879,9 +68962,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(535); -var hasValues = __webpack_require__(589); -var get = __webpack_require__(581); +var isObject = __webpack_require__(537); +var hasValues = __webpack_require__(591); +var get = __webpack_require__(583); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -68889,7 +68972,7 @@ module.exports = function(val, prop) { /***/ }), -/* 589 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68902,8 +68985,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(590); -var isNumber = __webpack_require__(559); +var typeOf = __webpack_require__(592); +var isNumber = __webpack_require__(561); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -68956,10 +69039,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 590 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(561); +var isBuffer = __webpack_require__(563); var toString = Object.prototype.toString; /** @@ -69081,7 +69164,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 591 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69094,10 +69177,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(554); -var extend = __webpack_require__(550); -var isPlainObject = __webpack_require__(544); -var isObject = __webpack_require__(551); +var split = __webpack_require__(556); +var extend = __webpack_require__(552); +var isPlainObject = __webpack_require__(546); +var isObject = __webpack_require__(553); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69143,14 +69226,14 @@ function isValidKey(key) { /***/ }), -/* 592 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(593); -var forIn = __webpack_require__(594); +var isExtendable = __webpack_require__(595); +var forIn = __webpack_require__(596); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69214,7 +69297,7 @@ module.exports = mixinDeep; /***/ }), -/* 593 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69227,7 +69310,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(544); +var isPlainObject = __webpack_require__(546); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -69235,7 +69318,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 594 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69258,7 +69341,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 595 */ +/* 597 */ /***/ (function(module, exports) { /*! @@ -69285,14 +69368,14 @@ module.exports = pascalcase; /***/ }), -/* 596 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(597); +var utils = __webpack_require__(599); /** * Expose class utils @@ -69657,7 +69740,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 597 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69671,10 +69754,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(580); -utils.define = __webpack_require__(598); -utils.isObj = __webpack_require__(535); -utils.staticExtend = __webpack_require__(605); +utils.union = __webpack_require__(582); +utils.define = __webpack_require__(600); +utils.isObj = __webpack_require__(537); +utils.staticExtend = __webpack_require__(607); /** @@ -69685,7 +69768,7 @@ module.exports = utils; /***/ }), -/* 598 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69698,7 +69781,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(599); +var isDescriptor = __webpack_require__(601); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -69723,7 +69806,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 599 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69736,9 +69819,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(600); -var isAccessor = __webpack_require__(601); -var isData = __webpack_require__(603); +var typeOf = __webpack_require__(602); +var isAccessor = __webpack_require__(603); +var isData = __webpack_require__(605); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -69752,7 +69835,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 600 */ +/* 602 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -69905,7 +69988,7 @@ function isBuffer(val) { /***/ }), -/* 601 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69918,7 +70001,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(602); +var typeOf = __webpack_require__(604); // accessor descriptor properties var accessor = { @@ -69981,10 +70064,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 602 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(561); +var isBuffer = __webpack_require__(563); var toString = Object.prototype.toString; /** @@ -70103,7 +70186,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 603 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70116,7 +70199,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(604); +var typeOf = __webpack_require__(606); // data descriptor properties var data = { @@ -70165,10 +70248,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 604 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(561); +var isBuffer = __webpack_require__(563); var toString = Object.prototype.toString; /** @@ -70287,7 +70370,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 605 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70300,8 +70383,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(606); -var define = __webpack_require__(598); +var copy = __webpack_require__(608); +var define = __webpack_require__(600); var util = __webpack_require__(112); /** @@ -70384,15 +70467,15 @@ module.exports = extend; /***/ }), -/* 606 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(560); -var copyDescriptor = __webpack_require__(607); -var define = __webpack_require__(598); +var typeOf = __webpack_require__(562); +var copyDescriptor = __webpack_require__(609); +var define = __webpack_require__(600); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -70565,7 +70648,7 @@ module.exports.has = has; /***/ }), -/* 607 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70653,16 +70736,16 @@ function isObject(val) { /***/ }), -/* 608 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(609); -var define = __webpack_require__(598); -var debug = __webpack_require__(611)('snapdragon:compiler'); -var utils = __webpack_require__(617); +var use = __webpack_require__(611); +var define = __webpack_require__(600); +var debug = __webpack_require__(613)('snapdragon:compiler'); +var utils = __webpack_require__(619); /** * Create a new `Compiler` with the given `options`. @@ -70816,7 +70899,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(636); + var sourcemaps = __webpack_require__(638); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -70837,7 +70920,7 @@ module.exports = Compiler; /***/ }), -/* 609 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70850,7 +70933,7 @@ module.exports = Compiler; -var utils = __webpack_require__(610); +var utils = __webpack_require__(612); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -70965,7 +71048,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 610 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70979,8 +71062,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(598); -utils.isObject = __webpack_require__(535); +utils.define = __webpack_require__(600); +utils.isObject = __webpack_require__(537); utils.isString = function(val) { @@ -70995,7 +71078,7 @@ module.exports = utils; /***/ }), -/* 611 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71004,14 +71087,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(612); + module.exports = __webpack_require__(614); } else { - module.exports = __webpack_require__(615); + module.exports = __webpack_require__(617); } /***/ }), -/* 612 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71020,7 +71103,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(613); +exports = module.exports = __webpack_require__(615); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -71202,7 +71285,7 @@ function localstorage() { /***/ }), -/* 613 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { @@ -71218,7 +71301,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(614); +exports.humanize = __webpack_require__(616); /** * The currently active debug mode names, and names to skip. @@ -71410,7 +71493,7 @@ function coerce(val) { /***/ }), -/* 614 */ +/* 616 */ /***/ (function(module, exports) { /** @@ -71568,7 +71651,7 @@ function plural(ms, n, name) { /***/ }), -/* 615 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71584,7 +71667,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(613); +exports = module.exports = __webpack_require__(615); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -71763,7 +71846,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(616); + var net = __webpack_require__(618); stream = new net.Socket({ fd: fd, readable: false, @@ -71822,13 +71905,13 @@ exports.enable(load()); /***/ }), -/* 616 */ +/* 618 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 617 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71838,9 +71921,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(550); -exports.SourceMap = __webpack_require__(618); -exports.sourceMapResolve = __webpack_require__(629); +exports.extend = __webpack_require__(552); +exports.SourceMap = __webpack_require__(620); +exports.sourceMapResolve = __webpack_require__(631); /** * Convert backslash in the given string to forward slashes @@ -71883,7 +71966,7 @@ exports.last = function(arr, n) { /***/ }), -/* 618 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -71891,13 +71974,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(619).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(625).SourceMapConsumer; -exports.SourceNode = __webpack_require__(628).SourceNode; +exports.SourceMapGenerator = __webpack_require__(621).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(627).SourceMapConsumer; +exports.SourceNode = __webpack_require__(630).SourceNode; /***/ }), -/* 619 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -71907,10 +71990,10 @@ exports.SourceNode = __webpack_require__(628).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(620); -var util = __webpack_require__(622); -var ArraySet = __webpack_require__(623).ArraySet; -var MappingList = __webpack_require__(624).MappingList; +var base64VLQ = __webpack_require__(622); +var util = __webpack_require__(624); +var ArraySet = __webpack_require__(625).ArraySet; +var MappingList = __webpack_require__(626).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -72319,7 +72402,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 620 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72359,7 +72442,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(621); +var base64 = __webpack_require__(623); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -72465,7 +72548,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 621 */ +/* 623 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72538,7 +72621,7 @@ exports.decode = function (charCode) { /***/ }), -/* 622 */ +/* 624 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72961,7 +73044,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 623 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72971,7 +73054,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(622); +var util = __webpack_require__(624); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73088,7 +73171,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 624 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73098,7 +73181,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(622); +var util = __webpack_require__(624); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73173,7 +73256,7 @@ exports.MappingList = MappingList; /***/ }), -/* 625 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73183,11 +73266,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(622); -var binarySearch = __webpack_require__(626); -var ArraySet = __webpack_require__(623).ArraySet; -var base64VLQ = __webpack_require__(620); -var quickSort = __webpack_require__(627).quickSort; +var util = __webpack_require__(624); +var binarySearch = __webpack_require__(628); +var ArraySet = __webpack_require__(625).ArraySet; +var base64VLQ = __webpack_require__(622); +var quickSort = __webpack_require__(629).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -74261,7 +74344,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 626 */ +/* 628 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74378,7 +74461,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 627 */ +/* 629 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74498,7 +74581,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 628 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74508,8 +74591,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(619).SourceMapGenerator; -var util = __webpack_require__(622); +var SourceMapGenerator = __webpack_require__(621).SourceMapGenerator; +var util = __webpack_require__(624); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -74917,17 +75000,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 629 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(630) -var resolveUrl = __webpack_require__(631) -var decodeUriComponent = __webpack_require__(632) -var urix = __webpack_require__(634) -var atob = __webpack_require__(635) +var sourceMappingURL = __webpack_require__(632) +var resolveUrl = __webpack_require__(633) +var decodeUriComponent = __webpack_require__(634) +var urix = __webpack_require__(636) +var atob = __webpack_require__(637) @@ -75225,7 +75308,7 @@ module.exports = { /***/ }), -/* 630 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -75288,7 +75371,7 @@ void (function(root, factory) { /***/ }), -/* 631 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75306,13 +75389,13 @@ module.exports = resolveUrl /***/ }), -/* 632 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(633) +var decodeUriComponent = __webpack_require__(635) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -75323,7 +75406,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 633 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75424,7 +75507,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 634 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75447,7 +75530,7 @@ module.exports = urix /***/ }), -/* 635 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75461,7 +75544,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 636 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75469,8 +75552,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(598); -var utils = __webpack_require__(617); +var define = __webpack_require__(600); +var utils = __webpack_require__(619); /** * Expose `mixin()`. @@ -75613,19 +75696,19 @@ exports.comment = function(node) { /***/ }), -/* 637 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(609); +var use = __webpack_require__(611); var util = __webpack_require__(112); -var Cache = __webpack_require__(638); -var define = __webpack_require__(598); -var debug = __webpack_require__(611)('snapdragon:parser'); -var Position = __webpack_require__(639); -var utils = __webpack_require__(617); +var Cache = __webpack_require__(640); +var define = __webpack_require__(600); +var debug = __webpack_require__(613)('snapdragon:parser'); +var Position = __webpack_require__(641); +var utils = __webpack_require__(619); /** * Create a new `Parser` with the given `input` and `options`. @@ -76153,7 +76236,7 @@ module.exports = Parser; /***/ }), -/* 638 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76260,13 +76343,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 639 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(598); +var define = __webpack_require__(600); /** * Store position for a node @@ -76281,14 +76364,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 640 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(641); -var assignSymbols = __webpack_require__(545); +var isExtendable = __webpack_require__(643); +var assignSymbols = __webpack_require__(547); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -76348,7 +76431,7 @@ function isEnum(obj, key) { /***/ }), -/* 641 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76361,7 +76444,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(544); +var isPlainObject = __webpack_require__(546); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -76369,14 +76452,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 642 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(643); -var extglob = __webpack_require__(658); +var nanomatch = __webpack_require__(645); +var extglob = __webpack_require__(660); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -76453,7 +76536,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 643 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76464,17 +76547,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(112); -var toRegex = __webpack_require__(527); -var extend = __webpack_require__(644); +var toRegex = __webpack_require__(529); +var extend = __webpack_require__(646); /** * Local dependencies */ -var compilers = __webpack_require__(646); -var parsers = __webpack_require__(647); -var cache = __webpack_require__(650); -var utils = __webpack_require__(652); +var compilers = __webpack_require__(648); +var parsers = __webpack_require__(649); +var cache = __webpack_require__(652); +var utils = __webpack_require__(654); var MAX_LENGTH = 1024 * 64; /** @@ -77298,14 +77381,14 @@ module.exports = nanomatch; /***/ }), -/* 644 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(645); -var assignSymbols = __webpack_require__(545); +var isExtendable = __webpack_require__(647); +var assignSymbols = __webpack_require__(547); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -77365,7 +77448,7 @@ function isEnum(obj, key) { /***/ }), -/* 645 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77378,7 +77461,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(544); +var isPlainObject = __webpack_require__(546); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -77386,7 +77469,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 646 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77732,15 +77815,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 647 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(546); -var toRegex = __webpack_require__(527); -var isOdd = __webpack_require__(648); +var regexNot = __webpack_require__(548); +var toRegex = __webpack_require__(529); +var isOdd = __webpack_require__(650); /** * Characters to use in negation regex (we want to "not" match @@ -78126,7 +78209,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 648 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78139,7 +78222,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(649); +var isNumber = __webpack_require__(651); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78153,7 +78236,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 649 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78181,14 +78264,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 650 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(651))(); +module.exports = new (__webpack_require__(653))(); /***/ }), -/* 651 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78201,7 +78284,7 @@ module.exports = new (__webpack_require__(651))(); -var MapCache = __webpack_require__(638); +var MapCache = __webpack_require__(640); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78323,7 +78406,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 652 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78336,14 +78419,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(653)(); -var Snapdragon = __webpack_require__(570); -utils.define = __webpack_require__(654); -utils.diff = __webpack_require__(655); -utils.extend = __webpack_require__(644); -utils.pick = __webpack_require__(656); -utils.typeOf = __webpack_require__(657); -utils.unique = __webpack_require__(549); +var isWindows = __webpack_require__(655)(); +var Snapdragon = __webpack_require__(572); +utils.define = __webpack_require__(656); +utils.diff = __webpack_require__(657); +utils.extend = __webpack_require__(646); +utils.pick = __webpack_require__(658); +utils.typeOf = __webpack_require__(659); +utils.unique = __webpack_require__(551); /** * Returns true if the given value is effectively an empty string @@ -78709,7 +78792,7 @@ utils.unixify = function(options) { /***/ }), -/* 653 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -78737,7 +78820,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 654 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78750,8 +78833,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(535); -var isDescriptor = __webpack_require__(536); +var isobject = __webpack_require__(537); +var isDescriptor = __webpack_require__(538); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -78782,7 +78865,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 655 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78836,7 +78919,7 @@ function diffArray(one, two) { /***/ }), -/* 656 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78849,7 +78932,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(535); +var isObject = __webpack_require__(537); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -78878,7 +78961,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 657 */ +/* 659 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -79013,7 +79096,7 @@ function isBuffer(val) { /***/ }), -/* 658 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79023,18 +79106,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(550); -var unique = __webpack_require__(549); -var toRegex = __webpack_require__(527); +var extend = __webpack_require__(552); +var unique = __webpack_require__(551); +var toRegex = __webpack_require__(529); /** * Local dependencies */ -var compilers = __webpack_require__(659); -var parsers = __webpack_require__(665); -var Extglob = __webpack_require__(668); -var utils = __webpack_require__(667); +var compilers = __webpack_require__(661); +var parsers = __webpack_require__(667); +var Extglob = __webpack_require__(670); +var utils = __webpack_require__(669); var MAX_LENGTH = 1024 * 64; /** @@ -79351,13 +79434,13 @@ module.exports = extglob; /***/ }), -/* 659 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(660); +var brackets = __webpack_require__(662); /** * Extglob compilers @@ -79527,7 +79610,7 @@ module.exports = function(extglob) { /***/ }), -/* 660 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79537,17 +79620,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(661); -var parsers = __webpack_require__(663); +var compilers = __webpack_require__(663); +var parsers = __webpack_require__(665); /** * Module dependencies */ -var debug = __webpack_require__(611)('expand-brackets'); -var extend = __webpack_require__(550); -var Snapdragon = __webpack_require__(570); -var toRegex = __webpack_require__(527); +var debug = __webpack_require__(613)('expand-brackets'); +var extend = __webpack_require__(552); +var Snapdragon = __webpack_require__(572); +var toRegex = __webpack_require__(529); /** * Parses the given POSIX character class `pattern` and returns a @@ -79745,13 +79828,13 @@ module.exports = brackets; /***/ }), -/* 661 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(662); +var posix = __webpack_require__(664); module.exports = function(brackets) { brackets.compiler @@ -79839,7 +79922,7 @@ module.exports = function(brackets) { /***/ }), -/* 662 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79868,14 +79951,14 @@ module.exports = { /***/ }), -/* 663 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(664); -var define = __webpack_require__(598); +var utils = __webpack_require__(666); +var define = __webpack_require__(600); /** * Text regex @@ -80094,14 +80177,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 664 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(527); -var regexNot = __webpack_require__(546); +var toRegex = __webpack_require__(529); +var regexNot = __webpack_require__(548); var cached; /** @@ -80135,15 +80218,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 665 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(660); -var define = __webpack_require__(666); -var utils = __webpack_require__(667); +var brackets = __webpack_require__(662); +var define = __webpack_require__(668); +var utils = __webpack_require__(669); /** * Characters to use in text regex (we want to "not" match @@ -80298,7 +80381,7 @@ module.exports = parsers; /***/ }), -/* 666 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80311,7 +80394,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(536); +var isDescriptor = __webpack_require__(538); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -80336,14 +80419,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 667 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(546); -var Cache = __webpack_require__(651); +var regex = __webpack_require__(548); +var Cache = __webpack_require__(653); /** * Utils @@ -80412,7 +80495,7 @@ utils.createRegex = function(str) { /***/ }), -/* 668 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80422,16 +80505,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(570); -var define = __webpack_require__(666); -var extend = __webpack_require__(550); +var Snapdragon = __webpack_require__(572); +var define = __webpack_require__(668); +var extend = __webpack_require__(552); /** * Local dependencies */ -var compilers = __webpack_require__(659); -var parsers = __webpack_require__(665); +var compilers = __webpack_require__(661); +var parsers = __webpack_require__(667); /** * Customize Snapdragon parser and renderer @@ -80497,16 +80580,16 @@ module.exports = Extglob; /***/ }), -/* 669 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(658); -var nanomatch = __webpack_require__(643); -var regexNot = __webpack_require__(546); -var toRegex = __webpack_require__(527); +var extglob = __webpack_require__(660); +var nanomatch = __webpack_require__(645); +var regexNot = __webpack_require__(548); +var toRegex = __webpack_require__(529); var not; /** @@ -80587,14 +80670,14 @@ function textRegex(pattern) { /***/ }), -/* 670 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(651))(); +module.exports = new (__webpack_require__(653))(); /***/ }), -/* 671 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80607,13 +80690,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(570); -utils.define = __webpack_require__(672); -utils.diff = __webpack_require__(655); -utils.extend = __webpack_require__(640); -utils.pick = __webpack_require__(656); -utils.typeOf = __webpack_require__(673); -utils.unique = __webpack_require__(549); +var Snapdragon = __webpack_require__(572); +utils.define = __webpack_require__(674); +utils.diff = __webpack_require__(657); +utils.extend = __webpack_require__(642); +utils.pick = __webpack_require__(658); +utils.typeOf = __webpack_require__(675); +utils.unique = __webpack_require__(551); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -80910,7 +80993,7 @@ utils.unixify = function(options) { /***/ }), -/* 672 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80923,8 +81006,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(535); -var isDescriptor = __webpack_require__(536); +var isobject = __webpack_require__(537); +var isDescriptor = __webpack_require__(538); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -80955,7 +81038,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 673 */ +/* 675 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81090,7 +81173,7 @@ function isBuffer(val) { /***/ }), -/* 674 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81109,9 +81192,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(675); -var reader_1 = __webpack_require__(688); -var fs_stream_1 = __webpack_require__(692); +var readdir = __webpack_require__(677); +var reader_1 = __webpack_require__(690); +var fs_stream_1 = __webpack_require__(694); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81172,15 +81255,15 @@ exports.default = ReaderAsync; /***/ }), -/* 675 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(676); -const readdirAsync = __webpack_require__(684); -const readdirStream = __webpack_require__(687); +const readdirSync = __webpack_require__(678); +const readdirAsync = __webpack_require__(686); +const readdirStream = __webpack_require__(689); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81264,7 +81347,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 676 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81272,11 +81355,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(677); +const DirectoryReader = __webpack_require__(679); let syncFacade = { - fs: __webpack_require__(682), - forEach: __webpack_require__(683), + fs: __webpack_require__(684), + forEach: __webpack_require__(685), sync: true }; @@ -81305,7 +81388,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 677 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81314,9 +81397,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(678); -const stat = __webpack_require__(680); -const call = __webpack_require__(681); +const normalizeOptions = __webpack_require__(680); +const stat = __webpack_require__(682); +const call = __webpack_require__(683); /** * Asynchronously reads the contents of a directory and streams the results @@ -81692,14 +81775,14 @@ module.exports = DirectoryReader; /***/ }), -/* 678 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(679); +const globToRegExp = __webpack_require__(681); module.exports = normalizeOptions; @@ -81876,7 +81959,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 679 */ +/* 681 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82013,13 +82096,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 680 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(681); +const call = __webpack_require__(683); module.exports = stat; @@ -82094,7 +82177,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 681 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82155,14 +82238,14 @@ function callOnce (fn) { /***/ }), -/* 682 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(681); +const call = __webpack_require__(683); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82226,7 +82309,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 683 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82255,7 +82338,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 684 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82263,12 +82346,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(685); -const DirectoryReader = __webpack_require__(677); +const maybe = __webpack_require__(687); +const DirectoryReader = __webpack_require__(679); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(686), + forEach: __webpack_require__(688), async: true }; @@ -82310,7 +82393,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 685 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82337,7 +82420,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 686 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82373,7 +82456,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 687 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82381,11 +82464,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(677); +const DirectoryReader = __webpack_require__(679); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(686), + forEach: __webpack_require__(688), async: true }; @@ -82405,16 +82488,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 688 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(689); -var entry_1 = __webpack_require__(691); -var pathUtil = __webpack_require__(690); +var deep_1 = __webpack_require__(691); +var entry_1 = __webpack_require__(693); +var pathUtil = __webpack_require__(692); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -82480,14 +82563,14 @@ exports.default = Reader; /***/ }), -/* 689 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(690); -var patternUtils = __webpack_require__(521); +var pathUtils = __webpack_require__(692); +var patternUtils = __webpack_require__(523); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -82570,7 +82653,7 @@ exports.default = DeepFilter; /***/ }), -/* 690 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82601,14 +82684,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 691 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(690); -var patternUtils = __webpack_require__(521); +var pathUtils = __webpack_require__(692); +var patternUtils = __webpack_require__(523); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -82693,7 +82776,7 @@ exports.default = EntryFilter; /***/ }), -/* 692 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82713,8 +82796,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(693); -var fs_1 = __webpack_require__(697); +var fsStat = __webpack_require__(695); +var fs_1 = __webpack_require__(699); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -82764,14 +82847,14 @@ exports.default = FileSystemStream; /***/ }), -/* 693 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(694); -const statProvider = __webpack_require__(696); +const optionsManager = __webpack_require__(696); +const statProvider = __webpack_require__(698); /** * Asynchronous API. */ @@ -82802,13 +82885,13 @@ exports.statSync = statSync; /***/ }), -/* 694 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(695); +const fsAdapter = __webpack_require__(697); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -82821,7 +82904,7 @@ exports.prepare = prepare; /***/ }), -/* 695 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82844,7 +82927,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 696 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82896,7 +82979,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 697 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82927,7 +83010,7 @@ exports.default = FileSystem; /***/ }), -/* 698 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82947,9 +83030,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(675); -var reader_1 = __webpack_require__(688); -var fs_stream_1 = __webpack_require__(692); +var readdir = __webpack_require__(677); +var reader_1 = __webpack_require__(690); +var fs_stream_1 = __webpack_require__(694); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83017,7 +83100,7 @@ exports.default = ReaderStream; /***/ }), -/* 699 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83036,9 +83119,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(675); -var reader_1 = __webpack_require__(688); -var fs_sync_1 = __webpack_require__(700); +var readdir = __webpack_require__(677); +var reader_1 = __webpack_require__(690); +var fs_sync_1 = __webpack_require__(702); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83098,7 +83181,7 @@ exports.default = ReaderSync; /***/ }), -/* 700 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83117,8 +83200,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(693); -var fs_1 = __webpack_require__(697); +var fsStat = __webpack_require__(695); +var fs_1 = __webpack_require__(699); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83164,7 +83247,7 @@ exports.default = FileSystemSync; /***/ }), -/* 701 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83180,7 +83263,7 @@ exports.flatten = flatten; /***/ }), -/* 702 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83201,13 +83284,13 @@ exports.merge = merge; /***/ }), -/* 703 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(704); +const pathType = __webpack_require__(706); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83273,13 +83356,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 704 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(705); +const pify = __webpack_require__(707); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83322,7 +83405,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 705 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83413,17 +83496,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 706 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(517); -const gitIgnore = __webpack_require__(707); -const pify = __webpack_require__(708); -const slash = __webpack_require__(709); +const fastGlob = __webpack_require__(519); +const gitIgnore = __webpack_require__(709); +const pify = __webpack_require__(710); +const slash = __webpack_require__(711); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -83521,7 +83604,7 @@ module.exports.sync = options => { /***/ }), -/* 707 */ +/* 709 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -83990,7 +84073,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 708 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84065,7 +84148,7 @@ module.exports = (input, options) => { /***/ }), -/* 709 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84083,7 +84166,7 @@ module.exports = input => { /***/ }), -/* 710 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84096,7 +84179,7 @@ module.exports = input => { -var isGlob = __webpack_require__(711); +var isGlob = __webpack_require__(713); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84116,7 +84199,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 711 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84147,17 +84230,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 712 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(713); -const CpFileError = __webpack_require__(716); -const fs = __webpack_require__(718); -const ProgressEmitter = __webpack_require__(721); +const pEvent = __webpack_require__(715); +const CpFileError = __webpack_require__(718); +const fs = __webpack_require__(720); +const ProgressEmitter = __webpack_require__(723); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84271,12 +84354,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 713 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(714); +const pTimeout = __webpack_require__(716); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -84567,12 +84650,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 714 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(715); +const pFinally = __webpack_require__(717); class TimeoutError extends Error { constructor(message) { @@ -84618,7 +84701,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 715 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84640,12 +84723,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 716 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(717); +const NestedError = __webpack_require__(719); class CpFileError extends NestedError { constructor(message, nested) { @@ -84659,7 +84742,7 @@ module.exports = CpFileError; /***/ }), -/* 717 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -84715,16 +84798,16 @@ module.exports = NestedError; /***/ }), -/* 718 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(719); -const pEvent = __webpack_require__(713); -const CpFileError = __webpack_require__(716); +const makeDir = __webpack_require__(721); +const pEvent = __webpack_require__(715); +const CpFileError = __webpack_require__(718); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -84821,7 +84904,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 719 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84829,7 +84912,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(720); +const semver = __webpack_require__(722); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -84984,7 +85067,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 720 */ +/* 722 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -86586,7 +86669,7 @@ function coerce (version, options) { /***/ }), -/* 721 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86627,7 +86710,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 722 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86673,12 +86756,12 @@ exports.default = module.exports; /***/ }), -/* 723 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(724); +const pMap = __webpack_require__(726); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -86695,7 +86778,7 @@ module.exports.default = pFilter; /***/ }), -/* 724 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86774,12 +86857,12 @@ module.exports.default = pMap; /***/ }), -/* 725 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(717); +const NestedError = __webpack_require__(719); class CpyError extends NestedError { constructor(message, nested) { diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 0ad420899870d..8cd346a56f278 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -17,12 +17,13 @@ import { getAllChecksums } from '../utils/project_checksums'; import { BootstrapCacheFile } from '../utils/bootstrap_cache_file'; import { readYarnLock } from '../utils/yarn_lock'; import { validateDependencies } from '../utils/validate_dependencies'; +import { installBazelTools } from '../utils/bazel'; export const BootstrapCommand: ICommand = { description: 'Install dependencies and crosslink projects', name: 'bootstrap', - async run(projects, projectGraph, { options, kbn }) { + async run(projects, projectGraph, { options, kbn, rootPath }) { const batchedProjects = topologicallyBatchProjects(projects, projectGraph); const kibanaProjectPath = projects.get('kibana')?.path; const extraArgs = [ @@ -30,6 +31,10 @@ export const BootstrapCommand: ICommand = { ...(options['prefer-offline'] === true ? ['--prefer-offline'] : []), ]; + // Install bazel machinery tools if needed + await installBazelTools(rootPath); + + // Install monorepo npm dependencies for (const batch of batchedProjects) { for (const project of batch) { const isExternalPlugin = project.path.includes(`${kibanaProjectPath}${sep}plugins`); diff --git a/packages/kbn-pm/src/utils/bazel/index.ts b/packages/kbn-pm/src/utils/bazel/index.ts new file mode 100644 index 0000000000000..957c4bdf7f6aa --- /dev/null +++ b/packages/kbn-pm/src/utils/bazel/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export * from './install_tools'; diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts new file mode 100644 index 0000000000000..4e19974590e83 --- /dev/null +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { resolve } from 'path'; +import { spawn } from '../child_process'; +import { readFile } from '../fs'; +import { log } from '../log'; + +async function readBazelToolsVersionFile(repoRootPath: string, versionFilename: string) { + const version = (await readFile(resolve(repoRootPath, versionFilename))) + .toString() + .split('\n')[0]; + + if (!version) { + throw new Error( + `[bazel_tools] Failed on reading bazel tools versions\n ${versionFilename} file do not contain any version set` + ); + } + + return version; +} + +export async function installBazelTools(repoRootPath: string) { + log.debug(`[bazel_tools] reading bazel tools versions from version files`); + const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); + const bazelVersion = await readBazelToolsVersionFile(repoRootPath, '.bazelversion'); + + // Check what globals are installed + log.debug(`[bazel_tools] verify if bazelisk is installed`); + const { stdout } = await spawn('yarn', ['global', 'list'], { stdio: 'pipe' }); + + // Install bazelisk if not installed + if (!stdout.includes(`@bazel/bazelisk@${bazeliskVersion}`)) { + log.info(`[bazel_tools] installing Bazel tools`); + + log.debug( + `[bazel_tools] bazelisk is not installed. Installing @bazel/bazelisk@${bazeliskVersion} and bazel@${bazelVersion}` + ); + await spawn('yarn', ['global', 'add', `@bazel/bazelisk@${bazeliskVersion}`], { + env: { + USE_BAZEL_VERSION: bazelVersion, + }, + stdio: 'pipe', + }); + } + + log.success(`[bazel_tools] all bazel tools are correctly installed`); +} diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 1a870e3c878f7..d18e2e49d6b83 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -65,6 +65,10 @@ export const IGNORE_FILE_GLOBS = [ 'x-pack/test/fleet_api_integration/apis/fixtures/test_packages/**/*', '.teamcity/**/*', + + // Bazel default files + '**/WORKSPACE.bazel', + '**/BUILD.bazel', ]; /** diff --git a/yarn.lock b/yarn.lock index ed861b58773b9..8532bd89ec397 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1997,6 +1997,11 @@ resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz#860ce718b0b73f4009e153541faff2cb6b85d047" integrity sha512-4Th98KlMHr5+JkxfcoDT//6vY8vM+iSPrLNpHhRyLx2CFYi8e2RfqPLdpbnpo0Q5lQC5hNB79yes07zb02fvCw== +"@bazel/ibazel@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.14.0.tgz#86fa0002bed2ce1123b7ad98d4dd4623a0d93244" + integrity sha512-s0gyec6lArcRDwVfIP6xpY8iEaFpzrSpyErSppd3r2O49pOEg7n6HGS/qJ8ncvme56vrDk6crl/kQ6VAdEO+rg== + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" From 4fc49ece4d8e304032d564a2208fefc961aceaab Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 27 Jan 2021 20:02:12 -0700 Subject: [PATCH 066/163] skip flaky suite (#86950) --- .../apps/dashboard/feature_controls/dashboard_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index 112a855c61201..b5f55180419ef 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -29,7 +29,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - describe('dashboard feature controls security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/86950 + describe.skip('dashboard feature controls security', () => { before(async () => { await esArchiver.load('dashboard/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); From 9b5e41a9c5a55865a1ccd5e9927add448397c271 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 27 Jan 2021 22:59:33 -0500 Subject: [PATCH 067/163] [Fleet] Do not defined aliases inside datastream template (#89512) Co-authored-by: spalger --- x-pack/plugins/fleet/common/types/models/epm.ts | 1 - .../template/__snapshots__/template.test.ts.snap | 9 +++------ .../services/epm/elasticsearch/template/template.ts | 2 -- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index e09fbfc80b196..d0df9b73dd88a 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -326,7 +326,6 @@ export interface IndexTemplate { template: { settings: any; mappings: any; - aliases: object; }; data_stream: { hidden?: boolean }; composed_of: string[]; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 0333fb024a717..2d2478843c454 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -95,8 +95,7 @@ exports[`tests loading base.yml: base.yml 1`] = ` "managed_by": "ingest-manager", "managed": true } - }, - "aliases": {} + } }, "data_stream": {}, "composed_of": [], @@ -205,8 +204,7 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "managed_by": "ingest-manager", "managed": true } - }, - "aliases": {} + } }, "data_stream": {}, "composed_of": [], @@ -1699,8 +1697,7 @@ exports[`tests loading system.yml: system.yml 1`] = ` "managed_by": "ingest-manager", "managed": true } - }, - "aliases": {} + } }, "data_stream": {}, "composed_of": [], diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index fd75139d4cd45..e1fa2a0b18b59 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -335,8 +335,6 @@ function getBaseTemplate( properties: mappings.properties, _meta, }, - // To be filled with the aliases that we need - aliases: {}, }, data_stream: { hidden }, composed_of: composedOfTemplates, From d7028e1a5fb4224b59e189cd50aecfca567abbb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Thu, 28 Jan 2021 08:24:55 +0100 Subject: [PATCH 068/163] [Security Solution] Init Osquery plugin (#87109) --- .eslintrc.js | 26 ++ .github/CODEOWNERS | 3 + docs/developer/plugin-list.asciidoc | 4 + packages/kbn-optimizer/limits.yml | 1 + x-pack/.i18nrc.json | 1 + x-pack/plugins/osquery/README.md | 9 + x-pack/plugins/osquery/common/constants.ts | 8 + .../plugins/osquery/common/ecs/agent/index.ts | 9 + .../osquery/common/ecs/auditd/index.ts | 33 ++ .../plugins/osquery/common/ecs/cloud/index.ts | 20 + .../osquery/common/ecs/destination/index.ts | 16 + .../plugins/osquery/common/ecs/dns/index.ts | 16 + .../common/ecs/ecs_fields/extend_map.test.ts | 56 +++ .../common/ecs/ecs_fields/extend_map.ts | 14 + .../osquery/common/ecs/ecs_fields/index.ts | 358 ++++++++++++++++++ .../osquery/common/ecs/endgame/index.ts | 21 + .../plugins/osquery/common/ecs/event/index.ts | 27 ++ .../plugins/osquery/common/ecs/file/index.ts | 36 ++ .../plugins/osquery/common/ecs/geo/index.ts | 20 + .../plugins/osquery/common/ecs/host/index.ts | 24 ++ .../plugins/osquery/common/ecs/http/index.ts | 29 ++ x-pack/plugins/osquery/common/ecs/index.ts | 57 +++ .../osquery/common/ecs/network/index.ts | 14 + .../osquery/common/ecs/process/index.ts | 29 ++ .../plugins/osquery/common/ecs/rule/index.ts | 45 +++ .../osquery/common/ecs/signal/index.ts | 16 + .../osquery/common/ecs/source/index.ts | 16 + .../osquery/common/ecs/suricata/index.ts | 20 + .../osquery/common/ecs/system/index.ts | 32 ++ .../plugins/osquery/common/ecs/tls/index.ts | 31 ++ .../plugins/osquery/common/ecs/url/index.ts | 12 + .../plugins/osquery/common/ecs/user/index.ts | 15 + .../osquery/common/ecs/winlog/index.ts | 9 + .../plugins/osquery/common/ecs/zeek/index.ts | 83 ++++ x-pack/plugins/osquery/common/index.ts | 10 + .../common/search_strategy/common/index.ts | 125 ++++++ .../osquery/common/search_strategy/index.ts | 8 + .../search_strategy/osquery/actions/index.ts | 43 +++ .../search_strategy/osquery/agents/index.ts | 20 + .../search_strategy/osquery/common/index.ts | 112 ++++++ .../common/search_strategy/osquery/index.ts | 73 ++++ .../search_strategy/osquery/results/index.ts | 24 ++ .../plugins/osquery/common/shared_imports.ts | 7 + x-pack/plugins/osquery/common/typed_json.ts | 57 +++ .../plugins/osquery/common/utility_types.ts | 46 +++ .../common/utils/build_query/filters.ts | 12 + .../osquery/common/utils/build_query/index.ts | 15 + x-pack/plugins/osquery/jest.config.js | 11 + x-pack/plugins/osquery/kibana.json | 29 ++ .../action_results/action_results_table.tsx | 113 ++++++ .../osquery/public/action_results/helpers.ts | 37 ++ .../public/action_results/translations.ts | 15 + .../action_results/use_action_results.ts | 164 ++++++++ .../osquery/public/actions/actions_table.tsx | 107 ++++++ .../plugins/osquery/public/actions/helpers.ts | 37 ++ .../osquery/public/actions/translations.ts | 43 +++ .../public/actions/use_action_details.ts | 141 +++++++ .../osquery/public/actions/use_all_actions.ts | 161 ++++++++ .../osquery/public/agents/agents_table.tsx | 149 ++++++++ .../plugins/osquery/public/agents/helpers.ts | 37 ++ .../osquery/public/agents/translations.ts | 15 + .../osquery/public/agents/use_all_agents.ts | 161 ++++++++ x-pack/plugins/osquery/public/application.tsx | 70 ++++ .../osquery/public/common/helpers.test.ts | 29 ++ .../plugins/osquery/public/common/helpers.ts | 12 + x-pack/plugins/osquery/public/common/index.ts | 7 + .../osquery/public/common/lib/kibana/index.ts | 7 + .../public/common/lib/kibana/kibana_react.ts | 30 ++ .../plugins/osquery/public/components/app.tsx | 58 +++ .../plugins/osquery/public/editor/index.tsx | 49 +++ x-pack/plugins/osquery/public/index.ts | 15 + .../osquery/public/live_query/edit/index.tsx | 37 ++ .../osquery/public/live_query/edit/tabs.tsx | 57 +++ .../live_query/form/agents_table_field.tsx | 29 ++ .../live_query/form/code_editor_field.tsx | 31 ++ .../osquery/public/live_query/form/index.tsx | 56 +++ .../osquery/public/live_query/form/schema.ts | 17 + .../osquery/public/live_query/index.tsx | 32 ++ .../osquery/public/live_query/new/index.tsx | 29 ++ .../public/live_query/queries/index.tsx | 24 ++ x-pack/plugins/osquery/public/plugin.ts | 64 ++++ .../plugins/osquery/public/results/helpers.ts | 37 ++ .../osquery/public/results/results_table.tsx | 119 ++++++ .../osquery/public/results/translations.ts | 15 + .../osquery/public/results/use_all_results.ts | 164 ++++++++ .../plugins/osquery/public/shared_imports.ts | 29 ++ x-pack/plugins/osquery/public/types.ts | 26 ++ x-pack/plugins/osquery/server/config.ts | 13 + .../plugins/osquery/server/create_config.ts | 17 + x-pack/plugins/osquery/server/index.ts | 21 + x-pack/plugins/osquery/server/plugin.ts | 56 +++ x-pack/plugins/osquery/server/routes/index.ts | 50 +++ .../osquery/factory/actions/all/index.ts | 46 +++ .../actions/all/query.all_actions.dsl.ts | 33 ++ .../osquery/factory/actions/details/index.ts | 36 ++ .../details/query.action_details.dsl.ts | 37 ++ .../osquery/factory/actions/index.ts | 9 + .../osquery/factory/actions/results/index.ts | 47 +++ .../results/query.action_results.dsl.ts | 41 ++ .../osquery/factory/agents/index.ts | 48 +++ .../factory/agents/query.all_agents.dsl.ts | 32 ++ .../search_strategy/osquery/factory/index.ts | 21 + .../osquery/factory/results/index.ts | 46 +++ .../factory/results/query.all_results.dsl.ts | 40 ++ .../search_strategy/osquery/factory/types.ts | 23 ++ .../server/search_strategy/osquery/index.ts | 52 +++ x-pack/plugins/osquery/server/types.ts | 25 ++ 107 files changed, 4618 insertions(+) create mode 100755 x-pack/plugins/osquery/README.md create mode 100644 x-pack/plugins/osquery/common/constants.ts create mode 100644 x-pack/plugins/osquery/common/ecs/agent/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/auditd/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/cloud/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/destination/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/dns/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.test.ts create mode 100644 x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.ts create mode 100644 x-pack/plugins/osquery/common/ecs/ecs_fields/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/endgame/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/event/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/file/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/geo/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/host/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/http/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/network/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/process/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/rule/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/signal/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/source/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/suricata/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/system/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/tls/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/url/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/user/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/winlog/index.ts create mode 100644 x-pack/plugins/osquery/common/ecs/zeek/index.ts create mode 100644 x-pack/plugins/osquery/common/index.ts create mode 100644 x-pack/plugins/osquery/common/search_strategy/common/index.ts create mode 100644 x-pack/plugins/osquery/common/search_strategy/index.ts create mode 100644 x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts create mode 100644 x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts create mode 100644 x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts create mode 100644 x-pack/plugins/osquery/common/search_strategy/osquery/index.ts create mode 100644 x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts create mode 100644 x-pack/plugins/osquery/common/shared_imports.ts create mode 100644 x-pack/plugins/osquery/common/typed_json.ts create mode 100644 x-pack/plugins/osquery/common/utility_types.ts create mode 100644 x-pack/plugins/osquery/common/utils/build_query/filters.ts create mode 100644 x-pack/plugins/osquery/common/utils/build_query/index.ts create mode 100644 x-pack/plugins/osquery/jest.config.js create mode 100644 x-pack/plugins/osquery/kibana.json create mode 100644 x-pack/plugins/osquery/public/action_results/action_results_table.tsx create mode 100644 x-pack/plugins/osquery/public/action_results/helpers.ts create mode 100644 x-pack/plugins/osquery/public/action_results/translations.ts create mode 100644 x-pack/plugins/osquery/public/action_results/use_action_results.ts create mode 100644 x-pack/plugins/osquery/public/actions/actions_table.tsx create mode 100644 x-pack/plugins/osquery/public/actions/helpers.ts create mode 100644 x-pack/plugins/osquery/public/actions/translations.ts create mode 100644 x-pack/plugins/osquery/public/actions/use_action_details.ts create mode 100644 x-pack/plugins/osquery/public/actions/use_all_actions.ts create mode 100644 x-pack/plugins/osquery/public/agents/agents_table.tsx create mode 100644 x-pack/plugins/osquery/public/agents/helpers.ts create mode 100644 x-pack/plugins/osquery/public/agents/translations.ts create mode 100644 x-pack/plugins/osquery/public/agents/use_all_agents.ts create mode 100644 x-pack/plugins/osquery/public/application.tsx create mode 100644 x-pack/plugins/osquery/public/common/helpers.test.ts create mode 100644 x-pack/plugins/osquery/public/common/helpers.ts create mode 100644 x-pack/plugins/osquery/public/common/index.ts create mode 100644 x-pack/plugins/osquery/public/common/lib/kibana/index.ts create mode 100644 x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts create mode 100644 x-pack/plugins/osquery/public/components/app.tsx create mode 100644 x-pack/plugins/osquery/public/editor/index.tsx create mode 100644 x-pack/plugins/osquery/public/index.ts create mode 100644 x-pack/plugins/osquery/public/live_query/edit/index.tsx create mode 100644 x-pack/plugins/osquery/public/live_query/edit/tabs.tsx create mode 100644 x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx create mode 100644 x-pack/plugins/osquery/public/live_query/form/code_editor_field.tsx create mode 100644 x-pack/plugins/osquery/public/live_query/form/index.tsx create mode 100644 x-pack/plugins/osquery/public/live_query/form/schema.ts create mode 100644 x-pack/plugins/osquery/public/live_query/index.tsx create mode 100644 x-pack/plugins/osquery/public/live_query/new/index.tsx create mode 100644 x-pack/plugins/osquery/public/live_query/queries/index.tsx create mode 100644 x-pack/plugins/osquery/public/plugin.ts create mode 100644 x-pack/plugins/osquery/public/results/helpers.ts create mode 100644 x-pack/plugins/osquery/public/results/results_table.tsx create mode 100644 x-pack/plugins/osquery/public/results/translations.ts create mode 100644 x-pack/plugins/osquery/public/results/use_all_results.ts create mode 100644 x-pack/plugins/osquery/public/shared_imports.ts create mode 100644 x-pack/plugins/osquery/public/types.ts create mode 100644 x-pack/plugins/osquery/server/config.ts create mode 100644 x-pack/plugins/osquery/server/create_config.ts create mode 100644 x-pack/plugins/osquery/server/index.ts create mode 100644 x-pack/plugins/osquery/server/plugin.ts create mode 100644 x-pack/plugins/osquery/server/routes/index.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/index.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/query.action_details.dsl.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/index.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/query.action_results.dsl.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/index.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/factory/types.ts create mode 100644 x-pack/plugins/osquery/server/search_strategy/osquery/index.ts create mode 100644 x-pack/plugins/osquery/server/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index 29528c249d279..d8b9a9d7cdd99 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1192,6 +1192,32 @@ module.exports = { }, }, + /** + * Osquery overrides + */ + { + extends: ['eslint:recommended', 'plugin:react/recommended'], + plugins: ['react'], + files: ['x-pack/plugins/osquery/**/*.{js,mjs,ts,tsx}'], + rules: { + 'arrow-body-style': ['error', 'as-needed'], + 'prefer-arrow-callback': 'error', + 'no-unused-vars': 'off', + 'react/prop-types': 'off', + }, + }, + { + // typescript and javascript for front end react performance + files: ['x-pack/plugins/osquery/public/**/!(*.test).{js,mjs,ts,tsx}'], + plugins: ['react', 'react-perf'], + rules: { + 'react-perf/jsx-no-new-object-as-prop': 'error', + 'react-perf/jsx-no-new-array-as-prop': 'error', + 'react-perf/jsx-no-new-function-as-prop': 'error', + 'react/jsx-no-bind': 'error', + }, + }, + /** * Prettier disables all conflicting rules, listing as last override so it takes precedence */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0630937d5ac4b..01bbd59ba090f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -330,6 +330,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics +# Security Asset Management +/x-pack/plugins/osquery @elastic/security-asset-management + # Design (at the bottom for specificity of SASS files) **/*.scss @elastic/kibana-design #CC# /packages/kbn-ui-framework/ @elastic/kibana-design diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index fd4ed75352b1f..fd7ca58d88994 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -460,6 +460,10 @@ Elastic. |This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. +|{kib-repo}blob/{branch}/x-pack/plugins/osquery/README.md[osquery] +|This plugin adds extended support to Security Solution Fleet Osquery integration + + |{kib-repo}blob/{branch}/x-pack/plugins/painless_lab/README.md[painlessLab] |This plugin helps users learn how to use the Painless scripting language. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1a4fb390d0c17..a13976d148738 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -103,4 +103,5 @@ pageLoadAssetSize: stackAlerts: 29684 presentationUtil: 28545 spacesOss: 18817 + osquery: 107090 mapsFileUpload: 23775 diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index bfac437f3500a..f95c4286b3f26 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -38,6 +38,7 @@ "xpack.maps": ["plugins/maps"], "xpack.ml": ["plugins/ml"], "xpack.monitoring": ["plugins/monitoring"], + "xpack.osquery": ["plugins/osquery"], "xpack.painlessLab": "plugins/painless_lab", "xpack.remoteClusters": "plugins/remote_clusters", "xpack.reporting": ["plugins/reporting"], diff --git a/x-pack/plugins/osquery/README.md b/x-pack/plugins/osquery/README.md new file mode 100755 index 0000000000000..e0861fab2040b --- /dev/null +++ b/x-pack/plugins/osquery/README.md @@ -0,0 +1,9 @@ +# osquery + +This plugin adds extended support to Security Solution Fleet Osquery integration + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/x-pack/plugins/osquery/common/constants.ts b/x-pack/plugins/osquery/common/constants.ts new file mode 100644 index 0000000000000..f6027d416beb1 --- /dev/null +++ b/x-pack/plugins/osquery/common/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; +export const DEFAULT_DARK_MODE = 'theme:darkMode'; diff --git a/x-pack/plugins/osquery/common/ecs/agent/index.ts b/x-pack/plugins/osquery/common/ecs/agent/index.ts new file mode 100644 index 0000000000000..6f29a2020c944 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/agent/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface AgentEcs { + type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/auditd/index.ts b/x-pack/plugins/osquery/common/ecs/auditd/index.ts new file mode 100644 index 0000000000000..7611e5424e297 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/auditd/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface AuditdEcs { + result?: string[]; + session?: string[]; + data?: AuditdDataEcs; + summary?: SummaryEcs; + sequence?: string[]; +} + +export interface AuditdDataEcs { + acct?: string[]; + terminal?: string[]; + op?: string[]; +} + +export interface SummaryEcs { + actor?: PrimarySecondaryEcs; + object?: PrimarySecondaryEcs; + how?: string[]; + message_type?: string[]; + sequence?: string[]; +} + +export interface PrimarySecondaryEcs { + primary?: string[]; + secondary?: string[]; + type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/cloud/index.ts b/x-pack/plugins/osquery/common/ecs/cloud/index.ts new file mode 100644 index 0000000000000..812b30bcc13f1 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/cloud/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface CloudEcs { + instance?: CloudInstanceEcs; + machine?: CloudMachineEcs; + provider?: string[]; + region?: string[]; +} + +export interface CloudMachineEcs { + type?: string[]; +} + +export interface CloudInstanceEcs { + id?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/destination/index.ts b/x-pack/plugins/osquery/common/ecs/destination/index.ts new file mode 100644 index 0000000000000..be12e829108a9 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/destination/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 { GeoEcs } from '../geo'; + +export interface DestinationEcs { + bytes?: number[]; + ip?: string[]; + port?: number[]; + domain?: string[]; + geo?: GeoEcs; + packets?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/dns/index.ts b/x-pack/plugins/osquery/common/ecs/dns/index.ts new file mode 100644 index 0000000000000..45192d03a10b6 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/dns/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. + */ + +export interface DnsEcs { + question?: DnsQuestionEcs; + resolved_ip?: string[]; + response_code?: string[]; +} + +export interface DnsQuestionEcs { + name?: string[]; + type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.test.ts b/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.test.ts new file mode 100644 index 0000000000000..9ba22e83b4b4d --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { extendMap } from './extend_map'; + +describe('ecs_fields test', () => { + describe('extendMap', () => { + test('it should extend a record', () => { + const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record = { + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + expect(extendMap('host', osFieldsMap)).toEqual(expected); + }); + + test('it should extend a sample hosts record', () => { + const hostMap: Record = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + }; + const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + const output = { ...hostMap, ...extendMap('host', osFieldsMap) }; + expect(output).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.ts b/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.ts new file mode 100644 index 0000000000000..c25979cbcdcee --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.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. + */ + +export const extendMap = ( + path: string, + map: Readonly> +): Readonly> => + Object.entries(map).reduce>((accum, [key, value]) => { + accum[`${path}.${key}`] = `${path}.${value}`; + return accum; + }, {}); diff --git a/x-pack/plugins/osquery/common/ecs/ecs_fields/index.ts b/x-pack/plugins/osquery/common/ecs/ecs_fields/index.ts new file mode 100644 index 0000000000000..19b16bd4bc6d2 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/ecs_fields/index.ts @@ -0,0 +1,358 @@ +/* + * 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 { extendMap } from './extend_map'; + +export const auditdMap: Readonly> = { + 'auditd.result': 'auditd.result', + 'auditd.session': 'auditd.session', + 'auditd.data.acct': 'auditd.data.acct', + 'auditd.data.terminal': 'auditd.data.terminal', + 'auditd.data.op': 'auditd.data.op', + 'auditd.summary.actor.primary': 'auditd.summary.actor.primary', + 'auditd.summary.actor.secondary': 'auditd.summary.actor.secondary', + 'auditd.summary.object.primary': 'auditd.summary.object.primary', + 'auditd.summary.object.secondary': 'auditd.summary.object.secondary', + 'auditd.summary.object.type': 'auditd.summary.object.type', + 'auditd.summary.how': 'auditd.summary.how', + 'auditd.summary.message_type': 'auditd.summary.message_type', + 'auditd.summary.sequence': 'auditd.summary.sequence', +}; + +export const cloudFieldsMap: Readonly> = { + 'cloud.account.id': 'cloud.account.id', + 'cloud.availability_zone': 'cloud.availability_zone', + 'cloud.instance.id': 'cloud.instance.id', + 'cloud.instance.name': 'cloud.instance.name', + 'cloud.machine.type': 'cloud.machine.type', + 'cloud.provider': 'cloud.provider', + 'cloud.region': 'cloud.region', +}; + +export const fileMap: Readonly> = { + 'file.name': 'file.name', + 'file.path': 'file.path', + 'file.target_path': 'file.target_path', + 'file.extension': 'file.extension', + 'file.type': 'file.type', + 'file.device': 'file.device', + 'file.inode': 'file.inode', + 'file.uid': 'file.uid', + 'file.owner': 'file.owner', + 'file.gid': 'file.gid', + 'file.group': 'file.group', + 'file.mode': 'file.mode', + 'file.size': 'file.size', + 'file.mtime': 'file.mtime', + 'file.ctime': 'file.ctime', +}; + +export const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.name': 'os.name', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', +}; + +export const hostFieldsMap: Readonly> = { + 'host.architecture': 'host.architecture', + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.mac': 'host.mac', + 'host.name': 'host.name', + ...extendMap('host', osFieldsMap), +}; + +export const processFieldsMap: Readonly> = { + 'process.hash.md5': 'process.hash.md5', + 'process.hash.sha1': 'process.hash.sha1', + 'process.hash.sha256': 'process.hash.sha256', + 'process.pid': 'process.pid', + 'process.name': 'process.name', + 'process.ppid': 'process.ppid', + 'process.args': 'process.args', + 'process.entity_id': 'process.entity_id', + 'process.executable': 'process.executable', + 'process.title': 'process.title', + 'process.thread': 'process.thread', + 'process.working_directory': 'process.working_directory', +}; + +export const agentFieldsMap: Readonly> = { + 'agent.type': 'agent.type', +}; + +export const userFieldsMap: Readonly> = { + 'user.domain': 'user.domain', + 'user.id': 'user.id', + 'user.name': 'user.name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.full_name': 'user.full_name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.email': 'user.email', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.hash': 'user.hash', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.group': 'user.group', +}; + +export const winlogFieldsMap: Readonly> = { + 'winlog.event_id': 'winlog.event_id', +}; + +export const suricataFieldsMap: Readonly> = { + 'suricata.eve.flow_id': 'suricata.eve.flow_id', + 'suricata.eve.proto': 'suricata.eve.proto', + 'suricata.eve.alert.signature': 'suricata.eve.alert.signature', + 'suricata.eve.alert.signature_id': 'suricata.eve.alert.signature_id', +}; + +export const tlsFieldsMap: Readonly> = { + 'tls.client_certificate.fingerprint.sha1': 'tls.client_certificate.fingerprint.sha1', + 'tls.fingerprints.ja3.hash': 'tls.fingerprints.ja3.hash', + 'tls.server_certificate.fingerprint.sha1': 'tls.server_certificate.fingerprint.sha1', +}; + +export const urlFieldsMap: Readonly> = { + 'url.original': 'url.original', + 'url.domain': 'url.domain', + 'user.username': 'user.username', + 'user.password': 'user.password', +}; + +export const httpFieldsMap: Readonly> = { + 'http.version': 'http.version', + 'http.request': 'http.request', + 'http.request.method': 'http.request.method', + 'http.request.body.bytes': 'http.request.body.bytes', + 'http.request.body.content': 'http.request.body.content', + 'http.request.referrer': 'http.request.referrer', + 'http.response.status_code': 'http.response.status_code', + 'http.response.body': 'http.response.body', + 'http.response.body.bytes': 'http.response.body.bytes', + 'http.response.body.content': 'http.response.body.content', +}; + +export const zeekFieldsMap: Readonly> = { + 'zeek.session_id': 'zeek.session_id', + 'zeek.connection.local_resp': 'zeek.connection.local_resp', + 'zeek.connection.local_orig': 'zeek.connection.local_orig', + 'zeek.connection.missed_bytes': 'zeek.connection.missed_bytes', + 'zeek.connection.state': 'zeek.connection.state', + 'zeek.connection.history': 'zeek.connection.history', + 'zeek.notice.suppress_for': 'zeek.notice.suppress_for', + 'zeek.notice.msg': 'zeek.notice.msg', + 'zeek.notice.note': 'zeek.notice.note', + 'zeek.notice.sub': 'zeek.notice.sub', + 'zeek.notice.dst': 'zeek.notice.dst', + 'zeek.notice.dropped': 'zeek.notice.dropped', + 'zeek.notice.peer_descr': 'zeek.notice.peer_descr', + 'zeek.dns.AA': 'zeek.dns.AA', + 'zeek.dns.qclass_name': 'zeek.dns.qclass_name', + 'zeek.dns.RD': 'zeek.dns.RD', + 'zeek.dns.qtype_name': 'zeek.dns.qtype_name', + 'zeek.dns.qtype': 'zeek.dns.qtype', + 'zeek.dns.query': 'zeek.dns.query', + 'zeek.dns.trans_id': 'zeek.dns.trans_id', + 'zeek.dns.qclass': 'zeek.dns.qclass', + 'zeek.dns.RA': 'zeek.dns.RA', + 'zeek.dns.TC': 'zeek.dns.TC', + 'zeek.http.resp_mime_types': 'zeek.http.resp_mime_types', + 'zeek.http.trans_depth': 'zeek.http.trans_depth', + 'zeek.http.status_msg': 'zeek.http.status_msg', + 'zeek.http.resp_fuids': 'zeek.http.resp_fuids', + 'zeek.http.tags': 'zeek.http.tags', + 'zeek.files.session_ids': 'zeek.files.session_ids', + 'zeek.files.timedout': 'zeek.files.timedout', + 'zeek.files.local_orig': 'zeek.files.local_orig', + 'zeek.files.tx_host': 'zeek.files.tx_host', + 'zeek.files.source': 'zeek.files.source', + 'zeek.files.is_orig': 'zeek.files.is_orig', + 'zeek.files.overflow_bytes': 'zeek.files.overflow_bytes', + 'zeek.files.sha1': 'zeek.files.sha1', + 'zeek.files.duration': 'zeek.files.duration', + 'zeek.files.depth': 'zeek.files.depth', + 'zeek.files.analyzers': 'zeek.files.analyzers', + 'zeek.files.mime_type': 'zeek.files.mime_type', + 'zeek.files.rx_host': 'zeek.files.rx_host', + 'zeek.files.total_bytes': 'zeek.files.total_bytes', + 'zeek.files.fuid': 'zeek.files.fuid', + 'zeek.files.seen_bytes': 'zeek.files.seen_bytes', + 'zeek.files.missing_bytes': 'zeek.files.missing_bytes', + 'zeek.files.md5': 'zeek.files.md5', + 'zeek.ssl.cipher': 'zeek.ssl.cipher', + 'zeek.ssl.established': 'zeek.ssl.established', + 'zeek.ssl.resumed': 'zeek.ssl.resumed', + 'zeek.ssl.version': 'zeek.ssl.version', +}; + +export const sourceFieldsMap: Readonly> = { + 'source.bytes': 'source.bytes', + 'source.ip': 'source.ip', + 'source.packets': 'source.packets', + 'source.port': 'source.port', + 'source.domain': 'source.domain', + 'source.geo.continent_name': 'source.geo.continent_name', + 'source.geo.country_name': 'source.geo.country_name', + 'source.geo.country_iso_code': 'source.geo.country_iso_code', + 'source.geo.city_name': 'source.geo.city_name', + 'source.geo.region_iso_code': 'source.geo.region_iso_code', + 'source.geo.region_name': 'source.geo.region_name', +}; + +export const destinationFieldsMap: Readonly> = { + 'destination.bytes': 'destination.bytes', + 'destination.ip': 'destination.ip', + 'destination.packets': 'destination.packets', + 'destination.port': 'destination.port', + 'destination.domain': 'destination.domain', + 'destination.geo.continent_name': 'destination.geo.continent_name', + 'destination.geo.country_name': 'destination.geo.country_name', + 'destination.geo.country_iso_code': 'destination.geo.country_iso_code', + 'destination.geo.city_name': 'destination.geo.city_name', + 'destination.geo.region_iso_code': 'destination.geo.region_iso_code', + 'destination.geo.region_name': 'destination.geo.region_name', +}; + +export const networkFieldsMap: Readonly> = { + 'network.bytes': 'network.bytes', + 'network.community_id': 'network.community_id', + 'network.direction': 'network.direction', + 'network.packets': 'network.packets', + 'network.protocol': 'network.protocol', + 'network.transport': 'network.transport', +}; + +export const geoFieldsMap: Readonly> = { + 'geo.region_name': 'destination.geo.region_name', + 'geo.country_iso_code': 'destination.geo.country_iso_code', +}; + +export const dnsFieldsMap: Readonly> = { + 'dns.question.name': 'dns.question.name', + 'dns.question.type': 'dns.question.type', + 'dns.resolved_ip': 'dns.resolved_ip', + 'dns.response_code': 'dns.response_code', +}; + +export const endgameFieldsMap: Readonly> = { + 'endgame.exit_code': 'endgame.exit_code', + 'endgame.file_name': 'endgame.file_name', + 'endgame.file_path': 'endgame.file_path', + 'endgame.logon_type': 'endgame.logon_type', + 'endgame.parent_process_name': 'endgame.parent_process_name', + 'endgame.pid': 'endgame.pid', + 'endgame.process_name': 'endgame.process_name', + 'endgame.subject_domain_name': 'endgame.subject_domain_name', + 'endgame.subject_logon_id': 'endgame.subject_logon_id', + 'endgame.subject_user_name': 'endgame.subject_user_name', + 'endgame.target_domain_name': 'endgame.target_domain_name', + 'endgame.target_logon_id': 'endgame.target_logon_id', + 'endgame.target_user_name': 'endgame.target_user_name', +}; + +export const eventBaseFieldsMap: Readonly> = { + 'event.action': 'event.action', + 'event.category': 'event.category', + 'event.code': 'event.code', + 'event.created': 'event.created', + 'event.dataset': 'event.dataset', + 'event.duration': 'event.duration', + 'event.end': 'event.end', + 'event.hash': 'event.hash', + 'event.id': 'event.id', + 'event.kind': 'event.kind', + 'event.module': 'event.module', + 'event.original': 'event.original', + 'event.outcome': 'event.outcome', + 'event.risk_score': 'event.risk_score', + 'event.risk_score_norm': 'event.risk_score_norm', + 'event.severity': 'event.severity', + 'event.start': 'event.start', + 'event.timezone': 'event.timezone', + 'event.type': 'event.type', +}; + +export const systemFieldsMap: Readonly> = { + 'system.audit.package.arch': 'system.audit.package.arch', + 'system.audit.package.entity_id': 'system.audit.package.entity_id', + 'system.audit.package.name': 'system.audit.package.name', + 'system.audit.package.size': 'system.audit.package.size', + 'system.audit.package.summary': 'system.audit.package.summary', + 'system.audit.package.version': 'system.audit.package.version', + 'system.auth.ssh.signature': 'system.auth.ssh.signature', + 'system.auth.ssh.method': 'system.auth.ssh.method', +}; + +export const signalFieldsMap: Readonly> = { + 'signal.original_time': 'signal.original_time', + 'signal.rule.id': 'signal.rule.id', + 'signal.rule.saved_id': 'signal.rule.saved_id', + 'signal.rule.timeline_id': 'signal.rule.timeline_id', + 'signal.rule.timeline_title': 'signal.rule.timeline_title', + 'signal.rule.output_index': 'signal.rule.output_index', + 'signal.rule.from': 'signal.rule.from', + 'signal.rule.index': 'signal.rule.index', + 'signal.rule.language': 'signal.rule.language', + 'signal.rule.query': 'signal.rule.query', + 'signal.rule.to': 'signal.rule.to', + 'signal.rule.filters': 'signal.rule.filters', + 'signal.rule.rule_id': 'signal.rule.rule_id', + 'signal.rule.false_positives': 'signal.rule.false_positives', + 'signal.rule.max_signals': 'signal.rule.max_signals', + 'signal.rule.risk_score': 'signal.rule.risk_score', + 'signal.rule.description': 'signal.rule.description', + 'signal.rule.name': 'signal.rule.name', + 'signal.rule.immutable': 'signal.rule.immutable', + 'signal.rule.references': 'signal.rule.references', + 'signal.rule.severity': 'signal.rule.severity', + 'signal.rule.tags': 'signal.rule.tags', + 'signal.rule.threat': 'signal.rule.threat', + 'signal.rule.type': 'signal.rule.type', + 'signal.rule.size': 'signal.rule.size', + 'signal.rule.enabled': 'signal.rule.enabled', + 'signal.rule.created_at': 'signal.rule.created_at', + 'signal.rule.updated_at': 'signal.rule.updated_at', + 'signal.rule.created_by': 'signal.rule.created_by', + 'signal.rule.updated_by': 'signal.rule.updated_by', + 'signal.rule.version': 'signal.rule.version', + 'signal.rule.note': 'signal.rule.note', + 'signal.rule.threshold': 'signal.rule.threshold', + 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', +}; + +export const ruleFieldsMap: Readonly> = { + 'rule.reference': 'rule.reference', +}; + +export const eventFieldsMap: Readonly> = { + timestamp: '@timestamp', + '@timestamp': '@timestamp', + message: 'message', + ...{ ...agentFieldsMap }, + ...{ ...auditdMap }, + ...{ ...destinationFieldsMap }, + ...{ ...dnsFieldsMap }, + ...{ ...endgameFieldsMap }, + ...{ ...eventBaseFieldsMap }, + ...{ ...fileMap }, + ...{ ...geoFieldsMap }, + ...{ ...hostFieldsMap }, + ...{ ...networkFieldsMap }, + ...{ ...ruleFieldsMap }, + ...{ ...signalFieldsMap }, + ...{ ...sourceFieldsMap }, + ...{ ...suricataFieldsMap }, + ...{ ...systemFieldsMap }, + ...{ ...tlsFieldsMap }, + ...{ ...zeekFieldsMap }, + ...{ ...httpFieldsMap }, + ...{ ...userFieldsMap }, + ...{ ...winlogFieldsMap }, + ...{ ...processFieldsMap }, +}; diff --git a/x-pack/plugins/osquery/common/ecs/endgame/index.ts b/x-pack/plugins/osquery/common/ecs/endgame/index.ts new file mode 100644 index 0000000000000..d2fc5d61527a5 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/endgame/index.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. + */ + +export interface EndgameEcs { + exit_code?: number[]; + file_name?: string[]; + file_path?: string[]; + logon_type?: number[]; + parent_process_name?: string[]; + pid?: number[]; + process_name?: string[]; + subject_domain_name?: string[]; + subject_logon_id?: string[]; + subject_user_name?: string[]; + target_domain_name?: string[]; + target_logon_id?: string[]; + target_user_name?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/event/index.ts b/x-pack/plugins/osquery/common/ecs/event/index.ts new file mode 100644 index 0000000000000..c3b7b1d0b8436 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/event/index.ts @@ -0,0 +1,27 @@ +/* + * 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 interface EventEcs { + action?: string[]; + category?: string[]; + code?: string[]; + created?: string[]; + dataset?: string[]; + duration?: number[]; + end?: string[]; + hash?: string[]; + id?: string[]; + kind?: string[]; + module?: string[]; + original?: string[]; + outcome?: string[]; + risk_score?: number[]; + risk_score_norm?: number[]; + severity?: number[]; + start?: string[]; + timezone?: string[]; + type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/file/index.ts b/x-pack/plugins/osquery/common/ecs/file/index.ts new file mode 100644 index 0000000000000..b01e9514bf425 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/file/index.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. + */ + +export interface CodeSignature { + subject_name: string[]; + trusted: string[]; +} +export interface Ext { + code_signature: CodeSignature[] | CodeSignature; +} +export interface Hash { + sha256: string[]; +} + +export interface FileEcs { + name?: string[]; + path?: string[]; + target_path?: string[]; + extension?: string[]; + Ext?: Ext; + type?: string[]; + device?: string[]; + inode?: string[]; + uid?: string[]; + owner?: string[]; + gid?: string[]; + group?: string[]; + mode?: string[]; + size?: number[]; + mtime?: string[]; + ctime?: string[]; + hash?: Hash; +} diff --git a/x-pack/plugins/osquery/common/ecs/geo/index.ts b/x-pack/plugins/osquery/common/ecs/geo/index.ts new file mode 100644 index 0000000000000..4a4c76adb097b --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/geo/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface GeoEcs { + city_name?: string[]; + continent_name?: string[]; + country_iso_code?: string[]; + country_name?: string[]; + location?: Location; + region_iso_code?: string[]; + region_name?: string[]; +} + +export interface Location { + lon?: number[]; + lat?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/host/index.ts b/x-pack/plugins/osquery/common/ecs/host/index.ts new file mode 100644 index 0000000000000..27cbe433f9bf7 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/host/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface HostEcs { + architecture?: string[]; + id?: string[]; + ip?: string[]; + mac?: string[]; + name?: string[]; + os?: OsEcs; + type?: string[]; +} + +export interface OsEcs { + platform?: string[]; + name?: string[]; + full?: string[]; + family?: string[]; + version?: string[]; + kernel?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/http/index.ts b/x-pack/plugins/osquery/common/ecs/http/index.ts new file mode 100644 index 0000000000000..c5c5d1e140d0a --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/http/index.ts @@ -0,0 +1,29 @@ +/* + * 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 interface HttpEcs { + version?: string[]; + request?: HttpRequestData; + response?: HttpResponseData; +} + +export interface HttpRequestData { + method?: string[]; + body?: HttpBodyData; + referrer?: string[]; + bytes?: number[]; +} + +export interface HttpBodyData { + content?: string[]; + bytes?: number[]; +} + +export interface HttpResponseData { + status_code?: number[]; + body?: HttpBodyData; + bytes?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/index.ts b/x-pack/plugins/osquery/common/ecs/index.ts new file mode 100644 index 0000000000000..b8190463f5da5 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/index.ts @@ -0,0 +1,57 @@ +/* + * 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 { AgentEcs } from './agent'; +import { AuditdEcs } from './auditd'; +import { DestinationEcs } from './destination'; +import { DnsEcs } from './dns'; +import { EndgameEcs } from './endgame'; +import { EventEcs } from './event'; +import { FileEcs } from './file'; +import { GeoEcs } from './geo'; +import { HostEcs } from './host'; +import { NetworkEcs } from './network'; +import { RuleEcs } from './rule'; +import { SignalEcs } from './signal'; +import { SourceEcs } from './source'; +import { SuricataEcs } from './suricata'; +import { TlsEcs } from './tls'; +import { ZeekEcs } from './zeek'; +import { HttpEcs } from './http'; +import { UrlEcs } from './url'; +import { UserEcs } from './user'; +import { WinlogEcs } from './winlog'; +import { ProcessEcs } from './process'; +import { SystemEcs } from './system'; + +export interface Ecs { + _id: string; + _index?: string; + agent?: AgentEcs; + auditd?: AuditdEcs; + destination?: DestinationEcs; + dns?: DnsEcs; + endgame?: EndgameEcs; + event?: EventEcs; + geo?: GeoEcs; + host?: HostEcs; + network?: NetworkEcs; + rule?: RuleEcs; + signal?: SignalEcs; + source?: SourceEcs; + suricata?: SuricataEcs; + tls?: TlsEcs; + zeek?: ZeekEcs; + http?: HttpEcs; + url?: UrlEcs; + timestamp?: string; + message?: string[]; + user?: UserEcs; + winlog?: WinlogEcs; + process?: ProcessEcs; + file?: FileEcs; + system?: SystemEcs; +} diff --git a/x-pack/plugins/osquery/common/ecs/network/index.ts b/x-pack/plugins/osquery/common/ecs/network/index.ts new file mode 100644 index 0000000000000..18f7583d12231 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/network/index.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. + */ + +export interface NetworkEcs { + bytes?: number[]; + community_id?: string[]; + direction?: string[]; + packets?: number[]; + protocol?: string[]; + transport?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/process/index.ts b/x-pack/plugins/osquery/common/ecs/process/index.ts new file mode 100644 index 0000000000000..451f1455f55d4 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/process/index.ts @@ -0,0 +1,29 @@ +/* + * 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 interface ProcessEcs { + entity_id?: string[]; + hash?: ProcessHashData; + pid?: number[]; + name?: string[]; + ppid?: number[]; + args?: string[]; + executable?: string[]; + title?: string[]; + thread?: Thread; + working_directory?: string[]; +} + +export interface ProcessHashData { + md5?: string[]; + sha1?: string[]; + sha256?: string[]; +} + +export interface Thread { + id?: number[]; + start?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/rule/index.ts b/x-pack/plugins/osquery/common/ecs/rule/index.ts new file mode 100644 index 0000000000000..47d1323371941 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/rule/index.ts @@ -0,0 +1,45 @@ +/* + * 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 interface RuleEcs { + id?: string[]; + rule_id?: string[]; + name?: string[]; + false_positives: string[]; + saved_id?: string[]; + timeline_id?: string[]; + timeline_title?: string[]; + max_signals?: number[]; + risk_score?: string[]; + output_index?: string[]; + description?: string[]; + from?: string[]; + immutable?: boolean[]; + index?: string[]; + interval?: string[]; + language?: string[]; + query?: string[]; + references?: string[]; + severity?: string[]; + tags?: string[]; + threat?: unknown; + threshold?: { + field: string; + value: number; + }; + type?: string[]; + size?: string[]; + to?: string[]; + enabled?: boolean[]; + filters?: unknown; + created_at?: string[]; + updated_at?: string[]; + created_by?: string[]; + updated_by?: string[]; + version?: string[]; + note?: string[]; + building_block_type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/signal/index.ts b/x-pack/plugins/osquery/common/ecs/signal/index.ts new file mode 100644 index 0000000000000..6482b892bc18d --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/signal/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 { RuleEcs } from '../rule'; + +export interface SignalEcs { + rule?: RuleEcs; + original_time?: string[]; + status?: string[]; + group?: { + id?: string[]; + }; +} diff --git a/x-pack/plugins/osquery/common/ecs/source/index.ts b/x-pack/plugins/osquery/common/ecs/source/index.ts new file mode 100644 index 0000000000000..2c8618f4edcd0 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/source/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 { GeoEcs } from '../geo'; + +export interface SourceEcs { + bytes?: number[]; + ip?: string[]; + port?: number[]; + domain?: string[]; + geo?: GeoEcs; + packets?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/suricata/index.ts b/x-pack/plugins/osquery/common/ecs/suricata/index.ts new file mode 100644 index 0000000000000..0ef253ada2620 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/suricata/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SuricataEcs { + eve?: SuricataEveData; +} + +export interface SuricataEveData { + alert?: SuricataAlertData; + flow_id?: number[]; + proto?: string[]; +} + +export interface SuricataAlertData { + signature?: string[]; + signature_id?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/system/index.ts b/x-pack/plugins/osquery/common/ecs/system/index.ts new file mode 100644 index 0000000000000..641a10209c150 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/system/index.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. + */ + +export interface SystemEcs { + audit?: AuditEcs; + auth?: AuthEcs; +} + +export interface AuditEcs { + package?: PackageEcs; +} + +export interface PackageEcs { + arch?: string[]; + entity_id?: string[]; + name?: string[]; + size?: number[]; + summary?: string[]; + version?: string[]; +} + +export interface AuthEcs { + ssh?: SshEcs; +} + +export interface SshEcs { + method?: string[]; + signature?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/tls/index.ts b/x-pack/plugins/osquery/common/ecs/tls/index.ts new file mode 100644 index 0000000000000..1533d46417d0a --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/tls/index.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. + */ + +export interface TlsEcs { + client_certificate?: TlsClientCertificateData; + fingerprints?: TlsFingerprintsData; + server_certificate?: TlsServerCertificateData; +} + +export interface TlsClientCertificateData { + fingerprint?: FingerprintData; +} + +export interface FingerprintData { + sha1?: string[]; +} + +export interface TlsFingerprintsData { + ja3?: TlsJa3Data; +} + +export interface TlsJa3Data { + hash?: string[]; +} + +export interface TlsServerCertificateData { + fingerprint?: FingerprintData; +} diff --git a/x-pack/plugins/osquery/common/ecs/url/index.ts b/x-pack/plugins/osquery/common/ecs/url/index.ts new file mode 100644 index 0000000000000..91d7958c813a3 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/url/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface UrlEcs { + domain?: string[]; + original?: string[]; + username?: string[]; + password?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/user/index.ts b/x-pack/plugins/osquery/common/ecs/user/index.ts new file mode 100644 index 0000000000000..35de2e0459ceb --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/user/index.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 interface UserEcs { + domain?: string[]; + id?: string[]; + name?: string[]; + full_name?: string[]; + email?: string[]; + hash?: string[]; + group?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/winlog/index.ts b/x-pack/plugins/osquery/common/ecs/winlog/index.ts new file mode 100644 index 0000000000000..a449fb9130e6f --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/winlog/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface WinlogEcs { + event_id?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/zeek/index.ts b/x-pack/plugins/osquery/common/ecs/zeek/index.ts new file mode 100644 index 0000000000000..2563612f09bfb --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/zeek/index.ts @@ -0,0 +1,83 @@ +/* + * 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 interface ZeekEcs { + session_id?: string[]; + connection?: ZeekConnectionData; + notice?: ZeekNoticeData; + dns?: ZeekDnsData; + http?: ZeekHttpData; + files?: ZeekFileData; + ssl?: ZeekSslData; +} + +export interface ZeekConnectionData { + local_resp?: boolean[]; + local_orig?: boolean[]; + missed_bytes?: number[]; + state?: string[]; + history?: string[]; +} + +export interface ZeekNoticeData { + suppress_for?: number[]; + msg?: string[]; + note?: string[]; + sub?: string[]; + dst?: string[]; + dropped?: boolean[]; + peer_descr?: string[]; +} + +export interface ZeekDnsData { + AA?: boolean[]; + qclass_name?: string[]; + RD?: boolean[]; + qtype_name?: string[]; + rejected?: boolean[]; + qtype?: string[]; + query?: string[]; + trans_id?: number[]; + qclass?: string[]; + RA?: boolean[]; + TC?: boolean[]; +} + +export interface ZeekHttpData { + resp_mime_types?: string[]; + trans_depth?: string[]; + status_msg?: string[]; + resp_fuids?: string[]; + tags?: string[]; +} + +export interface ZeekFileData { + session_ids?: string[]; + timedout?: boolean[]; + local_orig?: boolean[]; + tx_host?: string[]; + source?: string[]; + is_orig?: boolean[]; + overflow_bytes?: number[]; + sha1?: string[]; + duration?: number[]; + depth?: number[]; + analyzers?: string[]; + mime_type?: string[]; + rx_host?: string[]; + total_bytes?: number[]; + fuid?: string[]; + seen_bytes?: number[]; + missing_bytes?: number[]; + md5?: string[]; +} + +export interface ZeekSslData { + cipher?: string[]; + established?: boolean[]; + resumed?: boolean[]; + version?: string[]; +} diff --git a/x-pack/plugins/osquery/common/index.ts b/x-pack/plugins/osquery/common/index.ts new file mode 100644 index 0000000000000..e4bbf4781e881 --- /dev/null +++ b/x-pack/plugins/osquery/common/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './constants'; + +export const PLUGIN_ID = 'osquery'; +export const PLUGIN_NAME = 'osquery'; diff --git a/x-pack/plugins/osquery/common/search_strategy/common/index.ts b/x-pack/plugins/osquery/common/search_strategy/common/index.ts new file mode 100644 index 0000000000000..0c1f13dac2e69 --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/common/index.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../src/plugins/data/common'; + +export type Maybe = T | null; + +export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0]; + +export interface TotalValue { + value: number; + relation: string; +} + +export interface Inspect { + dsl: string[]; +} + +export interface PageInfoPaginated { + activePage: number; + fakeTotalCount: number; + showMorePagesIndicator: boolean; +} + +export interface CursorType { + value?: Maybe; + tiebreaker?: Maybe; +} + +export enum Direction { + asc = 'asc', + desc = 'desc', +} + +export interface SortField { + field: Field; + direction: Direction; +} + +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: string; + /** The beginning of the timerange */ + from: string; +} + +export interface PaginationInput { + /** The limit parameter allows you to configure the maximum amount of items to be returned */ + limit: number; + /** The cursor parameter defines the next result you want to fetch */ + cursor?: Maybe; + /** The tiebreaker parameter allow to be more precise to fetch the next item */ + tiebreaker?: Maybe; +} + +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + +export interface DocValueFields { + field: string; + format?: string | null; +} + +export interface Explanation { + value: number; + description: string; + details: Explanation[]; +} + +export interface ShardsResponse { + total: number; + successful: number; + failed: number; + skipped: number; +} + +export interface TotalHit { + value: number; + relation: string; +} + +export interface Hit { + _index: string; + _type: string; + _id: string; + _score: number | null; +} + +export interface Hits { + hits: { + total: T; + max_score: number | null; + hits: U[]; + }; +} + +export interface GenericBuckets { + key: string; + doc_count: number; +} + +export type StringOrNumber = string | number; + +export interface TimerangeFilter { + range: { + [timestamp: string]: { + gte: string; + lte: string; + format: string; + }; + }; +} diff --git a/x-pack/plugins/osquery/common/search_strategy/index.ts b/x-pack/plugins/osquery/common/search_strategy/index.ts new file mode 100644 index 0000000000000..ff9a8d1aa64c9 --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/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 * from './common'; +export * from './osquery'; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts new file mode 100644 index 0000000000000..076fa02747573 --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.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 { SearchResponse } from 'elasticsearch'; +import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; + +import { Inspect, Maybe, PageInfoPaginated } from '../../common'; +import { RequestOptions, RequestOptionsPaginated } from '../..'; + +export type ActionEdges = SearchResponse['hits']['hits']; + +export type ActionResultEdges = SearchResponse['hits']['hits']; +export interface ActionsStrategyResponse extends IEsSearchResponse { + edges: ActionEdges; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export type ActionsRequestOptions = RequestOptionsPaginated<{}>; + +export interface ActionDetailsStrategyResponse extends IEsSearchResponse { + actionDetails: Record; + inspect?: Maybe; +} + +export interface ActionDetailsRequestOptions extends RequestOptions { + actionId: string; +} + +export interface ActionResultsStrategyResponse extends IEsSearchResponse { + edges: ActionResultEdges; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface ActionResultsRequestOptions extends RequestOptionsPaginated { + actionId: string; +} diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts new file mode 100644 index 0000000000000..64a570ef5525b --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; + +import { Inspect, Maybe, PageInfoPaginated } from '../../common'; +import { RequestOptionsPaginated } from '../..'; +import { Agent } from '../../../shared_imports'; + +export interface AgentsStrategyResponse extends IEsSearchResponse { + edges: Agent[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export type AgentsRequestOptions = RequestOptionsPaginated<{}>; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts new file mode 100644 index 0000000000000..fc58184f40afe --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CloudEcs } from '../../../ecs/cloud'; +import { HostEcs, OsEcs } from '../../../ecs/host'; +import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../common'; + +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + +export enum HostsFields { + lastSeen = 'lastSeen', + hostName = 'hostName', +} + +export interface EndpointFields { + endpointPolicy?: Maybe; + sensorVersion?: Maybe; + policyStatus?: Maybe; +} + +export interface HostItem { + _id?: Maybe; + cloud?: Maybe; + endpoint?: Maybe; + host?: Maybe; + lastSeen?: Maybe; +} + +export interface HostValue { + value: number; + value_as_string: string; +} + +export interface HostBucketItem { + key: string; + doc_count: number; + timestamp: HostValue; +} + +export interface HostBuckets { + buckets: HostBucketItem[]; +} + +export interface HostOsHitsItem { + hits: { + total: TotalValue | number; + max_score: number | null; + hits: Array<{ + _source: { host: { os: Maybe } }; + sort?: [number]; + _index?: string; + _type?: string; + _id?: string; + _score?: number | null; + }>; + }; +} + +export interface HostAggEsItem { + cloud_instance_id?: HostBuckets; + cloud_machine_type?: HostBuckets; + cloud_provider?: HostBuckets; + cloud_region?: HostBuckets; + firstSeen?: HostValue; + host_architecture?: HostBuckets; + host_id?: HostBuckets; + host_ip?: HostBuckets; + host_mac?: HostBuckets; + host_name?: HostBuckets; + host_os_name?: HostBuckets; + host_os_version?: HostBuckets; + host_type?: HostBuckets; + key?: string; + lastSeen?: HostValue; + os?: HostOsHitsItem; +} + +export interface HostEsData extends SearchHit { + sort: string[]; + aggregations: { + host_count: { + value: number; + }; + host_data: { + buckets: HostAggEsItem[]; + }; + }; +} + +export interface HostAggEsData extends SearchHit { + sort: string[]; + aggregations: HostAggEsItem; +} + +export interface HostHit extends Hit { + _source: { + '@timestamp'?: string; + host: HostEcs; + }; + cursor?: string; + firstSeen?: string; + sort?: StringOrNumber[]; +} + +export type HostHits = Hits; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts new file mode 100644 index 0000000000000..70882ffcc2e5c --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts @@ -0,0 +1,73 @@ +/* + * 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 { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; +import { ESQuery } from '../../typed_json'; +import { + ActionsStrategyResponse, + ActionsRequestOptions, + ActionDetailsStrategyResponse, + ActionDetailsRequestOptions, + ActionResultsStrategyResponse, + ActionResultsRequestOptions, +} from './actions'; +import { AgentsStrategyResponse, AgentsRequestOptions } from './agents'; +import { ResultsStrategyResponse, ResultsRequestOptions } from './results'; + +import { DocValueFields, SortField, PaginationInputPaginated } from '../common'; + +export * from './actions'; +export * from './agents'; +export * from './results'; + +export enum OsqueryQueries { + actions = 'actions', + actionDetails = 'actionDetails', + actionResults = 'actionResults', + agents = 'agents', + results = 'results', +} + +export type FactoryQueryTypes = OsqueryQueries; + +export interface RequestBasicOptions extends IEsSearchRequest { + filterQuery: ESQuery | string | undefined; + docValueFields?: DocValueFields[]; + factoryQueryType?: FactoryQueryTypes; +} + +/** A mapping of semantic fields to their document counterparts */ + +export type RequestOptions = RequestBasicOptions; + +export interface RequestOptionsPaginated extends RequestBasicOptions { + pagination: PaginationInputPaginated; + sort: SortField; +} + +export type StrategyResponseType = T extends OsqueryQueries.actions + ? ActionsStrategyResponse + : T extends OsqueryQueries.actionDetails + ? ActionDetailsStrategyResponse + : T extends OsqueryQueries.actionResults + ? ActionResultsStrategyResponse + : T extends OsqueryQueries.agents + ? AgentsStrategyResponse + : T extends OsqueryQueries.results + ? ResultsStrategyResponse + : never; + +export type StrategyRequestType = T extends OsqueryQueries.actions + ? ActionsRequestOptions + : T extends OsqueryQueries.actionDetails + ? ActionDetailsRequestOptions + : T extends OsqueryQueries.actionResults + ? ActionResultsRequestOptions + : T extends OsqueryQueries.agents + ? AgentsRequestOptions + : T extends OsqueryQueries.results + ? ResultsRequestOptions + : never; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts new file mode 100644 index 0000000000000..65df2591338e4 --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; + +import { Inspect, Maybe, PageInfoPaginated } from '../../common'; +import { RequestOptionsPaginated } from '../..'; + +export type ResultEdges = SearchResponse['hits']['hits']; + +export interface ResultsStrategyResponse extends IEsSearchResponse { + edges: ResultEdges; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface ResultsRequestOptions extends RequestOptionsPaginated<{}> { + actionId: string; +} diff --git a/x-pack/plugins/osquery/common/shared_imports.ts b/x-pack/plugins/osquery/common/shared_imports.ts new file mode 100644 index 0000000000000..58133db6aa1b0 --- /dev/null +++ b/x-pack/plugins/osquery/common/shared_imports.ts @@ -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 { Agent } from '../../fleet/common'; diff --git a/x-pack/plugins/osquery/common/typed_json.ts b/x-pack/plugins/osquery/common/typed_json.ts new file mode 100644 index 0000000000000..0d6e3877eae55 --- /dev/null +++ b/x-pack/plugins/osquery/common/typed_json.ts @@ -0,0 +1,57 @@ +/* + * 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 { DslQuery, Filter } from 'src/plugins/data/common'; + +import { JsonObject } from '../../../../src/plugins/kibana_utils/common'; + +export type ESQuery = + | ESRangeQuery + | ESQueryStringQuery + | ESMatchQuery + | ESTermQuery + | ESBoolQuery + | JsonObject; + +export interface ESRangeQuery { + range: { + [name: string]: { + gte: number; + lte: number; + format: string; + }; + }; +} + +export interface ESMatchQuery { + match: { + [name: string]: { + query: string; + operator: string; + zero_terms_query: string; + }; + }; +} + +export interface ESQueryStringQuery { + query_string: { + query: string; + analyze_wildcard: boolean; + }; +} + +export interface ESTermQuery { + term: Record; +} + +export interface ESBoolQuery { + bool: { + must: DslQuery[]; + filter: Filter[]; + should: never[]; + must_not: Filter[]; + }; +} diff --git a/x-pack/plugins/osquery/common/utility_types.ts b/x-pack/plugins/osquery/common/utility_types.ts new file mode 100644 index 0000000000000..4a7bd02d0442b --- /dev/null +++ b/x-pack/plugins/osquery/common/utility_types.ts @@ -0,0 +1,46 @@ +/* + * 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 runtimeTypes from 'io-ts'; +import { ReactNode } from 'react'; + +// This type is for typing EuiDescriptionList +export interface DescriptionList { + title: NonNullable; + description: NonNullable; +} + +export const unionWithNullType = (type: T) => + runtimeTypes.union([type, runtimeTypes.null]); + +export const stringEnum = (enumObj: T, enumName = 'enum') => + new runtimeTypes.Type( + enumName, + (u): u is T[keyof T] => Object.values(enumObj).includes(u), + (u, c) => + Object.values(enumObj).includes(u) + ? runtimeTypes.success(u as T[keyof T]) + : runtimeTypes.failure(u, c), + (a) => (a as unknown) as string + ); + +/** + * Unreachable Assertion helper for scenarios like exhaustive switches. + * For references see: https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript + * This "x" should _always_ be a type of "never" and not change to "unknown" or any other type. See above link or the generic + * concept of exhaustive checks in switch blocks. + * + * Optionally you can avoid the use of this by using early returns and TypeScript will clear your type checking without complaints + * but there are situations and times where this function might still be needed. + * @param x Unreachable field + * @param message Message of error thrown + */ +export const assertUnreachable = ( + x: never, // This should always be a type of "never" + message = 'Unknown Field in switch statement' +): never => { + throw new Error(`${message}: ${x}`); +}; diff --git a/x-pack/plugins/osquery/common/utils/build_query/filters.ts b/x-pack/plugins/osquery/common/utils/build_query/filters.ts new file mode 100644 index 0000000000000..bde03be3f5edc --- /dev/null +++ b/x-pack/plugins/osquery/common/utils/build_query/filters.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty, isString } from 'lodash/fp'; + +import { ESQuery } from '../../../common/typed_json'; + +export const createQueryFilterClauses = (filterQuery: ESQuery | string | undefined) => + !isEmpty(filterQuery) ? [isString(filterQuery) ? JSON.parse(filterQuery) : filterQuery] : []; diff --git a/x-pack/plugins/osquery/common/utils/build_query/index.ts b/x-pack/plugins/osquery/common/utils/build_query/index.ts new file mode 100644 index 0000000000000..05606d556528c --- /dev/null +++ b/x-pack/plugins/osquery/common/utils/build_query/index.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 * from './filters'; + +export const inspectStringifyObject = (obj: unknown) => { + try { + return JSON.stringify(obj, null, 2); + } catch { + return 'Sorry about that, something went wrong.'; + } +}; diff --git a/x-pack/plugins/osquery/jest.config.js b/x-pack/plugins/osquery/jest.config.js new file mode 100644 index 0000000000000..8132491df8534 --- /dev/null +++ b/x-pack/plugins/osquery/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/osquery'], +}; diff --git a/x-pack/plugins/osquery/kibana.json b/x-pack/plugins/osquery/kibana.json new file mode 100644 index 0000000000000..f6e90b9460506 --- /dev/null +++ b/x-pack/plugins/osquery/kibana.json @@ -0,0 +1,29 @@ +{ + "configPath": [ + "xpack", + "osquery" + ], + "extraPublicDirs": [ + "common" + ], + "id": "osquery", + "kibanaVersion": "kibana", + "optionalPlugins": [ + "home" + ], + "requiredBundles": [ + "esUiShared", + "kibanaUtils", + "kibanaReact", + "kibanaUtils" + ], + "requiredPlugins": [ + "data", + "dataEnhanced", + "fleet", + "navigation" + ], + "server": true, + "ui": true, + "version": "8.0.0" +} diff --git a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx new file mode 100644 index 0000000000000..68424d848a9c7 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx @@ -0,0 +1,113 @@ +/* + * 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 { isEmpty, isEqual, keys, map } from 'lodash/fp'; +import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn, EuiDataGridSorting } from '@elastic/eui'; +import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; + +import { useAllResults } from './use_action_results'; +import { Direction, ResultEdges } from '../../common/search_strategy'; + +const DataContext = createContext([]); + +interface ActionResultsTableProps { + actionId: string; +} + +const ActionResultsTableComponent: React.FC = ({ actionId }) => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((currentPagination) => ({ + ...currentPagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => setPagination((currentPagination) => ({ ...currentPagination, pageIndex })), + [setPagination] + ); + + const [columns, setColumns] = useState([]); + + // ** Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + + const [, { results, totalCount }] = useAllResults({ + actionId, + activePage: pagination.pageIndex, + limit: pagination.pageSize, + direction: Direction.asc, + sortField: '@timestamp', + }); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); // initialize to the full set of columns + + const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [ + visibleColumns, + setVisibleColumns, + ]); + + const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( + () => ({ rowIndex, columnId, setCellProps }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const data = useContext(DataContext); + const value = data[rowIndex].fields[columnId]; + + return !isEmpty(value) ? value : '-'; + }, + [] + ); + + const tableSorting: EuiDataGridSorting = useMemo( + () => ({ columns: sortingColumns, onSort: setSortingColumns }), + [sortingColumns] + ); + + const tablePagination = useMemo( + () => ({ + ...pagination, + pageSizeOptions: [10, 50, 100], + onChangeItemsPerPage, + onChangePage, + }), + [onChangeItemsPerPage, onChangePage, pagination] + ); + + useEffect(() => { + const newColumns = keys(results[0]?.fields) + .sort() + .map((fieldName) => ({ + id: fieldName, + displayAsText: fieldName.split('.')[1], + defaultSortDirection: Direction.asc, + })); + + if (!isEqual(columns, newColumns)) { + setColumns(newColumns); + setVisibleColumns(map('id', newColumns)); + } + }, [columns, results]); + + return ( + + + + ); +}; + +export const ActionResultsTable = React.memo(ActionResultsTableComponent); diff --git a/x-pack/plugins/osquery/public/action_results/helpers.ts b/x-pack/plugins/osquery/public/action_results/helpers.ts new file mode 100644 index 0000000000000..9f908e16c2eb2 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/helpers.ts @@ -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 { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number, + isBucketSort?: boolean +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: isBucketSort ? limit : limit + cursorStart, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/action_results/translations.ts b/x-pack/plugins/osquery/public/action_results/translations.ts new file mode 100644 index 0000000000000..54c8ecebc60c0 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_RESULTS = i18n.translate('xpack.osquery.results.errorSearchDescription', { + defaultMessage: `An error has occurred on all results search`, +}); + +export const FAIL_ALL_RESULTS = i18n.translate('xpack.osquery.results.failSearchDescription', { + defaultMessage: `Failed to fetch results`, +}); diff --git a/x-pack/plugins/osquery/public/action_results/use_action_results.ts b/x-pack/plugins/osquery/public/action_results/use_action_results.ts new file mode 100644 index 0000000000000..2c54606bf3fbb --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/use_action_results.ts @@ -0,0 +1,164 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + ResultEdges, + PageInfoPaginated, + DocValueFields, + OsqueryQueries, + ResultsRequestOptions, + ResultsStrategyResponse, + Direction, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'resultsAllQuery'; + +export interface ResultsArgs { + results: ResultEdges; + id: string; + inspect: InspectResponse; + isInspected: boolean; + pageInfo: PageInfoPaginated; + totalCount: number; +} + +interface UseAllResults { + actionId: string; + activePage: number; + direction: Direction; + limit: number; + sortField: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useAllResults = ({ + actionId, + activePage, + direction, + limit, + sortField, + docValueFields, + filterQuery, + skip = false, +}: UseAllResults): [boolean, ResultsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [resultsRequest, setHostRequest] = useState(null); + + const [resultsResponse, setResultsResponse] = useState({ + results: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + totalCount: -1, + }); + + const resultsSearch = useCallback( + (request: ResultsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setResultsResponse((prevResponse) => ({ + ...prevResponse, + results: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_RESULTS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_RESULTS, text: msg.message }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + actionId, + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.actionResults, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [actionId, activePage, direction, docValueFields, filterQuery, limit, sortField]); + + useEffect(() => { + resultsSearch(resultsRequest); + }, [resultsRequest, resultsSearch]); + + return [loading, resultsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/actions/actions_table.tsx b/x-pack/plugins/osquery/public/actions/actions_table.tsx new file mode 100644 index 0000000000000..917e915d9d9dc --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/actions_table.tsx @@ -0,0 +1,107 @@ +/* + * 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 { isEmpty, isEqual, keys, map } from 'lodash/fp'; +import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn, EuiDataGridSorting } from '@elastic/eui'; +import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; + +import { useAllActions } from './use_all_actions'; +import { ActionEdges, Direction } from '../../common/search_strategy'; + +const DataContext = createContext([]); + +const ActionsTableComponent = () => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((currentPagination) => ({ + ...currentPagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => setPagination((currentPagination) => ({ ...currentPagination, pageIndex })), + [setPagination] + ); + + const [columns, setColumns] = useState([]); + + // ** Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + + const [, { actions, totalCount }] = useAllActions({ + activePage: pagination.pageIndex, + limit: pagination.pageSize, + direction: Direction.asc, + sortField: '@timestamp', + }); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); // initialize to the full set of columns + + const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [ + visibleColumns, + setVisibleColumns, + ]); + + const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(() => { + return ({ rowIndex, columnId, setCellProps }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const data = useContext(DataContext); + const value = data[rowIndex].fields[columnId]; + + return !isEmpty(value) ? value : '-'; + }; + }, []); + + const tableSorting: EuiDataGridSorting = useMemo( + () => ({ columns: sortingColumns, onSort: setSortingColumns }), + [setSortingColumns, sortingColumns] + ); + + const tablePagination = useMemo( + () => ({ + ...pagination, + pageSizeOptions: [10, 50, 100], + onChangeItemsPerPage, + onChangePage, + }), + [onChangeItemsPerPage, onChangePage, pagination] + ); + + useEffect(() => { + const newColumns = keys(actions[0]?.fields) + .sort() + .map((fieldName) => ({ + id: fieldName, + displayAsText: fieldName.split('.')[1], + defaultSortDirection: Direction.asc, + })); + + if (!isEqual(columns, newColumns)) { + setColumns(newColumns); + setVisibleColumns(map('id', newColumns)); + } + }, [columns, actions]); + + return ( + + + + ); +}; + +export const ActionsTable = React.memo(ActionsTableComponent); diff --git a/x-pack/plugins/osquery/public/actions/helpers.ts b/x-pack/plugins/osquery/public/actions/helpers.ts new file mode 100644 index 0000000000000..9f908e16c2eb2 --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/helpers.ts @@ -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 { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number, + isBucketSort?: boolean +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: isBucketSort ? limit : limit + cursorStart, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/actions/translations.ts b/x-pack/plugins/osquery/public/actions/translations.ts new file mode 100644 index 0000000000000..3bf2d81e5e092 --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/translations.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 { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_ACTIONS = i18n.translate('xpack.osquery.actions.errorSearchDescription', { + defaultMessage: `An error has occurred on all actions search`, +}); + +export const FAIL_ALL_ACTIONS = i18n.translate('xpack.osquery.actions.failSearchDescription', { + defaultMessage: `Failed to fetch actions`, +}); + +export const ERROR_ACTION_DETAILS = i18n.translate( + 'xpack.osquery.actionDetails.errorSearchDescription', + { + defaultMessage: `An error has occurred on action details search`, + } +); + +export const FAIL_ACTION_DETAILS = i18n.translate( + 'xpack.osquery.actionDetails.failSearchDescription', + { + defaultMessage: `Failed to fetch action details`, + } +); + +export const ERROR_ACTION_RESULTS = i18n.translate( + 'xpack.osquery.actionResults.errorSearchDescription', + { + defaultMessage: `An error has occurred on action results search`, + } +); + +export const FAIL_ACTION_RESULTS = i18n.translate( + 'xpack.osquery.actionResults.failSearchDescription', + { + defaultMessage: `Failed to fetch action results`, + } +); diff --git a/x-pack/plugins/osquery/public/actions/use_action_details.ts b/x-pack/plugins/osquery/public/actions/use_action_details.ts new file mode 100644 index 0000000000000..3112d7cbf221e --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/use_action_details.ts @@ -0,0 +1,141 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + DocValueFields, + OsqueryQueries, + ActionDetailsRequestOptions, + ActionDetailsStrategyResponse, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'actionDetailsQuery'; + +export interface ActionDetailsArgs { + actionDetails: Record; + id: string; + inspect: InspectResponse; + isInspected: boolean; +} + +interface UseActionDetails { + actionId: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useActionDetails = ({ + actionId, + docValueFields, + filterQuery, + skip = false, +}: UseActionDetails): [boolean, ActionDetailsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [actionDetailsRequest, setHostRequest] = useState(null); + + const [actionDetailsResponse, setActionDetailsResponse] = useState({ + actionDetails: {}, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + }); + + const actionDetailsSearch = useCallback( + (request: ActionDetailsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setActionDetailsResponse((prevResponse) => ({ + ...prevResponse, + actionDetails: response.actionDetails, + inspect: getInspectResponse(response, prevResponse.inspect), + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ACTION_DETAILS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_ACTION_DETAILS, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + actionId, + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.actionDetails, + filterQuery: createFilter(filterQuery), + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [actionId, docValueFields, filterQuery]); + + useEffect(() => { + actionDetailsSearch(actionDetailsRequest); + }, [actionDetailsRequest, actionDetailsSearch]); + + return [loading, actionDetailsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/actions/use_all_actions.ts b/x-pack/plugins/osquery/public/actions/use_all_actions.ts new file mode 100644 index 0000000000000..192f5b1eb410c --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/use_all_actions.ts @@ -0,0 +1,161 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + ActionEdges, + PageInfoPaginated, + DocValueFields, + OsqueryQueries, + ActionsRequestOptions, + ActionsStrategyResponse, + Direction, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'actionsAllQuery'; + +export interface ActionsArgs { + actions: ActionEdges; + id: string; + inspect: InspectResponse; + isInspected: boolean; + pageInfo: PageInfoPaginated; + totalCount: number; +} + +interface UseAllActions { + activePage: number; + direction: Direction; + limit: number; + sortField: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useAllActions = ({ + activePage, + direction, + limit, + sortField, + docValueFields, + filterQuery, + skip = false, +}: UseAllActions): [boolean, ActionsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [actionsRequest, setHostRequest] = useState(null); + + const [actionsResponse, setActionsResponse] = useState({ + actions: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + totalCount: -1, + }); + + const actionsSearch = useCallback( + (request: ActionsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setActionsResponse((prevResponse) => ({ + ...prevResponse, + actions: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_ACTIONS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_ACTIONS, text: msg.message }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.actions, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, direction, docValueFields, filterQuery, limit, sortField]); + + useEffect(() => { + actionsSearch(actionsRequest); + }, [actionsRequest, actionsSearch]); + + return [loading, actionsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx new file mode 100644 index 0000000000000..1c0083b8252e8 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { find } from 'lodash/fp'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiBasicTableProps, + EuiTableSelectionType, + EuiHealth, +} from '@elastic/eui'; + +import { useAllAgents } from './use_all_agents'; +import { Direction } from '../../common/search_strategy'; +import { Agent } from '../../common/shared_imports'; + +interface AgentsTableProps { + selectedAgents: string[]; + onChange: (payload: string[]) => void; +} + +const AgentsTableComponent: React.FC = ({ selectedAgents, onChange }) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(5); + const [sortField, setSortField] = useState('id'); + const [sortDirection, setSortDirection] = useState(Direction.asc); + const [selectedItems, setSelectedItems] = useState([]); + const tableRef = useRef>(null); + + const onTableChange: EuiBasicTableProps['onChange'] = useCallback( + ({ page = {}, sort = {} }) => { + const { index: newPageIndex, size: newPageSize } = page; + + const { field: newSortField, direction: newSortDirection } = sort; + + setPageIndex(newPageIndex); + setPageSize(newPageSize); + setSortField(newSortField); + setSortDirection(newSortDirection); + }, + [] + ); + + const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback( + (newSelectedItems) => { + setSelectedItems(newSelectedItems); + // @ts-expect-error + onChange(newSelectedItems.map((item) => item._id)); + }, + [onChange] + ); + + const renderStatus = (online: string) => { + const color = online ? 'success' : 'danger'; + const label = online ? 'Online' : 'Offline'; + return {label}; + }; + + const [, { agents, totalCount }] = useAllAgents({ + activePage: pageIndex, + limit: pageSize, + direction: sortDirection, + sortField, + }); + + const columns: Array> = useMemo( + () => [ + { + field: 'local_metadata.elastic.agent.id', + name: 'id', + sortable: true, + truncateText: true, + }, + { + field: 'local_metadata.host.name', + name: 'hostname', + truncateText: true, + }, + + { + field: 'active', + name: 'Online', + dataType: 'boolean', + render: (active: string) => renderStatus(active), + }, + ], + [] + ); + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount: totalCount, + pageSizeOptions: [3, 5, 8], + }), + [pageIndex, pageSize, totalCount] + ); + + const sorting = useMemo( + () => ({ + sort: { + field: sortField, + direction: sortDirection, + }, + }), + [sortDirection, sortField] + ); + + const selection: EuiBasicTableProps['selection'] = useMemo( + () => ({ + selectable: (agent: Agent) => agent.active, + selectableMessage: (selectable: boolean) => (!selectable ? 'User is currently offline' : ''), + onSelectionChange, + initialSelected: selectedItems, + }), + [onSelectionChange, selectedItems] + ); + + useEffect(() => { + if (selectedAgents?.length && agents.length && selectedItems.length !== selectedAgents.length) { + tableRef?.current?.setSelection( + // @ts-expect-error + selectedAgents.map((agentId) => find({ _id: agentId }, agents)) + ); + } + }, [selectedAgents, agents, selectedItems.length]); + + return ( + + ref={tableRef} + items={agents} + itemId="_id" + columns={columns} + pagination={pagination} + sorting={sorting} + isSelectable={true} + selection={selection} + onChange={onTableChange} + rowHeader="firstName" + /> + ); +}; + +export const AgentsTable = React.memo(AgentsTableComponent); diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts new file mode 100644 index 0000000000000..9f908e16c2eb2 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -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 { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number, + isBucketSort?: boolean +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: isBucketSort ? limit : limit + cursorStart, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts new file mode 100644 index 0000000000000..a95ad5e4ce163 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { + defaultMessage: `An error has occurred on all agents search`, +}); + +export const FAIL_ALL_AGENTS = i18n.translate('xpack.osquery.agents.failSearchDescription', { + defaultMessage: `Failed to fetch agents`, +}); diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts new file mode 100644 index 0000000000000..ad1a09486961a --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -0,0 +1,161 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + PageInfoPaginated, + DocValueFields, + OsqueryQueries, + AgentsRequestOptions, + AgentsStrategyResponse, + Direction, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; +import { Agent } from '../../common/shared_imports'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'agentsAllQuery'; + +export interface AgentsArgs { + agents: Agent[]; + id: string; + inspect: InspectResponse; + isInspected: boolean; + pageInfo: PageInfoPaginated; + totalCount: number; +} + +interface UseAllAgents { + activePage: number; + direction: Direction; + limit: number; + sortField: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useAllAgents = ({ + activePage, + direction, + limit, + sortField, + docValueFields, + filterQuery, + skip = false, +}: UseAllAgents): [boolean, AgentsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [agentsRequest, setHostRequest] = useState(null); + + const [agentsResponse, setAgentsResponse] = useState({ + agents: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + totalCount: -1, + }); + + const agentsSearch = useCallback( + (request: AgentsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setAgentsResponse((prevResponse) => ({ + ...prevResponse, + agents: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_AGENTS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_AGENTS, text: msg.message }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.agents, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, direction, docValueFields, filterQuery, limit, sortField]); + + useEffect(() => { + agentsSearch(agentsRequest); + }, [agentsRequest, agentsSearch]); + + return [loading, agentsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/application.tsx b/x-pack/plugins/osquery/public/application.tsx new file mode 100644 index 0000000000000..1a5c826df3310 --- /dev/null +++ b/x-pack/plugins/osquery/public/application.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiErrorBoundary } from '@elastic/eui'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import React, { useMemo } from 'react'; +import ReactDOM from 'react-dom'; +import { Router } from 'react-router-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { ThemeProvider } from 'styled-components'; + +import { useUiSetting$ } from '../../../../src/plugins/kibana_react/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { AppMountParameters, CoreStart } from '../../../../src/core/public'; +import { AppPluginStartDependencies } from './types'; +import { OsqueryApp } from './components/app'; +import { DEFAULT_DARK_MODE, PLUGIN_NAME } from '../common'; +import { KibanaContextProvider } from './common/lib/kibana'; + +const OsqueryAppContext = () => { + const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); + const theme = useMemo( + () => ({ + eui: darkMode ? euiDarkVars : euiLightVars, + darkMode, + }), + [darkMode] + ); + + return ( + + + + ); +}; + +export const renderApp = ( + core: CoreStart, + services: AppPluginStartDependencies, + { element, history }: AppMountParameters, + storage: Storage, + kibanaVersion: string +) => { + ReactDOM.render( + + + + + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/plugins/osquery/public/common/helpers.test.ts b/x-pack/plugins/osquery/public/common/helpers.test.ts new file mode 100644 index 0000000000000..5d378d79acc7a --- /dev/null +++ b/x-pack/plugins/osquery/public/common/helpers.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { ESQuery } from '../../common/typed_json'; + +import { createFilter } from './helpers'; + +describe('Helpers', () => { + describe('#createFilter', () => { + test('if it is a string it returns untouched', () => { + const filter = createFilter('even invalid strings return the same'); + expect(filter).toBe('even invalid strings return the same'); + }); + + test('if it is an ESQuery object it will be returned as a string', () => { + const query: ESQuery = { term: { 'host.id': 'host-value' } }; + const filter = createFilter(query); + expect(filter).toBe(JSON.stringify(query)); + }); + + test('if it is undefined, then undefined is returned', () => { + const filter = createFilter(undefined); + expect(filter).toBe(undefined); + }); + }); +}); diff --git a/x-pack/plugins/osquery/public/common/helpers.ts b/x-pack/plugins/osquery/public/common/helpers.ts new file mode 100644 index 0000000000000..e922e030c9330 --- /dev/null +++ b/x-pack/plugins/osquery/public/common/helpers.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isString } from 'lodash/fp'; + +import { ESQuery } from '../../common/typed_json'; + +export const createFilter = (filterQuery: ESQuery | string | undefined) => + isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); diff --git a/x-pack/plugins/osquery/public/common/index.ts b/x-pack/plugins/osquery/public/common/index.ts new file mode 100644 index 0000000000000..d805555791e2a --- /dev/null +++ b/x-pack/plugins/osquery/public/common/index.ts @@ -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 * from './helpers'; diff --git a/x-pack/plugins/osquery/public/common/lib/kibana/index.ts b/x-pack/plugins/osquery/public/common/lib/kibana/index.ts new file mode 100644 index 0000000000000..b9cb71d4adb47 --- /dev/null +++ b/x-pack/plugins/osquery/public/common/lib/kibana/index.ts @@ -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 * from './kibana_react'; diff --git a/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts b/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts new file mode 100644 index 0000000000000..b4fb307a62b6c --- /dev/null +++ b/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts @@ -0,0 +1,30 @@ +/* + * 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 { + KibanaContextProvider, + KibanaReactContextValue, + useKibana, + useUiSetting, + useUiSetting$, + withKibana, +} from '../../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../../types'; + +export type KibanaContext = KibanaReactContextValue; +export interface WithKibanaProps { + kibana: KibanaContext; +} + +const useTypedKibana = () => useKibana(); + +export { + KibanaContextProvider, + useTypedKibana as useKibana, + useUiSetting, + useUiSetting$, + withKibana, +}; diff --git a/x-pack/plugins/osquery/public/components/app.tsx b/x-pack/plugins/osquery/public/components/app.tsx new file mode 100644 index 0000000000000..49ff7e2bfb4da --- /dev/null +++ b/x-pack/plugins/osquery/public/components/app.tsx @@ -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 React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Switch, Route } from 'react-router-dom'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; + +import { PLUGIN_NAME } from '../../common'; +import { LiveQuery } from '../live_query'; + +export const OsqueryAppComponent = () => { + return ( + + + + +

+ +

+
+
+ + + + + + + + + + + + + +
+
+ ); +}; + +export const OsqueryApp = React.memo(OsqueryAppComponent); diff --git a/x-pack/plugins/osquery/public/editor/index.tsx b/x-pack/plugins/osquery/public/editor/index.tsx new file mode 100644 index 0000000000000..a0e549e77467b --- /dev/null +++ b/x-pack/plugins/osquery/public/editor/index.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, { useCallback } from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; +import 'brace/mode/sql'; +import 'brace/theme/tomorrow'; +import 'brace/ext/language_tools'; + +const EDITOR_SET_OPTIONS = { + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, +}; + +const EDITOR_PROPS = { + $blockScrolling: true, +}; + +interface OsqueryEditorProps { + defaultValue: string; + onChange: (newValue: string) => void; +} + +const OsqueryEditorComponent: React.FC = ({ defaultValue, onChange }) => { + const handleChange = useCallback( + (newValue) => { + onChange(newValue); + }, + [onChange] + ); + + return ( + + ); +}; + +export const OsqueryEditor = React.memo(OsqueryEditorComponent); diff --git a/x-pack/plugins/osquery/public/index.ts b/x-pack/plugins/osquery/public/index.ts new file mode 100644 index 0000000000000..32b0a30c24e7a --- /dev/null +++ b/x-pack/plugins/osquery/public/index.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. + */ + +import { PluginInitializerContext } from 'src/core/public'; +import { OsqueryPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin(initializerContext: PluginInitializerContext) { + return new OsqueryPlugin(initializerContext); +} +export { OsqueryPluginSetup, OsqueryPluginStart } from './types'; diff --git a/x-pack/plugins/osquery/public/live_query/edit/index.tsx b/x-pack/plugins/osquery/public/live_query/edit/index.tsx new file mode 100644 index 0000000000000..5626e78069d01 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/edit/index.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 { isEmpty } from 'lodash/fp'; +import { EuiSpacer } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { useParams } from 'react-router-dom'; + +import { useActionDetails } from '../../actions/use_action_details'; +import { ResultTabs } from './tabs'; +import { LiveQueryForm } from '../form'; + +const EditLiveQueryPageComponent = () => { + const { actionId } = useParams<{ actionId: string }>(); + const [loading, { actionDetails }] = useActionDetails({ actionId }); + + const handleSubmit = useCallback(() => Promise.resolve(), []); + + if (loading) { + return <>{'Loading...'}; + } + + return ( + <> + {!isEmpty(actionDetails) && ( + + )} + + + + ); +}; + +export const EditLiveQueryPage = React.memo(EditLiveQueryPageComponent); diff --git a/x-pack/plugins/osquery/public/live_query/edit/tabs.tsx b/x-pack/plugins/osquery/public/live_query/edit/tabs.tsx new file mode 100644 index 0000000000000..564b91873e11d --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/edit/tabs.tsx @@ -0,0 +1,57 @@ +/* + * 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 { EuiTabbedContent, EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +import { ResultsTable } from '../../results/results_table'; +import { ActionResultsTable } from '../../action_results/action_results_table'; + +const ResultTabsComponent = () => { + const { actionId } = useParams<{ actionId: string }>(); + const tabs = useMemo( + () => [ + { + id: 'status', + name: 'Status', + content: ( + <> + + + + ), + }, + { + id: 'results', + name: 'Results', + content: ( + <> + + + + ), + }, + ], + [actionId] + ); + + const handleTabClick = useCallback((tab) => { + // eslint-disable-next-line no-console + console.log('clicked tab', tab); + }, []); + + return ( + + ); +}; + +export const ResultTabs = React.memo(ResultTabsComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx new file mode 100644 index 0000000000000..a6d7fbd404321 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx @@ -0,0 +1,29 @@ +/* + * 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 { FieldHook } from '../../shared_imports'; +import { AgentsTable } from '../../agents/agents_table'; + +interface AgentsTableFieldProps { + field: FieldHook; +} + +const AgentsTableFieldComponent: React.FC = ({ field }) => { + const { value, setValue } = field; + const handleChange = useCallback( + (props) => { + if (props !== value) { + return setValue(props); + } + }, + [value, setValue] + ); + + return ; +}; + +export const AgentsTableField = React.memo(AgentsTableFieldComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/code_editor_field.tsx b/x-pack/plugins/osquery/public/live_query/form/code_editor_field.tsx new file mode 100644 index 0000000000000..458d2263fe9c2 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/form/code_editor_field.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; + +import { OsqueryEditor } from '../../editor'; +import { FieldHook } from '../../shared_imports'; + +interface CodeEditorFieldProps { + field: FieldHook<{ query: string }>; +} + +const CodeEditorFieldComponent: React.FC = ({ field }) => { + const { value, setValue } = field; + const handleChange = useCallback( + (newQuery) => { + setValue({ + ...value, + query: newQuery, + }); + }, + [value, setValue] + ); + + return ; +}; + +export const CodeEditorField = React.memo(CodeEditorFieldComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/index.tsx b/x-pack/plugins/osquery/public/live_query/form/index.tsx new file mode 100644 index 0000000000000..23aa94b46a569 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/form/index.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiSpacer } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { UseField, Form, useForm } from '../../shared_imports'; +import { AgentsTableField } from './agents_table_field'; +import { CodeEditorField } from './code_editor_field'; + +const FORM_ID = 'liveQueryForm'; + +interface LiveQueryFormProps { + actionDetails?: Record; + onSubmit: (payload: Record) => Promise; +} + +const LiveQueryFormComponent: React.FC = ({ actionDetails, onSubmit }) => { + const handleSubmit = useCallback( + (payload) => { + onSubmit(payload); + return Promise.resolve(); + }, + [onSubmit] + ); + + const { form } = useForm({ + id: FORM_ID, + // schema: formSchema, + onSubmit: handleSubmit, + options: { + stripEmptyFields: false, + }, + defaultValue: actionDetails, + deserializer: ({ fields, _source }) => ({ + agents: fields?.agents, + command: _source?.data?.commands[0], + }), + }); + + const { submit } = form; + + return ( +
+ + + + Send query + + ); +}; + +export const LiveQueryForm = React.memo(LiveQueryFormComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/schema.ts b/x-pack/plugins/osquery/public/live_query/form/schema.ts new file mode 100644 index 0000000000000..e55b3d6cd6a5b --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/form/schema.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FIELD_TYPES, FormSchema } from '../../shared_imports'; + +export const formSchema: FormSchema = { + agents: { + type: FIELD_TYPES.MULTI_SELECT, + }, + command: { + type: FIELD_TYPES.TEXTAREA, + validations: [], + }, +}; diff --git a/x-pack/plugins/osquery/public/live_query/index.tsx b/x-pack/plugins/osquery/public/live_query/index.tsx new file mode 100644 index 0000000000000..646d2637a4c40 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/index.tsx @@ -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 React from 'react'; +import { Switch, Route, useRouteMatch } from 'react-router-dom'; + +import { QueriesPage } from './queries'; +import { NewLiveQueryPage } from './new'; +import { EditLiveQueryPage } from './edit'; + +const LiveQueryComponent = () => { + const match = useRouteMatch(); + + return ( + + + + + + + + + + + + ); +}; + +export const LiveQuery = React.memo(LiveQueryComponent); diff --git a/x-pack/plugins/osquery/public/live_query/new/index.tsx b/x-pack/plugins/osquery/public/live_query/new/index.tsx new file mode 100644 index 0000000000000..40f934b4690f9 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/new/index.tsx @@ -0,0 +1,29 @@ +/* + * 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 { useHistory } from 'react-router-dom'; + +import { useKibana } from '../../common/lib/kibana'; +import { LiveQueryForm } from '../form'; + +const NewLiveQueryPageComponent = () => { + const { http } = useKibana().services; + const history = useHistory(); + + const handleSubmit = useCallback( + async (props) => { + const response = await http.post('/api/osquery/queries', { body: JSON.stringify(props) }); + const requestParamsActionId = JSON.parse(response.meta.request.params.body).action_id; + history.push(`/live_query/queries/${requestParamsActionId}`); + }, + [history, http] + ); + + return ; +}; + +export const NewLiveQueryPage = React.memo(NewLiveQueryPageComponent); diff --git a/x-pack/plugins/osquery/public/live_query/queries/index.tsx b/x-pack/plugins/osquery/public/live_query/queries/index.tsx new file mode 100644 index 0000000000000..5600284b8c147 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/queries/index.tsx @@ -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 { EuiSpacer, EuiTitle } from '@elastic/eui'; +import React from 'react'; + +import { ActionsTable } from '../../actions/actions_table'; + +const QueriesPageComponent = () => { + return ( + <> + +

{'Queries'}

+
+ + + + ); +}; + +export const QueriesPage = React.memo(QueriesPageComponent); diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts new file mode 100644 index 0000000000000..41698d3a1740d --- /dev/null +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + AppMountParameters, + CoreSetup, + Plugin, + PluginInitializerContext, + CoreStart, +} from 'src/core/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { OsqueryPluginSetup, OsqueryPluginStart, AppPluginStartDependencies } from './types'; +import { PLUGIN_NAME } from '../common'; + +export class OsqueryPlugin implements Plugin { + private kibanaVersion: string; + private storage = new Storage(localStorage); + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.kibanaVersion = this.initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup): OsqueryPluginSetup { + const config = this.initializerContext.config.get<{ enabled: boolean }>(); + + if (!config.enabled) { + return {}; + } + + const storage = this.storage; + const kibanaVersion = this.kibanaVersion; + // Register an application into the side navigation menu + core.application.register({ + id: 'osquery', + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + // Load application bundle + const { renderApp } = await import('./application'); + // Render the application + return renderApp( + coreStart, + depsStart as AppPluginStartDependencies, + params, + storage, + kibanaVersion + ); + }, + }); + + // Return methods that should be available to other plugins + return {}; + } + + public start(core: CoreStart): OsqueryPluginStart { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/osquery/public/results/helpers.ts b/x-pack/plugins/osquery/public/results/helpers.ts new file mode 100644 index 0000000000000..9f908e16c2eb2 --- /dev/null +++ b/x-pack/plugins/osquery/public/results/helpers.ts @@ -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 { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number, + isBucketSort?: boolean +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: isBucketSort ? limit : limit + cursorStart, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx new file mode 100644 index 0000000000000..69b350e461a5c --- /dev/null +++ b/x-pack/plugins/osquery/public/results/results_table.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 { isEmpty, isEqual, keys, map } from 'lodash/fp'; +import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn } from '@elastic/eui'; +import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; + +import { EuiDataGridSorting } from '@elastic/eui'; +import { useAllResults } from './use_all_results'; +import { Direction, ResultEdges } from '../../common/search_strategy'; + +const DataContext = createContext([]); + +interface ResultsTableComponentProps { + actionId: string; +} + +const ResultsTableComponent: React.FC = ({ actionId }) => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((currentPagination) => ({ + ...currentPagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => setPagination((currentPagination) => ({ ...currentPagination, pageIndex })), + [setPagination] + ); + + const [columns, setColumns] = useState([]); + + // ** Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (newSortingColumns) => { + setSortingColumns(newSortingColumns); + }, + [setSortingColumns] + ); + + const [, { results, totalCount }] = useAllResults({ + actionId, + activePage: pagination.pageIndex, + limit: pagination.pageSize, + direction: Direction.asc, + sortField: '@timestamp', + }); + + const [visibleColumns, setVisibleColumns] = useState([]); + const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [ + visibleColumns, + setVisibleColumns, + ]); + + const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( + () => ({ rowIndex, columnId, setCellProps }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const data = useContext(DataContext); + + const value = data[rowIndex].fields[columnId]; + + return !isEmpty(value) ? value : '-'; + }, + [] + ); + + const tableSorting = useMemo(() => ({ columns: sortingColumns, onSort }), [ + onSort, + sortingColumns, + ]); + + const tablePagination = useMemo( + () => ({ + ...pagination, + pageSizeOptions: [10, 50, 100], + onChangeItemsPerPage, + onChangePage, + }), + [onChangeItemsPerPage, onChangePage, pagination] + ); + + useEffect(() => { + const newColumns: EuiDataGridColumn[] = keys(results[0]?.fields) + .sort() + .map((fieldName) => ({ + id: fieldName, + displayAsText: fieldName.split('.')[1], + defaultSortDirection: 'asc', + })); + + if (!isEqual(columns, newColumns)) { + setColumns(newColumns); + setVisibleColumns(map('id', newColumns)); + } + }, [columns, results]); + + return ( + + + + ); +}; + +export const ResultsTable = React.memo(ResultsTableComponent); diff --git a/x-pack/plugins/osquery/public/results/translations.ts b/x-pack/plugins/osquery/public/results/translations.ts new file mode 100644 index 0000000000000..54c8ecebc60c0 --- /dev/null +++ b/x-pack/plugins/osquery/public/results/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_RESULTS = i18n.translate('xpack.osquery.results.errorSearchDescription', { + defaultMessage: `An error has occurred on all results search`, +}); + +export const FAIL_ALL_RESULTS = i18n.translate('xpack.osquery.results.failSearchDescription', { + defaultMessage: `Failed to fetch results`, +}); diff --git a/x-pack/plugins/osquery/public/results/use_all_results.ts b/x-pack/plugins/osquery/public/results/use_all_results.ts new file mode 100644 index 0000000000000..2fc5f9ae869a7 --- /dev/null +++ b/x-pack/plugins/osquery/public/results/use_all_results.ts @@ -0,0 +1,164 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + ResultEdges, + PageInfoPaginated, + DocValueFields, + OsqueryQueries, + ResultsRequestOptions, + ResultsStrategyResponse, + Direction, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'resultsAllQuery'; + +export interface ResultsArgs { + results: ResultEdges; + id: string; + inspect: InspectResponse; + isInspected: boolean; + pageInfo: PageInfoPaginated; + totalCount: number; +} + +interface UseAllResults { + actionId: string; + activePage: number; + direction: Direction; + limit: number; + sortField: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useAllResults = ({ + actionId, + activePage, + direction, + limit, + sortField, + docValueFields, + filterQuery, + skip = false, +}: UseAllResults): [boolean, ResultsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [resultsRequest, setHostRequest] = useState(null); + + const [resultsResponse, setResultsResponse] = useState({ + results: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + totalCount: -1, + }); + + const resultsSearch = useCallback( + (request: ResultsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setResultsResponse((prevResponse) => ({ + ...prevResponse, + results: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_RESULTS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_RESULTS, text: msg.message }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + actionId, + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.results, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [actionId, activePage, direction, docValueFields, filterQuery, limit, sortField]); + + useEffect(() => { + resultsSearch(resultsRequest); + }, [resultsRequest, resultsSearch]); + + return [loading, resultsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/shared_imports.ts b/x-pack/plugins/osquery/public/shared_imports.ts new file mode 100644 index 0000000000000..5f7503a00702c --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_imports.ts @@ -0,0 +1,29 @@ +/* + * 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 { + getUseField, + getFieldValidityAndErrorMessage, + FieldHook, + FieldValidateResponse, + FIELD_TYPES, + Form, + FormData, + FormDataProvider, + FormHook, + FormSchema, + UseField, + UseMultiFields, + useForm, + useFormContext, + useFormData, + ValidationError, + ValidationFunc, + VALIDATION_TYPES, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field, SelectField } from '../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts new file mode 100644 index 0000000000000..faaccfc29d5f1 --- /dev/null +++ b/x-pack/plugins/osquery/public/types.ts @@ -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 { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { FleetStart } from '../../fleet/public'; +import { CoreStart } from '../../../../src/core/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OsqueryPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OsqueryPluginStart {} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart; +} + +export interface StartPlugins { + data: DataPublicPluginStart; + fleet?: FleetStart; +} + +export type StartServices = CoreStart & StartPlugins; diff --git a/x-pack/plugins/osquery/server/config.ts b/x-pack/plugins/osquery/server/config.ts new file mode 100644 index 0000000000000..633a95b8f91a7 --- /dev/null +++ b/x-pack/plugins/osquery/server/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf, schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/osquery/server/create_config.ts b/x-pack/plugins/osquery/server/create_config.ts new file mode 100644 index 0000000000000..e46c71798eb9f --- /dev/null +++ b/x-pack/plugins/osquery/server/create_config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { map } from 'rxjs/operators'; +import { PluginInitializerContext } from 'kibana/server'; +import { Observable } from 'rxjs'; + +import { ConfigType } from './config'; + +export const createConfig$ = ( + context: PluginInitializerContext +): Observable> => { + return context.config.create().pipe(map((config) => config)); +}; diff --git a/x-pack/plugins/osquery/server/index.ts b/x-pack/plugins/osquery/server/index.ts new file mode 100644 index 0000000000000..c74ef6c95a2e7 --- /dev/null +++ b/x-pack/plugins/osquery/server/index.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 { PluginInitializerContext } from '../../../../src/core/server'; +import { OsqueryPlugin } from './plugin'; +import { ConfigSchema } from './config'; + +export const config = { + schema: ConfigSchema, + exposeToBrowser: { + enabled: true, + }, +}; +export function plugin(initializerContext: PluginInitializerContext) { + return new OsqueryPlugin(initializerContext); +} + +export { OsqueryPluginSetup, OsqueryPluginStart } from './types'; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts new file mode 100644 index 0000000000000..3e59faa55d057 --- /dev/null +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first } from 'rxjs/operators'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../../src/core/server'; + +import { createConfig$ } from './create_config'; +import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; +import { defineRoutes } from './routes'; +import { osquerySearchStrategyProvider } from './search_strategy/osquery'; + +export class OsqueryPlugin implements Plugin { + private readonly logger: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public async setup(core: CoreSetup, plugins: SetupPlugins) { + this.logger.debug('osquery: Setup'); + const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + + if (!config.enabled) { + return {}; + } + + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + core.getStartServices().then(([_, depsStart]) => { + const osquerySearchStrategy = osquerySearchStrategyProvider(depsStart.data); + + plugins.data.search.registerSearchStrategy('osquerySearchStrategy', osquerySearchStrategy); + }); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('osquery: Started'); + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/osquery/server/routes/index.ts b/x-pack/plugins/osquery/server/routes/index.ts new file mode 100644 index 0000000000000..f975865950a4d --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { schema } from '@kbn/config-schema'; +import moment from 'moment'; + +import { IRouter } from '../../../../../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.post( + { + path: '/api/osquery/queries', + validate: { + params: schema.object({}, { unknowns: 'allow' }), + body: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + const query = await esClient.index<{}, {}>({ + index: '.fleet-actions-new', + body: { + action_id: uuid.v4(), + '@timestamp': moment().toISOString(), + expiration: moment().add(2, 'days').toISOString(), + type: 'APP_ACTION', + input_id: 'osquery', + // @ts-expect-error + agents: request.body.agents, + data: { + commands: [ + { + id: uuid.v4(), + // @ts-expect-error + query: request.body.command.query, + }, + ], + }, + }, + }); + return response.ok({ + body: query, + }); + } + ); +} diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts new file mode 100644 index 0000000000000..75cdb67deed4d --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts @@ -0,0 +1,46 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + ActionsStrategyResponse, + ActionsRequestOptions, + OsqueryQueries, +} from '../../../../../../common/search_strategy/osquery'; +import { inspectStringifyObject } from '../../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../../types'; +import { buildActionsQuery } from './query.all_actions.dsl'; + +export const allActions: OsqueryFactory = { + buildDsl: (options: ActionsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildActionsQuery(options); + }, + parse: async ( + options: ActionsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage } = options.pagination; + const inspect = { + dsl: [inspectStringifyObject(buildActionsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits, + totalCount: response.rawResponse.hits.total, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts new file mode 100644 index 0000000000000..29af1df3a9e0c --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { AgentsRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../../common/utils/build_query'; + +export const buildActionsQuery = ({ + docValueFields, + filterQuery, + pagination: { activePage, querySize }, + sort, +}: AgentsRequestOptions): ISearchRequestParams => { + const filter = [...createQueryFilterClauses(filterQuery)]; + + const dslQuery = { + allowNoIndices: true, + index: '.fleet-actions', + ignoreUnavailable: true, + body: { + query: { bool: { filter } }, + from: activePage * querySize, + size: querySize, + track_total_hits: true, + fields: ['*'], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/index.ts new file mode 100644 index 0000000000000..09e317786e20f --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/index.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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + ActionDetailsStrategyResponse, + ActionDetailsRequestOptions, + OsqueryQueries, +} from '../../../../../../common/search_strategy/osquery'; + +import { inspectStringifyObject } from '../../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../../types'; +import { buildActionDetailsQuery } from './query.action_details.dsl'; + +export const actionDetails: OsqueryFactory = { + buildDsl: (options: ActionDetailsRequestOptions) => { + return buildActionDetailsQuery(options); + }, + parse: async ( + options: ActionDetailsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildActionDetailsQuery(options))], + }; + + return { + ...response, + inspect, + actionDetails: response.rawResponse.hits.hits[0], + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/query.action_details.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/query.action_details.dsl.ts new file mode 100644 index 0000000000000..f22066134cbca --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/query.action_details.dsl.ts @@ -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 { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { ActionDetailsRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../../common/utils/build_query'; + +export const buildActionDetailsQuery = ({ + actionId, + docValueFields, + filterQuery, +}: ActionDetailsRequestOptions): ISearchRequestParams => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + match_phrase: { + action_id: actionId, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: '.fleet-actions', + ignoreUnavailable: true, + body: { + query: { bool: { filter } }, + size: 1, + fields: ['*'], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/index.ts new file mode 100644 index 0000000000000..c5a6342c180ed --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './all'; +export * from './details'; +export * from './results'; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts new file mode 100644 index 0000000000000..4a049ca670cc6 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + ActionResultsStrategyResponse, + ActionResultsRequestOptions, + OsqueryQueries, +} from '../../../../../../common/search_strategy/osquery'; + +import { inspectStringifyObject } from '../../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../../types'; +import { buildActionResultsQuery } from './query.action_results.dsl'; + +export const actionResults: OsqueryFactory = { + buildDsl: (options: ActionResultsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildActionResultsQuery(options); + }, + parse: async ( + options: ActionResultsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage } = options.pagination; + const inspect = { + dsl: [inspectStringifyObject(buildActionResultsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits, + totalCount: response.rawResponse.hits.total, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/query.action_results.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/query.action_results.dsl.ts new file mode 100644 index 0000000000000..8b80427d4d0e0 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/query.action_results.dsl.ts @@ -0,0 +1,41 @@ +/* + * 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 { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { ActionResultsRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../../common/utils/build_query'; + +export const buildActionResultsQuery = ({ + actionId, + docValueFields, + filterQuery, + pagination: { activePage, querySize }, + sort, +}: ActionResultsRequestOptions): ISearchRequestParams => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + match_phrase: { + action_id: actionId, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: '.fleet-actions-results', + ignoreUnavailable: true, + body: { + query: { bool: { filter } }, + from: activePage * querySize, + size: querySize, + track_total_hits: true, + fields: ['*'], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts new file mode 100644 index 0000000000000..615343c738d78 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts @@ -0,0 +1,48 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { + AgentsStrategyResponse, + AgentsRequestOptions, + OsqueryQueries, +} from '../../../../../common/search_strategy/osquery'; + +import { Agent } from '../../../../../common/shared_imports'; +import { inspectStringifyObject } from '../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../types'; +import { buildAgentsQuery } from './query.all_agents.dsl'; + +export const allAgents: OsqueryFactory = { + buildDsl: (options: AgentsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildAgentsQuery(options); + }, + parse: async ( + options: AgentsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage } = options.pagination; + const inspect = { + dsl: [inspectStringifyObject(buildAgentsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits.map((hit) => ({ _id: hit._id, ...hit._source })), + totalCount: response.rawResponse.hits.total, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts new file mode 100644 index 0000000000000..935a6cd7b215e --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.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 { isEmpty } from 'lodash/fp'; +import { ISearchRequestParams } from '../../../../../../../../src/plugins/data/common'; +import { AgentsRequestOptions } from '../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; + +export const buildAgentsQuery = ({ + docValueFields, + filterQuery, + pagination: { querySize }, + sort, +}: AgentsRequestOptions): ISearchRequestParams => { + const filter = [...createQueryFilterClauses(filterQuery)]; + + const dslQuery = { + allowNoIndices: true, + index: '.fleet-agents', + ignoreUnavailable: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + query: { bool: { filter } }, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/index.ts new file mode 100644 index 0000000000000..dc2e741dbe3e4 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/index.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 { FactoryQueryTypes, OsqueryQueries } from '../../../../common/search_strategy/osquery'; + +import { allActions, actionDetails, actionResults } from './actions'; +import { allAgents } from './agents'; +import { allResults } from './results'; + +import { OsqueryFactory } from './types'; + +export const osqueryFactory: Record> = { + [OsqueryQueries.actions]: allActions, + [OsqueryQueries.actionDetails]: actionDetails, + [OsqueryQueries.actionResults]: actionResults, + [OsqueryQueries.agents]: allAgents, + [OsqueryQueries.results]: allResults, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts new file mode 100644 index 0000000000000..1460a0e5d331e --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts @@ -0,0 +1,46 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { + ResultsStrategyResponse, + ResultsRequestOptions, + OsqueryQueries, +} from '../../../../../common/search_strategy/osquery'; +import { inspectStringifyObject } from '../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../types'; +import { buildResultsQuery } from './query.all_results.dsl'; + +export const allResults: OsqueryFactory = { + buildDsl: (options: ResultsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildResultsQuery(options); + }, + parse: async ( + options: ResultsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage } = options.pagination; + const inspect = { + dsl: [inspectStringifyObject(buildResultsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits, + totalCount: response.rawResponse.hits.total, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts new file mode 100644 index 0000000000000..c099e2762d741 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts @@ -0,0 +1,40 @@ +/* + * 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 { ISearchRequestParams } from '../../../../../../../../src/plugins/data/common'; +import { ResultsRequestOptions } from '../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; + +export const buildResultsQuery = ({ + actionId, + filterQuery, + pagination: { activePage, querySize }, + sort, +}: ResultsRequestOptions): ISearchRequestParams => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + match_phrase: { + action_id: actionId, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: 'logs-elastic_agent.osquery*', + ignoreUnavailable: true, + body: { + query: { bool: { filter } }, + from: activePage * querySize, + size: querySize, + track_total_hits: true, + fields: ['agent.*', 'osquery.*'], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/types.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/types.ts new file mode 100644 index 0000000000000..bc2bf63958a09 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/types.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 { + IEsSearchResponse, + ISearchRequestParams, +} from '../../../../../../../src/plugins/data/common'; +import { + FactoryQueryTypes, + StrategyRequestType, + StrategyResponseType, +} from '../../../../common/search_strategy/osquery'; + +export interface OsqueryFactory { + buildDsl: (options: StrategyRequestType) => ISearchRequestParams; + parse: ( + options: StrategyRequestType, + response: IEsSearchResponse + ) => Promise>; +} diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts new file mode 100644 index 0000000000000..8d8a255f2fcdd --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/index.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. + */ + +import { map, mergeMap } from 'rxjs/operators'; +import { + ISearchStrategy, + PluginStart, + shimHitsTotal, +} from '../../../../../../src/plugins/data/server'; +import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../data_enhanced/common'; +import { + FactoryQueryTypes, + StrategyResponseType, + StrategyRequestType, +} from '../../../common/search_strategy/osquery'; +import { osqueryFactory } from './factory'; +import { OsqueryFactory } from './factory/types'; + +export const osquerySearchStrategyProvider = ( + data: PluginStart +): ISearchStrategy, StrategyResponseType> => { + const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + + return { + search: (request, options, deps) => { + if (request.factoryQueryType == null) { + throw new Error('factoryQueryType is required'); + } + const queryFactory: OsqueryFactory = osqueryFactory[request.factoryQueryType]; + const dsl = queryFactory.buildDsl(request); + return es.search({ ...request, params: dsl }, options, deps).pipe( + map((response) => { + return { + ...response, + ...{ + rawResponse: shimHitsTotal(response.rawResponse), + }, + }; + }), + mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) + ); + }, + cancel: async (id, options, deps) => { + if (es.cancel) { + return es.cancel(id, options, deps); + } + }, + }; +}; diff --git a/x-pack/plugins/osquery/server/types.ts b/x-pack/plugins/osquery/server/types.ts new file mode 100644 index 0000000000000..51ef28b4c3478 --- /dev/null +++ b/x-pack/plugins/osquery/server/types.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 { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; +import { FleetStartContract } from '../../fleet/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OsqueryPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OsqueryPluginStart {} + +export interface SetupPlugins { + data: DataPluginSetup; +} + +export interface StartPlugins { + data: DataPluginStart; + fleet?: FleetStartContract; +} From 9c410b81ca19058aba6a0a7aed325562bbef060f Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 28 Jan 2021 11:10:15 +0300 Subject: [PATCH 069/163] [TSVB] Remove vis_type_timeseries_enhanced plugin (#89274) * [TSVB] get rid of vis_type_timeseries_enhanced * add search strategy should be called from setup hook * remove vis_type_timeseries_enhanced from CODEOWNERS Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 - docs/developer/plugin-list.asciidoc | 4 --- .../vis_type_timeseries/server/index.ts | 9 ----- .../default_search_capabilities.test.ts | 4 +-- .../default_search_capabilities.ts | 8 ++--- .../rollup_search_capabilities.test.ts | 10 ++++-- .../rollup_search_capabilities.ts | 18 +++++----- .../server/lib/search_strategies/index.ts | 8 +++++ .../lib/interval_helper.test.ts | 7 ++-- .../search_strategies/lib/interval_helper.ts | 6 ++-- .../search_strategies_registry.test.ts | 23 ++++++------- .../search_strategy_registry.ts | 14 ++------ ...st.js => abstract_search_strategy.test.ts} | 34 +++++++++++-------- .../strategies/abstract_search_strategy.ts | 5 +-- .../default_search_strategy.test.ts | 5 ++- .../strategies/default_search_strategy.ts | 4 +-- .../lib/search_strategies/strategies/index.ts | 11 ++++++ .../rollup_search_strategy.test.ts | 17 ++++++---- .../strategies}/rollup_search_strategy.ts | 19 +++++------ .../lib/vis_data/helpers/fields_fetcher.ts | 6 +++- .../vis_data/helpers/get_timerange.test.ts | 3 +- .../lib/vis_data/helpers/get_timerange.ts | 3 +- .../series/date_histogram.test.js | 2 +- .../vis_type_timeseries/server/plugin.ts | 12 +++++-- .../vis_type_timeseries_enhanced/README.md | 10 ------ .../jest.config.js | 11 ------ .../vis_type_timeseries_enhanced/kibana.json | 10 ------ .../server/index.ts | 11 ------ .../server/plugin.ts | 33 ------------------ .../tsconfig.json | 15 -------- x-pack/tsconfig.json | 2 -- x-pack/tsconfig.refs.json | 1 - 32 files changed, 127 insertions(+), 199 deletions(-) rename src/plugins/vis_type_timeseries/server/lib/search_strategies/{ => capabilities}/default_search_capabilities.test.ts (95%) rename src/plugins/vis_type_timeseries/server/lib/search_strategies/{ => capabilities}/default_search_capabilities.ts (89%) rename {x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies => src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities}/rollup_search_capabilities.test.ts (92%) rename {x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies => src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities}/rollup_search_capabilities.ts (86%) rename {x-pack/plugins/vis_type_timeseries_enhanced/server => src/plugins/vis_type_timeseries/server/lib}/search_strategies/lib/interval_helper.test.ts (94%) rename {x-pack/plugins/vis_type_timeseries_enhanced/server => src/plugins/vis_type_timeseries/server/lib}/search_strategies/lib/interval_helper.ts (74%) rename src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/{abstract_search_strategy.test.js => abstract_search_strategy.test.ts} (72%) create mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/index.ts rename {x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies => src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies}/rollup_search_strategy.test.ts (90%) rename {x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies => src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies}/rollup_search_strategy.ts (81%) delete mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/README.md delete mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js delete mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/kibana.json delete mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/server/index.ts delete mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/server/plugin.ts delete mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 01bbd59ba090f..dea2c12756b08 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,7 +9,6 @@ /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app -/x-pack/plugins/vis_type_timeseries_enhanced/ @elastic/kibana-app /src/plugins/advanced_settings/ @elastic/kibana-app /src/plugins/charts/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index fd7ca58d88994..c4be7a7367c16 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -559,10 +559,6 @@ in their infrastructure. |NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to ui_actions_enhanced plugin. -|{kib-repo}blob/{branch}/x-pack/plugins/vis_type_timeseries_enhanced/README.md[visTypeTimeseriesEnhanced] -|The vis_type_timeseries_enhanced plugin is the x-pack counterpart to the OSS vis_type_timeseries plugin. - - |{kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] |This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts index 5339266a47448..415133f711061 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -26,15 +26,6 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export { - AbstractSearchStrategy, - ReqFacade, -} from './lib/search_strategies/strategies/abstract_search_strategy'; - -export { VisPayload } from '../common/types'; - -export { DefaultSearchCapabilities } from './lib/search_strategies/default_search_capabilities'; - export function plugin(initializerContext: PluginInitializerContext) { return new VisTypeTimeseriesPlugin(initializerContext); } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts index d4e3064747ab0..105bfd53ce739 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts @@ -7,8 +7,8 @@ */ import { DefaultSearchCapabilities } from './default_search_capabilities'; -import { ReqFacade } from './strategies/abstract_search_strategy'; -import { VisPayload } from '../../../common/types'; +import type { ReqFacade } from '../strategies/abstract_search_strategy'; +import type { VisPayload } from '../../../../common/types'; describe('DefaultSearchCapabilities', () => { let defaultSearchCapabilities: DefaultSearchCapabilities; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts similarity index 89% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts index 1755e25138e8f..996efce4ce66e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts @@ -11,10 +11,10 @@ import { convertIntervalToUnit, parseInterval, getSuitableUnit, -} from '../vis_data/helpers/unit_to_seconds'; -import { RESTRICTIONS_KEYS } from '../../../common/ui_restrictions'; -import { ReqFacade } from './strategies/abstract_search_strategy'; -import { VisPayload } from '../../../common/types'; +} from '../../vis_data/helpers/unit_to_seconds'; +import { RESTRICTIONS_KEYS } from '../../../../common/ui_restrictions'; +import type { ReqFacade } from '../strategies/abstract_search_strategy'; +import type { VisPayload } from '../../../../common/types'; const getTimezoneFromRequest = (request: ReqFacade) => { return request.payload.timerange.timezone; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts similarity index 92% rename from x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.test.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts index 6c30895635fe5..443b700386c15 100644 --- a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts @@ -1,12 +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. + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. */ + import { Unit } from '@elastic/datemath'; import { RollupSearchCapabilities } from './rollup_search_capabilities'; -import { ReqFacade, VisPayload } from '../../../../../src/plugins/vis_type_timeseries/server'; +import type { VisPayload } from '../../../../common/types'; +import type { ReqFacade } from '../strategies/abstract_search_strategy'; describe('Rollup Search Capabilities', () => { const testTimeZone = 'time_zone'; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts similarity index 86% rename from x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts index 015a371bd2a35..787a8ff1b2051 100644 --- a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts @@ -1,16 +1,18 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. */ + import { get, has } from 'lodash'; -import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper'; +import { leastCommonInterval, isCalendarInterval } from '../lib/interval_helper'; + +import { DefaultSearchCapabilities } from './default_search_capabilities'; -import { - ReqFacade, - DefaultSearchCapabilities, - VisPayload, -} from '../../../../../src/plugins/vis_type_timeseries/server'; +import type { VisPayload } from '../../../../common/types'; +import type { ReqFacade } from '../strategies/abstract_search_strategy'; export class RollupSearchCapabilities extends DefaultSearchCapabilities { rollupIndex: string; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/index.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/index.ts index 7dd7bfe780b52..2df6f002481b5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/index.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/index.ts @@ -7,3 +7,11 @@ */ export { SearchStrategyRegistry } from './search_strategy_registry'; +export { DefaultSearchCapabilities } from './capabilities/default_search_capabilities'; + +export { + AbstractSearchStrategy, + ReqFacade, + RollupSearchStrategy, + DefaultSearchStrategy, +} from './strategies'; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.test.ts similarity index 94% rename from x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.test.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.test.ts index 31baeadce6527..158c1d74964b3 100644 --- a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.test.ts @@ -1,8 +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. + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. */ + import { isCalendarInterval, leastCommonInterval } from './interval_helper'; describe('interval_helper', () => { diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.ts similarity index 74% rename from x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.ts index 91d73cecdf401..f4ac715b5b0f2 100644 --- a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.ts @@ -1,7 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. */ import dateMath from '@elastic/datemath'; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts index 81bf8920c54fc..21b746656c043 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts @@ -5,14 +5,13 @@ * compliance with, at your election, the Elastic License or the Server Side * Public License, v 1. */ +import { get } from 'lodash'; +import { RequestFacade, SearchStrategyRegistry } from './search_strategy_registry'; +import { AbstractSearchStrategy, DefaultSearchStrategy } from './strategies'; +import { DefaultSearchCapabilities } from './capabilities/default_search_capabilities'; -import { SearchStrategyRegistry } from './search_strategy_registry'; -// @ts-ignore -import { AbstractSearchStrategy } from './strategies/abstract_search_strategy'; -// @ts-ignore -import { DefaultSearchStrategy } from './strategies/default_search_strategy'; -// @ts-ignore -import { DefaultSearchCapabilities } from './default_search_capabilities'; +const getPrivateField = (registry: SearchStrategyRegistry, field: string) => + get(registry, field) as T; class MockSearchStrategy extends AbstractSearchStrategy { checkForViability() { @@ -28,23 +27,21 @@ describe('SearchStrategyRegister', () => { beforeAll(() => { registry = new SearchStrategyRegistry(); + registry.addStrategy(new DefaultSearchStrategy()); }); test('should init strategies register', () => { - expect( - registry.addStrategy({} as AbstractSearchStrategy)[0] instanceof DefaultSearchStrategy - ).toBe(true); + expect(getPrivateField(registry, 'strategies')).toHaveLength(1); }); test('should not add a strategy if it is not an instance of AbstractSearchStrategy', () => { const addedStrategies = registry.addStrategy({} as AbstractSearchStrategy); expect(addedStrategies.length).toEqual(1); - expect(addedStrategies[0] instanceof DefaultSearchStrategy).toBe(true); }); test('should return a DefaultSearchStrategy instance', async () => { - const req = {}; + const req = {} as RequestFacade; const indexPattern = '*'; const { searchStrategy, capabilities } = (await registry.getViableStrategy(req, indexPattern))!; @@ -62,7 +59,7 @@ describe('SearchStrategyRegister', () => { }); test('should return a MockSearchStrategy instance', async () => { - const req = {}; + const req = {} as RequestFacade; const indexPattern = '*'; const anotherSearchStrategy = new MockSearchStrategy(); registry.addStrategy(anotherSearchStrategy); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts index 9e7272f14f146..f3bf854f00ef4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts @@ -6,23 +6,15 @@ * Public License, v 1. */ -import { AbstractSearchStrategy } from './strategies/abstract_search_strategy'; -// @ts-ignore -import { DefaultSearchStrategy } from './strategies/default_search_strategy'; -// @ts-ignore import { extractIndexPatterns } from '../../../common/extract_index_patterns'; - -export type RequestFacade = any; - import { PanelSchema } from '../../../common/types'; +import { AbstractSearchStrategy, ReqFacade } from './strategies'; + +export type RequestFacade = ReqFacade; export class SearchStrategyRegistry { private strategies: AbstractSearchStrategy[] = []; - constructor() { - this.addStrategy(new DefaultSearchStrategy()); - } - public addStrategy(searchStrategy: AbstractSearchStrategy) { if (searchStrategy instanceof AbstractSearchStrategy) { this.strategies.unshift(searchStrategy); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts similarity index 72% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts index a4fc48ccc6266..97876ec2579f0 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts @@ -7,29 +7,35 @@ */ import { from } from 'rxjs'; -import { AbstractSearchStrategy } from './abstract_search_strategy'; +import { AbstractSearchStrategy, ReqFacade } from './abstract_search_strategy'; +import type { VisPayload } from '../../../../common/types'; +import type { IFieldType } from '../../../../../data/common'; + +class FooSearchStrategy extends AbstractSearchStrategy {} describe('AbstractSearchStrategy', () => { - let abstractSearchStrategy; - let req; - let mockedFields; - let indexPattern; + let abstractSearchStrategy: AbstractSearchStrategy; + let req: ReqFacade; + let mockedFields: IFieldType[]; + let indexPattern: string; beforeEach(() => { mockedFields = []; - req = { + req = ({ payload: {}, pre: { indexPatternsFetcher: { getFieldsForWildcard: jest.fn().mockReturnValue(mockedFields), }, }, - getIndexPatternsService: jest.fn(() => ({ - find: jest.fn(() => []), - })), - }; + getIndexPatternsService: jest.fn(() => + Promise.resolve({ + find: jest.fn(() => []), + }) + ), + } as unknown) as ReqFacade; - abstractSearchStrategy = new AbstractSearchStrategy(); + abstractSearchStrategy = new FooSearchStrategy(); }); test('should init an AbstractSearchStrategy instance', () => { @@ -42,7 +48,7 @@ describe('AbstractSearchStrategy', () => { const fields = await abstractSearchStrategy.getFieldsForWildcard(req, indexPattern); expect(fields).toEqual(mockedFields); - expect(req.pre.indexPatternsFetcher.getFieldsForWildcard).toHaveBeenCalledWith({ + expect(req.pre.indexPatternsFetcher!.getFieldsForWildcard).toHaveBeenCalledWith({ pattern: indexPattern, metaFields: [], fieldCapsOptions: { allow_no_indices: true }, @@ -54,7 +60,7 @@ describe('AbstractSearchStrategy', () => { const searchFn = jest.fn().mockReturnValue(from(Promise.resolve({}))); const responses = await abstractSearchStrategy.search( - { + ({ payload: { searchSession: { sessionId: '1', @@ -65,7 +71,7 @@ describe('AbstractSearchStrategy', () => { requestContext: { search: { search: searchFn }, }, - }, + } as unknown) as ReqFacade, searches ); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 966daca87a208..bf7088145f347 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -8,12 +8,13 @@ import type { FakeRequest, IUiSettingsClient, SavedObjectsClientContract } from 'kibana/server'; +import { indexPatterns } from '../../../../../data/server'; + import type { Framework } from '../../../plugin'; import type { IndexPatternsFetcher, IFieldType } from '../../../../../data/server'; import type { VisPayload } from '../../../../common/types'; import type { IndexPatternsService } from '../../../../../data/common'; -import { indexPatterns } from '../../../../../data/server'; -import { SanitizedFieldType } from '../../../../common/types'; +import type { SanitizedFieldType } from '../../../../common/types'; import type { VisTypeTimeseriesRequestHandlerContext } from '../../../types'; /** diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts index c6f7474ed86bf..00dbf17945011 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts @@ -7,8 +7,8 @@ */ import { DefaultSearchStrategy } from './default_search_strategy'; -import { ReqFacade } from './abstract_search_strategy'; -import { VisPayload } from '../../../../common/types'; +import type { ReqFacade } from './abstract_search_strategy'; +import type { VisPayload } from '../../../../common/types'; describe('DefaultSearchStrategy', () => { let defaultSearchStrategy: DefaultSearchStrategy; @@ -20,7 +20,6 @@ describe('DefaultSearchStrategy', () => { }); test('should init an DefaultSearchStrategy instance', () => { - expect(defaultSearchStrategy.name).toBe('default'); expect(defaultSearchStrategy.checkForViability).toBeDefined(); expect(defaultSearchStrategy.search).toBeDefined(); expect(defaultSearchStrategy.getFieldsForWildcard).toBeDefined(); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts index 803926ad58c50..791ff4efd3936 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts @@ -7,12 +7,10 @@ */ import { AbstractSearchStrategy, ReqFacade } from './abstract_search_strategy'; -import { DefaultSearchCapabilities } from '../default_search_capabilities'; +import { DefaultSearchCapabilities } from '../capabilities/default_search_capabilities'; import { VisPayload } from '../../../../common/types'; export class DefaultSearchStrategy extends AbstractSearchStrategy { - name = 'default'; - checkForViability(req: ReqFacade) { return Promise.resolve({ isViable: true, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/index.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/index.ts new file mode 100644 index 0000000000000..953624e476dc8 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export { AbstractSearchStrategy, ReqFacade } from './abstract_search_strategy'; +export { DefaultSearchStrategy } from './default_search_strategy'; +export { RollupSearchStrategy } from './rollup_search_strategy'; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts similarity index 90% rename from x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.test.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts index e3fbe2daa3756..8e5c2fdabca5d 100644 --- a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts @@ -1,13 +1,17 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. */ + import { RollupSearchStrategy } from './rollup_search_strategy'; -import type { ReqFacade, VisPayload } from '../../../../../src/plugins/vis_type_timeseries/server'; -jest.mock('../../../../../src/plugins/vis_type_timeseries/server', () => { - const actual = jest.requireActual('../../../../../src/plugins/vis_type_timeseries/server'); +import type { VisPayload } from '../../../../common/types'; +import type { ReqFacade } from './abstract_search_strategy'; + +jest.mock('./abstract_search_strategy', () => { class AbstractSearchStrategyMock { getFieldsForWildcard() { return [ @@ -23,7 +27,6 @@ jest.mock('../../../../../src/plugins/vis_type_timeseries/server', () => { } return { - ...actual, AbstractSearchStrategy: AbstractSearchStrategyMock, }; }); @@ -52,7 +55,7 @@ describe('Rollup Search Strategy', () => { test('should create instance of RollupSearchRequest', () => { const rollupSearchStrategy = new RollupSearchStrategy(); - expect(rollupSearchStrategy.name).toBe('rollup'); + expect(rollupSearchStrategy).toBeDefined(); }); describe('checkForViability', () => { diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts similarity index 81% rename from x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.ts rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts index 60fa51d0995db..5b5a1bd5db79e 100644 --- a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts @@ -1,17 +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. + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. */ -import { - AbstractSearchStrategy, - ReqFacade, - VisPayload, -} from '../../../../../src/plugins/vis_type_timeseries/server'; -import { getCapabilitiesForRollupIndices } from '../../../../../src/plugins/data/server'; +import { ReqFacade, AbstractSearchStrategy } from './abstract_search_strategy'; +import { RollupSearchCapabilities } from '../capabilities/rollup_search_capabilities'; +import type { VisPayload } from '../../../../common/types'; -import { RollupSearchCapabilities } from './rollup_search_capabilities'; +import { getCapabilitiesForRollupIndices } from '../../../../../data/server'; const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData); const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*'); @@ -19,8 +18,6 @@ const isIndexPatternValid = (indexPattern: string) => indexPattern && typeof indexPattern === 'string' && !isIndexPatternContainsWildcard(indexPattern); export class RollupSearchStrategy extends AbstractSearchStrategy { - name = 'rollup'; - async search(req: ReqFacade, bodies: any[]) { return super.search(req, bodies, 'rollup'); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts index d94362e681642..d13efcfe37149 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts @@ -6,7 +6,11 @@ * Public License, v 1. */ -import { AbstractSearchStrategy, DefaultSearchCapabilities, ReqFacade } from '../../..'; +import { + AbstractSearchStrategy, + DefaultSearchCapabilities, + ReqFacade, +} from '../../search_strategies'; export const createFieldsFetcher = ( req: ReqFacade, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.ts index d97e948551b1a..a20df9145d987 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.ts @@ -8,7 +8,8 @@ import moment from 'moment'; import { getTimerange } from './get_timerange'; -import { ReqFacade, VisPayload } from '../../..'; +import type { ReqFacade } from '../../search_strategies'; +import type { VisPayload } from '../../../../common/types'; describe('getTimerange(req)', () => { test('should return a moment object for to and from', () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.ts index b690ad0fb0325..2797839988ded 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.ts @@ -7,7 +7,8 @@ */ import { utc } from 'moment'; -import { ReqFacade, VisPayload } from '../../..'; +import type { ReqFacade } from '../../search_strategies'; +import type { VisPayload } from '../../../../common/types'; export const getTimerange = (req: ReqFacade) => { const { min, max } = req.payload.timerange; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index 06ff882190ce5..bcb158ebfe2bb 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { DefaultSearchCapabilities } from '../../../search_strategies/default_search_capabilities'; +import { DefaultSearchCapabilities } from '../../../search_strategies/capabilities/default_search_capabilities'; import { dateHistogram } from './date_histogram'; import { UI_SETTINGS } from '../../../../../../data/common'; diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index adcd7e8bbf0d5..43b61f37ba3d3 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -23,10 +23,15 @@ import { PluginStart } from '../../data/server'; import { visDataRoutes } from './routes/vis'; // @ts-ignore import { fieldsRoutes } from './routes/fields'; -import { SearchStrategyRegistry } from './lib/search_strategies'; import { uiSettings } from './ui_settings'; import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRouter } from './types'; +import { + SearchStrategyRegistry, + DefaultSearchStrategy, + RollupSearchStrategy, +} from './lib/search_strategies'; + export interface LegacySetup { server: Server; } @@ -45,7 +50,6 @@ export interface VisTypeTimeseriesSetup { fakeRequest: FakeRequest, options: GetVisDataOptions ) => ReturnType; - addSearchStrategy: SearchStrategyRegistry['addStrategy']; } export interface Framework { @@ -76,6 +80,9 @@ export class VisTypeTimeseriesPlugin implements Plugin { const searchStrategyRegistry = new SearchStrategyRegistry(); + searchStrategyRegistry.addStrategy(new DefaultSearchStrategy()); + searchStrategyRegistry.addStrategy(new RollupSearchStrategy()); + const framework: Framework = { core, plugins, @@ -97,7 +104,6 @@ export class VisTypeTimeseriesPlugin implements Plugin { ) => { return await getVisData(requestContext, { ...fakeRequest, body: options }, framework); }, - addSearchStrategy: searchStrategyRegistry.addStrategy.bind(searchStrategyRegistry), }; } diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/README.md b/x-pack/plugins/vis_type_timeseries_enhanced/README.md deleted file mode 100644 index 33aa16d8574ae..0000000000000 --- a/x-pack/plugins/vis_type_timeseries_enhanced/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# vis_type_timeseries_enhanced - -The `vis_type_timeseries_enhanced` plugin is the x-pack counterpart to the OSS `vis_type_timeseries` plugin. - -It exists to provide Elastic-licensed services, or parts of services, which -enhance existing OSS functionality from `vis_type_timeseries`. - -Currently the `vis_type_timeseries_enhanced` plugin doesn't return any APIs which you can -consume directly. - diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js b/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js deleted file mode 100644 index 17c5c87e3ccc2..0000000000000 --- a/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js +++ /dev/null @@ -1,11 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/plugins/vis_type_timeseries_enhanced'], -}; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/kibana.json b/x-pack/plugins/vis_type_timeseries_enhanced/kibana.json deleted file mode 100644 index 4b296856c3f97..0000000000000 --- a/x-pack/plugins/vis_type_timeseries_enhanced/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "visTypeTimeseriesEnhanced", - "version": "8.0.0", - "kibanaVersion": "kibana", - "server": true, - "ui": false, - "requiredPlugins": [ - "visTypeTimeseries" - ] -} diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/index.ts b/x-pack/plugins/vis_type_timeseries_enhanced/server/index.ts deleted file mode 100644 index d2665ec1e2813..0000000000000 --- a/x-pack/plugins/vis_type_timeseries_enhanced/server/index.ts +++ /dev/null @@ -1,11 +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 { PluginInitializerContext } from 'src/core/server'; -import { VisTypeTimeseriesEnhanced } from './plugin'; - -export const plugin = (initializerContext: PluginInitializerContext) => - new VisTypeTimeseriesEnhanced(initializerContext); diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/plugin.ts b/x-pack/plugins/vis_type_timeseries_enhanced/server/plugin.ts deleted file mode 100644 index 0598a691ab7c5..0000000000000 --- a/x-pack/plugins/vis_type_timeseries_enhanced/server/plugin.ts +++ /dev/null @@ -1,33 +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 { Plugin, PluginInitializerContext, Logger, CoreSetup } from 'src/core/server'; -import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; -import { RollupSearchStrategy } from './search_strategies/rollup_search_strategy'; - -interface VisTypeTimeseriesEnhancedSetupDependencies { - visTypeTimeseries: VisTypeTimeseriesSetup; -} - -export class VisTypeTimeseriesEnhanced - implements Plugin { - private logger: Logger; - - constructor(initializerContext: PluginInitializerContext) { - this.logger = initializerContext.logger.get('vis_type_timeseries_enhanced'); - } - - public async setup( - core: CoreSetup, - { visTypeTimeseries }: VisTypeTimeseriesEnhancedSetupDependencies - ) { - this.logger.debug('Starting plugin'); - - visTypeTimeseries.addSearchStrategy(new RollupSearchStrategy()); - } - - public start() {} -} diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/tsconfig.json b/x-pack/plugins/vis_type_timeseries_enhanced/tsconfig.json deleted file mode 100644 index c5ec5571917bd..0000000000000 --- a/x-pack/plugins/vis_type_timeseries_enhanced/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["*.ts", "server/**/*"], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" } - ] -} diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 4975dcfe885ab..64f3cd545a7b5 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -29,7 +29,6 @@ "plugins/translations/**/*", "plugins/triggers_actions_ui/**/*", "plugins/ui_actions_enhanced/**/*", - "plugins/vis_type_timeseries_enhanced/**/*", "plugins/spaces/**/*", "plugins/security/**/*", "plugins/stack_alerts/**/*", @@ -96,7 +95,6 @@ { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./plugins/vis_type_timeseries_enhanced/tsconfig.json" }, { "path": "./plugins/translations/tsconfig.json" }, { "path": "./plugins/spaces/tsconfig.json" }, { "path": "./plugins/security/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index fcbc4d40530e1..0de209546ac04 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -22,7 +22,6 @@ { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./plugins/vis_type_timeseries_enhanced/tsconfig.json" }, { "path": "./plugins/translations/tsconfig.json" }, { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, { "path": "./plugins/spaces/tsconfig.json" }, From 07b210e42de36a147fdc46ba1fc93f1e16ce4a4d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 28 Jan 2021 09:54:26 +0100 Subject: [PATCH 070/163] [Search Sessions] Improve session restoration back button (#87635) --- ...c-state_sync.ikbnurlstatestorage.cancel.md | 13 --- ...ic-state_sync.ikbnurlstatestorage.flush.md | 15 --- ...sync.ikbnurlstatestorage.kbnurlcontrols.md | 13 +++ ...s-public-state_sync.ikbnurlstatestorage.md | 3 +- .../public/application/dashboard_app.tsx | 96 ++++++++++++++----- .../application/dashboard_state_manager.ts | 4 +- .../dashboard_listing.test.tsx.snap | 60 +++++++++--- .../state_sync/sync_state_with_url.test.ts | 4 +- .../application/angular/context_state.ts | 2 +- .../public/application/angular/discover.js | 58 ++++++----- .../angular/discover_search_session.test.ts | 96 +++++++++++++++++++ .../angular/discover_search_session.ts | 85 ++++++++++++++++ .../application/angular/discover_state.ts | 2 +- .../state_sync/storages/kbn_url_storage.md | 8 +- .../public/history/history_observable.test.ts | 89 +++++++++++++++++ .../public/history/history_observable.ts | 60 ++++++++++++ .../kibana_utils/public/history/index.ts | 1 + src/plugins/kibana_utils/public/index.ts | 9 +- .../public/state_sync/public.api.md | 6 +- .../public/state_sync/state_sync.test.ts | 4 +- .../create_kbn_url_state_storage.test.ts | 12 +-- .../create_kbn_url_state_storage.ts | 19 +--- test/functional/apps/discover/_discover.ts | 4 +- .../test_suites/data_plugin/session.ts | 5 +- .../routes/map_page/url_state/global_sync.ts | 2 +- .../async_search/send_to_background.ts | 18 +++- .../tests/apps/discover/async_search.ts | 20 +++- 27 files changed, 560 insertions(+), 148 deletions(-) delete mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md delete mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.kbnurlcontrols.md create mode 100644 src/plugins/discover/public/application/angular/discover_search_session.test.ts create mode 100644 src/plugins/discover/public/application/angular/discover_search_session.ts create mode 100644 src/plugins/kibana_utils/public/history/history_observable.test.ts create mode 100644 src/plugins/kibana_utils/public/history/history_observable.ts diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md deleted file mode 100644 index 29a511d57d7bd..0000000000000 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) - -## IKbnUrlStateStorage.cancel property - -cancels any pending url updates - -Signature: - -```typescript -cancel: () => void; -``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md deleted file mode 100644 index dfeef1cdce22c..0000000000000 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) - -## IKbnUrlStateStorage.flush property - -Synchronously runs any pending url updates, returned boolean indicates if change occurred. - -Signature: - -```typescript -flush: (opts?: { - replace?: boolean; - }) => boolean; -``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.kbnurlcontrols.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.kbnurlcontrols.md new file mode 100644 index 0000000000000..8e3b9a7bfeb3f --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.kbnurlcontrols.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [kbnUrlControls](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.kbnurlcontrols.md) + +## IKbnUrlStateStorage.kbnUrlControls property + +Lower level wrapper around history library that handles batching multiple URL updates into one history change + +Signature: + +```typescript +kbnUrlControls: IKbnUrlControls; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md index 371f7b7c15362..7fb8717fae003 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md @@ -20,9 +20,8 @@ export interface IKbnUrlStateStorage extends IStateStorage | Property | Type | Description | | --- | --- | --- | -| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) | () => void | cancels any pending url updates | | [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | | -| [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) | (opts?: {
replace?: boolean;
}) => boolean | Synchronously runs any pending url updates, returned boolean indicates if change occurred. | | [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) | <State = unknown>(key: string) => State | null | | +| [kbnUrlControls](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.kbnurlcontrols.md) | IKbnUrlControls | Lower level wrapper around history library that handles batching multiple URL updates into one history change | | [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) | <State>(key: string, state: State, opts?: {
replace: boolean;
}) => Promise<string | undefined> | | diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index e1e2a49439de3..7ea181715717b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -6,22 +6,22 @@ * Public License, v 1. */ -import _ from 'lodash'; import { History } from 'history'; -import { merge, Subscription } from 'rxjs'; -import React, { useEffect, useCallback, useState } from 'react'; +import { merge, Subject, Subscription } from 'rxjs'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { debounceTime, tap } from 'rxjs/operators'; import { useKibana } from '../../../kibana_react/public'; import { DashboardConstants } from '../dashboard_constants'; import { DashboardTopNav } from './top_nav/dashboard_top_nav'; import { DashboardAppServices, DashboardEmbedSettings, DashboardRedirect } from './types'; import { + getChangesFromAppStateForContainerState, + getDashboardContainerInput, + getFiltersSubscription, getInputSubscription, getOutputSubscription, - getFiltersSubscription, getSearchSessionIdFromURL, - getDashboardContainerInput, - getChangesFromAppStateForContainerState, } from './dashboard_app_functions'; import { useDashboardBreadcrumbs, @@ -30,11 +30,11 @@ import { useSavedDashboard, } from './hooks'; -import { removeQueryParam } from '../services/kibana_utils'; import { IndexPattern } from '../services/data'; import { EmbeddableRenderer } from '../services/embeddable'; import { DashboardContainerInput } from '.'; import { leaveConfirmStrings } from '../dashboard_strings'; +import { createQueryParamObservable, replaceUrlHashQuery } from '../../../kibana_utils/public'; export interface DashboardAppProps { history: History; @@ -59,7 +59,7 @@ export function DashboardApp({ indexPatterns: indexPatternService, } = useKibana().services; - const [lastReloadTime, setLastReloadTime] = useState(0); + const triggerRefresh$ = useMemo(() => new Subject<{ force?: boolean }>(), []); const [indexPatterns, setIndexPatterns] = useState([]); const savedDashboard = useSavedDashboard(savedDashboardId, history); @@ -68,9 +68,13 @@ export function DashboardApp({ history ); const dashboardContainer = useDashboardContainer(dashboardStateManager, history, false); + const searchSessionIdQuery$ = useMemo( + () => createQueryParamObservable(history, DashboardConstants.SEARCH_SESSION_ID), + [history] + ); const refreshDashboardContainer = useCallback( - (lastReloadRequestTime?: number) => { + (force?: boolean) => { if (!dashboardContainer || !dashboardStateManager) { return; } @@ -80,7 +84,7 @@ export function DashboardApp({ appStateDashboardInput: getDashboardContainerInput({ isEmbeddedExternally: Boolean(embedSettings), dashboardStateManager, - lastReloadRequestTime, + lastReloadRequestTime: force ? Date.now() : undefined, dashboardCapabilities, query: data.query, }), @@ -100,10 +104,35 @@ export function DashboardApp({ const shouldRefetch = Object.keys(changes).some( (changeKey) => !noRefetchKeys.includes(changeKey as keyof DashboardContainerInput) ); - if (getSearchSessionIdFromURL(history)) { - // going away from a background search results - removeQueryParam(history, DashboardConstants.SEARCH_SESSION_ID, true); - } + + const newSearchSessionId: string | undefined = (() => { + // do not update session id if this is irrelevant state change to prevent excessive searches + if (!shouldRefetch) return; + + let searchSessionIdFromURL = getSearchSessionIdFromURL(history); + if (searchSessionIdFromURL) { + if ( + data.search.session.isRestore() && + data.search.session.isCurrentSession(searchSessionIdFromURL) + ) { + // navigating away from a restored session + dashboardStateManager.kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => { + if (nextUrl.includes(DashboardConstants.SEARCH_SESSION_ID)) { + return replaceUrlHashQuery(nextUrl, (query) => { + delete query[DashboardConstants.SEARCH_SESSION_ID]; + return query; + }); + } + return nextUrl; + }); + searchSessionIdFromURL = undefined; + } else { + data.search.session.restore(searchSessionIdFromURL); + } + } + + return searchSessionIdFromURL ?? data.search.session.start(); + })(); if (changes.viewMode) { setViewMode(changes.viewMode); @@ -111,8 +140,7 @@ export function DashboardApp({ dashboardContainer.updateInput({ ...changes, - // do not start a new session if this is irrelevant state change to prevent excessive searches - ...(shouldRefetch && { searchSessionId: data.search.session.start() }), + ...(newSearchSessionId && { searchSessionId: newSearchSessionId }), }); } }, @@ -159,23 +187,42 @@ export function DashboardApp({ subscriptions.add( merge( ...[timeFilter.getRefreshIntervalUpdate$(), timeFilter.getTimeUpdate$()] - ).subscribe(() => refreshDashboardContainer()) + ).subscribe(() => triggerRefresh$.next()) ); + subscriptions.add( merge( data.search.session.onRefresh$, - data.query.timefilter.timefilter.getAutoRefreshFetch$() + data.query.timefilter.timefilter.getAutoRefreshFetch$(), + searchSessionIdQuery$ ).subscribe(() => { - setLastReloadTime(() => new Date().getTime()); + triggerRefresh$.next({ force: true }); }) ); dashboardStateManager.registerChangeListener(() => { // we aren't checking dirty state because there are changes the container needs to know about // that won't make the dashboard "dirty" - like a view mode change. - refreshDashboardContainer(); + triggerRefresh$.next(); }); + // debounce `refreshDashboardContainer()` + // use `forceRefresh=true` in case at least one debounced trigger asked for it + let forceRefresh: boolean = false; + subscriptions.add( + triggerRefresh$ + .pipe( + tap((trigger) => { + forceRefresh = forceRefresh || (trigger?.force ?? false); + }), + debounceTime(50) + ) + .subscribe(() => { + refreshDashboardContainer(forceRefresh); + forceRefresh = false; + }) + ); + return () => { subscriptions.unsubscribe(); }; @@ -187,6 +234,8 @@ export function DashboardApp({ data.search.session, indexPatternService, dashboardStateManager, + searchSessionIdQuery$, + triggerRefresh$, refreshDashboardContainer, ]); @@ -216,11 +265,6 @@ export function DashboardApp({ }; }, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]); - // Refresh the dashboard container when lastReloadTime changes - useEffect(() => { - refreshDashboardContainer(lastReloadTime); - }, [lastReloadTime, refreshDashboardContainer]); - return (
{savedDashboard && dashboardStateManager && dashboardContainer && viewMode && ( @@ -242,7 +286,7 @@ export function DashboardApp({ // The user can still request a reload in the query bar, even if the // query is the same, and in that case, we have to explicitly ask for // a reload, since no state changes will cause it. - setLastReloadTime(() => new Date().getTime()); + triggerRefresh$.next({ force: true }); } }} /> diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 90706a11b8ce2..c52bd1b4d47b8 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -72,7 +72,7 @@ export class DashboardStateManager { >; private readonly stateContainerChangeSub: Subscription; private readonly STATE_STORAGE_KEY = '_a'; - private readonly kbnUrlStateStorage: IKbnUrlStateStorage; + public readonly kbnUrlStateStorage: IKbnUrlStateStorage; private readonly stateSyncRef: ISyncStateRef; private readonly history: History; private readonly usageCollection: UsageCollectionSetup | undefined; @@ -596,7 +596,7 @@ export class DashboardStateManager { this.toUrlState(this.stateContainer.get()) ); // immediately forces scheduled updates and changes location - return this.kbnUrlStateStorage.flush({ replace }); + return !!this.kbnUrlStateStorage.kbnUrlControls.flush(replace); } // TODO: find nicer solution for this diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap index bce8a661634f6..faec6b4f6f24b 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -4,10 +4,16 @@ exports[`after fetch When given a title that matches multiple dashboards, filter { test('url is actually changed when data in services changes', () => { const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManager.setFilters([gF, aF]); - kbnUrlStateStorage.flush(); // sync force location change + kbnUrlStateStorage.kbnUrlControls.flush(); // sync force location change expect(history.location.hash).toMatchInlineSnapshot( `"#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!t,index:'logstash-*',key:query,negate:!t,type:custom,value:'%7B%22match%22:%7B%22key1%22:%22value1%22%7D%7D'),query:(match:(key1:value1)))),refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"` ); @@ -126,7 +126,7 @@ describe('sync_query_state_with_url', () => { test('when url is changed, filters synced back to filterManager', () => { const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); - kbnUrlStateStorage.cancel(); // stop initial syncing pending update + kbnUrlStateStorage.kbnUrlControls.cancel(); // stop initial syncing pending update history.push(pathWithFilter); expect(filterManager.getGlobalFilters()).toHaveLength(1); stop(); diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 73523b218df7c..e8c2f1d397ba5 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -206,7 +206,7 @@ export function getState({ } }, // helper function just needed for testing - flushToUrl: (replace?: boolean) => stateStorage.flush({ replace }), + flushToUrl: (replace?: boolean) => stateStorage.kbnUrlControls.flush(replace), }; } diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 41c80a717ce75..dcf86babaa5e1 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -47,8 +47,6 @@ import { popularizeField } from '../helpers/popularize_field'; import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state'; import { addFatalError } from '../../../../kibana_legacy/public'; import { METRIC_TYPE } from '@kbn/analytics'; -import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator'; -import { getQueryParams, removeQueryParam } from '../../../../kibana_utils/public'; import { DEFAULT_COLUMNS_SETTING, MODIFY_COLUMNS_ON_SWITCH, @@ -62,6 +60,7 @@ import { getTopNavLinks } from '../components/top_nav/get_top_nav_links'; import { updateSearchSource } from '../helpers/update_search_source'; import { calcFieldCounts } from '../helpers/calc_field_counts'; import { getDefaultSort } from './doc_table/lib/get_default_sort'; +import { DiscoverSearchSessionManager } from './discover_search_session'; const services = getServices(); @@ -86,9 +85,6 @@ const fetchStatuses = { ERROR: 'error', }; -const getSearchSessionIdFromURL = (history) => - getQueryParams(history.location)[SEARCH_SESSION_ID_QUERY_PARAM]; - const app = getAngularModule(); app.config(($routeProvider) => { @@ -177,7 +173,9 @@ function discoverController($route, $scope, Promise) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); const refetch$ = new Subject(); + let inspectorRequest; + let isChangingIndexPattern = false; const savedSearch = $route.current.locals.savedObjects.savedSearch; $scope.searchSource = savedSearch.searchSource; $scope.indexPattern = resolveIndexPattern( @@ -195,15 +193,10 @@ function discoverController($route, $scope, Promise) { }; const history = getHistory(); - // used for restoring a search session - let isInitialSearch = true; - - // search session requested a data refresh - subscriptions.add( - data.search.session.onRefresh$.subscribe(() => { - refetch$.next(); - }) - ); + const searchSessionManager = new DiscoverSearchSessionManager({ + history, + session: data.search.session, + }); const state = getState({ getStateDefaults, @@ -255,6 +248,7 @@ function discoverController($route, $scope, Promise) { $scope.$evalAsync(async () => { if (oldStatePartial.index !== newStatePartial.index) { //in case of index pattern switch the route has currently to be reloaded, legacy + isChangingIndexPattern = true; $route.reload(); return; } @@ -351,7 +345,12 @@ function discoverController($route, $scope, Promise) { if (abortController) abortController.abort(); savedSearch.destroy(); subscriptions.unsubscribe(); - data.search.session.clear(); + if (!isChangingIndexPattern) { + // HACK: + // do not clear session when changing index pattern due to how state management around it is setup + // it will be cleared by searchSessionManager on controller reload instead + data.search.session.clear(); + } appStateUnsubscribe(); stopStateSync(); stopSyncingGlobalStateWithUrl(); @@ -475,7 +474,8 @@ function discoverController($route, $scope, Promise) { return ( config.get(SEARCH_ON_PAGE_LOAD_SETTING) || savedSearch.id !== undefined || - timefilter.getRefreshInterval().pause === false + timefilter.getRefreshInterval().pause === false || + searchSessionManager.hasSearchSessionIdInURL() ); }; @@ -486,7 +486,8 @@ function discoverController($route, $scope, Promise) { filterManager.getFetches$(), timefilter.getFetch$(), timefilter.getAutoRefreshFetch$(), - data.query.queryString.getUpdates$() + data.query.queryString.getUpdates$(), + searchSessionManager.newSearchSessionIdFromURL$ ).pipe(debounceTime(100)); subscriptions.add( @@ -512,6 +513,13 @@ function discoverController($route, $scope, Promise) { ) ); + subscriptions.add( + data.search.session.onRefresh$.subscribe(() => { + searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); + refetch$.next(); + }) + ); + $scope.changeInterval = (interval) => { if (interval) { setAppState({ interval }); @@ -591,20 +599,7 @@ function discoverController($route, $scope, Promise) { if (abortController) abortController.abort(); abortController = new AbortController(); - const searchSessionId = (() => { - const searchSessionIdFromURL = getSearchSessionIdFromURL(history); - if (searchSessionIdFromURL) { - if (isInitialSearch) { - data.search.session.restore(searchSessionIdFromURL); - isInitialSearch = false; - return searchSessionIdFromURL; - } else { - // navigating away from background search - removeQueryParam(history, SEARCH_SESSION_ID_QUERY_PARAM); - } - } - return data.search.session.start(); - })(); + const searchSessionId = searchSessionManager.getNextSearchSessionId(); $scope .updateDataSource() @@ -631,6 +626,7 @@ function discoverController($route, $scope, Promise) { $scope.handleRefresh = function (_payload, isUpdate) { if (isUpdate === false) { + searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); refetch$.next(); } }; diff --git a/src/plugins/discover/public/application/angular/discover_search_session.test.ts b/src/plugins/discover/public/application/angular/discover_search_session.test.ts new file mode 100644 index 0000000000000..abec6aedeaf5c --- /dev/null +++ b/src/plugins/discover/public/application/angular/discover_search_session.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { DiscoverSearchSessionManager } from './discover_search_session'; +import { createMemoryHistory } from 'history'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { DataPublicPluginStart } from '../../../../data/public'; + +describe('DiscoverSearchSessionManager', () => { + const history = createMemoryHistory(); + const session = dataPluginMock.createStartContract().search.session as jest.Mocked< + DataPublicPluginStart['search']['session'] + >; + const searchSessionManager = new DiscoverSearchSessionManager({ + history, + session, + }); + + beforeEach(() => { + history.push('/'); + session.start.mockReset(); + session.restore.mockReset(); + session.getSessionId.mockReset(); + session.isCurrentSession.mockReset(); + session.isRestore.mockReset(); + }); + + describe('getNextSearchSessionId', () => { + test('starts a new session', () => { + const nextId = 'id'; + session.start.mockImplementationOnce(() => nextId); + + const id = searchSessionManager.getNextSearchSessionId(); + expect(id).toEqual(nextId); + expect(session.start).toBeCalled(); + }); + + test('restores a session using query param from the URL', () => { + const nextId = 'id_from_url'; + history.push(`/?searchSessionId=${nextId}`); + + const id = searchSessionManager.getNextSearchSessionId(); + expect(id).toEqual(nextId); + expect(session.restore).toBeCalled(); + }); + + test('removes query param from the URL when navigating away from a restored session', () => { + const idFromUrl = 'id_from_url'; + history.push(`/?searchSessionId=${idFromUrl}`); + + const nextId = 'id'; + session.start.mockImplementationOnce(() => nextId); + session.isCurrentSession.mockImplementationOnce(() => true); + session.isRestore.mockImplementationOnce(() => true); + + const id = searchSessionManager.getNextSearchSessionId(); + expect(id).toEqual(nextId); + expect(session.start).toBeCalled(); + expect(history.location.search).toMatchInlineSnapshot(`""`); + }); + }); + + describe('newSearchSessionIdFromURL$', () => { + test('notifies about searchSessionId changes in the URL', () => { + const emits: Array = []; + + const sub = searchSessionManager.newSearchSessionIdFromURL$.subscribe((newId) => { + emits.push(newId); + }); + + history.push(`/?searchSessionId=id1`); + history.push(`/?searchSessionId=id1`); + session.isCurrentSession.mockImplementationOnce(() => true); + history.replace(`/?searchSessionId=id2`); // should skip current this + history.replace(`/`); + history.push(`/?searchSessionId=id1`); + history.push(`/`); + + expect(emits).toMatchInlineSnapshot(` + Array [ + "id1", + null, + "id1", + null, + ] + `); + + sub.unsubscribe(); + }); + }); +}); diff --git a/src/plugins/discover/public/application/angular/discover_search_session.ts b/src/plugins/discover/public/application/angular/discover_search_session.ts new file mode 100644 index 0000000000000..a53d7d6d2c333 --- /dev/null +++ b/src/plugins/discover/public/application/angular/discover_search_session.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { History } from 'history'; +import { filter } from 'rxjs/operators'; +import { DataPublicPluginStart } from '../../../../data/public'; +import { + createQueryParamObservable, + getQueryParams, + removeQueryParam, +} from '../../../../kibana_utils/public'; +import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator'; + +export interface DiscoverSearchSessionManagerDeps { + history: History; + session: DataPublicPluginStart['search']['session']; +} + +/** + * Helps with state management of search session and {@link SEARCH_SESSION_ID_QUERY_PARAM} in the URL + */ +export class DiscoverSearchSessionManager { + /** + * Notifies about `searchSessionId` changes in the URL, + * skips if `searchSessionId` matches current search session id + */ + readonly newSearchSessionIdFromURL$ = createQueryParamObservable( + this.deps.history, + SEARCH_SESSION_ID_QUERY_PARAM + ).pipe( + filter((searchSessionId) => { + if (!searchSessionId) return true; + return !this.deps.session.isCurrentSession(searchSessionId); + }) + ); + + constructor(private readonly deps: DiscoverSearchSessionManagerDeps) {} + + /** + * Get next session id by either starting or restoring a session. + * When navigating away from the restored session {@link SEARCH_SESSION_ID_QUERY_PARAM} is removed from the URL using history.replace + */ + getNextSearchSessionId() { + let searchSessionIdFromURL = this.getSearchSessionIdFromURL(); + if (searchSessionIdFromURL) { + if ( + this.deps.session.isRestore() && + this.deps.session.isCurrentSession(searchSessionIdFromURL) + ) { + // navigating away from a restored session + this.removeSearchSessionIdFromURL({ replace: true }); + searchSessionIdFromURL = undefined; + } else { + this.deps.session.restore(searchSessionIdFromURL); + } + } + + return searchSessionIdFromURL ?? this.deps.session.start(); + } + + /** + * Removes Discovers {@link SEARCH_SESSION_ID_QUERY_PARAM} from the URL + * @param replace - methods to change the URL + */ + removeSearchSessionIdFromURL({ replace = true }: { replace?: boolean } = { replace: true }) { + if (this.hasSearchSessionIdInURL()) { + removeQueryParam(this.deps.history, SEARCH_SESSION_ID_QUERY_PARAM, replace); + } + } + + /** + * If there is a {@link SEARCH_SESSION_ID_QUERY_PARAM} currently in the URL + */ + hasSearchSessionIdInURL(): boolean { + return !!this.getSearchSessionIdFromURL(); + } + + private getSearchSessionIdFromURL = () => + getQueryParams(this.deps.history.location)[SEARCH_SESSION_ID_QUERY_PARAM] as string | undefined; +} diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index c769e263655ab..65a8dded11092 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -200,7 +200,7 @@ export function getState({ setState(appStateContainerModified, defaultState); }, getPreviousAppState: () => previousAppState, - flushToUrl: () => stateStorage.flush(), + flushToUrl: () => stateStorage.kbnUrlControls.flush(), isAppStateDirty: () => !isEqualState(initialAppState, appStateContainer.getState()), }; } diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md index ec27895eed666..36c7d7119ffe5 100644 --- a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md +++ b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md @@ -96,11 +96,11 @@ setTimeout(() => { }, 0); ``` -For cases, where granular control over URL updates is needed, `kbnUrlStateStorage` provides these advanced apis: +For cases, where granular control over URL updates is needed, `kbnUrlStateStorage` exposes `kbnUrlStateStorage.kbnUrlControls` that exposes these advanced apis: -- `kbnUrlStateStorage.flush({replace: boolean})` - allows to synchronously apply any pending updates. - `replace` option allows to use `history.replace()` instead of `history.push()`. Returned boolean indicates if any update happened -- `kbnUrlStateStorage.cancel()` - cancels any pending updates +- `kbnUrlStateStorage.kbnUrlControls.flush({replace: boolean})` - allows to synchronously apply any pending updates. + `replace` option allows using `history.replace()` instead of `history.push()`. +- `kbnUrlStateStorage.kbnUrlControls.cancel()` - cancels any pending updates. ### Sharing one `kbnUrlStateStorage` instance diff --git a/src/plugins/kibana_utils/public/history/history_observable.test.ts b/src/plugins/kibana_utils/public/history/history_observable.test.ts new file mode 100644 index 0000000000000..818c0d7739283 --- /dev/null +++ b/src/plugins/kibana_utils/public/history/history_observable.test.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { + createHistoryObservable, + createQueryParamObservable, + createQueryParamsObservable, +} from './history_observable'; +import { createMemoryHistory, History } from 'history'; +import { ParsedQuery } from 'query-string'; + +let history: History; + +beforeEach(() => { + history = createMemoryHistory(); +}); + +test('createHistoryObservable', () => { + const obs$ = createHistoryObservable(history); + const emits: string[] = []; + obs$.subscribe(({ location }) => { + emits.push(location.pathname + location.search); + }); + + history.push('/test'); + history.push('/'); + + expect(emits.length).toEqual(2); + expect(emits).toMatchInlineSnapshot(` + Array [ + "/test", + "/", + ] + `); +}); + +test('createQueryParamsObservable', () => { + const obs$ = createQueryParamsObservable(history); + const emits: ParsedQuery[] = []; + obs$.subscribe((params) => { + emits.push(params); + }); + + history.push('/test'); + history.push('/test?foo=bar'); + history.push('/?foo=bar'); + history.push('/test?foo=bar&foo1=bar1'); + + expect(emits.length).toEqual(2); + expect(emits).toMatchInlineSnapshot(` + Array [ + Object { + "foo": "bar", + }, + Object { + "foo": "bar", + "foo1": "bar1", + }, + ] + `); +}); + +test('createQueryParamObservable', () => { + const obs$ = createQueryParamObservable(history, 'foo'); + const emits: unknown[] = []; + obs$.subscribe((param) => { + emits.push(param); + }); + + history.push('/test'); + history.push('/test?foo=bar'); + history.push('/?foo=bar'); + history.push('/test?foo=baaaar&foo1=bar1'); + history.push('/test?foo1=bar1'); + + expect(emits.length).toEqual(3); + expect(emits).toMatchInlineSnapshot(` + Array [ + "bar", + "baaaar", + null, + ] + `); +}); diff --git a/src/plugins/kibana_utils/public/history/history_observable.ts b/src/plugins/kibana_utils/public/history/history_observable.ts new file mode 100644 index 0000000000000..f02a5e340b1a0 --- /dev/null +++ b/src/plugins/kibana_utils/public/history/history_observable.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Action, History, Location } from 'history'; +import { Observable } from 'rxjs'; +import { ParsedQuery } from 'query-string'; +import deepEqual from 'fast-deep-equal'; +import { map } from 'rxjs/operators'; +import { getQueryParams } from './get_query_params'; +import { distinctUntilChangedWithInitialValue } from '../../common'; + +/** + * Convert history.listen into an observable + * @param history - {@link History} instance + */ +export function createHistoryObservable( + history: History +): Observable<{ location: Location; action: Action }> { + return new Observable((observer) => { + const unlisten = history.listen((location, action) => observer.next({ location, action })); + return () => { + unlisten(); + }; + }); +} + +/** + * Create an observable that emits every time any of query params change. + * Uses deepEqual check. + * @param history - {@link History} instance + */ +export function createQueryParamsObservable(history: History): Observable { + return createHistoryObservable(history).pipe( + map(({ location }) => ({ ...getQueryParams(location) })), + distinctUntilChangedWithInitialValue({ ...getQueryParams(history.location) }, deepEqual) + ); +} + +/** + * Create an observable that emits every time _paramKey_ changes + * @param history - {@link History} instance + * @param paramKey - query param key to observe + */ +export function createQueryParamObservable( + history: History, + paramKey: string +): Observable { + return createQueryParamsObservable(history).pipe( + map((params) => (params[paramKey] ?? null) as Param | null), + distinctUntilChangedWithInitialValue( + (getQueryParams(history.location)[paramKey] ?? null) as Param | null, + deepEqual + ) + ); +} diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index 4b1b610d560e2..b2ac9ed6c739e 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -9,3 +9,4 @@ export { removeQueryParam } from './remove_query_param'; export { redirectWhenMissing } from './redirect_when_missing'; export { getQueryParams } from './get_query_params'; +export * from './history_observable'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index fa9cf5a52371d..29936da0117c1 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -68,7 +68,14 @@ export { StopSyncStateFnType, } from './state_sync'; export { Configurable, CollectConfigProps } from './ui'; -export { removeQueryParam, redirectWhenMissing, getQueryParams } from './history'; +export { + removeQueryParam, + redirectWhenMissing, + getQueryParams, + createQueryParamsObservable, + createHistoryObservable, + createQueryParamObservable, +} from './history'; export { applyDiff } from './state_management/utils/diff_object'; export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter'; diff --git a/src/plugins/kibana_utils/public/state_sync/public.api.md b/src/plugins/kibana_utils/public/state_sync/public.api.md index a4dfea82cdb59..5524563c034a8 100644 --- a/src/plugins/kibana_utils/public/state_sync/public.api.md +++ b/src/plugins/kibana_utils/public/state_sync/public.api.md @@ -22,14 +22,12 @@ export const createSessionStorageStateStorage: (storage?: Storage) => ISessionSt // @public export interface IKbnUrlStateStorage extends IStateStorage { - cancel: () => void; // (undocumented) change$: (key: string) => Observable; - flush: (opts?: { - replace?: boolean; - }) => boolean; // (undocumented) get: (key: string) => State | null; + // Warning: (ae-forgotten-export) The symbol "IKbnUrlControls" needs to be exported by the entry point index.d.ts + kbnUrlControls: IKbnUrlControls; // (undocumented) set: (key: string, state: State, opts?: { replace: boolean; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index c7f04bc9cdbe3..890de8f6ed6a1 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -255,7 +255,7 @@ describe('state_sync', () => { expect(history.length).toBe(startHistoryLength); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); - urlSyncStrategy.flush(); + urlSyncStrategy.kbnUrlControls.flush(); expect(history.length).toBe(startHistoryLength + 1); expect(getCurrentUrl()).toMatchInlineSnapshot( @@ -290,7 +290,7 @@ describe('state_sync', () => { expect(history.length).toBe(startHistoryLength); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); - urlSyncStrategy.cancel(); + urlSyncStrategy.kbnUrlControls.cancel(); expect(history.length).toBe(startHistoryLength); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts index fbd3c3f933791..037c6f9fc666d 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -39,11 +39,11 @@ describe('KbnUrlStateStorage', () => { const key = '_s'; urlStateStorage.set(key, state); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); - expect(urlStateStorage.flush()).toBe(true); + expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(true); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_s=(ok:1,test:test)"`); expect(urlStateStorage.get(key)).toEqual(state); - expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update + expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(false); // nothing to flush, not update }); it('should cancel url updates', async () => { @@ -51,7 +51,7 @@ describe('KbnUrlStateStorage', () => { const key = '_s'; const pr = urlStateStorage.set(key, state); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); - urlStateStorage.cancel(); + urlStateStorage.kbnUrlControls.cancel(); await pr; expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); expect(urlStateStorage.get(key)).toEqual(null); @@ -215,11 +215,11 @@ describe('KbnUrlStateStorage', () => { const key = '_s'; urlStateStorage.set(key, state); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); - expect(urlStateStorage.flush()).toBe(true); + expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(true); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/#?_s=(ok:1,test:test)"`); expect(urlStateStorage.get(key)).toEqual(state); - expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update + expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(false); // nothing to flush, not update }); it('should cancel url updates', async () => { @@ -227,7 +227,7 @@ describe('KbnUrlStateStorage', () => { const key = '_s'; const pr = urlStateStorage.set(key, state); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); - urlStateStorage.cancel(); + urlStateStorage.kbnUrlControls.cancel(); await pr; expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); expect(urlStateStorage.get(key)).toEqual(null); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index 700420447bf4f..0935ecd20111f 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -13,6 +13,7 @@ import { IStateStorage } from './types'; import { createKbnUrlControls, getStateFromKbnUrl, + IKbnUrlControls, setStateToKbnUrl, } from '../../state_management/url'; @@ -39,16 +40,9 @@ export interface IKbnUrlStateStorage extends IStateStorage { change$: (key: string) => Observable; /** - * cancels any pending url updates + * Lower level wrapper around history library that handles batching multiple URL updates into one history change */ - cancel: () => void; - - /** - * Synchronously runs any pending url updates, returned boolean indicates if change occurred. - * @param opts: {replace? boolean} - allows to specify if push or replace should be used for flushing update - * @returns boolean - indicates if there was an update to flush - */ - flush: (opts?: { replace?: boolean }) => boolean; + kbnUrlControls: IKbnUrlControls; } /** @@ -114,11 +108,6 @@ export const createKbnUrlStateStorage = ( }), share() ), - flush: ({ replace = false }: { replace?: boolean } = {}) => { - return !!url.flush(replace); - }, - cancel() { - url.cancel(); - }, + kbnUrlControls: url, }; }; diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 1176dd6395d2c..bf0a027553832 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -227,7 +227,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); describe('usage of discover:searchOnPageLoad', () => { - it('should fetch data from ES initially when discover:searchOnPageLoad is false', async function () { + it('should not fetch data from ES initially when discover:searchOnPageLoad is false', async function () { await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': false }); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); @@ -235,7 +235,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.discover.getNrOfFetches()).to.be(0); }); - it('should not fetch data from ES initially when discover:searchOnPageLoad is true', async function () { + it('should fetch data from ES initially when discover:searchOnPageLoad is true', async function () { await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': true }); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); diff --git a/test/plugin_functional/test_suites/data_plugin/session.ts b/test/plugin_functional/test_suites/data_plugin/session.ts index ac958ead321bc..5567958cfd878 100644 --- a/test/plugin_functional/test_suites/data_plugin/session.ts +++ b/test/plugin_functional/test_suites/data_plugin/session.ts @@ -42,10 +42,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide await PageObjects.header.waitUntilLoadingHasFinished(); const sessionIds = await getSessionIds(); - // Discover calls destroy on index pattern change, which explicitly closes a session - expect(sessionIds.length).to.be(2); - expect(sessionIds[0].length).to.be(0); - expect(sessionIds[1].length).not.to.be(0); + expect(sessionIds.length).to.be(1); }); it('Starts on a refresh', async () => { diff --git a/x-pack/plugins/maps/public/routes/map_page/url_state/global_sync.ts b/x-pack/plugins/maps/public/routes/map_page/url_state/global_sync.ts index 7fefc6662ada7..398c05b8ed69a 100644 --- a/x-pack/plugins/maps/public/routes/map_page/url_state/global_sync.ts +++ b/x-pack/plugins/maps/public/routes/map_page/url_state/global_sync.ts @@ -30,6 +30,6 @@ export function updateGlobalState(newState: MapsGlobalState, flushUrlState = fal ...newState, }); if (flushUrlState) { - kbnUrlStateStorage.flush({ replace: true }); + kbnUrlStateStorage.kbnUrlControls.flush(true); } } diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts index 03635efb6113d..7e878e763bfc1 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts @@ -30,9 +30,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await searchSessions.deleteAllSearchSessions(); }); - it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => { + it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes. Back button restores a session.', async () => { await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); - const url = await browser.getCurrentUrl(); + let url = await browser.getCurrentUrl(); const fakeSessionId = '__fake__'; const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; await browser.get(savedSessionURL); @@ -53,6 +53,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Sum of Bytes by Extension' ); expect(session2).not.to.be(fakeSessionId); + + // back button should restore the session: + url = await browser.getCurrentUrl(); + expect(url).not.to.contain('searchSessionId'); + + await browser.goBack(); + + url = await browser.getCurrentUrl(); + expect(url).to.contain('searchSessionId'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await searchSessions.expectState('restored'); + expect( + await dashboardPanelActions.getSearchSessionIdByTitle('Sum of Bytes by Extension') + ).to.be(fakeSessionId); }); it('Saves and restores a session', async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/async_search.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/async_search.ts index d64df98c98601..b5e65158c573a 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/discover/async_search.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/async_search.ts @@ -13,6 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const browser = getService('browser'); const inspector = getService('inspector'); const PageObjects = getPageObjects(['discover', 'common', 'timePicker', 'header']); + const searchSessions = getService('searchSessions'); describe('discover async search', () => { before(async () => { @@ -31,18 +32,33 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(searchSessionId2).not.to.be(searchSessionId1); }); - it('search session id should be picked up from the URL, non existing session id errors out', async () => { - const url = await browser.getCurrentUrl(); + it('search session id should be picked up from the URL, non existing session id errors out, back button restores a session', async () => { + let url = await browser.getCurrentUrl(); const fakeSearchSessionId = '__test__'; const savedSessionURL = url + `&searchSessionId=${fakeSearchSessionId}`; await browser.navigateTo(savedSessionURL); await PageObjects.header.waitUntilLoadingHasFinished(); + await searchSessions.expectState('restored'); await testSubjects.existOrFail('discoverNoResultsError'); // expect error because of fake searchSessionId const searchSessionId1 = await getSearchSessionId(); expect(searchSessionId1).to.be(fakeSearchSessionId); await queryBar.clickQuerySubmitButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await searchSessions.expectState('completed'); const searchSessionId2 = await getSearchSessionId(); expect(searchSessionId2).not.to.be(searchSessionId1); + + // back button should restore the session: + url = await browser.getCurrentUrl(); + expect(url).not.to.contain('searchSessionId'); + + await browser.goBack(); + + url = await browser.getCurrentUrl(); + expect(url).to.contain('searchSessionId'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await searchSessions.expectState('restored'); + expect(await getSearchSessionId()).to.be(fakeSearchSessionId); }); }); From 457f0111515eda3ff24637eb238fae229d8ed986 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 28 Jan 2021 10:16:39 +0100 Subject: [PATCH 071/163] [Discover] Add grid flyout jest test (#89088) --- .../discover_grid_flyout.test.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx new file mode 100644 index 0000000000000..f9428e30569f7 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl } from '@kbn/test/jest'; +import { DiscoverGridFlyout } from './discover_grid_flyout'; +import { esHits } from '../../../__mocks__/es_hits'; +import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../../build_services'; +import { DocViewsRegistry } from '../../doc_views/doc_views_registry'; +import { setDocViewsRegistry } from '../../../kibana_services'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; + +describe('Discover flyout', function () { + setDocViewsRegistry(new DocViewsRegistry()); + + it('should be rendered correctly using an index pattern without timefield', async () => { + const onClose = jest.fn(); + const component = mountWithIntl( + + ); + + const url = findTestSubject(component, 'docTableRowAction').prop('href'); + expect(url).toMatchInlineSnapshot(`"#/doc/the-index-pattern-id/i?id=1"`); + findTestSubject(component, 'euiFlyoutCloseButton').simulate('click'); + expect(onClose).toHaveBeenCalled(); + }); + + it('should be rendered correctly using an index pattern with timefield', async () => { + const onClose = jest.fn(); + const component = mountWithIntl( + + ); + + const actions = findTestSubject(component, 'docTableRowAction'); + expect(actions.length).toBe(2); + expect(actions.first().prop('href')).toMatchInlineSnapshot( + `"#/doc/index-pattern-with-timefield-id/i?id=1"` + ); + expect(actions.last().prop('href')).toMatchInlineSnapshot( + `"#/context/index-pattern-with-timefield-id/1?_g=(filters:!())&_a=(columns:!(date),filters:!())"` + ); + findTestSubject(component, 'euiFlyoutCloseButton').simulate('click'); + expect(onClose).toHaveBeenCalled(); + }); +}); From f3fba95955277220ea95b7d7b8e7851c21ce11c2 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 28 Jan 2021 10:14:28 +0000 Subject: [PATCH 072/163] [Task Manager] ignore version conflicts that exceed max_docs in the claiming process (#89415) This is a first step in attempting to address the over zealous shifting we've identified in TM. It [turns out](https://github.com/elastic/elasticsearch/issues/63671) `version_conflicts` don't always count against `max_docs`, so in this PR we correct the `version_conflicts` returned by updateByQuery in TaskManager to only count the conflicts that _may_ have counted against `max_docs`. This correction isn't necessarily accurate, but it will ensure we don't shift if we are in fact managing to claim tasks. --- .../task_manager/server/task_store.test.ts | 74 ++++++++++++++++++- .../plugins/task_manager/server/task_store.ts | 16 +++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 81d72c68b3a9e..a2a0ee11380ff 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -269,12 +269,13 @@ describe('TaskStore', () => { opts = {}, hits = generateFakeTasks(1), claimingOpts, + versionConflicts = 2, }: { opts: Partial; hits?: unknown[]; claimingOpts: OwnershipClaimingOpts; + versionConflicts?: number; }) { - const versionConflicts = 2; const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; esClient.search.mockResolvedValue(asApiResponse({ hits: { hits } })); esClient.updateByQuery.mockResolvedValue( @@ -971,6 +972,77 @@ if (doc['task.runAt'].size()!=0) { ]); }); + test('it returns version_conflicts that do not include conflicts that were proceeded against', async () => { + const taskManagerId = uuid.v1(); + const claimOwnershipUntil = new Date(Date.now()); + const runAt = new Date(); + const tasks = [ + { + _id: 'task:aaa', + _source: { + type: 'task', + task: { + runAt, + taskType: 'foo', + schedule: undefined, + attempts: 0, + status: 'claiming', + params: '{ "hello": "world" }', + state: '{ "baby": "Henhen" }', + user: 'jimbo', + scope: ['reporting'], + ownerId: taskManagerId, + }, + }, + _seq_no: 1, + _primary_term: 2, + sort: ['a', 1], + }, + { + _id: 'task:bbb', + _source: { + type: 'task', + task: { + runAt, + taskType: 'bar', + schedule: { interval: '5m' }, + attempts: 2, + status: 'claiming', + params: '{ "shazm": 1 }', + state: '{ "henry": "The 8th" }', + user: 'dabo', + scope: ['reporting', 'ceo'], + ownerId: taskManagerId, + }, + }, + _seq_no: 3, + _primary_term: 4, + sort: ['b', 2], + }, + ]; + const maxDocs = 10; + const { + result: { stats: { tasksUpdated, tasksConflicted, tasksClaimed } = {} } = {}, + } = await testClaimAvailableTasks({ + opts: { + taskManagerId, + }, + claimingOpts: { + claimOwnershipUntil, + size: maxDocs, + }, + hits: tasks, + // assume there were 20 version conflists, but thanks to `conflicts="proceed"` + // we proceeded to claim tasks + versionConflicts: 20, + }); + + expect(tasksUpdated).toEqual(2); + // ensure we only count conflicts that *may* have counted against max_docs, no more than that + expect(tasksConflicted).toEqual(10 - tasksUpdated!); + expect(tasksClaimed).toEqual(2); + }); + test('pushes error from saved objects client to errors$', async () => { const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const store = new TaskStore({ diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 5d17c6246088a..4b02e35c61582 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -529,7 +529,7 @@ export class TaskStore { private async updateByQuery( opts: UpdateByQuerySearchOpts = {}, // eslint-disable-next-line @typescript-eslint/naming-convention - { max_docs }: UpdateByQueryOpts = {} + { max_docs: max_docs }: UpdateByQueryOpts = {} ): Promise { const { query } = ensureQueryOnlyReturnsTaskObjects(opts); try { @@ -548,10 +548,22 @@ export class TaskStore { }, }); + /** + * When we run updateByQuery with conflicts='proceed', it's possible for the `version_conflicts` + * to count against the specified `max_docs`, as per https://github.com/elastic/elasticsearch/issues/63671 + * In order to correct for that happening, we only count `version_conflicts` if we haven't updated as + * many docs as we could have. + * This is still no more than an estimation, as there might have been less docuemnt to update that the + * `max_docs`, but we bias in favour of over zealous `version_conflicts` as that's the best indicator we + * have for an unhealthy cluster distribution of Task Manager polling intervals + */ + const conflictsCorrectedForContinuation = + max_docs && version_conflicts + updated > max_docs ? max_docs - updated : version_conflicts; + return { total, updated, - version_conflicts, + version_conflicts: conflictsCorrectedForContinuation, }; } catch (e) { this.errors$.next(e); From ec8738a06097402399436372278c37912af8a203 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 28 Jan 2021 11:52:38 +0100 Subject: [PATCH 073/163] add synchronous config access API (#88981) * add synchronous config accessor * add `config.get` to plugin context and add tsdoc * remove useless markAsHandled calls * fix mocks * update generated docs * fix unit tests * add sync accessor for legacy config --- ...-server.plugininitializercontext.config.md | 5 +- ...-server.plugininitializercontext.logger.md | 21 +++ ...in-core-server.plugininitializercontext.md | 4 +- .../kbn-config/src/config_service.mock.ts | 3 +- .../kbn-config/src/config_service.test.ts | 112 ++++++++------ packages/kbn-config/src/config_service.ts | 63 ++++---- .../legacy/legacy_object_to_config_adapter.ts | 2 +- src/core/server/mocks.ts | 7 +- src/core/server/plugins/legacy_config.test.ts | 82 ++++++++++ src/core/server/plugins/legacy_config.ts | 69 +++++++++ .../server/plugins/plugin_context.test.ts | 142 ++++++++++++------ src/core/server/plugins/plugin_context.ts | 50 +----- src/core/server/plugins/plugins_service.ts | 5 +- src/core/server/plugins/types.ts | 90 ++++++++++- src/core/server/root/index.ts | 2 +- src/core/server/server.api.md | 7 +- src/core/server/server.ts | 4 +- 17 files changed, 478 insertions(+), 190 deletions(-) create mode 100644 src/core/server/plugins/legacy_config.test.ts create mode 100644 src/core/server/plugins/legacy_config.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md index 4ab0cb74f809f..3b5754eb4fa39 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md @@ -4,14 +4,17 @@ ## PluginInitializerContext.config property +Accessors for the plugin's configuration + Signature: ```typescript config: { legacy: { globalConfig$: Observable; + get: () => SharedGlobalConfig; }; create: () => Observable; - createIfExists: () => Observable; + get: () => T; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md index 106fdaad9bc22..e5de046eccf1d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md @@ -4,8 +4,29 @@ ## PluginInitializerContext.logger property + instance already bound to the plugin's logging context + Signature: ```typescript logger: LoggerFactory; ``` + +## Example + + +```typescript +// plugins/my-plugin/server/plugin.ts +// "id: myPlugin" in `plugins/my-plugin/kibana.yaml` + +export class MyPlugin implements Plugin { + constructor(private readonly initContext: PluginInitializerContext) { + this.logger = initContext.logger.get(); + // `logger` context: `plugins.myPlugin` + this.mySubLogger = initContext.logger.get('sub'); // or this.logger.get('sub'); + // `mySubLogger` context: `plugins.myPlugin.sub` + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md index 18760170afa1f..90a19d53bd5e1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md @@ -16,8 +16,8 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | -| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
};
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
} | | +| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
get: () => SharedGlobalConfig;
};
create: <T = ConfigSchema>() => Observable<T>;
get: <T = ConfigSchema>() => T;
} | Accessors for the plugin's configuration | | [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
instanceUuid: string;
} | | -| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | LoggerFactory | | +| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | LoggerFactory | instance already bound to the plugin's logging context | | [opaqueId](./kibana-plugin-core-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | diff --git a/packages/kbn-config/src/config_service.mock.ts b/packages/kbn-config/src/config_service.mock.ts index cd6f399ddcce2..59f788767004c 100644 --- a/packages/kbn-config/src/config_service.mock.ts +++ b/packages/kbn-config/src/config_service.mock.ts @@ -17,8 +17,8 @@ const createConfigServiceMock = ({ }: { atPath?: Record; getConfig$?: Record } = {}) => { const mocked: jest.Mocked = { atPath: jest.fn(), + atPathSync: jest.fn(), getConfig$: jest.fn(), - optionalAtPath: jest.fn(), getUsedPaths: jest.fn(), getUnusedPaths: jest.fn(), isEnabledAtPath: jest.fn(), @@ -27,6 +27,7 @@ const createConfigServiceMock = ({ validate: jest.fn(), }; mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); + mocked.atPathSync.mockReturnValue(atPath); mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$))); mocked.getUsedPaths.mockResolvedValue([]); mocked.getUnusedPaths.mockResolvedValue([]); diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index 96d1f794a691c..e55916d7d348c 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -105,27 +105,6 @@ test('re-validate config when updated', async () => { `); }); -test("returns undefined if fetching optional config at a path that doesn't exist", async () => { - const rawConfig = getRawConfigProvider({}); - const configService = new ConfigService(rawConfig, defaultEnv, logger); - - const value$ = configService.optionalAtPath('unique-name'); - const value = await value$.pipe(first()).toPromise(); - - expect(value).toBeUndefined(); -}); - -test('returns observable config at optional path if it exists', async () => { - const rawConfig = getRawConfigProvider({ value: 'bar' }); - const configService = new ConfigService(rawConfig, defaultEnv, logger); - await configService.setSchema('value', schema.string()); - - const value$ = configService.optionalAtPath('value'); - const value: any = await value$.pipe(first()).toPromise(); - - expect(value).toBe('bar'); -}); - test("does not push new configs when reloading if config at path hasn't changed", async () => { const rawConfig$ = new BehaviorSubject>({ key: 'value' }); const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); @@ -209,34 +188,38 @@ test('flags schema paths as handled when registering a schema', async () => { test('tracks unhandled paths', async () => { const initialConfig = { - bar: { - deep1: { - key: '123', - }, - deep2: { - key: '321', - }, + service: { + string: 'str', + number: 42, }, - foo: 'value', - quux: { - deep1: { - key: 'hello', - }, - deep2: { - key: 'world', - }, + plugin: { + foo: 'bar', + }, + unknown: { + hello: 'dolly', + number: 9000, }, }; const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); - - configService.atPath('foo'); - configService.atPath(['bar', 'deep2']); + await configService.setSchema( + 'service', + schema.object({ + string: schema.string(), + number: schema.number(), + }) + ); + await configService.setSchema( + 'plugin', + schema.object({ + foo: schema.string(), + }) + ); const unused = await configService.getUnusedPaths(); - expect(unused).toEqual(['bar.deep1.key', 'quux.deep1.key', 'quux.deep2.key']); + expect(unused).toEqual(['unknown.hello', 'unknown.number']); }); test('correctly passes context', async () => { @@ -339,22 +322,18 @@ test('does not throw if schema does not define "enabled" schema', async () => { const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); - await expect( + expect( configService.setSchema( 'pid', schema.object({ file: schema.string(), }) ) - ).resolves.toBeUndefined(); + ).toBeUndefined(); const value$ = configService.atPath('pid'); const value: any = await value$.pipe(first()).toPromise(); expect(value.enabled).toBe(undefined); - - const valueOptional$ = configService.optionalAtPath('pid'); - const valueOptional: any = await valueOptional$.pipe(first()).toPromise(); - expect(valueOptional.enabled).toBe(undefined); }); test('treats config as enabled if config path is not present in config', async () => { @@ -457,3 +436,44 @@ test('logs deprecation warning during validation', async () => { ] `); }); + +describe('atPathSync', () => { + test('returns the value at path', async () => { + const rawConfig = getRawConfigProvider({ key: 'foo' }); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + const stringSchema = schema.string(); + await configService.setSchema('key', stringSchema); + + await configService.validate(); + + const value = configService.atPathSync('key'); + expect(value).toBe('foo'); + }); + + test('throws if called before `validate`', async () => { + const rawConfig = getRawConfigProvider({ key: 'foo' }); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + const stringSchema = schema.string(); + await configService.setSchema('key', stringSchema); + + expect(() => configService.atPathSync('key')).toThrowErrorMatchingInlineSnapshot( + `"\`atPathSync\` called before config was validated"` + ); + }); + + test('returns the last config value', async () => { + const rawConfig$ = new BehaviorSubject>({ key: 'value' }); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); + + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + await configService.setSchema('key', schema.string()); + + await configService.validate(); + + expect(configService.atPathSync('key')).toEqual('value'); + + rawConfig$.next({ key: 'new-value' }); + + expect(configService.atPathSync('key')).toEqual('new-value'); + }); +}); diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index 9518279f35766..929735ffc15f2 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -10,7 +10,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { Type } from '@kbn/config-schema'; import { isEqual } from 'lodash'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, first, map, shareReplay, take } from 'rxjs/operators'; +import { distinctUntilChanged, first, map, shareReplay, take, tap } from 'rxjs/operators'; import { Logger, LoggerFactory } from '@kbn/logging'; import { Config, ConfigPath, Env } from '.'; @@ -32,13 +32,15 @@ export class ConfigService { private readonly log: Logger; private readonly deprecationLog: Logger; + private validated = false; private readonly config$: Observable; + private lastConfig?: Config; /** * Whenever a config if read at a path, we mark that path as 'handled'. We can * then list all unhandled config paths when the startup process is completed. */ - private readonly handledPaths: ConfigPath[] = []; + private readonly handledPaths: Set = new Set(); private readonly schemas = new Map>(); private readonly deprecations = new BehaviorSubject([]); @@ -55,6 +57,9 @@ export class ConfigService { const migrated = applyDeprecations(rawConfig, deprecations); return new LegacyObjectToConfigAdapter(migrated); }), + tap((config) => { + this.lastConfig = config; + }), shareReplay(1) ); } @@ -62,7 +67,7 @@ export class ConfigService { /** * Set config schema for a path and performs its validation */ - public async setSchema(path: ConfigPath, schema: Type) { + public setSchema(path: ConfigPath, schema: Type) { const namespace = pathToString(path); if (this.schemas.has(namespace)) { throw new Error(`Validation schema for [${path}] was already registered.`); @@ -94,15 +99,16 @@ export class ConfigService { public async validate() { const namespaces = [...this.schemas.keys()]; for (let i = 0; i < namespaces.length; i++) { - await this.validateConfigAtPath(namespaces[i]).pipe(first()).toPromise(); + await this.getValidatedConfigAtPath$(namespaces[i]).pipe(first()).toPromise(); } await this.logDeprecation(); + this.validated = true; } /** * Returns the full config object observable. This is not intended for - * "normal use", but for features that _need_ access to the full object. + * "normal use", but for internal features that _need_ access to the full object. */ public getConfig$() { return this.config$; @@ -110,27 +116,26 @@ export class ConfigService { /** * Reads the subset of the config at the specified `path` and validates it - * against the static `schema` on the given `ConfigClass`. + * against its registered schema. * * @param path - The path to the desired subset of the config. */ public atPath(path: ConfigPath) { - return this.validateConfigAtPath(path) as Observable; + return this.getValidatedConfigAtPath$(path) as Observable; } /** - * Same as `atPath`, but returns `undefined` if there is no config at the - * specified path. + * Similar to {@link atPath}, but return the last emitted value synchronously instead of an + * observable. * - * {@link ConfigService.atPath} + * @param path - The path to the desired subset of the config. */ - public optionalAtPath(path: ConfigPath) { - return this.getDistinctConfig(path).pipe( - map((config) => { - if (config === undefined) return undefined; - return this.validateAtPath(path, config) as TSchema; - }) - ); + public atPathSync(path: ConfigPath) { + if (!this.validated) { + throw new Error('`atPathSync` called before config was validated'); + } + const configAtPath = this.lastConfig!.get(path); + return this.validateAtPath(path, configAtPath) as TSchema; } public async isEnabledAtPath(path: ConfigPath) { @@ -144,10 +149,7 @@ export class ConfigService { const config = await this.config$.pipe(first()).toPromise(); // if plugin hasn't got a config schema, we try to read "enabled" directly - const isEnabled = - validatedConfig && validatedConfig.enabled !== undefined - ? validatedConfig.enabled - : config.get(enabledPath); + const isEnabled = validatedConfig?.enabled ?? config.get(enabledPath); // not declared. consider that plugin is enabled by default if (isEnabled === undefined) { @@ -170,15 +172,13 @@ export class ConfigService { public async getUnusedPaths() { const config = await this.config$.pipe(first()).toPromise(); - const handledPaths = this.handledPaths.map(pathToString); - + const handledPaths = [...this.handledPaths.values()].map(pathToString); return config.getFlattenedPaths().filter((path) => !isPathHandled(path, handledPaths)); } public async getUsedPaths() { const config = await this.config$.pipe(first()).toPromise(); - const handledPaths = this.handledPaths.map(pathToString); - + const handledPaths = [...this.handledPaths.values()].map(pathToString); return config.getFlattenedPaths().filter((path) => isPathHandled(path, handledPaths)); } @@ -210,22 +210,17 @@ export class ConfigService { ); } - private validateConfigAtPath(path: ConfigPath) { - return this.getDistinctConfig(path).pipe(map((config) => this.validateAtPath(path, config))); - } - - private getDistinctConfig(path: ConfigPath) { - this.markAsHandled(path); - + private getValidatedConfigAtPath$(path: ConfigPath) { return this.config$.pipe( map((config) => config.get(path)), - distinctUntilChanged(isEqual) + distinctUntilChanged(isEqual), + map((config) => this.validateAtPath(path, config)) ); } private markAsHandled(path: ConfigPath) { this.log.debug(`Marking config path as handled: ${path}`); - this.handledPaths.push(path); + this.handledPaths.add(path); } } diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts index c037c5f0308c8..c12a147fddddc 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts @@ -84,7 +84,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { }; } - private static transformPlugins(configValue: LegacyVars) { + private static transformPlugins(configValue: LegacyVars = {}) { // These properties are the only ones we use from the existing `plugins` config node // since `scanDirs` isn't respected by new platform plugin discovery. return { diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 2d053300273fb..b86e2e4c6dedb 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -69,9 +69,12 @@ export function pluginInitializerContextConfigMock(config: T) { }; const mock: jest.Mocked['config']> = { - legacy: { globalConfig$: of(globalConfig) }, + legacy: { + globalConfig$: of(globalConfig), + get: () => globalConfig, + }, create: jest.fn().mockReturnValue(of(config)), - createIfExists: jest.fn().mockReturnValue(of(config)), + get: jest.fn().mockReturnValue(config), }; return mock; diff --git a/src/core/server/plugins/legacy_config.test.ts b/src/core/server/plugins/legacy_config.test.ts new file mode 100644 index 0000000000000..fd8234d72bd17 --- /dev/null +++ b/src/core/server/plugins/legacy_config.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { take } from 'rxjs/operators'; +import { ConfigService, Env } from '@kbn/config'; +import { getEnvOptions, rawConfigServiceMock } from '../config/mocks'; +import { getGlobalConfig, getGlobalConfig$ } from './legacy_config'; +import { REPO_ROOT } from '@kbn/utils'; +import { loggingSystemMock } from '../logging/logging_system.mock'; +import { duration } from 'moment'; +import { fromRoot } from '../utils'; +import { ByteSizeValue } from '@kbn/config-schema'; +import { Server } from '../server'; + +describe('Legacy config', () => { + let env: Env; + let logger: ReturnType; + + beforeEach(() => { + env = Env.createDefault(REPO_ROOT, getEnvOptions()); + logger = loggingSystemMock.create(); + }); + + const createConfigService = (rawConfig: Record = {}): ConfigService => { + const rawConfigService = rawConfigServiceMock.create({ rawConfig }); + const server = new Server(rawConfigService, env, logger); + server.setupCoreConfig(); + return server.configService; + }; + + describe('getGlobalConfig', () => { + it('should return the global config', async () => { + const configService = createConfigService(); + await configService.validate(); + + const legacyConfig = getGlobalConfig(configService); + + expect(legacyConfig).toStrictEqual({ + kibana: { + index: '.kibana', + autocompleteTerminateAfter: duration(100000), + autocompleteTimeout: duration(1000), + }, + elasticsearch: { + shardTimeout: duration(30, 's'), + requestTimeout: duration(30, 's'), + pingTimeout: duration(30, 's'), + }, + path: { data: fromRoot('data') }, + savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) }, + }); + }); + }); + + describe('getGlobalConfig$', () => { + it('should return an observable for the global config', async () => { + const configService = createConfigService(); + + const legacyConfig = await getGlobalConfig$(configService).pipe(take(1)).toPromise(); + + expect(legacyConfig).toStrictEqual({ + kibana: { + index: '.kibana', + autocompleteTerminateAfter: duration(100000), + autocompleteTimeout: duration(1000), + }, + elasticsearch: { + shardTimeout: duration(30, 's'), + requestTimeout: duration(30, 's'), + pingTimeout: duration(30, 's'), + }, + path: { data: fromRoot('data') }, + savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) }, + }); + }); + }); +}); diff --git a/src/core/server/plugins/legacy_config.ts b/src/core/server/plugins/legacy_config.ts new file mode 100644 index 0000000000000..748a1e3190640 --- /dev/null +++ b/src/core/server/plugins/legacy_config.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { map, shareReplay } from 'rxjs/operators'; +import { combineLatest, Observable } from 'rxjs'; +import { PathConfigType, config as pathConfig } from '@kbn/utils'; +import { pick, deepFreeze } from '@kbn/std'; +import { IConfigService } from '@kbn/config'; + +import { SharedGlobalConfig, SharedGlobalConfigKeys } from './types'; +import { KibanaConfigType, config as kibanaConfig } from '../kibana_config'; +import { + ElasticsearchConfigType, + config as elasticsearchConfig, +} from '../elasticsearch/elasticsearch_config'; +import { SavedObjectsConfigType, savedObjectsConfig } from '../saved_objects/saved_objects_config'; + +const createGlobalConfig = ({ + kibana, + elasticsearch, + path, + savedObjects, +}: { + kibana: KibanaConfigType; + elasticsearch: ElasticsearchConfigType; + path: PathConfigType; + savedObjects: SavedObjectsConfigType; +}): SharedGlobalConfig => { + return deepFreeze({ + kibana: pick(kibana, SharedGlobalConfigKeys.kibana), + elasticsearch: pick(elasticsearch, SharedGlobalConfigKeys.elasticsearch), + path: pick(path, SharedGlobalConfigKeys.path), + savedObjects: pick(savedObjects, SharedGlobalConfigKeys.savedObjects), + }); +}; + +export const getGlobalConfig = (configService: IConfigService): SharedGlobalConfig => { + return createGlobalConfig({ + kibana: configService.atPathSync(kibanaConfig.path), + elasticsearch: configService.atPathSync(elasticsearchConfig.path), + path: configService.atPathSync(pathConfig.path), + savedObjects: configService.atPathSync(savedObjectsConfig.path), + }); +}; + +export const getGlobalConfig$ = (configService: IConfigService): Observable => { + return combineLatest([ + configService.atPath(kibanaConfig.path), + configService.atPath(elasticsearchConfig.path), + configService.atPath(pathConfig.path), + configService.atPath(savedObjectsConfig.path), + ]).pipe( + map( + ([kibana, elasticsearch, path, savedObjects]) => + createGlobalConfig({ + kibana, + elasticsearch, + path, + savedObjects, + }), + shareReplay(1) + ) + ); +}; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 3d212bc555828..c71102df9929b 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -17,15 +17,8 @@ import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; import { PluginManifest } from './types'; import { Server } from '../server'; import { fromRoot } from '../utils'; -import { ByteSizeValue } from '@kbn/config-schema'; - -const logger = loggingSystemMock.create(); - -let coreId: symbol; -let env: Env; -let coreContext: CoreContext; -let server: Server; -let instanceInfo: InstanceInfo; +import { schema, ByteSizeValue } from '@kbn/config-schema'; +import { ConfigService } from '@kbn/config'; function createPluginManifest(manifestProps: Partial = {}): PluginManifest { return { @@ -43,61 +36,112 @@ function createPluginManifest(manifestProps: Partial = {}): Plug } describe('createPluginInitializerContext', () => { + let logger: ReturnType; + let coreId: symbol; + let opaqueId: symbol; + let env: Env; + let coreContext: CoreContext; + let server: Server; + let instanceInfo: InstanceInfo; + beforeEach(async () => { + logger = loggingSystemMock.create(); coreId = Symbol('core'); + opaqueId = Symbol(); instanceInfo = { uuid: 'instance-uuid', }; env = Env.createDefault(REPO_ROOT, getEnvOptions()); const config$ = rawConfigServiceMock.create({ rawConfig: {} }); server = new Server(config$, env, logger); - await server.setupCoreConfig(); + server.setupCoreConfig(); coreContext = { coreId, env, logger, configService: server.configService }; }); - it('should return a globalConfig handler in the context', async () => { - const manifest = createPluginManifest(); - const opaqueId = Symbol(); - const pluginInitializerContext = createPluginInitializerContext( - coreContext, - opaqueId, - manifest, - instanceInfo - ); + describe('context.config', () => { + it('config.get() should return the plugin config synchronously', async () => { + const config$ = rawConfigServiceMock.create({ + rawConfig: { + plugin: { + foo: 'bar', + answer: 42, + }, + }, + }); + + const configService = new ConfigService(config$, env, logger); + configService.setSchema( + 'plugin', + schema.object({ + foo: schema.string(), + answer: schema.number(), + }) + ); + await configService.validate(); - expect(pluginInitializerContext.config.legacy.globalConfig$).toBeDefined(); + coreContext = { coreId, env, logger, configService }; - const configObject = await pluginInitializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); - expect(configObject).toStrictEqual({ - kibana: { - index: '.kibana', - autocompleteTerminateAfter: duration(100000), - autocompleteTimeout: duration(1000), - }, - elasticsearch: { - shardTimeout: duration(30, 's'), - requestTimeout: duration(30, 's'), - pingTimeout: duration(30, 's'), - }, - path: { data: fromRoot('data') }, - savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) }, + const manifest = createPluginManifest({ + configPath: 'plugin', + }); + + const pluginInitializerContext = createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ); + + expect(pluginInitializerContext.config.get()).toEqual({ + foo: 'bar', + answer: 42, + }); + }); + + it('config.globalConfig$ should be an observable for the global config', async () => { + const manifest = createPluginManifest(); + const pluginInitializerContext = createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ); + + expect(pluginInitializerContext.config.legacy.globalConfig$).toBeDefined(); + + const configObject = await pluginInitializerContext.config.legacy.globalConfig$ + .pipe(first()) + .toPromise(); + expect(configObject).toStrictEqual({ + kibana: { + index: '.kibana', + autocompleteTerminateAfter: duration(100000), + autocompleteTimeout: duration(1000), + }, + elasticsearch: { + shardTimeout: duration(30, 's'), + requestTimeout: duration(30, 's'), + pingTimeout: duration(30, 's'), + }, + path: { data: fromRoot('data') }, + savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) }, + }); }); }); - it('allow to access the provided instance uuid', () => { - const manifest = createPluginManifest(); - const opaqueId = Symbol(); - instanceInfo = { - uuid: 'kibana-uuid', - }; - const pluginInitializerContext = createPluginInitializerContext( - coreContext, - opaqueId, - manifest, - instanceInfo - ); - expect(pluginInitializerContext.env.instanceUuid).toBe('kibana-uuid'); + describe('context.env', () => { + it('should expose the correct instance uuid', () => { + const manifest = createPluginManifest(); + instanceInfo = { + uuid: 'kibana-uuid', + }; + const pluginInitializerContext = createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ); + expect(pluginInitializerContext.env.instanceUuid).toBe('kibana-uuid'); + }); }); }); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 5b0e2ee21a887..3b7dc70b9c054 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -6,27 +6,14 @@ * Public License, v 1. */ -import { map, shareReplay } from 'rxjs/operators'; -import { combineLatest } from 'rxjs'; -import { PathConfigType, config as pathConfig } from '@kbn/utils'; -import { pick, deepFreeze } from '@kbn/std'; +import { shareReplay } from 'rxjs/operators'; import type { RequestHandlerContext } from 'src/core/server'; import { CoreContext } from '../core_context'; import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; -import { - PluginInitializerContext, - PluginManifest, - PluginOpaqueId, - SharedGlobalConfigKeys, -} from './types'; -import { KibanaConfigType, config as kibanaConfig } from '../kibana_config'; -import { - ElasticsearchConfigType, - config as elasticsearchConfig, -} from '../elasticsearch/elasticsearch_config'; +import { PluginInitializerContext, PluginManifest, PluginOpaqueId } from './types'; import { IRouter, RequestHandlerContextProvider } from '../http'; -import { SavedObjectsConfigType, savedObjectsConfig } from '../saved_objects/saved_objects_config'; +import { getGlobalConfig, getGlobalConfig$ } from './legacy_config'; import { CoreSetup, CoreStart } from '..'; export interface InstanceInfo { @@ -78,40 +65,19 @@ export function createPluginInitializerContext( */ config: { legacy: { - /** - * Global configuration - * Note: naming not final here, it will be renamed in a near future (https://github.com/elastic/kibana/issues/46240) - * @deprecated - */ - globalConfig$: combineLatest([ - coreContext.configService.atPath(kibanaConfig.path), - coreContext.configService.atPath(elasticsearchConfig.path), - coreContext.configService.atPath(pathConfig.path), - coreContext.configService.atPath(savedObjectsConfig.path), - ]).pipe( - map(([kibana, elasticsearch, path, savedObjects]) => - deepFreeze({ - kibana: pick(kibana, SharedGlobalConfigKeys.kibana), - elasticsearch: pick(elasticsearch, SharedGlobalConfigKeys.elasticsearch), - path: pick(path, SharedGlobalConfigKeys.path), - savedObjects: pick(savedObjects, SharedGlobalConfigKeys.savedObjects), - }) - ) - ), + globalConfig$: getGlobalConfig$(coreContext.configService), + get: () => getGlobalConfig(coreContext.configService), }, /** * Reads the subset of the config at the `configPath` defined in the plugin - * manifest and validates it against the schema in the static `schema` on - * the given `ConfigClass`. - * @param ConfigClass A class (not an instance of a class) that contains a - * static `schema` that we validate the config at the given `path` against. + * manifest. */ create() { return coreContext.configService.atPath(pluginManifest.configPath).pipe(shareReplay(1)); }, - createIfExists() { - return coreContext.configService.optionalAtPath(pluginManifest.configPath); + get() { + return coreContext.configService.atPathSync(pluginManifest.configPath); }, }, }; diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 9a1403bda3bca..dd2831f77f537 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -219,10 +219,7 @@ export class PluginsService implements CoreService { packageInfo: Readonly; instanceUuid: string; }; + /** + * {@link LoggerFactory | logger factory} instance already bound to the plugin's logging context + * + * @example + * ```typescript + * // plugins/my-plugin/server/plugin.ts + * // "id: myPlugin" in `plugins/my-plugin/kibana.yaml` + * + * export class MyPlugin implements Plugin { + * constructor(private readonly initContext: PluginInitializerContext) { + * this.logger = initContext.logger.get(); + * // `logger` context: `plugins.myPlugin` + * this.mySubLogger = initContext.logger.get('sub'); // or this.logger.get('sub'); + * // `mySubLogger` context: `plugins.myPlugin.sub` + * } + * } + * ``` + */ logger: LoggerFactory; + /** + * Accessors for the plugin's configuration + */ config: { - legacy: { globalConfig$: Observable }; + /** + * Provide access to Kibana legacy configuration values. + * + * @remarks Naming not final here, it may be renamed in a near future + * @deprecated Accessing configuration values outside of the plugin's config scope is highly discouraged + */ + legacy: { + globalConfig$: Observable; + get: () => SharedGlobalConfig; + }; + /** + * Return an observable of the plugin's configuration + * + * @example + * ```typescript + * // plugins/my-plugin/server/plugin.ts + * + * export class MyPlugin implements Plugin { + * constructor(private readonly initContext: PluginInitializerContext) {} + * setup(core) { + * this.configSub = this.initContext.config.create().subscribe((config) => { + * this.myService.reconfigure(config); + * }); + * } + * stop() { + * this.configSub.unsubscribe(); + * } + * ``` + * + * @example + * ```typescript + * // plugins/my-plugin/server/plugin.ts + * + * export class MyPlugin implements Plugin { + * constructor(private readonly initContext: PluginInitializerContext) {} + * async setup(core) { + * this.config = await this.initContext.config.create().pipe(take(1)).toPromise(); + * } + * stop() { + * this.configSub.unsubscribe(); + * } + * ``` + * + * @remarks The underlying observable has a replay effect, meaning that awaiting for the first emission + * will be resolved at next tick, without risks to delay any asynchronous code's workflow. + */ create: () => Observable; - createIfExists: () => Observable; + /** + * Return the current value of the plugin's configuration synchronously. + * + * @example + * ```typescript + * // plugins/my-plugin/server/plugin.ts + * + * export class MyPlugin implements Plugin { + * constructor(private readonly initContext: PluginInitializerContext) {} + * setup(core) { + * const config = this.initContext.config.get(); + * // do something with the config + * } + * } + * ``` + * + * @remarks This should only be used when synchronous access is an absolute necessity, such + * as during the plugin's setup or start lifecycle. For all other usages, + * {@link create} should be used instead. + */ + get: () => T; }; } diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index 17cb209897c25..a918580392caa 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -36,7 +36,7 @@ export class Root { public async setup() { try { - await this.server.setupCoreConfig(); + this.server.setupCoreConfig(); await this.setupLogging(); this.log.debug('setting up root'); return await this.server.setup(); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index fc90284ffe5b2..aadd16bde0ee6 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1841,13 +1841,13 @@ export type PluginInitializer { - // (undocumented) config: { legacy: { globalConfig$: Observable; + get: () => SharedGlobalConfig; }; create: () => Observable; - createIfExists: () => Observable; + get: () => T; }; // (undocumented) env: { @@ -1855,7 +1855,7 @@ export interface PluginInitializerContext { packageInfo: Readonly; instanceUuid: string; }; - // (undocumented) + // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported logger: LoggerFactory; // (undocumented) opaqueId: PluginOpaqueId; @@ -3139,5 +3139,6 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:371:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 60f3f90428d40..cc1087a422e39 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -300,7 +300,7 @@ export class Server { ); } - public async setupCoreConfig() { + public setupCoreConfig() { const configDescriptors: Array> = [ pathConfig, cspConfig, @@ -325,7 +325,7 @@ export class Server { if (descriptor.deprecations) { this.configService.addDeprecationProvider(descriptor.path, descriptor.deprecations); } - await this.configService.setSchema(descriptor.path, descriptor.schema); + this.configService.setSchema(descriptor.path, descriptor.schema); } } } From 8534faf7eeb090a59c3edb5f917cdb89ae7ff179 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Thu, 28 Jan 2021 14:05:54 +0100 Subject: [PATCH 074/163] migrations v2: fix snapshot builds (#89541) * migrations v2: fix snapshot builds * Revert "Fix sharing saved objects phase 2 CI (#89056)" This reverts commit 8263d47d378315bdcad990620010c8452f3133d1. --- .../migrations/core/document_migrator.test.ts | 14 -------------- .../migrations/core/document_migrator.ts | 3 +-- .../migrations/kibana/kibana_migrator.test.ts | 8 ++++++++ .../migrations/kibana/kibana_migrator.ts | 3 +-- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 6ba652abda3d5..741f715ba6ebe 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -206,20 +206,6 @@ describe('DocumentMigrator', () => { ); }); - it('coerces the current Kibana version if it has a hyphen', () => { - const validDefinition = { - kibanaVersion: '3.2.0-SNAPSHOT', - typeRegistry: createRegistry({ - name: 'foo', - convertToMultiNamespaceTypeVersion: '3.2.0', - namespaceType: 'multiple', - }), - minimumConvertVersion: '0.0.0', - log: mockLogger, - }; - expect(() => new DocumentMigrator(validDefinition)).not.toThrowError(); - }); - it('validates convertToMultiNamespaceTypeVersion is not used on a patch version', () => { const invalidDefinition = { kibanaVersion: '3.2.3', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index e93586ec7ce4c..e4b89a949d3cf 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -159,11 +159,10 @@ export class DocumentMigrator implements VersionedTransformer { */ constructor({ typeRegistry, - kibanaVersion: rawKibanaVersion, + kibanaVersion, minimumConvertVersion = DEFAULT_MINIMUM_CONVERT_VERSION, log, }: DocumentMigratorOptions) { - const kibanaVersion = rawKibanaVersion.split('-')[0]; // coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) to a regular semver (x.y.z) validateMigrationDefinition(typeRegistry, kibanaVersion, minimumConvertVersion); this.documentMigratorOptions = { typeRegistry, kibanaVersion, log }; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 281d2ce8d03cf..dac93ff29b68f 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -31,6 +31,14 @@ const createRegistry = (types: Array>) => { }; describe('KibanaMigrator', () => { + describe('constructor', () => { + it('coerces the current Kibana version if it has a hyphen', () => { + const options = mockOptions(); + options.kibanaVersion = '3.2.1-SNAPSHOT'; + const migrator = new KibanaMigrator(options); + expect(migrator.kibanaVersion).toEqual('3.2.1'); + }); + }); describe('getActiveMappings', () => { it('returns full index mappings w/ core properties', () => { const options = mockOptions(); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index c8bc4b2e14123..ecef84a6e297c 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -90,13 +90,12 @@ export class KibanaMigrator { }: KibanaMigratorOptions) { this.client = client; this.kibanaConfig = kibanaConfig; - this.kibanaVersion = kibanaVersion; this.savedObjectsConfig = savedObjectsConfig; this.typeRegistry = typeRegistry; this.serializer = new SavedObjectsSerializer(this.typeRegistry); this.mappingProperties = mergeTypes(this.typeRegistry.getAllTypes()); this.log = logger; - this.kibanaVersion = kibanaVersion; + this.kibanaVersion = kibanaVersion.split('-')[0]; // coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) to a regular semver (x.y.z); this.documentMigrator = new DocumentMigrator({ kibanaVersion, typeRegistry, From b4931e6f5eddd8f838ef89d9cf3d2f6e40a24722 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 28 Jan 2021 15:42:47 +0200 Subject: [PATCH 075/163] Enable right click on visualizations and dashboards listings (#88936) * Enable right-click on visualizations listing page * Make it simpler * Enable right click to the dashboard listing * Add unit tests * Fix link on dashboard * Fix visualize link * Fix PR comments * Fix functional test failure * Use kbnUrlStateStorage instead * Change method to getDashboardListItemLink * Change method to getVisualizeListItemLink Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/listing/dashboard_listing.tsx | 27 +++- .../get_dashboard_list_item_link.test.ts | 142 ++++++++++++++++++ .../listing/get_dashboard_list_item_link.ts | 33 ++++ src/plugins/visualize/common/constants.ts | 2 + .../components/visualize_listing.tsx | 10 +- .../application/utils/get_table_columns.tsx | 27 ++-- .../get_visualize_list_item_link.test.ts | 125 +++++++++++++++ .../utils/get_visualize_list_item_link.ts | 31 ++++ src/plugins/visualize/public/url_generator.ts | 4 +- 9 files changed, 370 insertions(+), 31 deletions(-) create mode 100644 src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts create mode 100644 src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts create mode 100644 src/plugins/visualize/public/application/utils/get_visualize_list_item_link.test.ts create mode 100644 src/plugins/visualize/public/application/utils/get_visualize_list_item_link.ts diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index cdf809e078b7b..07de4cd52bba6 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -9,16 +9,15 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import React, { Fragment, useCallback, useEffect, useMemo } from 'react'; - import { attemptLoadDashboardByTitle } from '../lib'; import { DashboardAppServices, DashboardRedirect } from '../types'; import { getDashboardBreadcrumb, dashboardListingTable } from '../../dashboard_strings'; import { ApplicationStart, SavedObjectsFindOptionsReference } from '../../../../../core/public'; - import { syncQueryStateWithUrl } from '../../services/data'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; import { TableListView, useKibana } from '../../services/kibana_react'; import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss'; +import { getDashboardListItemLink } from './get_dashboard_list_item_link'; export interface DashboardListingProps { kbnUrlStateStorage: IKbnUrlStateStorage; @@ -83,8 +82,13 @@ export const DashboardListing = ({ const tableColumns = useMemo( () => - getTableColumns((id) => redirectTo({ destination: 'dashboard', id }), savedObjectsTagging), - [savedObjectsTagging, redirectTo] + getTableColumns( + core.application, + kbnUrlStateStorage, + core.uiSettings.get('state:storeInSessionStorage'), + savedObjectsTagging + ), + [core.application, core.uiSettings, kbnUrlStateStorage, savedObjectsTagging] ); const noItemsFragment = useMemo( @@ -99,7 +103,6 @@ export const DashboardListing = ({ (filter: string) => { let searchTerm = filter; let references: SavedObjectsFindOptionsReference[] | undefined; - if (savedObjectsTagging) { const parsed = savedObjectsTagging.ui.parseSearchQuery(filter, { useName: true, @@ -164,7 +167,9 @@ export const DashboardListing = ({ }; const getTableColumns = ( - redirectTo: (id?: string) => void, + application: ApplicationStart, + kbnUrlStateStorage: IKbnUrlStateStorage, + useHash: boolean, savedObjectsTagging?: SavedObjectsTaggingApi ) => { return [ @@ -172,9 +177,15 @@ const getTableColumns = ( field: 'title', name: dashboardListingTable.getTitleColumnName(), sortable: true, - render: (field: string, record: { id: string; title: string }) => ( + render: (field: string, record: { id: string; title: string; timeRestore: boolean }) => ( redirectTo(record.id)} + href={getDashboardListItemLink( + application, + kbnUrlStateStorage, + useHash, + record.id, + record.timeRestore + )} data-test-subj={`dashboardListingTitleLink-${record.title.split(' ').join('-')}`} > {field} diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts new file mode 100644 index 0000000000000..6dbc76803af90 --- /dev/null +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { getDashboardListItemLink } from './get_dashboard_list_item_link'; +import { ApplicationStart } from 'kibana/public'; +import { esFilters } from '../../../../data/public'; +import { createHashHistory } from 'history'; +import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; + +const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647'; + +const application = ({ + getUrlForApp: jest.fn((appId: string, options?: { path?: string; absolute?: boolean }) => { + return `/app/${appId}${options?.path}`; + }), +} as unknown) as ApplicationStart; + +const history = createHashHistory(); +const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + useHash: false, +}); +kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { time: { from: 'now-7d', to: 'now' } }); + +describe('listing dashboard link', () => { + test('creates a link to a dashboard without the timerange query if time is saved on the dashboard', async () => { + const url = getDashboardListItemLink( + application, + kbnUrlStateStorage, + false, + DASHBOARD_ID, + true + ); + expect(url).toMatchInlineSnapshot(`"/app/dashboards#/view/${DASHBOARD_ID}?_g=()"`); + }); + + test('creates a link to a dashboard with the timerange query if time is not saved on the dashboard', async () => { + const url = getDashboardListItemLink( + application, + kbnUrlStateStorage, + false, + DASHBOARD_ID, + false + ); + expect(url).toMatchInlineSnapshot( + `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(time:(from:now-7d,to:now))"` + ); + }); +}); + +describe('when global time changes', () => { + beforeEach(() => { + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { + time: { + from: '2021-01-05T11:45:53.375Z', + to: '2021-01-21T11:46:00.990Z', + }, + }); + }); + + test('propagates the correct time on the query', async () => { + const url = getDashboardListItemLink( + application, + kbnUrlStateStorage, + false, + DASHBOARD_ID, + false + ); + expect(url).toMatchInlineSnapshot( + `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(time:(from:'2021-01-05T11:45:53.375Z',to:'2021-01-21T11:46:00.990Z'))"` + ); + }); +}); + +describe('when global refreshInterval changes', () => { + beforeEach(() => { + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { + refreshInterval: { pause: false, value: 300 }, + }); + }); + + test('propagates the refreshInterval on the query', async () => { + const url = getDashboardListItemLink( + application, + kbnUrlStateStorage, + false, + DASHBOARD_ID, + false + ); + expect(url).toMatchInlineSnapshot( + `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(refreshInterval:(pause:!f,value:300))"` + ); + }); +}); + +describe('when global filters change', () => { + beforeEach(() => { + const filters = [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'q1' }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'q1' }, + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + }, + ]; + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { + filters, + }); + }); + + test('propagates the filters on the query', async () => { + const url = getDashboardListItemLink( + application, + kbnUrlStateStorage, + false, + DASHBOARD_ID, + false + ); + expect(url).toMatchInlineSnapshot( + `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1)),('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))))"` + ); + }); +}); diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts new file mode 100644 index 0000000000000..d14638b9e231f --- /dev/null +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +import { ApplicationStart } from 'kibana/public'; +import { QueryState } from '../../../../data/public'; +import { setStateToKbnUrl } from '../../../../kibana_utils/public'; +import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { IKbnUrlStateStorage } from '../../services/kibana_utils'; + +export const getDashboardListItemLink = ( + application: ApplicationStart, + kbnUrlStateStorage: IKbnUrlStateStorage, + useHash: boolean, + id: string, + timeRestore: boolean +) => { + let url = application.getUrlForApp(DashboardConstants.DASHBOARDS_ID, { + path: `#${createDashboardEditUrl(id)}`, + }); + const globalStateInUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY) || {}; + + if (timeRestore) { + delete globalStateInUrl.time; + delete globalStateInUrl.refreshInterval; + } + url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, globalStateInUrl, { useHash }, url); + return url; +}; diff --git a/src/plugins/visualize/common/constants.ts b/src/plugins/visualize/common/constants.ts index b37eadd9b67e5..7e353ca86698a 100644 --- a/src/plugins/visualize/common/constants.ts +++ b/src/plugins/visualize/common/constants.ts @@ -7,3 +7,5 @@ */ export const AGGS_TERMS_SIZE_SETTING = 'discover:aggs:terms:size'; +export const STATE_STORAGE_KEY = '_a'; +export const GLOBAL_STATE_STORAGE_KEY = '_g'; diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 38e2b59009b38..34131ae2dc7fb 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -40,6 +40,7 @@ export const VisualizeListing = () => { savedObjectsTagging, uiSettings, visualizeCapabilities, + kbnUrlStateStorage, }, } = useKibana(); const { pathname } = useLocation(); @@ -94,11 +95,10 @@ export const VisualizeListing = () => { ); const noItemsFragment = useMemo(() => getNoItemsMessage(createNewVis), [createNewVis]); - const tableColumns = useMemo(() => getTableColumns(application, history, savedObjectsTagging), [ - application, - history, - savedObjectsTagging, - ]); + const tableColumns = useMemo( + () => getTableColumns(application, kbnUrlStateStorage, savedObjectsTagging), + [application, kbnUrlStateStorage, savedObjectsTagging] + ); const fetchItems = useCallback( (filter) => { diff --git a/src/plugins/visualize/public/application/utils/get_table_columns.tsx b/src/plugins/visualize/public/application/utils/get_table_columns.tsx index daa419b5f31b4..d9dafa7335671 100644 --- a/src/plugins/visualize/public/application/utils/get_table_columns.tsx +++ b/src/plugins/visualize/public/application/utils/get_table_columns.tsx @@ -7,14 +7,15 @@ */ import React from 'react'; -import { History } from 'history'; import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiLink, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - import { ApplicationStart } from 'kibana/public'; +import { IKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; import { VisualizationListItem } from 'src/plugins/visualizations/public'; import type { SavedObjectsTaggingApi } from 'src/plugins/saved_objects_tagging_oss/public'; +import { RedirectAppLinks } from '../../../../kibana_react/public'; +import { getVisualizeListItemLink } from './get_visualize_list_item_link'; const getBadge = (item: VisualizationListItem) => { if (item.stage === 'beta') { @@ -72,7 +73,7 @@ const renderItemTypeIcon = (item: VisualizationListItem) => { export const getTableColumns = ( application: ApplicationStart, - history: History, + kbnUrlStateStorage: IKbnUrlStateStorage, taggingApi?: SavedObjectsTaggingApi ) => [ { @@ -84,18 +85,14 @@ export const getTableColumns = ( render: (field: string, { editApp, editUrl, title, error }: VisualizationListItem) => // In case an error occurs i.e. the vis has wrong type, we render the vis but without the link !error ? ( - { - if (editApp) { - application.navigateToApp(editApp, { path: editUrl }); - } else if (editUrl) { - history.push(editUrl); - } - }} - data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`} - > - {field} - + + + {field} + + ) : ( field ), diff --git a/src/plugins/visualize/public/application/utils/get_visualize_list_item_link.test.ts b/src/plugins/visualize/public/application/utils/get_visualize_list_item_link.test.ts new file mode 100644 index 0000000000000..80fd1c8740f2c --- /dev/null +++ b/src/plugins/visualize/public/application/utils/get_visualize_list_item_link.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { getVisualizeListItemLink } from './get_visualize_list_item_link'; +import { ApplicationStart } from 'kibana/public'; +import { createHashHistory } from 'history'; +import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; +import { esFilters } from '../../../../data/public'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../../common/constants'; + +jest.mock('../../services', () => { + return { + getUISettings: () => ({ + get: jest.fn(), + }), + }; +}); + +const application = ({ + getUrlForApp: jest.fn((appId: string, options?: { path?: string; absolute?: boolean }) => { + return `/app/${appId}${options?.path}`; + }), +} as unknown) as ApplicationStart; + +const history = createHashHistory(); +const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + useHash: false, +}); +kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { time: { from: 'now-7d', to: 'now' } }); + +describe('listing item link is correct for each app', () => { + test('creates a link to classic visualization if editApp is not defined', async () => { + const editUrl = 'edit/id'; + const url = getVisualizeListItemLink(application, kbnUrlStateStorage, undefined, editUrl); + expect(url).toMatchInlineSnapshot(`"/app/visualize#${editUrl}?_g=(time:(from:now-7d,to:now))"`); + }); + + test('creates a link for the app given if editApp is defined', async () => { + const editUrl = '#/edit/id'; + const editApp = 'lens'; + const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl); + expect(url).toMatchInlineSnapshot(`"/app/${editApp}${editUrl}?_g=(time:(from:now-7d,to:now))"`); + }); + + describe('when global time changes', () => { + beforeEach(() => { + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { + time: { + from: '2021-01-05T11:45:53.375Z', + to: '2021-01-21T11:46:00.990Z', + }, + }); + }); + + test('it propagates the correct time on the query', async () => { + const editUrl = '#/edit/id'; + const editApp = 'lens'; + const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl); + expect(url).toMatchInlineSnapshot( + `"/app/${editApp}${editUrl}?_g=(time:(from:'2021-01-05T11:45:53.375Z',to:'2021-01-21T11:46:00.990Z'))"` + ); + }); + }); + + describe('when global refreshInterval changes', () => { + beforeEach(() => { + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { + refreshInterval: { pause: false, value: 300 }, + }); + }); + + test('it propagates the refreshInterval on the query', async () => { + const editUrl = '#/edit/id'; + const editApp = 'lens'; + const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl); + expect(url).toMatchInlineSnapshot( + `"/app/${editApp}${editUrl}?_g=(refreshInterval:(pause:!f,value:300))"` + ); + }); + }); + + describe('when global filters change', () => { + beforeEach(() => { + const filters = [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'q1' }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'q1' }, + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + }, + ]; + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { + filters, + }); + }); + + test('propagates the filters on the query', async () => { + const editUrl = '#/edit/id'; + const editApp = 'lens'; + const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl); + expect(url).toMatchInlineSnapshot( + `"/app/${editApp}${editUrl}?_g=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1)),('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))))"` + ); + }); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/get_visualize_list_item_link.ts b/src/plugins/visualize/public/application/utils/get_visualize_list_item_link.ts new file mode 100644 index 0000000000000..2ded3ce8c2745 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/get_visualize_list_item_link.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +import { ApplicationStart } from 'kibana/public'; +import { IKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; +import { QueryState } from '../../../../data/public'; +import { setStateToKbnUrl } from '../../../../kibana_utils/public'; +import { getUISettings } from '../../services'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../../common/constants'; +import { APP_NAME } from '../visualize_constants'; + +export const getVisualizeListItemLink = ( + application: ApplicationStart, + kbnUrlStateStorage: IKbnUrlStateStorage, + editApp: string | undefined, + editUrl: string +) => { + // for visualizations the editApp is undefined + let url = application.getUrlForApp(editApp ?? APP_NAME, { + path: editApp ? editUrl : `#${editUrl}`, + }); + const useHash = getUISettings().get('state:storeInSessionStorage'); + const globalStateInUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY) || {}; + + url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, globalStateInUrl, { useHash }, url); + return url; +}; diff --git a/src/plugins/visualize/public/url_generator.ts b/src/plugins/visualize/public/url_generator.ts index 15f05106130de..57fa9b2ae4801 100644 --- a/src/plugins/visualize/public/url_generator.ts +++ b/src/plugins/visualize/public/url_generator.ts @@ -16,9 +16,7 @@ import { } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { UrlGeneratorsDefinition } from '../../share/public'; - -const STATE_STORAGE_KEY = '_a'; -const GLOBAL_STATE_STORAGE_KEY = '_g'; +import { STATE_STORAGE_KEY, GLOBAL_STATE_STORAGE_KEY } from '../common/constants'; export const VISUALIZE_APP_URL_GENERATOR = 'VISUALIZE_APP_URL_GENERATOR'; From ecf7dd46289cde45794fb9af8a6c8b87494a9b28 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 28 Jan 2021 14:26:01 +0000 Subject: [PATCH 076/163] chore(NA): add ignore and rc files for bazel (#89524) * chore(NA): add bazel related ignore rules * chore(NA): add bazelrc files setup * chore(NA): reword on bazelrc comment * chore(NA): update .eslintignore Co-authored-by: Tyler Smalley * chore(NA): rename .bazelrc into .bazelrc-ci * chore(NA): update .gitignore Co-authored-by: Tyler Smalley Co-authored-by: Tyler Smalley --- .bazelignore | 15 ++++ .bazelrc | 9 +++ .bazelrc.common | 118 ++++++++++++++++++++++++++++ .eslintignore | 3 + .gitignore | 4 + src/dev/ci_setup/.bazelrc-ci | 10 +++ src/dev/ci_setup/.bazelrc-ci.common | 11 +++ src/dev/ci_setup/setup.sh | 5 ++ 8 files changed, 175 insertions(+) create mode 100644 .bazelignore create mode 100644 .bazelrc create mode 100644 .bazelrc.common create mode 100644 src/dev/ci_setup/.bazelrc-ci create mode 100644 src/dev/ci_setup/.bazelrc-ci.common diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000000000..b71007690f1cf --- /dev/null +++ b/.bazelignore @@ -0,0 +1,15 @@ +# Bazel does not support wildcards like .gitignore +# Issues are opened for to include that feature but not available yet +# https://github.com/bazelbuild/bazel/issues/7093 +# https://github.com/bazelbuild/bazel/issues/8106 +.ci +.git +.github +.idea +.teamcity +.yarn-local-mirror +bazel-cache +bazel-dist +build +node_modules +target diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000000000..fd469d1203a82 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,9 @@ +# Inspired on from https://raw.githubusercontent.com/bazelbuild/rules_nodejs/master/.bazelrc +# Import shared settings first so we can override below +import %workspace%/.bazelrc.common + +# Remote cache settings for local env +# build --remote_cache=https://storage.googleapis.com/kibana-bazel-cache +# build --incompatible_remote_results_ignore_disk=true +# build --remote_accept_cached=true +# build --remote_upload_local_results=false diff --git a/.bazelrc.common b/.bazelrc.common new file mode 100644 index 0000000000000..a53d1b8072483 --- /dev/null +++ b/.bazelrc.common @@ -0,0 +1,118 @@ +# Inspired on from https://raw.githubusercontent.com/bazelbuild/rules_nodejs/master/common.bazelrc +# Common Bazel settings for JavaScript/NodeJS workspaces +# This rc file is automatically discovered when Bazel is run in this workspace, +# see https://docs.bazel.build/versions/master/guide.html#bazelrc +# +# The full list of Bazel options: https://docs.bazel.build/versions/master/command-line-reference.html + +# Cache action outputs on disk so they persist across output_base and bazel shutdown (eg. changing branches) +build --disk_cache=bazel-cache/disk-cache + +# Bazel repo cache settings +build --repository_cache=bazel-cache/repository-cache + +# Bazel will create symlinks from the workspace directory to output artifacts. +# Build results will be placed in a directory called "bazel-dist/bin" +# This will still create a bazel-out symlink in +# the project directory, which must be excluded from the +# editor's search path. +build --symlink_prefix=bazel-dist/ +# To disable the symlinks altogether (including bazel-out) we can use +# build --symlink_prefix=/ +# however this makes it harder to find outputs. + +# Prevents the creation of bazel-out dir +build --experimental_no_product_name_out_symlink + +# Make direct file system calls to create symlink trees +build --experimental_inprocess_symlink_creation + +# Incompatible flags to run with +build --incompatible_no_implicit_file_export +build --incompatible_restrict_string_escapes + +# Log configs +## different from default +common --color=yes +build --show_task_finish +build --noshow_progress +build --noshow_loading_progress + +## enforced default values +build --show_result=1 + +# Specifies desired output mode for running tests. +# Valid values are +# 'summary' to output only test status summary +# 'errors' to also print test logs for failed tests +# 'all' to print logs for all tests +# 'streamed' to output logs for all tests in real time +# (this will force tests to be executed locally one at a time regardless of --test_strategy value). +test --test_output=errors + +# Support for debugging NodeJS tests +# Add the Bazel option `--config=debug` to enable this +# --test_output=streamed +# Stream stdout/stderr output from each test in real-time. +# See https://docs.bazel.build/versions/master/user-manual.html#flag--test_output for more details. +# --test_strategy=exclusive +# Run one test at a time. +# --test_timeout=9999 +# Prevent long running tests from timing out +# See https://docs.bazel.build/versions/master/user-manual.html#flag--test_timeout for more details. +# --nocache_test_results +# Always run tests +# --node_options=--inspect-brk +# Pass the --inspect-brk option to all tests which enables the node inspector agent. +# See https://nodejs.org/de/docs/guides/debugging-getting-started/#command-line-options for more details. +# --define=VERBOSE_LOGS=1 +# Rules will output verbose logs if the VERBOSE_LOGS environment variable is set. `VERBOSE_LOGS` will be passed to +# `nodejs_binary` and `nodejs_test` via the default value of the `default_env_vars` attribute of those rules. +# --compilation_mode=dbg +# Rules may change their build outputs if the compilation mode is set to dbg. For example, +# mininfiers such as terser may make their output more human readable when this is set. `COMPILATION_MODE` will be passed to +# `nodejs_binary` and `nodejs_test` via the default value of the `default_env_vars` attribute of those rules. +# See https://docs.bazel.build/versions/master/user-manual.html#flag--compilation_mode for more details. +test:debug --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results --define=VERBOSE_LOGS=1 +# Use bazel run with `--config=debug` to turn on the NodeJS inspector agent. +# The node process will break before user code starts and wait for the debugger to connect. +run:debug --define=VERBOSE_LOGS=1 -- --node_options=--inspect-brk +# The following option will change the build output of certain rules such as terser and may not be desirable in all cases +build:debug --compilation_mode=dbg + +# Turn off legacy external runfiles +# This prevents accidentally depending on this feature, which Bazel will remove. +build --nolegacy_external_runfiles +run --nolegacy_external_runfiles +test --nolegacy_external_runfiles + +# Turn on --incompatible_strict_action_env which was on by default +# in Bazel 0.21.0 but turned off again in 0.22.0. Follow +# https://github.com/bazelbuild/bazel/issues/7026 for more details. +# This flag is needed to so that the bazel cache is not invalidated +# when running bazel via `yarn bazel`. +# See https://github.com/angular/angular/issues/27514. +build --incompatible_strict_action_env +run --incompatible_strict_action_env +test --incompatible_strict_action_env + +# Do not build runfile trees by default. If an execution strategy relies on runfile +# symlink tree, the tree is created on-demand. See: https://github.com/bazelbuild/bazel/issues/6627 +# and https://github.com/bazelbuild/bazel/commit/03246077f948f2790a83520e7dccc2625650e6df +build --nobuild_runfile_links + +# When running `bazel coverage` --instrument_test_targets needs to be set in order to +# collect coverage information from test targets +coverage --instrument_test_targets + +# Settings for CI +# Bazel flags for CI are in /src/dev/ci_setup/.bazelrc-ci + +# Load any settings specific to the current user. +# .bazelrc.user should appear in .gitignore so that settings are not shared with team members +# This needs to be last statement in this +# config, as the user configuration should be able to overwrite flags from this file. +# See https://docs.bazel.build/versions/master/best-practices.html#bazelrc +# (Note that we use .bazelrc.user so the file appears next to .bazelrc in directory listing, +# rather than user.bazelrc as suggested in the Bazel docs) +try-import %workspace%/.bazelrc.user diff --git a/.eslintignore b/.eslintignore index 4ef96ebab062a..e74a3d6deaa8b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -43,3 +43,6 @@ snapshots.js /packages/kbn-ui-framework/dist /packages/kbn-ui-shared-deps/flot_charts /packages/kbn-monaco/src/painless/antlr + +# Bazel +/bazel-* diff --git a/.gitignore b/.gitignore index 79d022a2d701b..2d7dd52e3ef9e 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,7 @@ report.asciidoc # Yarn local mirror content .yarn-local-mirror + +# Bazel +/bazel-* +/.bazelrc.user diff --git a/src/dev/ci_setup/.bazelrc-ci b/src/dev/ci_setup/.bazelrc-ci new file mode 100644 index 0000000000000..5b345d3c9e207 --- /dev/null +++ b/src/dev/ci_setup/.bazelrc-ci @@ -0,0 +1,10 @@ +# Inspired on https://github.com/angular/angular/blob/master/.circleci/bazel.linux.rc +# These options are only enabled when running on CI +# That is done by copying this file into "$HOME/.bazelrc" which loads after the .bazelrc into the workspace + +# Import and load bazelrc common settings for ci env +try-import %workspace%/src/dev/ci_setup/.bazelrc-ci.common + +# Remote cache settings for ci env +# build --google_default_credentials +# build --remote_upload_local_results=true diff --git a/src/dev/ci_setup/.bazelrc-ci.common b/src/dev/ci_setup/.bazelrc-ci.common new file mode 100644 index 0000000000000..3f58e4e03a178 --- /dev/null +++ b/src/dev/ci_setup/.bazelrc-ci.common @@ -0,0 +1,11 @@ +# Inspired on https://github.com/angular/angular/blob/master/.circleci/bazel.common.rc +# Settings in this file should be OS agnostic + +# Don't be spammy in the logs +build --noshow_progress + +# Print all the options that apply to the build. +build --announce_rc + +# More details on failures +build --verbose_failures=true diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 61f578ba33971..e5e21e312b0dd 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -65,3 +65,8 @@ if [ "$GIT_CHANGES" ]; then echo -e "$GIT_CHANGES\n" exit 1 fi + +### +### copy .bazelrc-ci into $HOME/.bazelrc +### +cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; From 681b2397c61f38a4141928f6e1d7dbcb49fc13ab Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 28 Jan 2021 07:39:21 -0700 Subject: [PATCH 077/163] Migrates license_management to a TS project ref (#89472) --- .../plugins/license_management/tsconfig.json | 29 +++++++++++++++++++ x-pack/test/tsconfig.json | 3 +- x-pack/tsconfig.json | 4 ++- x-pack/tsconfig.refs.json | 3 +- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/license_management/tsconfig.json diff --git a/x-pack/plugins/license_management/tsconfig.json b/x-pack/plugins/license_management/tsconfig.json new file mode 100644 index 0000000000000..e6cb0101ee838 --- /dev/null +++ b/x-pack/plugins/license_management/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "common/**/*", + "__jest__/**/*", + "__mocks__/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/telemetry_management_section/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../../../src/plugins/telemetry/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../licensing/tsconfig.json"}, + { "path": "../features/tsconfig.json"}, + { "path": "../security/tsconfig.json"}, + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 6368751fedf75..6a75f0c7e02d3 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -58,6 +58,7 @@ { "path": "../plugins/beats_management/tsconfig.json" }, { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "../plugins/global_search_bar/tsconfig.json" } + { "path": "../plugins/global_search_bar/tsconfig.json" }, + { "path": "../plugins/license_management/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 64f3cd545a7b5..7ed53ca0abb6b 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -37,6 +37,7 @@ "plugins/cloud/**/*", "plugins/saved_objects_tagging/**/*", "plugins/global_search_bar/**/*", + "plugins/license_management/**/*", "test/**/*" ], "compilerOptions": { @@ -106,6 +107,7 @@ { "path": "./plugins/actions/tsconfig.json"}, { "path": "./plugins/alerts/tsconfig.json"}, { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, - { "path": "./plugins/stack_alerts/tsconfig.json"} + { "path": "./plugins/stack_alerts/tsconfig.json"}, + { "path": "./plugins/license_management/tsconfig.json" }, ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 0de209546ac04..eeba8dd770da6 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -31,6 +31,7 @@ { "path": "./plugins/beats_management/tsconfig.json" }, { "path": "./plugins/cloud/tsconfig.json" }, { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./plugins/global_search_bar/tsconfig.json" } + { "path": "./plugins/global_search_bar/tsconfig.json" }, + { "path": "./plugins/license_management/tsconfig.json" } ] } From 77f8e62d07bff254257dfac8285c918391c6731d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20=C3=81lvarez?= Date: Thu, 28 Jan 2021 15:42:33 +0100 Subject: [PATCH 078/163] update central config to include Go agent (#89445) --- .../agent_configuration/setting_definitions/general_settings.ts | 2 +- .../agent_configuration/setting_definitions/index.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index a41faba2e9382..b76dd85f5ee6c 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -254,6 +254,6 @@ export const generalSettings: RawSettingDefinition[] = [ 'Used to restrict requests to certain URLs from being instrumented. This config accepts a comma-separated list of wildcard patterns of URL paths that should be ignored. When an incoming HTTP request is detected, its request path will be tested against each element in this list. For example, adding `/home/index` to this list would match and remove instrumentation from `http://localhost/home/index` as well as `http://whatever.com/home/index?value1=123`', } ), - includeAgents: ['java', 'nodejs', 'python', 'dotnet', 'ruby'], + includeAgents: ['java', 'nodejs', 'python', 'dotnet', 'ruby', 'go'], }, ]; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index 4f319e4dd7016..1251f9c2f1bec 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -50,6 +50,7 @@ describe('filterByAgent', () => { 'sanitize_field_names', 'span_frames_min_duration', 'stack_trace_limit', + 'transaction_ignore_urls', 'transaction_max_spans', 'transaction_sample_rate', ]); From 45eae10e36703cf4f889c0bf2acdc09686321396 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 28 Jan 2021 15:44:29 +0100 Subject: [PATCH 079/163] update chromedriver dependency to 88 (#89539) --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a64995e57a1f7..4e83d4e1aa45c 100644 --- a/package.json +++ b/package.json @@ -596,7 +596,7 @@ "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^87.0.3", + "chromedriver": "^88.0.0", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compare-versions": "3.5.1", diff --git a/yarn.lock b/yarn.lock index 8532bd89ec397..fcafc23e42d73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8444,7 +8444,7 @@ axe-core@^4.0.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.0.2.tgz#c7cf7378378a51fcd272d3c09668002a4990b1cb" integrity sha512-arU1h31OGFu+LPrOLGZ7nB45v940NMDMEJeNmbutu57P+UFDVnkZg3e+J1I2HJRZ9hT7gO8J91dn/PMrAiKakA== -axios@^0.21.0, axios@^0.21.1: +axios@^0.21.1: version "0.21.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== @@ -10233,13 +10233,13 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^87.0.3: - version "87.0.5" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-87.0.5.tgz#5a56bae6e23fc5eaa0c5ac3b76f936e4dd0989a1" - integrity sha512-bWAKdZANrt3LXMUOKFP+DgW7DjVKfihCbjej6URkUcKsvbQBDYpf5YY5d/dXE3SOSzIFZ7fmLxogusxpsupCJg== +chromedriver@^88.0.0: + version "88.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-88.0.0.tgz#6833fffd516db23c811eeafa1ee1069b5a12fd2f" + integrity sha512-EE8rXh7mikxk3VWKjUsz0KCUX8d3HkQ4HgMNJhWrWjzju12dKPPVHO9MY+YaAI5ryXrXGNf0Y4HcNKgW36P/CA== dependencies: "@testim/chrome-version" "^1.0.7" - axios "^0.21.0" + axios "^0.21.1" del "^6.0.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.0" From b9fd79a9e02e95dffa9d4b621c91a42a23fc1fc4 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 28 Jan 2021 09:07:08 -0600 Subject: [PATCH 080/163] skip query string input render. #85715 --- .../public/ui/query_string_input/query_string_input.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index 426302689c8f0..9784ab7116cfb 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -89,7 +89,7 @@ describe('QueryStringInput', () => { jest.clearAllMocks(); }); - it('Should render the given query', async () => { + it.skip('Should render the given query', async () => { const { getByText } = render( wrapQueryStringInputInContext({ query: kqlQuery, From caed606fbd499b5c3c50cca839212387f8dc29f6 Mon Sep 17 00:00:00 2001 From: ncheckin <68351161+ncheckin@users.noreply.github.com> Date: Thu, 28 Jan 2021 10:19:18 -0500 Subject: [PATCH 081/163] Update redirects.asciidoc (#84719) * Update redirects.asciidoc * Update docs/redirects.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/redirects.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/redirects.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/redirects.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/redirects.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/redirects.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/redirects.asciidoc Co-authored-by: Kaarina Tungseth * Update redirects.asciidoc Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kaarina Tungseth --- docs/redirects.asciidoc | 83 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 4449b58b4bab3..c7bdff800bb0b 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -194,10 +194,89 @@ This page has moved. Refer to <>. This page has moved. Refer to <>. +[float] +[[time-series-visual-builder]] +=== Time Series Visual Builder + +This page was deleted. Refer to <>. + +[float] +[[kibana-keystore-has-moved-from-the-data-folder-to-the-config-folder]] +=== Kibana Keystore has moved from the Data Folder to the Config Folder + +This page has been deleted. Refer to link:https://www.elastic.co/guide/en/kibana/7.9/breaking-changes-7.9.html#user-facing-changes-79[Breaking changes in 7.9]. + +[float] +[[createvis]] +=== Create Visualization + +This page has been deleted. Refer to <>. + +[float] +[[data-table]] +=== Data Table + +This page has been deleted. Refer to <>. + + +[float] +[[xy-chart]] +=== Line, Area, and Bar Chart + +This page has been deleted. Refer to <>. + +[float] +[[add-canvas-events]] +=== Add Canvas Elements + +This page has been moved. Refer to <>. + +[float] +[[vega-lite-tutorial]] +=== Vega-Lite Tutorial + +This page has been moved. Refer to <>. + +[float] +[[heatmap-chart]] +=== Heatmap Chart + +This page has been moved. Refer to <>. + +[float] +[[interface-overview]] +=== Interface Overview + +This page has been moved. Refer to <>. + +[float] +[[time-series-visualizations]] +=== Featured Visualizations + +This page has been moved. Refer to <>. + +[float] +[[timelion-customize]] +=== Customize and format visualizations + +This page has been moved. Refer to <>. + +[float] +[[dashboard-drilldown]] +=== Dashboard Drilldowns + +This page has been moved. Refer to <>. + +[float] +[[development-plugin-localization]] +=== Localization for plugins + +This page has been moved. PRefer to <>. + [role="exclude",id="visualize"] == Visualize -This content has moved. See <>. +This content has moved. Refer to <>. [role="exclude",id="explore-dashboard-data"] -This content has moved. See <>. +This content has moved. Refer to <>. From 7593cf7ea5980fb78fadce5ab2305f62912d06be Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 28 Jan 2021 11:04:23 -0500 Subject: [PATCH 082/163] [Vega] use %type% in docs (#89453) --- docs/user/dashboard/vega-reference.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 4a0598cc569cd..2c961dca44474 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -223,7 +223,7 @@ experimental[] Access the Elastic Map Service files via the same mechanism: ---- url: { // "type" defaults to "elasticsearch" otherwise - type: emsfile + %type%: emsfile // Name of the file, exactly as in the Region map visualization name: World Countries } @@ -289,7 +289,7 @@ experimental[] You can use the *Vega* https://vega.github.io/vega/docs/data/[dat ---- url: { // "type" defaults to "elasticsearch" otherwise - type: emsfile + %type%: emsfile // Name of the file, exactly as in the Region map visualization name: World Countries } From 80b720da11637f600baba770c3d115966d3b6c08 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 28 Jan 2021 09:18:59 -0700 Subject: [PATCH 083/163] [Maps] Geo containment latency and concurrent containment fix (#86980) --- .../alert_types/geo_containment/types.ts | 1 - .../alert_types/geo_containment/alert_type.ts | 2 - .../geo_containment/geo_containment.ts | 105 +++--- .../geo_containment/tests/alert_type.test.ts | 1 - .../tests/geo_containment.test.ts | 325 +++++++++++++----- 5 files changed, 289 insertions(+), 145 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/types.ts index d1f64c9298f15..c67043106c0d1 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/types.ts @@ -18,7 +18,6 @@ export interface GeoContainmentAlertParams extends AlertTypeParams { boundaryIndexId: string; boundaryGeoField: string; boundaryNameField?: string; - delayOffsetWithUnits?: string; indexQuery?: Query; boundaryIndexQuery?: Query; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts index 85e02b21cc78c..6b6f17bf4ba2a 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts @@ -98,7 +98,6 @@ export const ParamsSchema = schema.object({ boundaryIndexId: schema.string({ minLength: 1 }), boundaryGeoField: schema.string({ minLength: 1 }), boundaryNameField: schema.maybe(schema.string({ minLength: 1 })), - delayOffsetWithUnits: schema.maybe(schema.string({ minLength: 1 })), indexQuery: schema.maybe(schema.any({})), boundaryIndexQuery: schema.maybe(schema.any({})), }); @@ -114,7 +113,6 @@ export interface GeoContainmentParams extends AlertTypeParams { boundaryIndexId: string; boundaryGeoField: string; boundaryNameField?: string; - delayOffsetWithUnits?: string; indexQuery?: Query; boundaryIndexQuery?: Query; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index 24232e47225f0..1648ad9ad2a62 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -24,7 +24,7 @@ export function transformResults( results: SearchResponse | undefined, dateField: string, geoField: string -): Map { +): Map { if (!results) { return new Map(); } @@ -64,12 +64,15 @@ export function transformResults( // Get unique .reduce( ( - accu: Map, + accu: Map, el: LatestEntityLocation & { entityName: string } ) => { const { entityName, ...locationData } = el; - if (!accu.has(entityName)) { - accu.set(entityName, locationData); + if (entityName) { + if (!accu.has(entityName)) { + accu.set(entityName, []); + } + accu.get(entityName)!.push(locationData); } return accu; }, @@ -78,26 +81,9 @@ export function transformResults( return orderedResults; } -function getOffsetTime(delayOffsetWithUnits: string, oldTime: Date): Date { - const timeUnit = delayOffsetWithUnits.slice(-1); - const time: number = +delayOffsetWithUnits.slice(0, -1); - - const adjustedDate = new Date(oldTime.getTime()); - if (timeUnit === 's') { - adjustedDate.setSeconds(adjustedDate.getSeconds() - time); - } else if (timeUnit === 'm') { - adjustedDate.setMinutes(adjustedDate.getMinutes() - time); - } else if (timeUnit === 'h') { - adjustedDate.setHours(adjustedDate.getHours() - time); - } else if (timeUnit === 'd') { - adjustedDate.setDate(adjustedDate.getDate() - time); - } - return adjustedDate; -} - export function getActiveEntriesAndGenerateAlerts( - prevLocationMap: Record, - currLocationMap: Map, + prevLocationMap: Map, + currLocationMap: Map, alertInstanceFactory: AlertServices< GeoContainmentInstanceState, GeoContainmentInstanceContext, @@ -106,32 +92,55 @@ export function getActiveEntriesAndGenerateAlerts( shapesIdsNamesMap: Record, currIntervalEndTime: Date ) { - const allActiveEntriesMap: Map = new Map([ - ...Object.entries(prevLocationMap || {}), + const allActiveEntriesMap: Map = new Map([ + ...prevLocationMap, ...currLocationMap, ]); - allActiveEntriesMap.forEach(({ location, shapeLocationId, dateInShape, docId }, entityName) => { - const containingBoundaryName = shapesIdsNamesMap[shapeLocationId] || shapeLocationId; - const context = { - entityId: entityName, - entityDateTime: dateInShape ? new Date(dateInShape).toISOString() : null, - entityDocumentId: docId, - detectionDateTime: new Date(currIntervalEndTime).toISOString(), - entityLocation: `POINT (${location[0]} ${location[1]})`, - containingBoundaryId: shapeLocationId, - containingBoundaryName, - }; - const alertInstanceId = `${entityName}-${containingBoundaryName}`; - if (shapeLocationId === OTHER_CATEGORY) { + allActiveEntriesMap.forEach((locationsArr, entityName) => { + // Generate alerts + locationsArr.forEach(({ location, shapeLocationId, dateInShape, docId }) => { + const context = { + entityId: entityName, + entityDateTime: dateInShape ? new Date(dateInShape).toISOString() : null, + entityDocumentId: docId, + detectionDateTime: new Date(currIntervalEndTime).toISOString(), + entityLocation: `POINT (${location[0]} ${location[1]})`, + containingBoundaryId: shapeLocationId, + containingBoundaryName: shapesIdsNamesMap[shapeLocationId] || shapeLocationId, + }; + const alertInstanceId = `${entityName}-${context.containingBoundaryName}`; + if (shapeLocationId !== OTHER_CATEGORY) { + alertInstanceFactory(alertInstanceId).scheduleActions(ActionGroupId, context); + } + }); + + if (locationsArr[0].shapeLocationId === OTHER_CATEGORY) { allActiveEntriesMap.delete(entityName); + return; + } + + const otherCatIndex = locationsArr.findIndex( + ({ shapeLocationId }) => shapeLocationId === OTHER_CATEGORY + ); + if (otherCatIndex >= 0) { + const afterOtherLocationsArr = locationsArr.slice(0, otherCatIndex); + allActiveEntriesMap.set(entityName, afterOtherLocationsArr); } else { - alertInstanceFactory(alertInstanceId).scheduleActions(ActionGroupId, context); + allActiveEntriesMap.set(entityName, locationsArr); } }); return allActiveEntriesMap; } + export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType['executor'] => - async function ({ previousStartedAt, startedAt, services, params, alertId, state }) { + async function ({ + previousStartedAt: currIntervalStartTime, + startedAt: currIntervalEndTime, + services, + params, + alertId, + state, + }) { const { shapesFilters, shapesIdsNamesMap } = state.shapesFilters ? state : await getShapesFilters( @@ -147,15 +156,6 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ const executeEsQuery = await executeEsQueryFactory(params, services, log, shapesFilters); - let currIntervalStartTime = previousStartedAt; - let currIntervalEndTime = startedAt; - if (params.delayOffsetWithUnits) { - if (currIntervalStartTime) { - currIntervalStartTime = getOffsetTime(params.delayOffsetWithUnits, currIntervalStartTime); - } - currIntervalEndTime = getOffsetTime(params.delayOffsetWithUnits, currIntervalEndTime); - } - // Start collecting data only on the first cycle let currentIntervalResults: SearchResponse | undefined; if (!currIntervalStartTime) { @@ -169,14 +169,17 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ currentIntervalResults = await executeEsQuery(currIntervalStartTime, currIntervalEndTime); } - const currLocationMap: Map = transformResults( + const currLocationMap: Map = transformResults( currentIntervalResults, params.dateField, params.geoField ); + const prevLocationMap: Map = new Map([ + ...Object.entries((state.prevLocationMap as Record) || {}), + ]); const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( - state.prevLocationMap as Record, + prevLocationMap, currLocationMap, services.alertInstanceFactory, shapesIdsNamesMap, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts index 98842c8cc2cba..1aba7d6fb1010 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts @@ -38,7 +38,6 @@ describe('alertType', () => { boundaryIndexId: 'testIndex', boundaryGeoField: 'testField', boundaryNameField: 'testField', - delayOffsetWithUnits: 'testOffset', }; expect(alertType.validate?.params?.validate(params)).toBeTruthy(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts index 26b51060c2e73..40ad6454c3673 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts @@ -27,39 +27,47 @@ describe('geo_containment', () => { new Map([ [ '936', - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, + [ + { + dateInShape: '2020-09-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + location: [-82.8814151789993, 40.62806099653244], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ], ], [ 'AAL2019', - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'iOng1XQB6yyY-xQxnGSM', - location: [-82.22068064846098, 39.006176185794175], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, + [ + { + dateInShape: '2020-09-28T18:01:41.191Z', + docId: 'iOng1XQB6yyY-xQxnGSM', + location: [-82.22068064846098, 39.006176185794175], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ], ], [ 'AAL2323', - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'n-ng1XQB6yyY-xQxnGSM', - location: [-84.71324851736426, 41.6677269525826], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, + [ + { + dateInShape: '2020-09-28T18:01:41.191Z', + docId: 'n-ng1XQB6yyY-xQxnGSM', + location: [-84.71324851736426, 41.6677269525826], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ], ], [ 'ABD5250', - { - dateInShape: '2020-09-28T18:01:41.192Z', - docId: 'GOng1XQB6yyY-xQxnGWM', - location: [6.073727197945118, 39.07997465226799], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, + [ + { + dateInShape: '2020-09-28T18:01:41.192Z', + docId: 'GOng1XQB6yyY-xQxnGWM', + location: [6.073727197945118, 39.07997465226799], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ], ], ]) ); @@ -77,39 +85,47 @@ describe('geo_containment', () => { new Map([ [ '936', - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, + [ + { + dateInShape: '2020-09-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + location: [-82.8814151789993, 40.62806099653244], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ], ], [ 'AAL2019', - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'iOng1XQB6yyY-xQxnGSM', - location: [-82.22068064846098, 39.006176185794175], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, + [ + { + dateInShape: '2020-09-28T18:01:41.191Z', + docId: 'iOng1XQB6yyY-xQxnGSM', + location: [-82.22068064846098, 39.006176185794175], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ], ], [ 'AAL2323', - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'n-ng1XQB6yyY-xQxnGSM', - location: [-84.71324851736426, 41.6677269525826], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, + [ + { + dateInShape: '2020-09-28T18:01:41.191Z', + docId: 'n-ng1XQB6yyY-xQxnGSM', + location: [-84.71324851736426, 41.6677269525826], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ], ], [ 'ABD5250', - { - dateInShape: '2020-09-28T18:01:41.192Z', - docId: 'GOng1XQB6yyY-xQxnGWM', - location: [6.073727197945118, 39.07997465226799], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, + [ + { + dateInShape: '2020-09-28T18:01:41.192Z', + docId: 'GOng1XQB6yyY-xQxnGWM', + location: [6.073727197945118, 39.07997465226799], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ], ], ]) ); @@ -131,30 +147,36 @@ describe('geo_containment', () => { const currLocationMap = new Map([ [ 'a', - { - location: [0, 0], - shapeLocationId: '123', - dateInShape: 'Wed Dec 09 2020 14:31:31 GMT-0700 (Mountain Standard Time)', - docId: 'docId1', - }, + [ + { + location: [0, 0], + shapeLocationId: '123', + dateInShape: 'Wed Dec 09 2020 14:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + ], ], [ 'b', - { - location: [0, 0], - shapeLocationId: '456', - dateInShape: 'Wed Dec 09 2020 15:31:31 GMT-0700 (Mountain Standard Time)', - docId: 'docId2', - }, + [ + { + location: [0, 0], + shapeLocationId: '456', + dateInShape: 'Wed Dec 16 2020 15:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId2', + }, + ], ], [ 'c', - { - location: [0, 0], - shapeLocationId: '789', - dateInShape: 'Wed Dec 09 2020 16:31:31 GMT-0700 (Mountain Standard Time)', - docId: 'docId3', - }, + [ + { + location: [0, 0], + shapeLocationId: '789', + dateInShape: 'Wed Dec 23 2020 16:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId3', + }, + ], ], ]); @@ -215,7 +237,7 @@ describe('geo_containment', () => { const currentDateTime = new Date(); it('should use currently active entities if no older entity entries', () => { - const emptyPrevLocationMap = {}; + const emptyPrevLocationMap = new Map(); const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMap, @@ -227,14 +249,19 @@ describe('geo_containment', () => { expect(testAlertActionArr).toMatchObject(expectedContext); }); it('should overwrite older identical entity entries', () => { - const prevLocationMapWithIdenticalEntityEntry = { - a: { - location: [0, 0], - shapeLocationId: '999', - dateInShape: 'Wed Dec 09 2020 12:31:31 GMT-0700 (Mountain Standard Time)', - docId: 'docId7', - }, - }; + const prevLocationMapWithIdenticalEntityEntry = new Map([ + [ + 'a', + [ + { + location: [0, 0], + shapeLocationId: '999', + dateInShape: 'Wed Dec 09 2020 12:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId7', + }, + ], + ], + ]); const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMapWithIdenticalEntityEntry, currLocationMap, @@ -246,14 +273,19 @@ describe('geo_containment', () => { expect(testAlertActionArr).toMatchObject(expectedContext); }); it('should preserve older non-identical entity entries', () => { - const prevLocationMapWithNonIdenticalEntityEntry = { - d: { - location: [0, 0], - shapeLocationId: '999', - dateInShape: 'Wed Dec 09 2020 12:31:31 GMT-0700 (Mountain Standard Time)', - docId: 'docId7', - }, - }; + const prevLocationMapWithNonIdenticalEntityEntry = new Map([ + [ + 'd', + [ + { + location: [0, 0], + shapeLocationId: '999', + dateInShape: 'Wed Dec 09 2020 12:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId7', + }, + ], + ], + ]); const expectedContextPlusD = [ { actionGroupId: 'Tracked entity contained', @@ -279,14 +311,17 @@ describe('geo_containment', () => { expect(allActiveEntriesMap.has('d')).toBeTruthy(); expect(testAlertActionArr).toMatchObject(expectedContextPlusD); }); + it('should remove "other" entries and schedule the expected number of actions', () => { - const emptyPrevLocationMap = {}; - const currLocationMapWithOther = new Map(currLocationMap).set('d', { - location: [0, 0], - shapeLocationId: OTHER_CATEGORY, - dateInShape: 'Wed Dec 09 2020 14:31:31 GMT-0700 (Mountain Standard Time)', - docId: 'docId1', - }); + const emptyPrevLocationMap = new Map(); + const currLocationMapWithOther = new Map([...currLocationMap]).set('d', [ + { + location: [0, 0], + shapeLocationId: OTHER_CATEGORY, + dateInShape: 'Wed Dec 09 2020 14:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + ]); expect(currLocationMapWithOther).not.toEqual(currLocationMap); const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, @@ -298,5 +333,115 @@ describe('geo_containment', () => { expect(allActiveEntriesMap).toEqual(currLocationMap); expect(testAlertActionArr).toMatchObject(expectedContext); }); + + it('should generate multiple alerts per entity if found in multiple shapes in interval', () => { + const emptyPrevLocationMap = new Map(); + const currLocationMapWithThreeMore = new Map([...currLocationMap]).set('d', [ + { + location: [0, 0], + shapeLocationId: '789', + dateInShape: 'Wed Dec 10 2020 14:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + { + location: [0, 0], + shapeLocationId: '123', + dateInShape: 'Wed Dec 08 2020 12:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId2', + }, + { + location: [0, 0], + shapeLocationId: '456', + dateInShape: 'Wed Dec 07 2020 10:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId3', + }, + ]); + getActiveEntriesAndGenerateAlerts( + emptyPrevLocationMap, + currLocationMapWithThreeMore, + alertInstanceFactory, + emptyShapesIdsNamesMap, + currentDateTime + ); + let numEntitiesInShapes = 0; + currLocationMapWithThreeMore.forEach((v) => { + numEntitiesInShapes += v.length; + }); + expect(testAlertActionArr.length).toEqual(numEntitiesInShapes); + }); + + it('should not return entity as active entry if most recent location is "other"', () => { + const emptyPrevLocationMap = new Map(); + const currLocationMapWithOther = new Map([...currLocationMap]).set('d', [ + { + location: [0, 0], + shapeLocationId: OTHER_CATEGORY, + dateInShape: 'Wed Dec 10 2020 14:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + { + location: [0, 0], + shapeLocationId: '123', + dateInShape: 'Wed Dec 08 2020 12:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + { + location: [0, 0], + shapeLocationId: '456', + dateInShape: 'Wed Dec 07 2020 10:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + ]); + expect(currLocationMapWithOther).not.toEqual(currLocationMap); + const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( + emptyPrevLocationMap, + currLocationMapWithOther, + alertInstanceFactory, + emptyShapesIdsNamesMap, + currentDateTime + ); + expect(allActiveEntriesMap).toEqual(currLocationMap); + }); + + it('should return entity as active entry if "other" not the latest location but remove "other" and earlier entries', () => { + const emptyPrevLocationMap = new Map(); + const currLocationMapWithOther = new Map([...currLocationMap]).set('d', [ + { + location: [0, 0], + shapeLocationId: '123', + dateInShape: 'Wed Dec 10 2020 14:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + { + location: [0, 0], + shapeLocationId: OTHER_CATEGORY, + dateInShape: 'Wed Dec 08 2020 12:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + { + location: [0, 0], + shapeLocationId: '456', + dateInShape: 'Wed Dec 07 2020 10:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + ]); + const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( + emptyPrevLocationMap, + currLocationMapWithOther, + alertInstanceFactory, + emptyShapesIdsNamesMap, + currentDateTime + ); + expect(allActiveEntriesMap).toEqual( + new Map([...currLocationMap]).set('d', [ + { + location: [0, 0], + shapeLocationId: '123', + dateInShape: 'Wed Dec 10 2020 14:31:31 GMT-0700 (Mountain Standard Time)', + docId: 'docId1', + }, + ]) + ); + }); }); }); From cd9c79bec042fc41f488a37a03409b5a55a30b63 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 28 Jan 2021 17:43:51 +0100 Subject: [PATCH 084/163] [APM] Add skip() method to registry.when (#89572) Closes #89431. --- .../apm_api_integration/common/registry.ts | 92 +++++++++++-------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/x-pack/test/apm_api_integration/common/registry.ts b/x-pack/test/apm_api_integration/common/registry.ts index 8c918eae5a5a8..ba43db2fd5835 100644 --- a/x-pack/test/apm_api_integration/common/registry.ts +++ b/x-pack/test/apm_api_integration/common/registry.ts @@ -36,55 +36,69 @@ let configName: APMFtrConfigName | undefined; let running: boolean = false; -export const registry = { - init: (config: APMFtrConfigName) => { - configName = config; - callbacks.length = 0; - running = false; - }, - when: ( - title: string, - conditions: RunCondition | RunCondition[], - callback: (condition: RunCondition) => void - ) => { - const allConditions = castArray(conditions); - - if (!allConditions.length) { - throw new Error('At least one condition should be defined'); - } +function when( + title: string, + conditions: RunCondition | RunCondition[], + callback: (condition: RunCondition) => void, + skip?: boolean +) { + const allConditions = castArray(conditions); + + if (!allConditions.length) { + throw new Error('At least one condition should be defined'); + } - if (running) { - throw new Error("Can't add tests when running"); - } + if (running) { + throw new Error("Can't add tests when running"); + } - const frame = maybe(callsites()[1]); + const frame = maybe(callsites()[1]); - const file = frame?.getFileName(); + const file = frame?.getFileName(); - if (!file) { - throw new Error('Could not infer file for suite'); - } + if (!file) { + throw new Error('Could not infer file for suite'); + } - allConditions.forEach((matchedCondition) => { - callbacks.push({ - ...matchedCondition, - runs: [ - { - cb: () => { - const suite = describe(title, () => { + allConditions.forEach((matchedCondition) => { + callbacks.push({ + ...matchedCondition, + runs: [ + { + cb: () => { + const suite: ReturnType = (skip ? describe.skip : describe)( + title, + () => { callback(matchedCondition); - }); + } + ) as any; - suite.file = file; - suite.eachTest((test) => { - test.file = file; - }); - }, + suite.file = file; + suite.eachTest((test) => { + test.file = file; + }); }, - ], - }); + }, + ], }); + }); +} + +when.skip = ( + title: string, + conditions: RunCondition | RunCondition[], + callback: (condition: RunCondition) => void +) => { + when(title, conditions, callback, true); +}; + +export const registry = { + init: (config: APMFtrConfigName) => { + configName = config; + callbacks.length = 0; + running = false; }, + when, run: (context: FtrProviderContext) => { if (!configName) { throw new Error(`registry was not init() before running`); From c73000f644cbfdc67c0583f9a2daa4a87b8e0a1f Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 28 Jan 2021 09:55:45 -0700 Subject: [PATCH 085/163] Fixes one liner where the UI was not setting the threat_filters (#89519) ## Summary Fixes: https://github.com/elastic/kibana/issues/89507 * One liner was missed on the front end to persist this * I fixed some type script issues in the tests which lead to a few missing types in the `DefineStepRuleJson` section * Adds a unit test for this. Test instructions: 1. Go to manage detections 2. Create a new rule for Indicator matches 3. Add a filter and click save. 4. Re-load the rule and it should be saved now. Repeat the steps for edit and it should also be saved there. You should see this after a reload of the rule after a save for example: Screen Shot 2021-01-27 at 4 28 51 PM And this if you have two filters saved in the definition section of the UI: Screen Shot 2021-01-27 at 4 30 10 PM ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../rules/create/helpers.test.ts | 210 ++++++++++++------ .../detection_engine/rules/create/helpers.ts | 1 + .../pages/detection_engine/rules/types.ts | 5 + 3 files changed, 143 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index 8451eb0dfbe6c..716585071c3f6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -99,7 +99,7 @@ describe('helpers', () => { { ...mockThreat, tactic: { ...mockThreat.tactic, name: 'none' } }, ]; const result = filterEmptyThreats(threat); - const expected = [mockThreat]; + const expected: Threats = [mockThreat]; expect(result).toEqual(expected); }); }); @@ -112,8 +112,8 @@ describe('helpers', () => { }); test('returns formatted object as DefineStepRuleJson', () => { - const result: DefineStepRuleJson = formatDefineStepData(mockData); - const expected = { + const result = formatDefineStepData(mockData); + const expected: DefineStepRuleJson = { language: 'kuery', filters: mockQueryBar.filters, query: 'test query', @@ -128,15 +128,15 @@ describe('helpers', () => { }); test('returns formatted object with no saved_id if no savedId provided', () => { - const mockStepData = { + const mockStepData: DefineStepRule = { ...mockData, queryBar: { ...mockData.queryBar, saved_id: '', }, }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); - const expected = { + const result = formatDefineStepData(mockStepData); + const expected: DefineStepRuleJson = { language: 'kuery', filters: mockQueryBar.filters, query: 'test query', @@ -151,15 +151,15 @@ describe('helpers', () => { }); test('returns formatted object without timeline_id and timeline_title if timeline.id is null', () => { - const mockStepData = { + const mockStepData: DefineStepRule = { ...mockData, }; // @ts-expect-error delete mockStepData.timeline.id; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const result = formatDefineStepData(mockStepData); - const expected = { + const expected: DefineStepRuleJson = { language: 'kuery', filters: mockQueryBar.filters, query: 'test query', @@ -172,16 +172,16 @@ describe('helpers', () => { }); test('returns formatted object with timeline_id and timeline_title if timeline.id is "', () => { - const mockStepData = { + const mockStepData: DefineStepRule = { ...mockData, timeline: { ...mockData.timeline, id: '', }, }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const result = formatDefineStepData(mockStepData); - const expected = { + const expected: DefineStepRuleJson = { language: 'kuery', filters: mockQueryBar.filters, query: 'test query', @@ -196,7 +196,7 @@ describe('helpers', () => { }); test('returns formatted object without timeline_id and timeline_title if timeline.title is null', () => { - const mockStepData = { + const mockStepData: DefineStepRule = { ...mockData, timeline: { ...mockData.timeline, @@ -205,9 +205,9 @@ describe('helpers', () => { }; // @ts-expect-error delete mockStepData.timeline.title; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const result = formatDefineStepData(mockStepData); - const expected = { + const expected: DefineStepRuleJson = { language: 'kuery', filters: mockQueryBar.filters, query: 'test query', @@ -220,16 +220,16 @@ describe('helpers', () => { }); test('returns formatted object with timeline_id and timeline_title if timeline.title is "', () => { - const mockStepData = { + const mockStepData: DefineStepRule = { ...mockData, timeline: { ...mockData.timeline, title: '', }, }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const result = formatDefineStepData(mockStepData); - const expected = { + const expected: DefineStepRuleJson = { language: 'kuery', filters: mockQueryBar.filters, query: 'test query', @@ -250,9 +250,9 @@ describe('helpers', () => { anomalyThreshold: 44, machineLearningJobId: 'some_jobert_id', }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const result = formatDefineStepData(mockStepData); - const expected = { + const expected: DefineStepRuleJson = { type: 'machine_learning', anomaly_threshold: 44, machine_learning_job_id: 'some_jobert_id', @@ -276,9 +276,9 @@ describe('helpers', () => { }, }, }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const result = formatDefineStepData(mockStepData); - const expected = { + const expected: DefineStepRuleJson = { filters: mockStepData.queryBar.filters, index: mockStepData.index, language: 'eql', @@ -288,6 +288,70 @@ describe('helpers', () => { expect(result).toEqual(expect.objectContaining(expected)); }); + + test('returns expected indicator matching rule type if all fields are filled out', () => { + const threatFilters: DefineStepRule['threatQueryBar']['filters'] = [ + { + meta: { alias: '', disabled: false, negate: false }, + query: { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [{ exists: { field: 'host.name' } }], + }, + }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }, + }, + ]; + const threatMapping: DefineStepRule['threatMapping'] = [ + { + entries: [ + { + field: 'host.name', + type: 'mapping', + value: 'host.name', + }, + ], + }, + ]; + const mockStepData: DefineStepRule = { + ...mockData, + ruleType: 'threat_match', + threatIndex: ['index_1', 'index_2'], + threatQueryBar: { + query: { language: 'kql', query: 'threat_host: *' }, + filters: threatFilters, + }, + threatMapping, + }; + const result = formatDefineStepData(mockStepData); + + const expected: DefineStepRuleJson = { + language: 'kuery', + query: 'test query', + saved_id: 'test123', + type: 'threat_match', + threat_query: 'threat_host: *', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + threat_mapping: threatMapping, + threat_language: mockStepData.threatQueryBar.query.language, + filters: mockStepData.queryBar.filters, + threat_index: mockStepData.threatIndex, + index: mockStepData.index, + threat_filters: threatFilters, + }; + + expect(result).toEqual(expected); + }); }); describe('formatScheduleStepData', () => { @@ -298,8 +362,8 @@ describe('helpers', () => { }); test('returns formatted object as ScheduleStepRuleJson', () => { - const result: ScheduleStepRuleJson = formatScheduleStepData(mockData); - const expected = { + const result = formatScheduleStepData(mockData); + const expected: ScheduleStepRuleJson = { from: 'now-660s', to: 'now', interval: '5m', @@ -312,12 +376,12 @@ describe('helpers', () => { }); test('returns formatted object with "to" as "now" if "to" not supplied', () => { - const mockStepData = { + const mockStepData: ScheduleStepRule = { ...mockData, }; delete mockStepData.to; - const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); - const expected = { + const result = formatScheduleStepData(mockStepData); + const expected: ScheduleStepRuleJson = { from: 'now-660s', to: 'now', interval: '5m', @@ -330,12 +394,12 @@ describe('helpers', () => { }); test('returns formatted object with "to" as "now" if "to" random string', () => { - const mockStepData = { + const mockStepData: ScheduleStepRule = { ...mockData, to: 'random', }; - const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); - const expected = { + const result = formatScheduleStepData(mockStepData); + const expected: ScheduleStepRuleJson = { from: 'now-660s', to: 'now', interval: '5m', @@ -348,12 +412,12 @@ describe('helpers', () => { }); test('returns formatted object if "from" random string', () => { - const mockStepData = { + const mockStepData: ScheduleStepRule = { ...mockData, from: 'random', }; - const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); - const expected = { + const result = formatScheduleStepData(mockStepData); + const expected: ScheduleStepRuleJson = { from: 'now-300s', to: 'now', interval: '5m', @@ -366,12 +430,12 @@ describe('helpers', () => { }); test('returns formatted object if "interval" random string', () => { - const mockStepData = { + const mockStepData: ScheduleStepRule = { ...mockData, interval: 'random', }; - const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); - const expected = { + const result = formatScheduleStepData(mockStepData); + const expected: ScheduleStepRuleJson = { from: 'now-360s', to: 'now', interval: 'random', @@ -392,8 +456,8 @@ describe('helpers', () => { }); test('returns formatted object as AboutStepRuleJson', () => { - const result: AboutStepRuleJson = formatAboutStepData(mockData); - const expected = { + const result = formatAboutStepData(mockData); + const expected: AboutStepRuleJson = { author: ['Elastic'], description: '24/7', false_positives: ['test'], @@ -413,7 +477,7 @@ describe('helpers', () => { }); test('returns formatted object with endpoint exceptions_list', () => { - const result: AboutStepRuleJson = formatAboutStepData( + const result = formatAboutStepData( { ...mockData, isAssociatedToEndpointList: true, @@ -424,12 +488,12 @@ describe('helpers', () => { }); test('returns formatted object with detections exceptions_list', () => { - const result: AboutStepRuleJson = formatAboutStepData(mockData, [getListMock()]); + const result = formatAboutStepData(mockData, [getListMock()]); expect(result.exceptions_list).toEqual([getListMock()]); }); test('returns formatted object with both exceptions_lists', () => { - const result: AboutStepRuleJson = formatAboutStepData( + const result = formatAboutStepData( { ...mockData, isAssociatedToEndpointList: true, @@ -441,7 +505,7 @@ describe('helpers', () => { test('returns formatted object with pre-existing exceptions lists', () => { const exceptionsLists: List[] = [getEndpointListMock(), getListMock()]; - const result: AboutStepRuleJson = formatAboutStepData( + const result = formatAboutStepData( { ...mockData, isAssociatedToEndpointList: true, @@ -453,18 +517,18 @@ describe('helpers', () => { test('returns formatted object with pre-existing endpoint exceptions list disabled', () => { const exceptionsLists: List[] = [getEndpointListMock(), getListMock()]; - const result: AboutStepRuleJson = formatAboutStepData(mockData, exceptionsLists); + const result = formatAboutStepData(mockData, exceptionsLists); expect(result.exceptions_list).toEqual([getListMock()]); }); test('returns formatted object with empty falsePositive and references filtered out', () => { - const mockStepData = { + const mockStepData: AboutStepRule = { ...mockData, falsePositives: ['', 'test', ''], references: ['www.test.co', ''], }; - const result: AboutStepRuleJson = formatAboutStepData(mockStepData); - const expected = { + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { author: ['Elastic'], description: '24/7', false_positives: ['test'], @@ -484,12 +548,12 @@ describe('helpers', () => { }); test('returns formatted object without note if note is empty string', () => { - const mockStepData = { + const mockStepData: AboutStepRule = { ...mockData, note: '', }; - const result: AboutStepRuleJson = formatAboutStepData(mockStepData); - const expected = { + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { author: ['Elastic'], description: '24/7', false_positives: ['test'], @@ -508,7 +572,7 @@ describe('helpers', () => { }); test('returns formatted object with threats filtered out where tactic.name is "none"', () => { - const mockStepData = { + const mockStepData: AboutStepRule = { ...mockData, threat: [ ...getThreatMock(), @@ -530,8 +594,8 @@ describe('helpers', () => { }, ], }; - const result: AboutStepRuleJson = formatAboutStepData(mockStepData); - const expected = { + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { author: ['Elastic'], license: 'Elastic License', description: '24/7', @@ -551,7 +615,7 @@ describe('helpers', () => { }); test('returns formatted object with threats that contains no subtechniques', () => { - const mockStepData = { + const mockStepData: AboutStepRule = { ...mockData, threat: [ ...getThreatMock(), @@ -573,8 +637,8 @@ describe('helpers', () => { }, ], }; - const result: AboutStepRuleJson = formatAboutStepData(mockStepData); - const expected = { + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { author: ['Elastic'], license: 'Elastic License', description: '24/7', @@ -611,8 +675,8 @@ describe('helpers', () => { }); test('returns formatted object as ActionsStepRuleJson', () => { - const result: ActionsStepRuleJson = formatActionsStepData(mockData); - const expected = { + const result = formatActionsStepData(mockData); + const expected: ActionsStepRuleJson = { actions: [], enabled: false, meta: { @@ -625,12 +689,12 @@ describe('helpers', () => { }); test('returns proper throttle value for no_actions', () => { - const mockStepData = { + const mockStepData: ActionsStepRule = { ...mockData, throttle: 'no_actions', }; - const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); - const expected = { + const result = formatActionsStepData(mockStepData); + const expected: ActionsStepRuleJson = { actions: [], enabled: false, meta: { @@ -643,7 +707,7 @@ describe('helpers', () => { }); test('returns proper throttle value for rule', () => { - const mockStepData = { + const mockStepData: ActionsStepRule = { ...mockData, throttle: 'rule', actions: [ @@ -655,8 +719,8 @@ describe('helpers', () => { }, ], }; - const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); - const expected = { + const result = formatActionsStepData(mockStepData); + const expected: ActionsStepRuleJson = { actions: [ { group: mockStepData.actions[0].group, @@ -676,7 +740,7 @@ describe('helpers', () => { }); test('returns proper throttle value for interval', () => { - const mockStepData = { + const mockStepData: ActionsStepRule = { ...mockData, throttle: '1d', actions: [ @@ -688,8 +752,8 @@ describe('helpers', () => { }, ], }; - const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); - const expected = { + const result = formatActionsStepData(mockStepData); + const expected: ActionsStepRuleJson = { actions: [ { group: mockStepData.actions[0].group, @@ -716,12 +780,12 @@ describe('helpers', () => { actionTypeId: '.slack', }; - const mockStepData = { + const mockStepData: ActionsStepRule = { ...mockData, actions: [mockAction], }; - const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); - const expected = { + const result = formatActionsStepData(mockStepData); + const expected: ActionsStepRuleJson = { actions: [ { group: mockAction.group, @@ -755,20 +819,20 @@ describe('helpers', () => { }); test('returns rule with type of saved_query when saved_id exists', () => { - const result: Rule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); + const result = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); expect(result.type).toEqual('saved_query'); }); test('returns rule with type of query when saved_id does not exist', () => { - const mockDefineStepRuleWithoutSavedId = { + const mockDefineStepRuleWithoutSavedId: DefineStepRule = { ...mockDefine, queryBar: { ...mockDefine.queryBar, saved_id: '', }, }; - const result: CreateRulesSchema = formatRule( + const result = formatRule( mockDefineStepRuleWithoutSavedId, mockAbout, mockSchedule, @@ -779,7 +843,7 @@ describe('helpers', () => { }); test('returns rule without id if ruleId does not exist', () => { - const result: CreateRulesSchema = formatRule( + const result = formatRule( mockDefine, mockAbout, mockSchedule, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 7952bd396b72a..34818e7f0e4e2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -232,6 +232,7 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep saved_id: ruleFields.queryBar?.saved_id, threat_index: ruleFields.threatIndex, threat_query: ruleFields.threatQueryBar?.query?.query as string, + threat_filters: ruleFields.threatQueryBar?.filters, threat_mapping: ruleFields.threatMapping, threat_language: ruleFields.threatQueryBar?.query?.language, } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 59cc7fba017e2..00206279be229 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -159,6 +159,11 @@ export interface DefineStepRuleJson { field: string; value: number; }; + threat_query?: string; + threat_mapping?: ThreatMapping; + threat_language?: string; + threat_index?: string[]; + threat_filters?: Filter[]; timeline_id?: string; timeline_title?: string; type: Type; From 10e6354d7774da933828b3c13bb9f0492cd8bfb8 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 28 Jan 2021 18:46:14 +0100 Subject: [PATCH 086/163] Expose anonymous access through a switch in sharing menu (#86965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add "Public URL" switch * feat: 🎸 add url subtitle * feat: 🎸 add public URL toggle state * feat: 🎸 allow to dynamically enable anonymous access switch * feat: 🎸 add anon access url parameters to share url * fix: 🐛 correctly add params to url * fix: 🐛 correctly add anon access to saved object URL * fix: 🐛 don't generate anon access urls twice * feat: 🎸 add ability to check anonymous user capabilities * feat: 🎸 add capability checks to Discover and Visualize apps * refactor: 💡 use early return * test: 💍 use security_oss mocks * feat: 🎸 add anon access url params to short url * test: 💍 fix jest snapshots * perf: ⚡️ make capabilities check synchronous * style: 💄 add stylistic review changes * perf: ⚡️ don't fetch anon user capabilities if anon not enabled * fix: 🐛 in discover app check if discover exists in capabilities * test: 💍 add tests for discover sharing check * test: 💍 add tests for showPublicUrlSwitch checks * feat: 🎸 make visualize capabilities props required * style: 💄 remove unused import * feat: 🎸 improve tooltip copy --- .../top_nav/show_share_modal.test.tsx | 51 ++++ .../application/top_nav/show_share_modal.tsx | 10 + .../components/top_nav/get_top_nav_links.ts | 3 +- .../helpers/get_sharing_data.test.ts | 44 ++- .../application/helpers/get_sharing_data.ts | 18 +- .../security_oss/public/plugin.mock.ts | 7 +- src/plugins/share/kibana.json | 3 +- .../url_panel_content.test.tsx.snap | 280 +++++++++++------- .../public/components/share_context_menu.tsx | 8 + .../public/components/url_panel_content.tsx | 174 +++++++++-- src/plugins/share/public/plugin.test.ts | 26 +- src/plugins/share/public/plugin.ts | 19 +- .../public/services/share_menu_manager.tsx | 13 +- src/plugins/share/public/types.ts | 2 + src/plugins/share/tsconfig.json | 3 +- .../utils/get_top_nav_config.test.tsx | 51 ++++ .../application/utils/get_top_nav_config.tsx | 18 ++ 17 files changed, 584 insertions(+), 146 deletions(-) create mode 100644 src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx create mode 100644 src/plugins/visualize/public/application/utils/get_top_nav_config.test.tsx diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx new file mode 100644 index 0000000000000..d4703d14627a4 --- /dev/null +++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Capabilities } from 'src/core/public'; +import { showPublicUrlSwitch } from './show_share_modal'; + +describe('showPublicUrlSwitch', () => { + test('returns false if "dashboard" app is not available', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(false); + }); + + test('returns false if "dashboard" app is not accessible', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + dashboard: { + show: false, + }, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(false); + }); + + test('returns true if "dashboard" app is not available an accessible', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + dashboard: { + show: true, + }, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(true); + }); +}); diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx index ecebef2ec3c9c..fe4f8ea411289 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx @@ -6,6 +6,7 @@ * Public License, v 1. */ +import { Capabilities } from 'src/core/public'; import { EuiCheckboxGroup } from '@elastic/eui'; import React from 'react'; import { ReactElement, useState } from 'react'; @@ -27,6 +28,14 @@ interface ShowShareModalProps { dashboardStateManager: DashboardStateManager; } +export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => { + if (!anonymousUserCapabilities.dashboard) return false; + + const dashboard = (anonymousUserCapabilities.dashboard as unknown) as DashboardCapabilities; + + return !!dashboard.show; +}; + export function ShowShareModal({ share, anchorElement, @@ -113,5 +122,6 @@ export function ShowShareModal({ component: EmbedUrlParamExtension, }, ], + showPublicUrlSwitch, }); } diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts index 1b7406496bb81..4d522f47ea87f 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { showOpenSearchPanel } from './show_open_search_panel'; -import { getSharingData } from '../../helpers/get_sharing_data'; +import { getSharingData, showPublicUrlSwitch } from '../../helpers/get_sharing_data'; import { unhashUrl } from '../../../../../kibana_utils/public'; import { DiscoverServices } from '../../../build_services'; import { Adapters } from '../../../../../inspector/common/adapters'; @@ -108,6 +108,7 @@ export const getTopNavLinks = ({ title: savedSearch.title, }, isDirty: !savedSearch.id || state.isAppStateDirty(), + showPublicUrlSwitch, }); }, }; diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index 1394ceab1dd18..ea16b81615e42 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -6,7 +6,8 @@ * Public License, v 1. */ -import { getSharingData } from './get_sharing_data'; +import { Capabilities } from 'kibana/public'; +import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; import { IUiSettingsClient } from 'kibana/public'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; import { indexPatternMock } from '../../__mocks__/index_pattern'; @@ -68,3 +69,44 @@ describe('getSharingData', () => { `); }); }); + +describe('showPublicUrlSwitch', () => { + test('returns false if "discover" app is not available', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(false); + }); + + test('returns false if "discover" app is not accessible', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + discover: { + show: false, + }, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(false); + }); + + test('returns true if "discover" app is not available an accessible', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + discover: { + show: true, + }, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(true); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts index 62478f1d2830f..1d780a5573e2a 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { IUiSettingsClient } from 'kibana/public'; +import { Capabilities, IUiSettingsClient } from 'kibana/public'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; import { getSortForSearchSource } from '../angular/doc_table'; import { SearchSource } from '../../../../data/common'; @@ -76,3 +76,19 @@ export async function getSharingData( indexPatternId: index.id, }; } + +export interface DiscoverCapabilities { + createShortUrl?: boolean; + save?: boolean; + saveQuery?: boolean; + show?: boolean; + storeSearchSession?: boolean; +} + +export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => { + if (!anonymousUserCapabilities.discover) return false; + + const discover = (anonymousUserCapabilities.discover as unknown) as DiscoverCapabilities; + + return !!discover.show; +}; diff --git a/src/plugins/security_oss/public/plugin.mock.ts b/src/plugins/security_oss/public/plugin.mock.ts index 53d96b9c7a303..8fe8d56ea6576 100644 --- a/src/plugins/security_oss/public/plugin.mock.ts +++ b/src/plugins/security_oss/public/plugin.mock.ts @@ -7,6 +7,7 @@ */ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import { InsecureClusterServiceStart } from './insecure_cluster_service'; import { mockInsecureClusterService } from './insecure_cluster_service/insecure_cluster_service.mock'; import { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin'; @@ -18,7 +19,11 @@ export const mockSecurityOssPlugin = { }, createStart: () => { return { - insecureCluster: mockInsecureClusterService.createStart(), + insecureCluster: mockInsecureClusterService.createStart() as jest.Mocked, + anonymousAccess: { + getAccessURLParameters: jest.fn().mockResolvedValue(null), + getCapabilities: jest.fn().mockResolvedValue({}), + }, } as DeeplyMockedKeys; }, }; diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index 7760ea321992d..8b1d28b1606d4 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredBundles": ["kibanaUtils"] + "requiredBundles": ["kibanaUtils"], + "optionalPlugins": ["securityOss"] } diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap index 9a7191519131c..e883b550fde04 100644 --- a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap +++ b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap @@ -115,49 +115,68 @@ exports[`share url panel content render 1`] = ` /> + } labelType="label" > - + - - - } - onChange={[Function]} - /> - - - - } - position="bottom" - /> - - + + + } + onChange={[Function]} + /> + + + + } + position="bottom" + /> + + + + } labelType="label" > - + - - - } - onChange={[Function]} - /> - - - - } - position="bottom" - /> - - + + + } + onChange={[Function]} + /> + + + + } + position="bottom" + /> + + + + + } + labelType="label" + > + + @@ -569,49 +626,68 @@ exports[`should show url param extensions 1`] = ` /> + } labelType="label" > - + - - - } - onChange={[Function]} - /> - - - - } - position="bottom" - /> - - + + + } + onChange={[Function]} + /> + + + + } + position="bottom" + /> + + + boolean; } export class ShareContextMenu extends Component { @@ -62,6 +66,8 @@ export class ShareContextMenu extends Component { basePath={this.props.basePath} post={this.props.post} shareableUrl={this.props.shareableUrl} + anonymousAccess={this.props.anonymousAccess} + showPublicUrlSwitch={this.props.showPublicUrlSwitch} /> ), }; @@ -91,6 +97,8 @@ export class ShareContextMenu extends Component { post={this.props.post} shareableUrl={this.props.shareableUrl} urlParamExtensions={this.props.embedUrlParamExtensions} + anonymousAccess={this.props.anonymousAccess} + showPublicUrlSwitch={this.props.showPublicUrlSwitch} /> ), }; diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index 5901d2452e9aa..ca9025f242b78 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -28,9 +28,11 @@ import { format as formatUrl, parse as parseUrl } from 'url'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { HttpStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; +import type { Capabilities } from 'src/core/public'; import { shortenUrl } from '../lib/url_shortener'; import { UrlParamExtension } from '../types'; +import type { SecurityOssPluginStart } from '../../../security_oss/public'; interface Props { allowShortUrl: boolean; @@ -41,6 +43,8 @@ interface Props { basePath: string; post: HttpStart['post']; urlParamExtensions?: UrlParamExtension[]; + anonymousAccess?: SecurityOssPluginStart['anonymousAccess']; + showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean; } export enum ExportUrlAsType { @@ -57,10 +61,13 @@ interface UrlParams { interface State { exportUrlAs: ExportUrlAsType; useShortUrl: boolean; + usePublicUrl: boolean; isCreatingShortUrl: boolean; url?: string; shortUrlErrorMsg?: string; urlParams?: UrlParams; + anonymousAccessParameters: Record | null; + showPublicUrlSwitch: boolean; } export class UrlPanelContent extends Component { @@ -75,8 +82,11 @@ export class UrlPanelContent extends Component { this.state = { exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, useShortUrl: false, + usePublicUrl: false, isCreatingShortUrl: false, url: '', + anonymousAccessParameters: null, + showPublicUrlSwitch: false, }; } @@ -91,6 +101,41 @@ export class UrlPanelContent extends Component { this.setUrl(); window.addEventListener('hashchange', this.resetUrl, false); + + if (this.props.anonymousAccess) { + (async () => { + const anonymousAccessParameters = await this.props.anonymousAccess!.getAccessURLParameters(); + + if (!this.mounted) { + return; + } + + if (!anonymousAccessParameters) { + return; + } + + let showPublicUrlSwitch: boolean = false; + + if (this.props.showPublicUrlSwitch) { + const anonymousUserCapabilities = await this.props.anonymousAccess!.getCapabilities(); + + if (!this.mounted) { + return; + } + + try { + showPublicUrlSwitch = this.props.showPublicUrlSwitch!(anonymousUserCapabilities); + } catch { + showPublicUrlSwitch = false; + } + } + + this.setState({ + anonymousAccessParameters, + showPublicUrlSwitch, + }); + })(); + } } public render() { @@ -99,7 +144,16 @@ export class UrlPanelContent extends Component { {this.renderExportAsRadioGroup()} {this.renderUrlParamExtensions()} - {this.renderShortUrlSwitch()} + + } + > + <> + + {this.renderShortUrlSwitch()} + {this.renderPublicUrlSwitch()} + + @@ -150,10 +204,10 @@ export class UrlPanelContent extends Component { }; private updateUrlParams = (url: string) => { - const embedUrl = this.props.isEmbedded ? this.makeUrlEmbeddable(url) : url; - const extendUrl = this.state.urlParams ? this.getUrlParamExtensions(embedUrl) : embedUrl; + url = this.props.isEmbedded ? this.makeUrlEmbeddable(url) : url; + url = this.state.urlParams ? this.getUrlParamExtensions(url) : url; - return extendUrl; + return url; }; private getSavedObjectUrl = () => { @@ -206,6 +260,20 @@ export class UrlPanelContent extends Component { return `${url}${embedParam}`; }; + private addUrlAnonymousAccessParameters = (url: string): string => { + if (!this.state.anonymousAccessParameters || !this.state.usePublicUrl) { + return url; + } + + const parsedUrl = new URL(url); + + for (const [name, value] of Object.entries(this.state.anonymousAccessParameters)) { + parsedUrl.searchParams.set(name, value); + } + + return parsedUrl.toString(); + }; + private getUrlParamExtensions = (url: string): string => { const { urlParams } = this.state; return urlParams @@ -232,7 +300,8 @@ export class UrlPanelContent extends Component { }; private setUrl = () => { - let url; + let url: string | undefined; + if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { url = this.getSavedObjectUrl(); } else if (this.state.useShortUrl) { @@ -241,6 +310,10 @@ export class UrlPanelContent extends Component { url = this.getSnapshotUrl(); } + if (url) { + url = this.addUrlAnonymousAccessParameters(url); + } + if (this.props.isEmbedded) { url = this.makeIframeTag(url); } @@ -269,6 +342,14 @@ export class UrlPanelContent extends Component { this.createShortUrl(); }; + private handlePublicUrlChange = () => { + this.setState(({ usePublicUrl }) => { + return { + usePublicUrl: !usePublicUrl, + }; + }, this.setUrl); + }; + private createShortUrl = async () => { this.setState({ isCreatingShortUrl: true, @@ -280,33 +361,38 @@ export class UrlPanelContent extends Component { basePath: this.props.basePath, post: this.props.post, }); - if (this.mounted) { - this.shortUrlCache = shortUrl; - this.setState( - { - isCreatingShortUrl: false, - useShortUrl: true, - }, - this.setUrl - ); + + if (!this.mounted) { + return; } + + this.shortUrlCache = shortUrl; + this.setState( + { + isCreatingShortUrl: false, + useShortUrl: true, + }, + this.setUrl + ); } catch (fetchError) { - if (this.mounted) { - this.shortUrlCache = undefined; - this.setState( - { - useShortUrl: false, - isCreatingShortUrl: false, - shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', { - defaultMessage: 'Unable to create short URL. Error: {errorMessage}', - values: { - errorMessage: fetchError.message, - }, - }), - }, - this.setUrl - ); + if (!this.mounted) { + return; } + + this.shortUrlCache = undefined; + this.setState( + { + useShortUrl: false, + isCreatingShortUrl: false, + shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', { + defaultMessage: 'Unable to create short URL. Error: {errorMessage}', + values: { + errorMessage: fetchError.message, + }, + }), + }, + this.setUrl + ); } }; @@ -421,6 +507,36 @@ export class UrlPanelContent extends Component { ); }; + private renderPublicUrlSwitch = () => { + if (!this.state.anonymousAccessParameters || !this.state.showPublicUrlSwitch) { + return null; + } + + const switchLabel = ( + + ); + const switchComponent = ( + + ); + const tipContent = ( + + ); + + return ( + + {this.renderWithIconTip(switchComponent, tipContent)} + + ); + }; + private renderUrlParamExtensions = (): ReactElement | void => { if (!this.props.urlParamExtensions) { return; diff --git a/src/plugins/share/public/plugin.test.ts b/src/plugins/share/public/plugin.test.ts index 2b85564ee9ef9..5a3b335115e0d 100644 --- a/src/plugins/share/public/plugin.test.ts +++ b/src/plugins/share/public/plugin.test.ts @@ -10,6 +10,7 @@ import { registryMock, managerMock } from './plugin.test.mocks'; import { SharePlugin } from './plugin'; import { CoreStart } from 'kibana/public'; import { coreMock } from '../../../core/public/mocks'; +import { mockSecurityOssPlugin } from '../../security_oss/public/mocks'; describe('SharePlugin', () => { beforeEach(() => { @@ -21,14 +22,20 @@ describe('SharePlugin', () => { describe('setup', () => { test('wires up and returns registry', async () => { const coreSetup = coreMock.createSetup(); - const setup = await new SharePlugin().setup(coreSetup); + const plugins = { + securityOss: mockSecurityOssPlugin.createSetup(), + }; + const setup = await new SharePlugin().setup(coreSetup, plugins); expect(registryMock.setup).toHaveBeenCalledWith(); expect(setup.register).toBeDefined(); }); test('registers redirect app', async () => { const coreSetup = coreMock.createSetup(); - await new SharePlugin().setup(coreSetup); + const plugins = { + securityOss: mockSecurityOssPlugin.createSetup(), + }; + await new SharePlugin().setup(coreSetup, plugins); expect(coreSetup.application.register).toHaveBeenCalledWith( expect.objectContaining({ id: 'short_url_redirect', @@ -40,13 +47,22 @@ describe('SharePlugin', () => { describe('start', () => { test('wires up and returns show function, but not registry', async () => { const coreSetup = coreMock.createSetup(); + const pluginsSetup = { + securityOss: mockSecurityOssPlugin.createSetup(), + }; const service = new SharePlugin(); - await service.setup(coreSetup); - const start = await service.start({} as CoreStart); + await service.setup(coreSetup, pluginsSetup); + const pluginsStart = { + securityOss: mockSecurityOssPlugin.createStart(), + }; + const start = await service.start({} as CoreStart, pluginsStart); expect(registryMock.start).toHaveBeenCalled(); expect(managerMock.start).toHaveBeenCalledWith( expect.anything(), - expect.objectContaining({ getShareMenuItems: expect.any(Function) }) + expect.objectContaining({ + getShareMenuItems: expect.any(Function), + }), + expect.anything() ); expect(start.toggleShareContextMenu).toBeDefined(); }); diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 55baf72cc4520..26fa1c9113f21 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -10,6 +10,7 @@ import './index.scss'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ShareMenuManager, ShareMenuManagerStart } from './services'; +import type { SecurityOssPluginSetup, SecurityOssPluginStart } from '../../security_oss/public'; import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services'; import { createShortUrlRedirectApp } from './services/short_url_redirect_app'; import { @@ -18,12 +19,20 @@ import { UrlGeneratorsStart, } from './url_generators/url_generator_service'; +export interface ShareSetupDependencies { + securityOss?: SecurityOssPluginSetup; +} + +export interface ShareStartDependencies { + securityOss?: SecurityOssPluginStart; +} + export class SharePlugin implements Plugin { private readonly shareMenuRegistry = new ShareMenuRegistry(); private readonly shareContextMenu = new ShareMenuManager(); private readonly urlGeneratorsService = new UrlGeneratorsService(); - public setup(core: CoreSetup): SharePluginSetup { + public setup(core: CoreSetup, plugins: ShareSetupDependencies): SharePluginSetup { core.application.register(createShortUrlRedirectApp(core, window.location)); return { ...this.shareMenuRegistry.setup(), @@ -31,9 +40,13 @@ export class SharePlugin implements Plugin { }; } - public start(core: CoreStart): SharePluginStart { + public start(core: CoreStart, plugins: ShareStartDependencies): SharePluginStart { return { - ...this.shareContextMenu.start(core, this.shareMenuRegistry.start()), + ...this.shareContextMenu.start( + core, + this.shareMenuRegistry.start(), + plugins.securityOss?.anonymousAccess + ), urlGenerators: this.urlGeneratorsService.start(core), }; } diff --git a/src/plugins/share/public/services/share_menu_manager.tsx b/src/plugins/share/public/services/share_menu_manager.tsx index cc3649d33d876..7284be6a8719c 100644 --- a/src/plugins/share/public/services/share_menu_manager.tsx +++ b/src/plugins/share/public/services/share_menu_manager.tsx @@ -15,13 +15,18 @@ import { CoreStart, HttpStart } from 'kibana/public'; import { ShareContextMenu } from '../components/share_context_menu'; import { ShareMenuItem, ShowShareMenuOptions } from '../types'; import { ShareMenuRegistryStart } from './share_menu_registry'; +import type { SecurityOssPluginStart } from '../../../security_oss/public'; export class ShareMenuManager { private isOpen = false; private container = document.createElement('div'); - start(core: CoreStart, shareRegistry: ShareMenuRegistryStart) { + start( + core: CoreStart, + shareRegistry: ShareMenuRegistryStart, + anonymousAccess?: SecurityOssPluginStart['anonymousAccess'] + ) { return { /** * Collects share menu items from registered providers and mounts the share context menu under @@ -35,6 +40,7 @@ export class ShareMenuManager { menuItems, post: core.http.post, basePath: core.http.basePath.get(), + anonymousAccess, }); }, }; @@ -57,10 +63,13 @@ export class ShareMenuManager { post, basePath, embedUrlParamExtensions, + anonymousAccess, + showPublicUrlSwitch, }: ShowShareMenuOptions & { menuItems: ShareMenuItem[]; post: HttpStart['post']; basePath: string; + anonymousAccess?: SecurityOssPluginStart['anonymousAccess']; }) { if (this.isOpen) { this.onClose(); @@ -92,6 +101,8 @@ export class ShareMenuManager { post={post} basePath={basePath} embedUrlParamExtensions={embedUrlParamExtensions} + anonymousAccess={anonymousAccess} + showPublicUrlSwitch={showPublicUrlSwitch} /> diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts index 88bb51389b001..31c9631571d35 100644 --- a/src/plugins/share/public/types.ts +++ b/src/plugins/share/public/types.ts @@ -9,6 +9,7 @@ import { ComponentType } from 'react'; import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiContextMenuPanelItemDescriptorEntry } from '@elastic/eui/src/components/context_menu/context_menu'; +import type { Capabilities } from 'src/core/public'; /** * @public @@ -35,6 +36,7 @@ export interface ShareContext { sharingData: { [key: string]: unknown }; isDirty: boolean; onClose: () => void; + showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean; } /** diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json index a6318af602b4d..985066915f1dd 100644 --- a/src/plugins/share/tsconfig.json +++ b/src/plugins/share/tsconfig.json @@ -10,6 +10,7 @@ "include": ["common/**/*", "public/**/*", "server/**/*"], "references": [ { "path": "../../core/tsconfig.json" }, - { "path": "../../plugins/kibana_utils/tsconfig.json" } + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../security_oss/tsconfig.json" } ] } diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.test.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.test.tsx new file mode 100644 index 0000000000000..2b7706d4f9d9f --- /dev/null +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.test.tsx @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Capabilities } from 'src/core/public'; +import { showPublicUrlSwitch } from './get_top_nav_config'; + +describe('showPublicUrlSwitch', () => { + test('returns false if "visualize" app is not available', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(false); + }); + + test('returns false if "visualize" app is not accessible', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + visualize: { + show: false, + }, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(false); + }); + + test('returns true if "visualize" app is not available an accessible', () => { + const anonymousUserCapabilities: Capabilities = { + catalogue: {}, + management: {}, + navLinks: {}, + visualize: { + show: true, + }, + }; + const result = showPublicUrlSwitch(anonymousUserCapabilities); + + expect(result).toBe(true); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index c4aefb397cd8a..b4ac98b672ee9 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { Capabilities } from 'src/core/public'; import { TopNavMenuData } from 'src/plugins/navigation/public'; import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public'; import { @@ -30,6 +31,14 @@ import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from './breadcrumbs'; import { EmbeddableStateTransfer } from '../../../../embeddable/public'; +interface VisualizeCapabilities { + createShortUrl: boolean; + delete: boolean; + save: boolean; + saveQuery: boolean; + show: boolean; +} + interface TopNavConfigParams { hasUnsavedChanges: boolean; setHasUnsavedChanges: (value: boolean) => void; @@ -45,6 +54,14 @@ interface TopNavConfigParams { embeddableId?: string; } +export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => { + if (!anonymousUserCapabilities.visualize) return false; + + const visualize = (anonymousUserCapabilities.visualize as unknown) as VisualizeCapabilities; + + return !!visualize.show; +}; + export const getTopNavConfig = ( { hasUnsavedChanges, @@ -243,6 +260,7 @@ export const getTopNavConfig = ( title: savedVis?.title, }, isDirty: hasUnappliedChanges || hasUnsavedChanges, + showPublicUrlSwitch, }); } }, From 99d176032487c9055927bd4b6e420a94c5c247a5 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 28 Jan 2021 12:14:52 -0600 Subject: [PATCH 087/163] Revert "[CI] Decrease number of Jest workers (#89504)" This reverts commit b2d441214608a71752b3e9ffdb29ccc5c10310bc. --- .ci/teamcity/oss/jest.sh | 2 +- .ci/teamcity/tests/jest.sh | 2 +- test/scripts/test/jest_unit.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/teamcity/oss/jest.sh b/.ci/teamcity/oss/jest.sh index 6d9396574c077..b323a88ef06bc 100755 --- a/.ci/teamcity/oss/jest.sh +++ b/.ci/teamcity/oss/jest.sh @@ -10,4 +10,4 @@ source "$(dirname "${0}")/../util.sh" export JOB=kibana-oss-jest checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --maxWorkers=5 --verbose + node scripts/jest --ci --verbose diff --git a/.ci/teamcity/tests/jest.sh b/.ci/teamcity/tests/jest.sh index 3d60915c1b1b5..c8b9b075e0e61 100755 --- a/.ci/teamcity/tests/jest.sh +++ b/.ci/teamcity/tests/jest.sh @@ -7,4 +7,4 @@ source "$(dirname "${0}")/../util.sh" export JOB=kibana-jest checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --maxWorkers=5 --verbose + node scripts/jest --ci --verbose --coverage diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index 06c159c0a4ace..14d7268c6f36d 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose --maxWorkers=6 --coverage + node scripts/jest --ci --verbose --maxWorkers=10 --coverage From 5c709bb6cc8e6c84a4f493f19d1c4c767d51547b Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 28 Jan 2021 12:15:45 -0600 Subject: [PATCH 088/163] Revert "[CI] Combines Jest test jobs (#85850)" This reverts commit 46ac4ed7a2827b2dd689d30a68c54e29b726ee0c. --- .ci/es-snapshots/Jenkinsfile_verify_es | 1 + .ci/jobs.yml | 1 + .ci/teamcity/default/jest.sh | 10 ++++++-- .ci/teamcity/oss/jest.sh | 7 ++---- .ci/teamcity/tests/jest.sh | 10 -------- .teamcity/src/Extensions.kt | 15 ++++++------ .teamcity/src/builds/test/AllTests.kt | 2 +- .teamcity/src/builds/test/Jest.kt | 2 +- .teamcity/src/builds/test/XPackJest.kt | 19 +++++++++++++++ .teamcity/src/projects/Kibana.kt | 1 + jest.config.integration.js | 5 +--- jest.config.js | 10 +------- jest.config.oss.js | 19 +++++++++++++++ packages/kbn-test/jest-preset.js | 2 +- .../shell_scripts/extract_archives.sh | 2 +- test/scripts/jenkins_unit.sh | 17 +++++++++++--- test/scripts/jenkins_xpack.sh | 23 +++++++++++++++++++ test/scripts/test/jest_integration.sh | 2 +- test/scripts/test/jest_unit.sh | 2 +- test/scripts/test/xpack_jest_unit.sh | 6 +++++ vars/kibanaCoverage.groovy | 7 ++++++ vars/kibanaPipeline.groovy | 15 ++++++------ vars/tasks.groovy | 1 + x-pack/jest.config.js | 11 +++++++++ 24 files changed, 135 insertions(+), 55 deletions(-) delete mode 100755 .ci/teamcity/tests/jest.sh create mode 100644 .teamcity/src/builds/test/XPackJest.kt create mode 100644 jest.config.oss.js create mode 100755 test/scripts/jenkins_xpack.sh create mode 100755 test/scripts/test/xpack_jest_unit.sh create mode 100644 x-pack/jest.config.js diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index b40cd91a45c57..11a39faa9aed0 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -29,6 +29,7 @@ kibanaPipeline(timeoutMinutes: 150) { withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { 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-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), diff --git a/.ci/jobs.yml b/.ci/jobs.yml index 6aa93d4a1056a..b05e834f5a459 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -2,6 +2,7 @@ JOB: - kibana-intake + - x-pack-intake - kibana-firefoxSmoke - kibana-ciGroup1 - kibana-ciGroup2 diff --git a/.ci/teamcity/default/jest.sh b/.ci/teamcity/default/jest.sh index dac1cc8986a1c..b900d1b6d6b4e 100755 --- a/.ci/teamcity/default/jest.sh +++ b/.ci/teamcity/default/jest.sh @@ -1,4 +1,10 @@ #!/bin/bash -# This file is temporary and can be removed once #85850 has been -# merged and the changes included in open PR's (~3 days after merging) +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-default-jest + +checks-reporter-with-killswitch "Jest Unit Tests" \ + node scripts/jest x-pack --ci --verbose --maxWorkers=5 diff --git a/.ci/teamcity/oss/jest.sh b/.ci/teamcity/oss/jest.sh index b323a88ef06bc..0dee07d00d2be 100755 --- a/.ci/teamcity/oss/jest.sh +++ b/.ci/teamcity/oss/jest.sh @@ -1,13 +1,10 @@ #!/bin/bash -# This file is temporary and can be removed once #85850 has been -# merged and the changes included in open PR's (~3 days after merging) - set -euo pipefail source "$(dirname "${0}")/../util.sh" export JOB=kibana-oss-jest -checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose +checks-reporter-with-killswitch "OSS Jest Unit Tests" \ + node scripts/jest --config jest.config.oss.js --ci --verbose --maxWorkers=5 diff --git a/.ci/teamcity/tests/jest.sh b/.ci/teamcity/tests/jest.sh deleted file mode 100755 index c8b9b075e0e61..0000000000000 --- a/.ci/teamcity/tests/jest.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -source "$(dirname "${0}")/../util.sh" - -export JOB=kibana-jest - -checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose --coverage diff --git a/.teamcity/src/Extensions.kt b/.teamcity/src/Extensions.kt index ce99c9c49e198..0a8abf4a149cf 100644 --- a/.teamcity/src/Extensions.kt +++ b/.teamcity/src/Extensions.kt @@ -20,21 +20,20 @@ fun BuildType.kibanaAgent(size: Int) { } val testArtifactRules = """ - target/junit/**/* target/kibana-* - target/kibana-coverage/**/* - target/kibana-security-solution/**/*.png target/test-metrics/* + target/kibana-security-solution/**/*.png + target/junit/**/* target/test-suites-ci-plan.json - test/**/screenshots/diff/*.png - test/**/screenshots/failure/*.png test/**/screenshots/session/*.png + test/**/screenshots/failure/*.png + test/**/screenshots/diff/*.png test/functional/failure_debug/html/*.html - x-pack/test/**/screenshots/diff/*.png - x-pack/test/**/screenshots/failure/*.png x-pack/test/**/screenshots/session/*.png - x-pack/test/functional/apps/reporting/reports/session/*.pdf + 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 """.trimIndent() fun BuildType.addTestSettings() { diff --git a/.teamcity/src/builds/test/AllTests.kt b/.teamcity/src/builds/test/AllTests.kt index a49d5f2b07f4c..9506d98cbe50e 100644 --- a/.teamcity/src/builds/test/AllTests.kt +++ b/.teamcity/src/builds/test/AllTests.kt @@ -9,5 +9,5 @@ object AllTests : BuildType({ description = "All Non-Functional Tests" type = Type.COMPOSITE - dependsOn(QuickTests, Jest, JestIntegration, OssApiServerIntegration) + dependsOn(QuickTests, Jest, XPackJest, JestIntegration, OssApiServerIntegration) }) diff --git a/.teamcity/src/builds/test/Jest.kt b/.teamcity/src/builds/test/Jest.kt index c9d170b5e5c3d..c33c9c2678ca4 100644 --- a/.teamcity/src/builds/test/Jest.kt +++ b/.teamcity/src/builds/test/Jest.kt @@ -12,7 +12,7 @@ object Jest : BuildType({ kibanaAgent(8) steps { - runbld("Jest Unit", "./.ci/teamcity/tests/jest.sh") + runbld("Jest Unit", "./.ci/teamcity/oss/jest.sh") } addTestSettings() diff --git a/.teamcity/src/builds/test/XPackJest.kt b/.teamcity/src/builds/test/XPackJest.kt new file mode 100644 index 0000000000000..8246b60823ff9 --- /dev/null +++ b/.teamcity/src/builds/test/XPackJest.kt @@ -0,0 +1,19 @@ +package builds.test + +import addTestSettings +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import kibanaAgent +import runbld + +object XPackJest : BuildType({ + name = "X-Pack Jest Unit" + description = "Executes X-Pack Jest Unit Tests" + + kibanaAgent(16) + + steps { + runbld("X-Pack Jest Unit", "./.ci/teamcity/default/jest.sh") + } + + addTestSettings() +}) diff --git a/.teamcity/src/projects/Kibana.kt b/.teamcity/src/projects/Kibana.kt index c84b65027dee6..5cddcf18e067f 100644 --- a/.teamcity/src/projects/Kibana.kt +++ b/.teamcity/src/projects/Kibana.kt @@ -77,6 +77,7 @@ fun Kibana(config: KibanaConfiguration = KibanaConfiguration()) : Project { name = "Jest" buildType(Jest) + buildType(XPackJest) buildType(JestIntegration) } diff --git a/jest.config.integration.js b/jest.config.integration.js index 99728c5471dfb..2064abb7e36a1 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -17,7 +17,6 @@ module.exports = { testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), - setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], reporters: [ 'default', [ @@ -25,7 +24,5 @@ module.exports = { { reportName: 'Jest Integration Tests' }, ], ], - coverageReporters: !!process.env.CI - ? [['json', { file: 'jest-integration.json' }]] - : ['html', 'text'], + setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], }; diff --git a/jest.config.js b/jest.config.js index 9ac5e57254e5a..f1833772c82a1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,14 +7,6 @@ */ module.exports = { - preset: '@kbn/test', rootDir: '.', - projects: [ - '/packages/*/jest.config.js', - '/src/*/jest.config.js', - '/src/legacy/*/jest.config.js', - '/src/plugins/*/jest.config.js', - '/test/*/jest.config.js', - '/x-pack/plugins/*/jest.config.js', - ], + projects: [...require('./jest.config.oss').projects, ...require('./x-pack/jest.config').projects], }; diff --git a/jest.config.oss.js b/jest.config.oss.js new file mode 100644 index 0000000000000..1b478aa85bdba --- /dev/null +++ b/jest.config.oss.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '.', + projects: [ + '/packages/*/jest.config.js', + '/src/*/jest.config.js', + '/src/legacy/*/jest.config.js', + '/src/plugins/*/jest.config.js', + '/test/*/jest.config.js', + ], +}; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index ebedb314f9594..ed88944ed862d 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -19,7 +19,7 @@ module.exports = { coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'], // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: !!process.env.CI ? [['json', { file: 'jest.json' }]] : ['html', 'text'], + coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], // An array of file extensions your modules use moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], diff --git a/src/dev/code_coverage/shell_scripts/extract_archives.sh b/src/dev/code_coverage/shell_scripts/extract_archives.sh index 14b35f8786d02..376467f9f2e55 100644 --- a/src/dev/code_coverage/shell_scripts/extract_archives.sh +++ b/src/dev/code_coverage/shell_scripts/extract_archives.sh @@ -6,7 +6,7 @@ EXTRACT_DIR=/tmp/extracted_coverage mkdir -p $EXTRACT_DIR echo "### Extracting downloaded artifacts" -for x in kibana-intake kibana-oss-tests kibana-xpack-tests; do +for x in kibana-intake x-pack-intake kibana-oss-tests kibana-xpack-tests; do tar -xzf $DOWNLOAD_DIR/coverage/${x}/kibana-coverage.tar.gz -C $EXTRACT_DIR || echo "### Error 'tarring': ${x}" done diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 9e387f97a016e..6e28f9c3ef56a 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -2,6 +2,12 @@ source test/scripts/jenkins_test_setup.sh +rename_coverage_file() { + test -f target/kibana-coverage/jest/coverage-final.json \ + && mv target/kibana-coverage/jest/coverage-final.json \ + target/kibana-coverage/jest/$1-coverage-final.json +} + if [[ -z "$CODE_COVERAGE" ]] ; then # Lint ./test/scripts/lint/eslint.sh @@ -28,8 +34,13 @@ if [[ -z "$CODE_COVERAGE" ]] ; then ./test/scripts/checks/test_hardening.sh else echo " -> Running jest tests with coverage" - node scripts/jest --ci --verbose --maxWorkers=6 --coverage || true; - + node scripts/jest --ci --verbose --coverage --config jest.config.oss.js || true; + rename_coverage_file "oss" + echo "" + echo "" echo " -> Running jest integration tests with coverage" - node scripts/jest_integration --ci --verbose --coverage || true; + node --max-old-space-size=8192 scripts/jest_integration --ci --verbose --coverage || true; + rename_coverage_file "oss-integration" + echo "" + echo "" fi diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh new file mode 100755 index 0000000000000..66fb5ae5370bc --- /dev/null +++ b/test/scripts/jenkins_xpack.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup.sh + +if [[ -z "$CODE_COVERAGE" ]] ; then + echo " -> Running jest tests" + + ./test/scripts/test/xpack_jest_unit.sh +else + echo " -> Build runtime for canvas" + # build runtime for canvas + echo "NODE_ENV=$NODE_ENV" + node ./x-pack/plugins/canvas/scripts/shareable_runtime + echo " -> Running jest tests with coverage" + cd x-pack + node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=5 --coverage --config jest.config.js || true; + # rename file in order to be unique one + test -f ../target/kibana-coverage/jest/coverage-final.json \ + && mv ../target/kibana-coverage/jest/coverage-final.json \ + ../target/kibana-coverage/jest/xpack-coverage-final.json + echo "" + echo "" +fi diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh index c48d9032466a3..78ed804f88430 100755 --- a/test/scripts/test/jest_integration.sh +++ b/test/scripts/test/jest_integration.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Integration Tests" \ - node scripts/jest_integration --ci --verbose --coverage + node scripts/jest_integration --ci --verbose diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index 14d7268c6f36d..88c0fe528b88c 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose --maxWorkers=10 --coverage + node scripts/jest --config jest.config.oss.js --ci --verbose --maxWorkers=5 diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh new file mode 100755 index 0000000000000..33b1c8a2b5183 --- /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 + +checks-reporter-with-killswitch "X-Pack Jest" \ + node scripts/jest x-pack --ci --verbose --maxWorkers=5 diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index e393f3a5d2150..609d8f78aeb96 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -197,6 +197,13 @@ def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, teamAssignmen def runTests() { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': { + withEnv([ + 'NODE_ENV=test' // Needed for jest tests only + ]) { + workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() + } + }, 'kibana-oss-agent' : workers.functional( 'kibana-oss-tests', { kibanaPipeline.buildOss() }, diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index e49692568cec8..93cb7a719bbe8 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -179,21 +179,20 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ - 'target/junit/**/*', 'target/kibana-*', - 'target/kibana-coverage/**/*', - 'target/kibana-security-solution/**/*.png', 'target/test-metrics/*', + 'target/kibana-security-solution/**/*.png', + 'target/junit/**/*', 'target/test-suites-ci-plan.json', - 'test/**/screenshots/diff/*.png', - 'test/**/screenshots/failure/*.png', 'test/**/screenshots/session/*.png', + 'test/**/screenshots/failure/*.png', + 'test/**/screenshots/diff/*.png', 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/diff/*.png', - 'x-pack/test/**/screenshots/failure/*.png', 'x-pack/test/**/screenshots/session/*.png', - 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', + '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', ] withEnv([ diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 74ad1267e9355..3493a95f0bdce 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -35,6 +35,7 @@ def test() { kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), + kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), ]) } diff --git a/x-pack/jest.config.js b/x-pack/jest.config.js new file mode 100644 index 0000000000000..8158987213cd2 --- /dev/null +++ b/x-pack/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '..', + projects: ['/x-pack/plugins/*/jest.config.js'], +}; From 074003d4b446e451f00cc75c11eacaca25bb41d3 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Thu, 28 Jan 2021 13:35:06 -0500 Subject: [PATCH 089/163] [Security Solution][Endpoint][Admin] Ransomware card (#87945) * [Security Solution][Endpoint][Admin] Ransomware card, package policy 7.12 migration --- .../fleet/server/saved_objects/index.ts | 6 +- .../common/endpoint/generate_data.ts | 2 +- .../common/endpoint/index_data.ts | 2 +- .../common/endpoint/models/policy_config.ts | 63 +++- ...{to_v7_11.0.test.ts => to_v7_11_0.test.ts} | 2 +- .../{to_v7_11.0.ts => to_v7_11_0.ts} | 0 .../policy/migrations/to_v7_12_0.test.ts | 199 ++++++++++ .../endpoint/policy/migrations/to_v7_12_0.ts | 35 ++ .../common/endpoint/types/index.ts | 22 +- .../common/license/policy_config.test.ts | 141 ++++++- .../common/license/policy_config.ts | 41 ++- .../common/shared_exports.ts | 3 +- .../policy/store/policy_details/index.test.ts | 15 +- .../policy/store/policy_details/middleware.ts | 4 + .../policy/store/policy_details/selectors.ts | 24 +- .../public/management/pages/policy/types.ts | 13 +- .../view/components/config_form/index.tsx | 2 +- .../pages/policy/view/policy_details.test.tsx | 12 +- .../pages/policy/view/policy_details_form.tsx | 13 +- .../view/policy_forms/protections/malware.tsx | 150 ++++---- .../protections/popup_options_to_versions.ts | 5 +- .../policy_forms/protections/ransomware.tsx | 346 ++++++++++++++++++ .../protections/supported_version.tsx | 29 ++ .../endpoint/endpoint_app_context_services.ts | 1 + .../endpoint/ingest_integration.test.ts | 30 +- .../server/endpoint/ingest_integration.ts | 16 +- .../endpoint/lib/policy/license_watch.test.ts | 4 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../apps/endpoint/policy_details.ts | 30 ++ .../services/endpoint_policy.ts | 4 +- 31 files changed, 1069 insertions(+), 149 deletions(-) rename x-pack/plugins/security_solution/common/endpoint/policy/migrations/{to_v7_11.0.test.ts => to_v7_11_0.test.ts} (98%) rename x-pack/plugins/security_solution/common/endpoint/policy/migrations/{to_v7_11.0.ts => to_v7_11_0.ts} (100%) create mode 100644 x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.test.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index dcc686e565b8e..7b4149819dc78 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -6,7 +6,10 @@ import { SavedObjectsServiceSetup, SavedObjectsType } from 'kibana/server'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; -import { migratePackagePolicyToV7110 } from '../../../security_solution/common'; +import { + migratePackagePolicyToV7110, + migratePackagePolicyToV7120, +} from '../../../security_solution/common'; import { OUTPUT_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE, @@ -273,6 +276,7 @@ const getSavedObjectTypes = ( migrations: { '7.10.0': migratePackagePolicyToV7100, '7.11.0': migratePackagePolicyToV7110, + '7.12.0': migratePackagePolicyToV7120, }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index fafd0c2772842..7e2c634b2f1cf 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -17,7 +17,7 @@ import { PolicyData, SafeEndpointEvent, } from './types'; -import { factory as policyFactory } from './models/policy_config'; +import { policyFactory } from './models/policy_config'; import { ancestryArray, entityIDSafeVersion, diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index b3259b19cf2c0..b9d7f6dfe7a5a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -30,7 +30,7 @@ import { PostAgentAcksResponse, PostAgentAcksRequest, } from '../../../fleet/common'; -import { factory as policyConfigFactory } from './models/policy_config'; +import { policyFactory as policyConfigFactory } from './models/policy_config'; import { HostMetadata } from './types'; import { KbnClientWithApiKeySupport } from '../../scripts/endpoint/kbn_client_with_api_key_support'; diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index 14941b019421b..614aac6ea8041 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -7,9 +7,9 @@ import { PolicyConfig, ProtectionModes } from '../types'; /** - * Return a new default `PolicyConfig`. + * Return a new default `PolicyConfig` for platinum and above licenses */ -export const factory = (): PolicyConfig => { +export const policyFactory = (): PolicyConfig => { return { windows: { events: { @@ -24,11 +24,18 @@ export const factory = (): PolicyConfig => { malware: { mode: ProtectionModes.prevent, }, + ransomware: { + mode: ProtectionModes.prevent, + }, popup: { malware: { message: '', enabled: true, }, + ransomware: { + message: '', + enabled: true, + }, }, logging: { file: 'info', @@ -46,11 +53,18 @@ export const factory = (): PolicyConfig => { malware: { mode: ProtectionModes.prevent, }, + ransomware: { + mode: ProtectionModes.prevent, + }, popup: { malware: { message: '', enabled: true, }, + ransomware: { + message: '', + enabled: true, + }, }, logging: { file: 'info', @@ -69,6 +83,51 @@ export const factory = (): PolicyConfig => { }; }; +/** + * Strips paid features from an existing or new `PolicyConfig` for gold and below license + */ +export const policyFactoryWithoutPaidFeatures = ( + policy: PolicyConfig = policyFactory() +): PolicyConfig => { + return { + ...policy, + windows: { + ...policy.windows, + ransomware: { + mode: ProtectionModes.off, + }, + popup: { + ...policy.windows.popup, + malware: { + message: '', + enabled: true, + }, + ransomware: { + message: '', + enabled: false, + }, + }, + }, + mac: { + ...policy.mac, + ransomware: { + mode: ProtectionModes.off, + }, + popup: { + ...policy.mac.popup, + malware: { + message: '', + enabled: true, + }, + ransomware: { + message: '', + enabled: false, + }, + }, + }, + }; +}; + /** * Reflects what string the Endpoint will use when message field is default/empty */ diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.test.ts similarity index 98% rename from x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts rename to x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.test.ts index 1b70a13935b7d..932afc6af3f16 100644 --- a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.test.ts @@ -6,7 +6,7 @@ import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server'; import { PackagePolicy } from '../../../../../fleet/common'; -import { migratePackagePolicyToV7110 } from './to_v7_11.0'; +import { migratePackagePolicyToV7110 } from './to_v7_11_0'; describe('7.11.0 Endpoint Package Policy migration', () => { const migration = migratePackagePolicyToV7110; diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.ts similarity index 100% rename from x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts rename to x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.test.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.test.ts new file mode 100644 index 0000000000000..2666b477921fc --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.test.ts @@ -0,0 +1,199 @@ +/* + * 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 { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { PackagePolicy } from '../../../../../fleet/common'; +import { PolicyData, ProtectionModes } from '../../types'; +import { migratePackagePolicyToV7120 } from './to_v7_12_0'; + +describe('7.12.0 Endpoint Package Policy migration', () => { + const migration = migratePackagePolicyToV7120; + it('adds ransomware option and notification customization', () => { + const doc: SavedObjectUnsanitizedDoc = { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'endpoint', + title: '', + version: '', + }, + id: 'endpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: { + windows: { + // @ts-expect-error + popup: { + malware: { + message: '', + enabled: false, + }, + }, + }, + mac: { + // @ts-expect-error + popup: { + malware: { + message: '', + enabled: false, + }, + }, + }, + }, + }, + }, + }, + ], + }, + type: ' nested', + }; + + expect( + migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc + ).toEqual({ + attributes: { + name: 'Some Policy Name', + package: { + name: 'endpoint', + title: '', + version: '', + }, + id: 'endpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: { + windows: { + ransomware: ProtectionModes.off, + popup: { + malware: { + message: '', + enabled: false, + }, + ransomware: { + message: '', + enabled: false, + }, + }, + }, + mac: { + ransomware: ProtectionModes.off, + popup: { + malware: { + message: '', + enabled: false, + }, + ransomware: { + message: '', + enabled: false, + }, + }, + }, + }, + }, + }, + }, + ], + }, + type: ' nested', + id: 'mock-saved-object-id', + }); + }); + + it('does not modify non-endpoint package policies', () => { + const doc: SavedObjectUnsanitizedDoc = { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'notEndpoint', + title: '', + version: '', + }, + id: 'notEndpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'notEndpoint', + enabled: true, + streams: [], + config: {}, + }, + ], + }, + type: ' nested', + }; + + expect( + migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc + ).toEqual({ + attributes: { + name: 'Some Policy Name', + package: { + name: 'notEndpoint', + title: '', + version: '', + }, + id: 'notEndpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'notEndpoint', + enabled: true, + streams: [], + config: {}, + }, + ], + }, + type: ' nested', + id: 'mock-saved-object-id', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.ts new file mode 100644 index 0000000000000..6004ef533d5ad --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.ts @@ -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. + */ + +import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { cloneDeep } from 'lodash'; +import { PackagePolicy } from '../../../../../fleet/common'; +import { ProtectionModes } from '../../types'; + +export const migratePackagePolicyToV7120: SavedObjectMigrationFn = ( + packagePolicyDoc +) => { + const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = cloneDeep( + packagePolicyDoc + ); + if (packagePolicyDoc.attributes.package?.name === 'endpoint') { + const input = updatedPackagePolicyDoc.attributes.inputs[0]; + const ransomware = { + message: '', + enabled: false, + }; + if (input && input.config) { + const policy = input.config.policy.value; + + policy.windows.ransomware = ProtectionModes.off; + policy.mac.ransomware = ProtectionModes.off; + policy.windows.popup.ransomware = ransomware; + policy.mac.popup.ransomware = ransomware; + } + } + + return updatedPackagePolicyDoc; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index fab5bd9daae00..f72373a6544a0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -816,7 +816,8 @@ export interface PolicyConfig { registry: boolean; security: boolean; }; - malware: MalwareFields; + malware: ProtectionFields; + ransomware: ProtectionFields; logging: { file: string; }; @@ -825,6 +826,10 @@ export interface PolicyConfig { message: string; enabled: boolean; }; + ransomware: { + message: string; + enabled: boolean; + }; }; antivirus_registration: { enabled: boolean; @@ -837,12 +842,17 @@ export interface PolicyConfig { process: boolean; network: boolean; }; - malware: MalwareFields; + malware: ProtectionFields; + ransomware: ProtectionFields; popup: { malware: { message: string; enabled: boolean; }; + ransomware: { + message: string; + enabled: boolean; + }; }; logging: { file: string; @@ -870,20 +880,20 @@ export interface UIPolicyConfig { */ windows: Pick< PolicyConfig['windows'], - 'events' | 'malware' | 'popup' | 'antivirus_registration' | 'advanced' + 'events' | 'malware' | 'ransomware' | 'popup' | 'antivirus_registration' | 'advanced' >; /** * Mac-specific policy configuration that is supported via the UI */ - mac: Pick; + mac: Pick; /** * Linux-specific policy configuration that is supported via the UI */ linux: Pick; } -/** Policy: Malware protection fields */ -export interface MalwareFields { +/** Policy: Protection fields */ +export interface ProtectionFields { mode: ProtectionModes; } diff --git a/x-pack/plugins/security_solution/common/license/policy_config.test.ts b/x-pack/plugins/security_solution/common/license/policy_config.test.ts index 6923bf00055f6..ef10bba7428ee 100644 --- a/x-pack/plugins/security_solution/common/license/policy_config.test.ts +++ b/x-pack/plugins/security_solution/common/license/policy_config.test.ts @@ -8,8 +8,13 @@ import { isEndpointPolicyValidForLicense, unsetPolicyFeaturesAboveLicenseLevel, } from './policy_config'; -import { DefaultMalwareMessage, factory } from '../endpoint/models/policy_config'; +import { + DefaultMalwareMessage, + policyFactory, + policyFactoryWithoutPaidFeatures, +} from '../endpoint/models/policy_config'; import { licenseMock } from '../../../licensing/common/licensing.mock'; +import { ProtectionModes } from '../endpoint/types'; describe('policy_config and licenses', () => { const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); @@ -18,13 +23,13 @@ describe('policy_config and licenses', () => { describe('isEndpointPolicyValidForLicense', () => { it('allows malware notification to be disabled with a Platinum license', () => { - const policy = factory(); + const policy = policyFactory(); policy.windows.popup.malware.enabled = false; // make policy change const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); }); it('blocks windows malware notification changes below Platinum licenses', () => { - const policy = factory(); + const policy = policyFactory(); policy.windows.popup.malware.enabled = false; // make policy change let valid = isEndpointPolicyValidForLicense(policy, Gold); expect(valid).toBeFalsy(); @@ -34,7 +39,7 @@ describe('policy_config and licenses', () => { }); it('blocks mac malware notification changes below Platinum licenses', () => { - const policy = factory(); + const policy = policyFactory(); policy.mac.popup.malware.enabled = false; // make policy change let valid = isEndpointPolicyValidForLicense(policy, Gold); expect(valid).toBeFalsy(); @@ -44,13 +49,13 @@ describe('policy_config and licenses', () => { }); it('allows malware notification message changes with a Platinum license', () => { - const policy = factory(); + const policy = policyFactory(); policy.windows.popup.malware.message = 'BOOM'; // make policy change const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); }); it('blocks windows malware notification message changes below Platinum licenses', () => { - const policy = factory(); + const policy = policyFactory(); policy.windows.popup.malware.message = 'BOOM'; // make policy change let valid = isEndpointPolicyValidForLicense(policy, Gold); expect(valid).toBeFalsy(); @@ -59,7 +64,7 @@ describe('policy_config and licenses', () => { expect(valid).toBeFalsy(); }); it('blocks mac malware notification message changes below Platinum licenses', () => { - const policy = factory(); + const policy = policyFactory(); policy.mac.popup.malware.message = 'BOOM'; // make policy change let valid = isEndpointPolicyValidForLicense(policy, Gold); expect(valid).toBeFalsy(); @@ -68,16 +73,71 @@ describe('policy_config and licenses', () => { expect(valid).toBeFalsy(); }); + it('allows ransomware to be turned on for Platinum licenses', () => { + const policy = policyFactoryWithoutPaidFeatures(); + policy.windows.ransomware.mode = ProtectionModes.prevent; + policy.mac.ransomware.mode = ProtectionModes.prevent; + + const valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeTruthy(); + }); + it('blocks ransomware to be turned on for Gold and below licenses', () => { + const policy = policyFactoryWithoutPaidFeatures(); + policy.windows.ransomware.mode = ProtectionModes.prevent; + policy.mac.ransomware.mode = ProtectionModes.prevent; + + let valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('allows ransomware notification to be turned on with a Platinum license', () => { + const policy = policyFactoryWithoutPaidFeatures(); + policy.windows.popup.ransomware.enabled = true; + policy.mac.popup.ransomware.enabled = true; + const valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeTruthy(); + }); + it('blocks ransomware notification to be turned on for Gold and below licenses', () => { + const policy = policyFactoryWithoutPaidFeatures(); + policy.windows.popup.ransomware.enabled = true; + policy.mac.popup.ransomware.enabled = true; + let valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('allows ransomware notification message changes with a Platinum license', () => { + const policy = policyFactory(); + policy.windows.popup.ransomware.message = 'BOOM'; + policy.mac.popup.ransomware.message = 'BOOM'; + const valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeTruthy(); + }); + it('blocks ransomware notification message changes for Gold and below licenses', () => { + const policy = policyFactory(); + policy.windows.popup.ransomware.message = 'BOOM'; + policy.mac.popup.ransomware.message = 'BOOM'; + let valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + it('allows default policyConfig with Basic', () => { - const policy = factory(); + const policy = policyFactoryWithoutPaidFeatures(); const valid = isEndpointPolicyValidForLicense(policy, Basic); expect(valid).toBeTruthy(); }); }); describe('unsetPolicyFeaturesAboveLicenseLevel', () => { - it('does not change any fields with a Platinum license', () => { - const policy = factory(); + it('does not change any malware fields with a Platinum license', () => { + const policy = policyFactory(); const popupMessage = 'WOOP WOOP'; policy.windows.popup.malware.message = popupMessage; policy.mac.popup.malware.message = popupMessage; @@ -88,14 +148,37 @@ describe('policy_config and licenses', () => { expect(retPolicy.windows.popup.malware.message).toEqual(popupMessage); expect(retPolicy.mac.popup.malware.message).toEqual(popupMessage); }); - it('resets Platinum-paid fields for lower license tiers', () => { - const defaults = factory(); // reference - const policy = factory(); // what we will modify, and should be reset + + it('does not change any ransomware fields with a Platinum license', () => { + const policy = policyFactory(); + const popupMessage = 'WOOP WOOP'; + policy.windows.ransomware.mode = ProtectionModes.detect; + policy.mac.ransomware.mode = ProtectionModes.detect; + policy.windows.popup.ransomware.enabled = false; + policy.mac.popup.ransomware.enabled = false; + policy.windows.popup.ransomware.message = popupMessage; + policy.mac.popup.ransomware.message = popupMessage; + + const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Platinum); + expect(retPolicy.windows.ransomware.mode).toEqual(ProtectionModes.detect); + expect(retPolicy.mac.ransomware.mode).toEqual(ProtectionModes.detect); + expect(retPolicy.windows.popup.ransomware.enabled).toBeFalsy(); + expect(retPolicy.mac.popup.ransomware.enabled).toBeFalsy(); + expect(retPolicy.windows.popup.ransomware.message).toEqual(popupMessage); + expect(retPolicy.mac.popup.ransomware.message).toEqual(popupMessage); + }); + + it('resets Platinum-paid malware fields for lower license tiers', () => { + const defaults = policyFactory(); // reference + const policy = policyFactory(); // what we will modify, and should be reset const popupMessage = 'WOOP WOOP'; policy.windows.popup.malware.message = popupMessage; policy.mac.popup.malware.message = popupMessage; policy.windows.popup.malware.enabled = false; + policy.windows.popup.ransomware.message = popupMessage; + policy.mac.popup.ransomware.message = popupMessage; + policy.windows.popup.ransomware.enabled = false; const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Gold); expect(retPolicy.windows.popup.malware.enabled).toEqual( defaults.windows.popup.malware.enabled @@ -106,5 +189,37 @@ describe('policy_config and licenses', () => { // need to invert the test, since it could be either value expect(['', DefaultMalwareMessage]).toContain(retPolicy.windows.popup.malware.message); }); + + it('resets Platinum-paid ransomware fields for lower license tiers', () => { + const defaults = policyFactoryWithoutPaidFeatures(); // reference + const policy = policyFactory(); // what we will modify, and should be reset + const popupMessage = 'WOOP WOOP'; + policy.windows.popup.ransomware.message = popupMessage; + policy.mac.popup.ransomware.message = popupMessage; + + const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Gold); + + expect(retPolicy.windows.ransomware.mode).toEqual(defaults.windows.ransomware.mode); + expect(retPolicy.mac.ransomware.mode).toEqual(defaults.mac.ransomware.mode); + expect(retPolicy.windows.popup.ransomware.enabled).toEqual( + defaults.windows.popup.ransomware.enabled + ); + expect(retPolicy.mac.popup.ransomware.enabled).toEqual(defaults.mac.popup.ransomware.enabled); + expect(retPolicy.windows.popup.ransomware.message).not.toEqual(popupMessage); + expect(retPolicy.mac.popup.ransomware.message).not.toEqual(popupMessage); + + // need to invert the test, since it could be either value + expect(['', DefaultMalwareMessage]).toContain(retPolicy.windows.popup.ransomware.message); + expect(['', DefaultMalwareMessage]).toContain(retPolicy.mac.popup.ransomware.message); + }); + }); + + describe('policyFactoryWithoutPaidFeatures for gold and below license', () => { + it('preserves non license-gated features', () => { + const policy = policyFactory(); // what we will modify, and should be reset + policy.windows.events.file = false; + const retPolicy = policyFactoryWithoutPaidFeatures(policy); + expect(retPolicy.windows.events.file).toBeFalsy(); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/license/policy_config.ts b/x-pack/plugins/security_solution/common/license/policy_config.ts index da2260ad55e8b..e791b68f12f40 100644 --- a/x-pack/plugins/security_solution/common/license/policy_config.ts +++ b/x-pack/plugins/security_solution/common/license/policy_config.ts @@ -7,7 +7,10 @@ import { ILicense } from '../../../licensing/common/types'; import { isAtLeast } from './license'; import { PolicyConfig } from '../endpoint/types'; -import { DefaultMalwareMessage, factory } from '../endpoint/models/policy_config'; +import { + DefaultMalwareMessage, + policyFactoryWithoutPaidFeatures, +} from '../endpoint/models/policy_config'; /** * Given an endpoint package policy, verifies that all enabled features that @@ -21,7 +24,7 @@ export const isEndpointPolicyValidForLicense = ( return true; // currently, platinum allows all features } - const defaults = factory(); + const defaults = policyFactoryWithoutPaidFeatures(); // only platinum or higher may disable malware notification if ( @@ -40,6 +43,32 @@ export const isEndpointPolicyValidForLicense = ( return false; } + // only platinum or higher may enable ransomware + if ( + policy.windows.ransomware.mode !== defaults.windows.ransomware.mode || + policy.mac.ransomware.mode !== defaults.mac.ransomware.mode + ) { + return false; + } + + // only platinum or higher may enable ransomware notification + if ( + policy.windows.popup.ransomware.enabled !== defaults.windows.popup.ransomware.enabled || + policy.mac.popup.ransomware.enabled !== defaults.mac.popup.ransomware.enabled + ) { + return false; + } + + // Only Platinum or higher may change the ransomware message (which can be blank or what Endpoint defaults) + if ( + [policy.windows, policy.mac].some( + (p) => + p.popup.ransomware.message !== '' && p.popup.ransomware.message !== DefaultMalwareMessage + ) + ) { + return false; + } + return true; }; @@ -55,12 +84,6 @@ export const unsetPolicyFeaturesAboveLicenseLevel = ( return policy; } - const defaults = factory(); // set any license-gated features back to the defaults - policy.windows.popup.malware.enabled = defaults.windows.popup.malware.enabled; - policy.mac.popup.malware.enabled = defaults.mac.popup.malware.enabled; - policy.windows.popup.malware.message = defaults.windows.popup.malware.message; - policy.mac.popup.malware.message = defaults.mac.popup.malware.message; - - return policy; + return policyFactoryWithoutPaidFeatures(policy); }; diff --git a/x-pack/plugins/security_solution/common/shared_exports.ts b/x-pack/plugins/security_solution/common/shared_exports.ts index fb457933f4b54..92a736ae601df 100644 --- a/x-pack/plugins/security_solution/common/shared_exports.ts +++ b/x-pack/plugins/security_solution/common/shared_exports.ts @@ -16,4 +16,5 @@ export { exactCheck } from './exact_check'; export { getPaths, foldLeftRight, removeExternalLinkText } from './test_utils'; export { validate, validateEither } from './validate'; export { formatErrors } from './format_errors'; -export { migratePackagePolicyToV7110 } from './endpoint/policy/migrations/to_v7_11.0'; +export { migratePackagePolicyToV7110 } from './endpoint/policy/migrations/to_v7_11_0'; +export { migratePackagePolicyToV7120 } from './endpoint/policy/migrations/to_v7_12_0'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 70ffc1f8a9fc4..bda268c1fad00 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -8,7 +8,7 @@ import { PolicyDetailsState } from '../../types'; import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction, policyDetailsMiddlewareFactory } from './index'; import { policyConfig } from './selectors'; -import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; +import { policyFactory } from '../../../../../../common/endpoint/models/policy_config'; import { PolicyData } from '../../../../../../common/endpoint/types'; import { createSpyMiddleware, @@ -54,7 +54,7 @@ describe('policy details: ', () => { }, }, policy: { - value: policyConfigFactory(), + value: policyFactory(), }, }, }, @@ -254,6 +254,7 @@ describe('policy details: ', () => { http.put.mock.calls.length - 1 ] as unknown) as [string, HttpFetchOptions])[1]; + // license is below platinum in this test, paid features are off expect(JSON.parse(lastPutCallPayload.body as string)).toEqual({ name: '', description: '', @@ -282,11 +283,16 @@ describe('policy details: ', () => { security: true, }, malware: { mode: 'prevent' }, + ransomware: { mode: 'off' }, popup: { malware: { enabled: true, message: '', }, + ransomware: { + enabled: false, + message: '', + }, }, logging: { file: 'info' }, antivirus_registration: { @@ -296,11 +302,16 @@ describe('policy details: ', () => { mac: { events: { process: true, file: true, network: true }, malware: { mode: 'prevent' }, + ransomware: { mode: 'off' }, popup: { malware: { enabled: true, message: '', }, + ransomware: { + enabled: false, + message: '', + }, }, logging: { file: 'info' }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts index cc286b4c478d3..4d54acb5eae13 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts @@ -41,6 +41,10 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory { if (policyData) { - unsetPolicyFeaturesAboveLicenseLevel( - policyData?.inputs[0]?.config.policy.value, + const policyValue = unsetPolicyFeaturesAboveLicenseLevel( + policyData.inputs[0].config.policy.value, license as ILicense ); + const newPolicyData: Immutable = { + ...policyData, + inputs: [ + { + ...policyData.inputs[0], + config: { + ...policyData.inputs[0].config, + policy: { + ...policyData.inputs[0].config.policy, + value: policyValue, + }, + }, + }, + ], + }; + return newPolicyData; } return policyData; } @@ -167,6 +183,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel advanced: windows.advanced, events: windows.events, malware: windows.malware, + ransomware: windows.ransomware, popup: windows.popup, antivirus_registration: windows.antivirus_registration, }, @@ -174,6 +191,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel advanced: mac.advanced, events: mac.events, malware: mac.malware, + ransomware: mac.ransomware, popup: mac.popup, }, linux: { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 228e8cc1c4385..a37e404cdc522 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -8,7 +8,7 @@ import { ILicense } from '../../../../../licensing/common/types'; import { AppLocation, Immutable, - MalwareFields, + ProtectionFields, PolicyData, UIPolicyConfig, } from '../../../../common/endpoint/types'; @@ -108,7 +108,16 @@ export type KeysByValueCriteria = { }[keyof O]; /** Returns an array of the policy OSes that have a malware protection field */ -export type MalwareProtectionOSes = KeysByValueCriteria; +export type MalwareProtectionOSes = KeysByValueCriteria< + UIPolicyConfig, + { malware: ProtectionFields } +>; + +/** Returns an array of the policy OSes that have a ransomware protection field */ +export type RansomwareProtectionOSes = KeysByValueCriteria< + UIPolicyConfig, + { ransomware: ProtectionFields } +>; export interface GetPolicyListResponse extends GetPackagePoliciesResponse { items: PolicyData[]; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx index ce5eb03d60cd0..aea4df5b2a6fd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -58,7 +58,7 @@ export const ConfigForm: FC = memo( {TITLES.type} {type} - + {TITLES.os} {supportedOss.map((os) => OS_TITLES[os]).join(', ')} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 1280f1c351c2b..55fc7703de44b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -301,11 +301,16 @@ describe('Policy Details', () => { const userNotificationCustomMessageTextArea = policyView.find( 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' ); - const tooltip = policyView.find('EuiIconTip'); + const tooltip = policyView.find('EuiIconTip[data-test-subj="malwareTooltip"]'); expect(userNotificationCheckbox).toHaveLength(1); expect(userNotificationCustomMessageTextArea).toHaveLength(1); expect(tooltip).toHaveLength(1); }); + + it('ransomware card is shown', () => { + const ransomware = policyView.find('EuiPanel[data-test-subj="ransomwareProtectionsForm"]'); + expect(ransomware).toHaveLength(1); + }); }); describe('when the subscription tier is gold or lower', () => { beforeEach(() => { @@ -325,6 +330,11 @@ describe('Policy Details', () => { expect(userNotificationCustomMessageTextArea).toHaveLength(0); expect(tooltip).toHaveLength(0); }); + + it('ransomware card is hidden', () => { + const ransomware = policyView.find('EuiPanel[data-test-subj="ransomwareProtectionsForm"]'); + expect(ransomware).toHaveLength(0); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx index a0bf2b37e8a12..8710f696fad41 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -11,12 +11,15 @@ import { MalwareProtections } from './policy_forms/protections/malware'; import { LinuxEvents, MacEvents, WindowsEvents } from './policy_forms/events'; import { AdvancedPolicyForms } from './policy_advanced'; import { AntivirusRegistrationForm } from './components/antivirus_registration_form'; +import { Ransomware } from './policy_forms/protections/ransomware'; +import { useLicense } from '../../../../common/hooks/use_license'; export const PolicyDetailsForm = memo(() => { const [showAdvancedPolicy, setShowAdvancedPolicy] = useState(false); const handleAdvancedPolicyClick = useCallback(() => { setShowAdvancedPolicy(!showAdvancedPolicy); }, [showAdvancedPolicy]); + const isPlatinumPlus = useLicense().isPlatinumPlus(); return ( <> @@ -31,6 +34,8 @@ export const PolicyDetailsForm = memo(() => { + + {isPlatinumPlus && } @@ -44,14 +49,14 @@ export const PolicyDetailsForm = memo(() => { - + - + - + - + props.theme.eui.euiSizeXXL}; +export const RadioFlexGroup = styled(EuiFlexGroup)` + .no-right-margin-radio { + margin-right: 0; + } + .no-horizontal-margin-radio { + margin: ${(props) => props.theme.eui.ruleMargins.marginSmall} 0; } `; const OSes: Immutable = [OS.windows, OS.mac]; const protection = 'malware'; -const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: string }) => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch(); - const radioButtonId = useMemo(() => htmlIdGenerator()(), []); - // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value - const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; - const isPlatinumPlus = useLicense().isPlatinumPlus(); +const ProtectionRadio = React.memo( + ({ protectionMode, label }: { protectionMode: ProtectionModes; label: string }) => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch<(action: AppAction) => void>(); + const radioButtonId = useMemo(() => htmlIdGenerator()(), []); + // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; + const isPlatinumPlus = useLicense().isPlatinumPlus(); - const handleRadioChange = useCallback(() => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of OSes) { - newPayload[os][protection].mode = id; - if (isPlatinumPlus) { - if (id === ProtectionModes.prevent) { - newPayload[os].popup[protection].enabled = true; - } else { - newPayload[os].popup[protection].enabled = false; + const handleRadioChange = useCallback(() => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + for (const os of OSes) { + newPayload[os][protection].mode = protectionMode; + if (isPlatinumPlus) { + if (protectionMode === ProtectionModes.prevent) { + newPayload[os].popup[protection].enabled = true; + } else { + newPayload[os].popup[protection].enabled = false; + } } } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, [dispatch, id, policyDetailsConfig, isPlatinumPlus]); + }, [dispatch, protectionMode, policyDetailsConfig, isPlatinumPlus]); - /** - * Passing an arbitrary id because EuiRadio - * requires an id if label is passed - */ + /** + * Passing an arbitrary id because EuiRadio + * requires an id if label is passed + */ - return ( - - ); -}); - -ProtectionRadio.displayName = 'ProtectionRadio'; - -const SupportedVersionNotice = ({ optionName }: { optionName: string }) => { - const version = popupVersionsMap.get(optionName); - if (!version) { - return null; + return ( + + ); } +); - return ( - - - - - - ); -}; +ProtectionRadio.displayName = 'ProtectionRadio'; /** The Malware Protections form for policy details * which will configure for all relevant OSes. */ export const MalwareProtections = React.memo(() => { const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch(); + const dispatch = useDispatch<(action: AppAction) => void>(); // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; const userNotificationSelected = @@ -224,19 +209,25 @@ export const MalwareProtections = React.memo(() => { /> - - {radios.map((radio) => { - return ( - - ); - })} - + + + + + + + + {isPlatinumPlus && ( <> + { @@ -327,7 +319,7 @@ export const MalwareProtections = React.memo(() => { label={i18n.translate( 'xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled', { - defaultMessage: 'Malware Protections {mode, select, true {Enabled} false {Disabled}}', + defaultMessage: 'Malware protections {mode, select, true {enabled} false {disabled}}', values: { mode: selected !== ProtectionModes.off, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts index d4c7d0102ebd4..795f7dda52499 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -const popupVersions: Array<[string, string]> = [['malware', '7.11+']]; +const popupVersions: Array<[string, string]> = [ + ['malware', '7.11+'], + ['ransomware', '7.12+'], +]; export const popupVersionsMap: ReadonlyMap = new Map(popupVersions); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx new file mode 100644 index 0000000000000..eb2dd4b2fe8d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx @@ -0,0 +1,346 @@ +/* + * 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, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCallOut, + EuiCheckbox, + EuiRadio, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTextArea, + htmlIdGenerator, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiCheckboxProps, + EuiRadioProps, + EuiSwitchProps, +} from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import { APP_ID } from '../../../../../../../common/constants'; +import { SecurityPageName } from '../../../../../../app/types'; +import { + Immutable, + OperatingSystem, + ProtectionModes, +} from '../../../../../../../common/endpoint/types'; +import { RansomwareProtectionOSes, OS } from '../../../types'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; +import { AppAction } from '../../../../../../common/store/actions'; +import { SupportedVersionNotice } from './supported_version'; +import { RadioFlexGroup } from './malware'; + +const OSes: Immutable = [OS.windows, OS.mac]; +const protection = 'ransomware'; + +const ProtectionRadio = React.memo( + ({ protectionMode, label }: { protectionMode: ProtectionModes; label: string }) => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch<(action: AppAction) => void>(); + const radioButtonId = useMemo(() => htmlIdGenerator()(), []); + // currently just taking windows.ransomware, but both windows.ransomware and mac.ransomware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.ransomware.mode; + + const handleRadioChange: EuiRadioProps['onChange'] = useCallback(() => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + for (const os of OSes) { + newPayload[os][protection].mode = protectionMode; + if (protectionMode === ProtectionModes.prevent) { + newPayload[os].popup[protection].enabled = true; + } else { + newPayload[os].popup[protection].enabled = false; + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, [dispatch, protectionMode, policyDetailsConfig]); + + /** + * Passing an arbitrary id because EuiRadio + * requires an id if label is passed + */ + + return ( + + ); + } +); + +ProtectionRadio.displayName = 'ProtectionRadio'; + +/** The Ransomware Protections form for policy details + * which will configure for all relevant OSes. + */ +export const Ransomware = React.memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch<(action: AppAction) => void>(); + // currently just taking windows.ransomware, but both windows.ransomware and mac.ransomware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.ransomware.mode; + const userNotificationSelected = + policyDetailsConfig && policyDetailsConfig.windows.popup.ransomware.enabled; + const userNotificationMessage = + policyDetailsConfig && policyDetailsConfig.windows.popup.ransomware.message; + + const radios: Immutable< + Array<{ + id: ProtectionModes; + label: string; + protection: 'ransomware'; + }> + > = useMemo(() => { + return [ + { + id: ProtectionModes.detect, + label: i18n.translate('xpack.securitySolution.endpoint.policy.details.detect', { + defaultMessage: 'Detect', + }), + protection: 'ransomware', + }, + { + id: ProtectionModes.prevent, + label: i18n.translate('xpack.securitySolution.endpoint.policy.details.prevent', { + defaultMessage: 'Prevent', + }), + protection: 'ransomware', + }, + ]; + }, []); + + const handleSwitchChange: EuiSwitchProps['onChange'] = useCallback( + (event) => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + if (event.target.checked === false) { + for (const os of OSes) { + newPayload[os][protection].mode = ProtectionModes.off; + newPayload[os].popup[protection].enabled = event.target.checked; + } + } else { + for (const os of OSes) { + newPayload[os][protection].mode = ProtectionModes.prevent; + newPayload[os].popup[protection].enabled = event.target.checked; + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [dispatch, policyDetailsConfig] + ); + + const handleUserNotificationCheckbox: EuiCheckboxProps['onChange'] = useCallback( + (event) => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + for (const os of OSes) { + newPayload[os].popup[protection].enabled = event.target.checked; + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [policyDetailsConfig, dispatch] + ); + + const handleCustomUserNotification = useCallback( + (event) => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + for (const os of OSes) { + newPayload[os].popup[protection].message = event.target.value; + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [policyDetailsConfig, dispatch] + ); + + const radioButtons = useMemo(() => { + return ( + <> + + + + + + + + + + + + + + + + + + + + {userNotificationSelected && ( + <> + + + + +

+ +

+
+
+ + + + + + + } + /> + +
+ + + + )} + + ); + }, [ + radios, + selected, + handleUserNotificationCheckbox, + userNotificationSelected, + userNotificationMessage, + handleCustomUserNotification, + ]); + + const protectionSwitch = useMemo(() => { + return ( + + ); + }, [handleSwitchChange, selected]); + + return ( + + {radioButtons} + + + + + + ), + }} + /> + + + ); +}); + +Ransomware.displayName = 'RansomwareProtections'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx new file mode 100644 index 0000000000000..dee6418b4f3ff --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx @@ -0,0 +1,29 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText } from '@elastic/eui'; +import { popupVersionsMap } from './popup_options_to_versions'; + +export const SupportedVersionNotice = ({ optionName }: { optionName: string }) => { + const version = popupVersionsMap.get(optionName); + if (!version) { + return null; + } + + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index ec3d35cbb6585..7f9e8b42490fa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -124,6 +124,7 @@ export class EndpointAppContextService { dependencies.config.maxTimelineImportExportSize, dependencies.security, dependencies.alerts, + dependencies.licenseService, dependencies.exceptionListsClient ) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts index d287ada74eebc..2710e4afb5968 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -6,7 +6,10 @@ import { httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; import { createNewPackagePolicyMock } from '../../../fleet/common/mocks'; -import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; +import { + policyFactory, + policyFactoryWithoutPaidFeatures, +} from '../../common/endpoint/models/policy_config'; import { getManifestManagerMock, ManifestManagerMockType, @@ -55,6 +58,9 @@ describe('ingest_integration tests ', () => { }); describe('ingest_integration sanity checks', () => { + beforeEach(() => { + licenseEmitter.next(Platinum); // set license level to platinum + }); test('policy is updated with initial manifest', async () => { const logger = loggingSystemMock.create().get('ingest_integration.test'); const manifestManager = getManifestManagerMock({ @@ -68,13 +74,14 @@ describe('ingest_integration tests ', () => { maxTimelineImportExportSize, endpointAppContextMock.security, endpointAppContextMock.alerts, + licenseService, exceptionListClient ); const policyConfig = createNewPackagePolicyMock(); // policy config without manifest const newPolicyConfig = await callback(policyConfig, ctx, req); // policy config WITH manifest expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual({ artifacts: { 'endpoint-exceptionlist-macos-v1': { @@ -146,13 +153,14 @@ describe('ingest_integration tests ', () => { maxTimelineImportExportSize, endpointAppContextMock.security, endpointAppContextMock.alerts, + licenseService, exceptionListClient ); const policyConfig = createNewPackagePolicyMock(); const newPolicyConfig = await callback(policyConfig, ctx, req); expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( lastComputed!.toEndpointFormat() ); @@ -174,13 +182,14 @@ describe('ingest_integration tests ', () => { maxTimelineImportExportSize, endpointAppContextMock.security, endpointAppContextMock.alerts, + licenseService, exceptionListClient ); const policyConfig = createNewPackagePolicyMock(); const newPolicyConfig = await callback(policyConfig, ctx, req); expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); }); test('subsequent policy creations succeed', async () => { @@ -196,13 +205,14 @@ describe('ingest_integration tests ', () => { maxTimelineImportExportSize, endpointAppContextMock.security, endpointAppContextMock.alerts, + licenseService, exceptionListClient ); const policyConfig = createNewPackagePolicyMock(); const newPolicyConfig = await callback(policyConfig, ctx, req); expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( lastComputed!.toEndpointFormat() ); @@ -221,6 +231,7 @@ describe('ingest_integration tests ', () => { maxTimelineImportExportSize, endpointAppContextMock.security, endpointAppContextMock.alerts, + licenseService, exceptionListClient ); const policyConfig = createNewPackagePolicyMock(); @@ -228,7 +239,7 @@ describe('ingest_integration tests ', () => { expect(exceptionListClient.createEndpointList).toHaveBeenCalled(); expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); + expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( lastComputed!.toEndpointFormat() ); @@ -239,8 +250,7 @@ describe('ingest_integration tests ', () => { licenseEmitter.next(Gold); // set license level to gold }); it('returns an error if paid features are turned on in the policy', async () => { - const mockPolicy = policyConfigFactory(); - mockPolicy.windows.popup.malware.message = 'paid feature'; + const mockPolicy = policyFactory(); // defaults with paid features on const logger = loggingSystemMock.create().get('ingest_integration.test'); const callback = getPackagePolicyUpdateCallback(logger, licenseService); const policyConfig = generator.generatePolicyPackagePolicy(); @@ -250,7 +260,7 @@ describe('ingest_integration tests ', () => { ); }); it('updates successfully if no paid features are turned on in the policy', async () => { - const mockPolicy = policyConfigFactory(); + const mockPolicy = policyFactoryWithoutPaidFeatures(); mockPolicy.windows.malware.mode = ProtectionModes.detect; const logger = loggingSystemMock.create().get('ingest_integration.test'); const callback = getPackagePolicyUpdateCallback(logger, licenseService); @@ -265,7 +275,7 @@ describe('ingest_integration tests ', () => { licenseEmitter.next(Platinum); // set license level to platinum }); it('updates successfully when paid features are turned on', async () => { - const mockPolicy = policyConfigFactory(); + const mockPolicy = policyFactory(); mockPolicy.windows.popup.malware.message = 'paid feature'; const logger = loggingSystemMock.create().get('ingest_integration.test'); const callback = getPackagePolicyUpdateCallback(logger, licenseService); diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index 1e7f440ed6788..114c6ba969227 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -10,7 +10,10 @@ import { SecurityPluginSetup } from '../../../security/server'; import { ExternalCallback } from '../../../fleet/server'; import { KibanaRequest, Logger, RequestHandlerContext } from '../../../../../src/core/server'; import { NewPackagePolicy, UpdatePackagePolicy } from '../../../fleet/common/types/models'; -import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; +import { + policyFactory as policyConfigFactory, + policyFactoryWithoutPaidFeatures as policyConfigFactoryWithoutPaidFeatures, +} from '../../common/endpoint/models/policy_config'; import { NewPolicyData } from '../../common/endpoint/types'; import { ManifestManager } from './services/artifacts'; import { Manifest } from './lib/artifacts'; @@ -22,7 +25,7 @@ import { createDetectionIndex } from '../lib/detection_engine/routes/index/creat import { createPrepackagedRules } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; import { buildFrameworkRequest } from '../lib/timeline/routes/utils/common'; import { isEndpointPolicyValidForLicense } from '../../common/license/policy_config'; -import { LicenseService } from '../../common/license/license'; +import { isAtLeast, LicenseService } from '../../common/license/license'; const getManifest = async (logger: Logger, manifestManager: ManifestManager): Promise => { let manifest: Manifest | null = null; @@ -86,6 +89,7 @@ export const getPackagePolicyCreateCallback = ( maxTimelineImportExportSize: number, securitySetup: SecurityPluginSetup, alerts: AlertsStartContract, + licenseService: LicenseService, exceptionsClient: ExceptionListClient | undefined ): ExternalCallback[1] => { const handlePackagePolicyCreate = async ( @@ -151,6 +155,12 @@ export const getPackagePolicyCreateCallback = ( // Until we get the Default Policy Configuration in the Endpoint package, // we will add it here manually at creation time. + + // generate the correct default policy depending on the license + const defaultPolicy = isAtLeast(licenseService.getLicenseInformation(), 'platinum') + ? policyConfigFactory() + : policyConfigFactoryWithoutPaidFeatures(); + updatedPackagePolicy = { ...newPackagePolicy, inputs: [ @@ -163,7 +173,7 @@ export const getPackagePolicyCreateCallback = ( value: serializedManifest, }, policy: { - value: policyConfigFactory(), + value: defaultPolicy, }, }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts index 225592fa8e686..8e7c4d2d4daf5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts @@ -18,7 +18,7 @@ import { licenseMock } from '../../../../../licensing/common/licensing.mock'; import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { PackagePolicy } from '../../../../../fleet/common'; import { createPackagePolicyMock } from '../../../../../fleet/common/mocks'; -import { factory } from '../../../../common/endpoint/models/policy_config'; +import { policyFactory } from '../../../../common/endpoint/models/policy_config'; import { PolicyConfig } from '../../../../common/endpoint/types'; const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): PackagePolicy => { @@ -27,7 +27,7 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa // eslint-disable-next-line no-param-reassign cb = (p) => p; } - const policyConfig = cb(factory()); + const policyConfig = cb(policyFactory()); packagePolicy.inputs[0].config = { policy: { value: policyConfig } }; return packagePolicy; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d798bc9b42c95..a20864c8c369f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18729,8 +18729,6 @@ "xpack.securitySolution.endpoint.policyDetails.malware.userNotification.placeholder": "カスタム通知メッセージを入力", "xpack.securitySolution.endpoint.policyDetails.supportedVersion": "エージェントバージョン {version}", "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification": "通知メッセージをカスタマイズ", - "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification.tooltip.a": "ユーザー通知オプションを選択すると、マルウェアが防御または検出されたときに、ホストユーザーに通知を表示します。", - "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification.tooltip.b": " ユーザー通知は、以下のテキストボックスでカスタマイズできます。括弧内のタグを使用すると、該当するアクション(防御または検出など)とファイル名を動的に入力できます。", "xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents": "イベント", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "ファイル", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "ネットワーク", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c6f2965803813..38beab0e5d931 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18776,8 +18776,6 @@ "xpack.securitySolution.endpoint.policyDetails.malware.userNotification.placeholder": "输入您的定制通知消息", "xpack.securitySolution.endpoint.policyDetails.supportedVersion": "代理版本 {version}", "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification": "定制通知消息", - "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification.tooltip.a": "选择用户通知选项后,在阻止或检测到恶意软件时将向主机用户显示通知。", - "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification.tooltip.b": " 可在下方文本框中定制用户通知。括号中的标签可用于动态填充适用操作(如已阻止或已检测)和文件名。", "xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents": "事件", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "文件", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "网络", diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index f53c1c589daab..ce1b58433362b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -255,11 +255,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { events: { file: false, network: true, process: true }, logging: { file: 'info' }, malware: { mode: 'prevent' }, + ransomware: { mode: 'prevent' }, popup: { malware: { enabled: true, message: 'Elastic Security {action} {filename}', }, + ransomware: { + enabled: true, + message: 'Elastic Security {action} {filename}', + }, }, }, windows: { @@ -274,11 +279,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, logging: { file: 'info' }, malware: { mode: 'prevent' }, + ransomware: { mode: 'prevent' }, popup: { malware: { enabled: true, message: 'Elastic Security {action} {filename}', }, + ransomware: { + enabled: true, + message: 'Elastic Security {action} {filename}', + }, }, antivirus_registration: { enabled: false, @@ -399,11 +409,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { events: { file: true, network: true, process: true }, logging: { file: 'info' }, malware: { mode: 'prevent' }, + ransomware: { mode: 'prevent' }, popup: { malware: { enabled: true, message: 'Elastic Security {action} {filename}', }, + ransomware: { + enabled: true, + message: 'Elastic Security {action} {filename}', + }, }, }, windows: { @@ -418,11 +433,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, logging: { file: 'info' }, malware: { mode: 'prevent' }, + ransomware: { mode: 'prevent' }, popup: { malware: { enabled: true, message: 'Elastic Security {action} {filename}', }, + ransomware: { + enabled: true, + message: 'Elastic Security {action} {filename}', + }, }, antivirus_registration: { enabled: false, @@ -536,11 +556,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { events: { file: true, network: true, process: true }, logging: { file: 'info' }, malware: { mode: 'prevent' }, + ransomware: { mode: 'prevent' }, popup: { malware: { enabled: true, message: 'Elastic Security {action} {filename}', }, + ransomware: { + enabled: true, + message: 'Elastic Security {action} {filename}', + }, }, }, windows: { @@ -555,11 +580,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, logging: { file: 'info' }, malware: { mode: 'prevent' }, + ransomware: { mode: 'prevent' }, popup: { malware: { enabled: true, message: 'Elastic Security {action} {filename}', }, + ransomware: { + enabled: true, + message: 'Elastic Security {action} {filename}', + }, }, antivirus_registration: { enabled: false, diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts index 5f54ab2539c5d..82e47896ce411 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts @@ -16,7 +16,7 @@ import { GetFullAgentPolicyResponse, GetPackagesResponse, } from '../../../plugins/fleet/common'; -import { factory as policyConfigFactory } from '../../../plugins/security_solution/common/endpoint/models/policy_config'; +import { policyFactory } from '../../../plugins/security_solution/common/endpoint/models/policy_config'; import { Immutable } from '../../../plugins/security_solution/common/endpoint/types'; // NOTE: import path below should be the deep path to the actual module - else we get CI errors @@ -178,7 +178,7 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC streams: [], config: { policy: { - value: policyConfigFactory(), + value: policyFactory(), }, }, }, From da8ce374cf8aad19ae29fdf6178adaabef4d564e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 28 Jan 2021 13:44:25 -0500 Subject: [PATCH 090/163] Make `xpack.actions.rejectUnauthorized` setting work (#88690) * Remove ActionsConfigType due to being a duplicate * Fix rejectUnauthorized not being configured * Move proxySettings to configurationUtilities * Fix isAxiosError check to code * Add functional test * Remove comment * Close webhook server Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../actions/server/actions_client.test.ts | 3 + .../actions/server/actions_config.mock.ts | 2 + .../actions/server/actions_config.test.ts | 66 +++++--- .../plugins/actions/server/actions_config.ts | 27 ++- .../server/builtin_action_types/email.test.ts | 22 ++- .../server/builtin_action_types/email.ts | 11 +- .../server/builtin_action_types/jira/index.ts | 9 +- .../builtin_action_types/jira/service.test.ts | 29 +++- .../builtin_action_types/jira/service.ts | 26 +-- .../lib/axios_utils.test.ts | 43 +++-- .../builtin_action_types/lib/axios_utils.ts | 14 +- .../lib/get_proxy_agents.test.ts | 38 ++--- .../lib/get_proxy_agents.ts | 26 +-- .../lib/post_pagerduty.ts | 11 +- .../lib/send_email.test.ts | 15 +- .../builtin_action_types/lib/send_email.ts | 9 +- .../server/builtin_action_types/pagerduty.ts | 14 +- .../builtin_action_types/resilient/index.ts | 9 +- .../resilient/service.test.ts | 23 ++- .../builtin_action_types/resilient/service.ts | 18 +- .../builtin_action_types/servicenow/index.ts | 9 +- .../servicenow/service.test.ts | 18 +- .../servicenow/service.ts | 14 +- .../server/builtin_action_types/slack.test.ts | 15 +- .../server/builtin_action_types/slack.ts | 22 +-- .../server/builtin_action_types/teams.test.ts | 149 +++++++++-------- .../server/builtin_action_types/teams.ts | 9 +- .../builtin_action_types/webhook.test.ts | 154 ++++++++++-------- .../server/builtin_action_types/webhook.ts | 11 +- x-pack/plugins/actions/server/index.ts | 5 +- .../actions/server/lib/action_executor.ts | 4 - x-pack/plugins/actions/server/plugin.ts | 9 - x-pack/plugins/actions/server/types.ts | 7 - .../alerting_api_integration/common/config.ts | 9 +- .../actions_simulators/server/plugin.ts | 9 +- .../server/webhook_simulation.ts | 25 ++- .../spaces_only/config.ts | 1 + .../actions/builtin_action_types/webhook.ts | 83 +++++++--- 38 files changed, 597 insertions(+), 371 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 8b6c25e1c3f24..cd97213a64dcc 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -402,6 +402,9 @@ describe('create()', () => { enabled: true, enabledActionTypes: ['some-not-ignored-action-type'], allowedHosts: ['*'], + preconfigured: {}, + proxyRejectUnauthorizedCertificates: true, + rejectUnauthorized: true, }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index 67ab495fc9678..e403fd99fe985 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -14,6 +14,8 @@ const createActionsConfigMock = () => { ensureHostnameAllowed: jest.fn().mockReturnValue({}), ensureUriAllowed: jest.fn().mockReturnValue({}), ensureActionTypeEnabled: jest.fn().mockReturnValue({}), + isRejectUnauthorizedCertificatesEnabled: jest.fn().mockReturnValue(true), + getProxySettings: jest.fn().mockReturnValue(undefined), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 56c58054ca799..c8b771b647e0d 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -4,22 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionsConfigType } from './types'; +import { ActionsConfig } from './config'; import { getActionsConfigurationUtilities, AllowedHosts, EnabledActionTypes, } from './actions_config'; -const DefaultActionsConfig: ActionsConfigType = { +const defaultActionsConfig: ActionsConfig = { enabled: false, allowedHosts: [], enabledActionTypes: [], + preconfigured: {}, + proxyRejectUnauthorizedCertificates: true, + rejectUnauthorized: true, }; describe('ensureUriAllowed', () => { test('returns true when "any" hostnames are allowed', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], @@ -30,7 +34,7 @@ describe('ensureUriAllowed', () => { }); test('throws when the hostname in the requested uri is not in the allowedHosts', () => { - const config: ActionsConfigType = DefaultActionsConfig; + const config: ActionsConfig = defaultActionsConfig; expect(() => getActionsConfigurationUtilities(config).ensureUriAllowed('https://github.com/elastic/kibana') ).toThrowErrorMatchingInlineSnapshot( @@ -39,7 +43,7 @@ describe('ensureUriAllowed', () => { }); test('throws when the uri cannot be parsed as a valid URI', () => { - const config: ActionsConfigType = DefaultActionsConfig; + const config: ActionsConfig = defaultActionsConfig; expect(() => getActionsConfigurationUtilities(config).ensureUriAllowed('github.com/elastic') ).toThrowErrorMatchingInlineSnapshot( @@ -48,7 +52,8 @@ describe('ensureUriAllowed', () => { }); test('returns true when the hostname in the requested uri is in the allowedHosts', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: ['github.com'], enabledActionTypes: [], @@ -61,7 +66,8 @@ describe('ensureUriAllowed', () => { describe('ensureHostnameAllowed', () => { test('returns true when "any" hostnames are allowed', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], @@ -72,7 +78,7 @@ describe('ensureHostnameAllowed', () => { }); test('throws when the hostname in the requested uri is not in the allowedHosts', () => { - const config: ActionsConfigType = DefaultActionsConfig; + const config: ActionsConfig = defaultActionsConfig; expect(() => getActionsConfigurationUtilities(config).ensureHostnameAllowed('github.com') ).toThrowErrorMatchingInlineSnapshot( @@ -81,7 +87,8 @@ describe('ensureHostnameAllowed', () => { }); test('returns true when the hostname in the requested uri is in the allowedHosts', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: ['github.com'], enabledActionTypes: [], @@ -94,7 +101,8 @@ describe('ensureHostnameAllowed', () => { describe('isUriAllowed', () => { test('returns true when "any" hostnames are allowed', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], @@ -105,21 +113,22 @@ describe('isUriAllowed', () => { }); test('throws when the hostname in the requested uri is not in the allowedHosts', () => { - const config: ActionsConfigType = DefaultActionsConfig; + const config: ActionsConfig = defaultActionsConfig; expect( getActionsConfigurationUtilities(config).isUriAllowed('https://github.com/elastic/kibana') ).toEqual(false); }); test('throws when the uri cannot be parsed as a valid URI', () => { - const config: ActionsConfigType = DefaultActionsConfig; + const config: ActionsConfig = defaultActionsConfig; expect(getActionsConfigurationUtilities(config).isUriAllowed('github.com/elastic')).toEqual( false ); }); test('returns true when the hostname in the requested uri is in the allowedHosts', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: ['github.com'], enabledActionTypes: [], @@ -132,7 +141,8 @@ describe('isUriAllowed', () => { describe('isHostnameAllowed', () => { test('returns true when "any" hostnames are allowed', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], @@ -141,12 +151,13 @@ describe('isHostnameAllowed', () => { }); test('throws when the hostname in the requested uri is not in the allowedHosts', () => { - const config: ActionsConfigType = DefaultActionsConfig; + const config: ActionsConfig = defaultActionsConfig; expect(getActionsConfigurationUtilities(config).isHostnameAllowed('github.com')).toEqual(false); }); test('returns true when the hostname in the requested uri is in the allowedHosts', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: ['github.com'], enabledActionTypes: [], @@ -157,7 +168,8 @@ describe('isHostnameAllowed', () => { describe('isActionTypeEnabled', () => { test('returns true when "any" actionTypes are allowed', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [], enabledActionTypes: ['ignore', EnabledActionTypes.Any], @@ -166,7 +178,8 @@ describe('isActionTypeEnabled', () => { }); test('returns false when no actionType is allowed', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [], enabledActionTypes: [], @@ -175,7 +188,8 @@ describe('isActionTypeEnabled', () => { }); test('returns false when the actionType is not in the enabled list', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [], enabledActionTypes: ['foo'], @@ -184,7 +198,8 @@ describe('isActionTypeEnabled', () => { }); test('returns true when the actionType is in the enabled list', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [], enabledActionTypes: ['ignore', 'foo'], @@ -195,7 +210,8 @@ describe('isActionTypeEnabled', () => { describe('ensureActionTypeEnabled', () => { test('does not throw when any actionType is allowed', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [], enabledActionTypes: ['ignore', EnabledActionTypes.Any], @@ -204,7 +220,7 @@ describe('ensureActionTypeEnabled', () => { }); test('throws when no actionType is not allowed', () => { - const config: ActionsConfigType = DefaultActionsConfig; + const config: ActionsConfig = defaultActionsConfig; expect(() => getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo') ).toThrowErrorMatchingInlineSnapshot( @@ -213,7 +229,8 @@ describe('ensureActionTypeEnabled', () => { }); test('throws when actionType is not enabled', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [], enabledActionTypes: ['ignore'], @@ -226,7 +243,8 @@ describe('ensureActionTypeEnabled', () => { }); test('does not throw when actionType is enabled', () => { - const config: ActionsConfigType = { + const config: ActionsConfig = { + ...defaultActionsConfig, enabled: false, allowedHosts: [], enabledActionTypes: ['ignore', 'foo'], diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index ebac80e70f4a8..396f59094a2d9 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -10,8 +10,9 @@ import url from 'url'; import { curry } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; -import { ActionsConfigType } from './types'; +import { ActionsConfig } from './config'; import { ActionTypeDisabledError } from './lib'; +import { ProxySettings } from './types'; export enum AllowedHosts { Any = '*', @@ -33,6 +34,8 @@ export interface ActionsConfigurationUtilities { ensureHostnameAllowed: (hostname: string) => void; ensureUriAllowed: (uri: string) => void; ensureActionTypeEnabled: (actionType: string) => void; + isRejectUnauthorizedCertificatesEnabled: () => boolean; + getProxySettings: () => undefined | ProxySettings; } function allowListErrorMessage(field: AllowListingField, value: string) { @@ -56,14 +59,14 @@ function disabledActionTypeErrorMessage(actionType: string) { }); } -function isAllowed({ allowedHosts }: ActionsConfigType, hostname: string | null): boolean { +function isAllowed({ allowedHosts }: ActionsConfig, hostname: string | null): boolean { const allowed = new Set(allowedHosts); if (allowed.has(AllowedHosts.Any)) return true; if (hostname && allowed.has(hostname)) return true; return false; } -function isHostnameAllowedInUri(config: ActionsConfigType, uri: string): boolean { +function isHostnameAllowedInUri(config: ActionsConfig, uri: string): boolean { return pipe( tryCatch(() => url.parse(uri)), map((parsedUrl) => parsedUrl.hostname), @@ -73,7 +76,7 @@ function isHostnameAllowedInUri(config: ActionsConfigType, uri: string): boolean } function isActionTypeEnabledInConfig( - { enabledActionTypes }: ActionsConfigType, + { enabledActionTypes }: ActionsConfig, actionType: string ): boolean { const enabled = new Set(enabledActionTypes); @@ -82,8 +85,20 @@ function isActionTypeEnabledInConfig( return false; } +function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySettings { + if (!config.proxyUrl) { + return undefined; + } + + return { + proxyUrl: config.proxyUrl, + proxyHeaders: config.proxyHeaders, + proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates, + }; +} + export function getActionsConfigurationUtilities( - config: ActionsConfigType + config: ActionsConfig ): ActionsConfigurationUtilities { const isHostnameAllowed = curry(isAllowed)(config); const isUriAllowed = curry(isHostnameAllowedInUri)(config); @@ -92,6 +107,8 @@ export function getActionsConfigurationUtilities( isHostnameAllowed, isUriAllowed, isActionTypeEnabled, + getProxySettings: () => getProxySettingsFromConfig(config), + isRejectUnauthorizedCertificatesEnabled: () => config.rejectUnauthorized, ensureUriAllowed(uri: string) { if (!isUriAllowed(uri)) { throw new Error(allowListErrorMessage(AllowListingField.URL, uri)); 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 91c71a78a8ee0..5d803e504593e 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 @@ -277,6 +277,16 @@ describe('execute()', () => { `); expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` Object { + "configurationUtilities": Object { + "ensureActionTypeEnabled": [MockFunction], + "ensureHostnameAllowed": [MockFunction], + "ensureUriAllowed": [MockFunction], + "getProxySettings": [MockFunction], + "isActionTypeEnabled": [MockFunction], + "isHostnameAllowed": [MockFunction], + "isRejectUnauthorizedCertificatesEnabled": [MockFunction], + "isUriAllowed": [MockFunction], + }, "content": Object { "message": "a message to you @@ -286,7 +296,6 @@ describe('execute()', () => { "subject": "the subject", }, "hasAuth": true, - "proxySettings": undefined, "routing": Object { "bcc": Array [ "jimmy@example.com", @@ -327,6 +336,16 @@ describe('execute()', () => { await actionType.executor(customExecutorOptions); expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` Object { + "configurationUtilities": Object { + "ensureActionTypeEnabled": [MockFunction], + "ensureHostnameAllowed": [MockFunction], + "ensureUriAllowed": [MockFunction], + "getProxySettings": [MockFunction], + "isActionTypeEnabled": [MockFunction], + "isHostnameAllowed": [MockFunction], + "isRejectUnauthorizedCertificatesEnabled": [MockFunction], + "isUriAllowed": [MockFunction], + }, "content": Object { "message": "a message to you @@ -336,7 +355,6 @@ describe('execute()', () => { "subject": "the subject", }, "hasAuth": false, - "proxySettings": undefined, "routing": Object { "bcc": Array [ "jimmy@example.com", 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 4afbbb3a33615..b8a3467b27b54 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -156,7 +156,7 @@ export function getActionType(params: GetActionTypeParams): EmailActionType { params: ParamsSchema, }, renderParameterTemplates, - executor: curry(executor)({ logger, publicBaseUrl }), + executor: curry(executor)({ logger, publicBaseUrl, configurationUtilities }), }; } @@ -178,7 +178,12 @@ async function executor( { logger, publicBaseUrl, - }: { logger: GetActionTypeParams['logger']; publicBaseUrl: GetActionTypeParams['publicBaseUrl'] }, + configurationUtilities, + }: { + logger: GetActionTypeParams['logger']; + publicBaseUrl: GetActionTypeParams['publicBaseUrl']; + configurationUtilities: ActionsConfigurationUtilities; + }, execOptions: EmailActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; @@ -221,8 +226,8 @@ async function executor( subject: params.subject, message: `${params.message}${EMAIL_FOOTER_DIVIDER}${footerMessage}`, }, - proxySettings: execOptions.proxySettings, hasAuth: config.hasAuth, + configurationUtilities, }; let result; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index d701fad0e0c2f..6fdd1cb28d7bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -72,13 +72,16 @@ export function getActionType( }), params: ExecutorParamsSchema, }, - executor: curry(executor)({ logger }), + executor: curry(executor)({ logger, configurationUtilities }), }; } // action executor async function executor( - { logger }: { logger: Logger }, + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: ActionTypeExecutorOptions< JiraPublicConfigurationType, JiraSecretConfigurationType, @@ -95,7 +98,7 @@ async function executor( secrets, }, logger, - execOptions.proxySettings + configurationUtilities ); if (!api[subAction]) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 7e8770ffbd629..aa33389303081 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -11,6 +11,7 @@ import * as utils from '../lib/axios_utils'; import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; const logger = loggingSystemMock.create().get() as jest.Mocked; interface ResponseError extends Error { @@ -28,6 +29,7 @@ jest.mock('../lib/axios_utils', () => { axios.create = jest.fn(() => axios); const requestMock = utils.request as jest.Mock; +const configurationUtilities = actionsConfigMock.create(); const issueTypesResponse = { data: { @@ -116,7 +118,8 @@ describe('Jira service', () => { config: { apiUrl: 'https://siem-kibana.atlassian.net/', projectKey: 'CK' }, secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, }, - logger + logger, + configurationUtilities ); }); @@ -132,7 +135,8 @@ describe('Jira service', () => { config: { apiUrl: null, projectKey: 'CK' }, secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -144,7 +148,8 @@ describe('Jira service', () => { config: { apiUrl: 'test.com', projectKey: null }, secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -156,7 +161,8 @@ describe('Jira service', () => { config: { apiUrl: 'test.com' }, secrets: { apiToken: '', email: 'elastic@elastic.com' }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -168,7 +174,8 @@ describe('Jira service', () => { config: { apiUrl: 'test.com' }, secrets: { apiToken: '', email: undefined }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -193,6 +200,7 @@ describe('Jira service', () => { axios, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', logger, + configurationUtilities, }); }); @@ -293,6 +301,7 @@ describe('Jira service', () => { url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', logger, method: 'post', + configurationUtilities, data: { fields: { summary: 'title', @@ -331,6 +340,7 @@ describe('Jira service', () => { url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', logger, method: 'post', + configurationUtilities, data: { fields: { summary: 'title', @@ -424,6 +434,7 @@ describe('Jira service', () => { axios, logger, method: 'put', + configurationUtilities, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', data: { fields: { @@ -510,6 +521,7 @@ describe('Jira service', () => { axios, logger, method: 'post', + configurationUtilities, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', data: { body: 'comment' }, }); @@ -568,6 +580,7 @@ describe('Jira service', () => { axios, logger, method: 'get', + configurationUtilities, url: 'https://siem-kibana.atlassian.net/rest/capabilities', }); }); @@ -642,6 +655,7 @@ describe('Jira service', () => { axios, logger, method: 'get', + configurationUtilities, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields', }); @@ -724,6 +738,7 @@ describe('Jira service', () => { axios, logger, method: 'get', + configurationUtilities, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes', }); }); @@ -807,6 +822,7 @@ describe('Jira service', () => { axios, logger, method: 'get', + configurationUtilities, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields', }); @@ -928,6 +944,7 @@ describe('Jira service', () => { axios, logger, method: 'get', + configurationUtilities, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10006', }); }); @@ -988,6 +1005,7 @@ describe('Jira service', () => { axios, logger, method: 'get', + configurationUtilities, url: `https://siem-kibana.atlassian.net/rest/api/2/search?jql=project%3D%22CK%22%20and%20summary%20~%22Test%20title%22`, }); }); @@ -1032,6 +1050,7 @@ describe('Jira service', () => { axios, logger, method: 'get', + configurationUtilities, url: `https://siem-kibana.atlassian.net/rest/api/2/issue/RJ-107`, }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index f5e1b2e4411e3..791bfbaf5d69b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -26,7 +26,7 @@ import { import * as i18n from './translations'; import { request, getErrorMessage } from '../lib/axios_utils'; -import { ProxySettings } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; @@ -39,7 +39,7 @@ const createMetaCapabilities = ['list-project-issuetypes', 'list-issuetype-field export const createExternalService = ( { config, secrets }: ExternalServiceCredentials, logger: Logger, - proxySettings?: ProxySettings + configurationUtilities: ActionsConfigurationUtilities ): ExternalService => { const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; const { apiToken, email } = secrets as JiraSecretConfigurationType; @@ -173,7 +173,7 @@ export const createExternalService = ( axios: axiosInstance, url: `${incidentUrl}/${id}`, logger, - proxySettings, + configurationUtilities, }); const { fields, ...rest } = res.data; @@ -222,7 +222,7 @@ export const createExternalService = ( data: { fields, }, - proxySettings, + configurationUtilities, }); const updatedIncident = await getIncident(res.data.id); @@ -263,7 +263,7 @@ export const createExternalService = ( url: `${incidentUrl}/${incidentId}`, logger, data: { fields }, - proxySettings, + configurationUtilities, }); const updatedIncident = await getIncident(incidentId as string); @@ -297,7 +297,7 @@ export const createExternalService = ( url: getCommentsURL(incidentId), logger, data: { body: comment.comment }, - proxySettings, + configurationUtilities, }); return { @@ -324,7 +324,7 @@ export const createExternalService = ( method: 'get', url: capabilitiesUrl, logger, - proxySettings, + configurationUtilities, }); return { ...res.data }; @@ -350,7 +350,7 @@ export const createExternalService = ( method: 'get', url: getIssueTypesOldAPIURL, logger, - proxySettings, + configurationUtilities, }); const issueTypes = res.data.projects[0]?.issuetypes ?? []; @@ -361,7 +361,7 @@ export const createExternalService = ( method: 'get', url: getIssueTypesUrl, logger, - proxySettings, + configurationUtilities, }); const issueTypes = res.data.values; @@ -389,7 +389,7 @@ export const createExternalService = ( method: 'get', url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId), logger, - proxySettings, + configurationUtilities, }); const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; @@ -400,7 +400,7 @@ export const createExternalService = ( method: 'get', url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), logger, - proxySettings, + configurationUtilities, }); const fields = res.data.values.reduce( @@ -459,7 +459,7 @@ export const createExternalService = ( method: 'get', url: query, logger, - proxySettings, + configurationUtilities, }); return normalizeSearchResults(res.data?.issues ?? []); @@ -483,7 +483,7 @@ export const createExternalService = ( method: 'get', url: getIssueUrl, logger, - proxySettings, + configurationUtilities, }); return normalizeIssue(res.data ?? {}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index e106b17ad223f..23e16b7463914 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -5,12 +5,15 @@ */ import axios from 'axios'; +import { Agent as HttpsAgent } from 'https'; import { Logger } from '../../../../../../src/core/server'; import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; import { getProxyAgents } from './get_proxy_agents'; const logger = loggingSystemMock.create().get() as jest.Mocked; +const configurationUtilities = actionsConfigMock.create(); jest.mock('axios'); const axiosMock = (axios as unknown) as jest.Mock; @@ -41,13 +44,14 @@ describe('request', () => { axios, url: '/test', logger, + configurationUtilities, }); expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {}, httpAgent: undefined, - httpsAgent: undefined, + httpsAgent: expect.any(HttpsAgent), proxy: false, }); expect(res).toEqual({ @@ -58,20 +62,17 @@ describe('request', () => { }); test('it have been called with proper proxy agent for a valid url', async () => { - const proxySettings = { + configurationUtilities.getProxySettings.mockReturnValue({ proxyRejectUnauthorizedCertificates: true, proxyUrl: 'https://localhost:1212', - }; - const { httpAgent, httpsAgent } = getProxyAgents(proxySettings, logger); + }); + const { httpAgent, httpsAgent } = getProxyAgents(configurationUtilities, logger); const res = await request({ axios, url: 'http://testProxy', logger, - proxySettings: { - proxyUrl: 'https://localhost:1212', - proxyRejectUnauthorizedCertificates: true, - }, + configurationUtilities, }); expect(axiosMock).toHaveBeenCalledWith('http://testProxy', { @@ -89,21 +90,22 @@ describe('request', () => { }); test('it have been called with proper proxy agent for an invalid url', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: ':nope:', + proxyRejectUnauthorizedCertificates: false, + }); const res = await request({ axios, url: 'https://testProxy', logger, - proxySettings: { - proxyUrl: ':nope:', - proxyRejectUnauthorizedCertificates: false, - }, + configurationUtilities, }); expect(axiosMock).toHaveBeenCalledWith('https://testProxy', { method: 'get', data: {}, httpAgent: undefined, - httpsAgent: undefined, + httpsAgent: expect.any(HttpsAgent), proxy: false, }); expect(res).toEqual({ @@ -114,13 +116,20 @@ describe('request', () => { }); test('it fetch correctly', async () => { - const res = await request({ axios, url: '/test', method: 'post', logger, data: { id: '123' } }); + const res = await request({ + axios, + url: '/test', + method: 'post', + logger, + data: { id: '123' }, + configurationUtilities, + }); expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' }, httpAgent: undefined, - httpsAgent: undefined, + httpsAgent: expect.any(HttpsAgent), proxy: false, }); expect(res).toEqual({ @@ -140,12 +149,12 @@ describe('patch', () => { }); test('it fetch correctly', async () => { - await patch({ axios, url: '/test', data: { id: '123' }, logger }); + await patch({ axios, url: '/test', data: { id: '123' }, logger, configurationUtilities }); expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' }, httpAgent: undefined, - httpsAgent: undefined, + httpsAgent: expect.any(HttpsAgent), proxy: false, }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index 78c6b91b57dc0..a70a452737dc6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -6,8 +6,8 @@ import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios'; import { Logger } from '../../../../../../src/core/server'; -import { ProxySettings } from '../../types'; import { getProxyAgents } from './get_proxy_agents'; +import { ActionsConfigurationUtilities } from '../../actions_config'; export const request = async ({ axios, @@ -15,7 +15,7 @@ export const request = async ({ logger, method = 'get', data, - proxySettings, + configurationUtilities, ...rest }: { axios: AxiosInstance; @@ -24,12 +24,12 @@ export const request = async ({ method?: Method; data?: T; params?: unknown; - proxySettings?: ProxySettings; + configurationUtilities: ActionsConfigurationUtilities; headers?: Record | null; validateStatus?: (status: number) => boolean; auth?: AxiosBasicCredentials; }): Promise => { - const { httpAgent, httpsAgent } = getProxyAgents(proxySettings, logger); + const { httpAgent, httpsAgent } = getProxyAgents(configurationUtilities, logger); return await axios(url, { ...rest, @@ -47,13 +47,13 @@ export const patch = async ({ url, data, logger, - proxySettings, + configurationUtilities, }: { axios: AxiosInstance; url: string; data: T; logger: Logger; - proxySettings?: ProxySettings; + configurationUtilities: ActionsConfigurationUtilities; }): Promise => { return request({ axios, @@ -61,7 +61,7 @@ export const patch = async ({ logger, method: 'patch', data, - proxySettings, + configurationUtilities, }); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.test.ts index 759ca92968263..da2ad9bb3990d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.test.ts @@ -4,41 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Agent as HttpsAgent } from 'https'; import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; import { getProxyAgents } from './get_proxy_agents'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; const logger = loggingSystemMock.create().get() as jest.Mocked; describe('getProxyAgents', () => { + const configurationUtilities = actionsConfigMock.create(); + test('get agents for valid proxy URL', () => { - const { httpAgent, httpsAgent } = getProxyAgents( - { proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false }, - logger - ); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + }); + const { httpAgent, httpsAgent } = getProxyAgents(configurationUtilities, logger); expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); }); - test('return undefined agents for invalid proxy URL', () => { - const { httpAgent, httpsAgent } = getProxyAgents( - { proxyUrl: ':nope: not a valid URL', proxyRejectUnauthorizedCertificates: false }, - logger - ); - expect(httpAgent).toBe(undefined); - expect(httpsAgent).toBe(undefined); - }); - - test('return undefined agents for null proxy options', () => { - const { httpAgent, httpsAgent } = getProxyAgents(null, logger); + test('return default agents for invalid proxy URL', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: ':nope: not a valid URL', + proxyRejectUnauthorizedCertificates: false, + }); + const { httpAgent, httpsAgent } = getProxyAgents(configurationUtilities, logger); expect(httpAgent).toBe(undefined); - expect(httpsAgent).toBe(undefined); + expect(httpsAgent instanceof HttpsAgent).toBeTruthy(); }); - test('return undefined agents for undefined proxy options', () => { - const { httpAgent, httpsAgent } = getProxyAgents(undefined, logger); + test('return default agents for undefined proxy options', () => { + const { httpAgent, httpsAgent } = getProxyAgents(configurationUtilities, logger); expect(httpAgent).toBe(undefined); - expect(httpsAgent).toBe(undefined); + expect(httpsAgent instanceof HttpsAgent).toBeTruthy(); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.ts index 45f962429ad2b..a49889570f4bf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.ts @@ -4,28 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Agent } from 'http'; +import { Agent as HttpAgent } from 'http'; +import { Agent as HttpsAgent } from 'https'; import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; -import { ProxySettings } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; interface GetProxyAgentsResponse { - httpAgent: Agent | undefined; - httpsAgent: Agent | undefined; + httpAgent: HttpAgent | undefined; + httpsAgent: HttpsAgent | undefined; } export function getProxyAgents( - proxySettings: ProxySettings | undefined | null, + configurationUtilities: ActionsConfigurationUtilities, logger: Logger ): GetProxyAgentsResponse { - const undefinedResponse = { + const proxySettings = configurationUtilities.getProxySettings(); + const defaultResponse = { httpAgent: undefined, - httpsAgent: undefined, + httpsAgent: new HttpsAgent({ + rejectUnauthorized: configurationUtilities.isRejectUnauthorizedCertificatesEnabled(), + }), }; if (!proxySettings) { - return undefinedResponse; + return defaultResponse; } logger.debug(`Creating proxy agents for proxy: ${proxySettings.proxyUrl}`); @@ -34,7 +38,7 @@ export function getProxyAgents( proxyUrl = new URL(proxySettings.proxyUrl); } catch (err) { logger.warn(`invalid proxy URL "${proxySettings.proxyUrl}" ignored`); - return undefinedResponse; + return defaultResponse; } const httpAgent = new HttpProxyAgent(proxySettings.proxyUrl); @@ -45,8 +49,8 @@ export function getProxyAgents( headers: proxySettings.proxyHeaders, // do not fail on invalid certs if value is false rejectUnauthorized: proxySettings.proxyRejectUnauthorizedCertificates, - }) as unknown) as Agent; - // vsCode wasn't convinced HttpsProxyAgent is an http.Agent, so we convinced it + }) as unknown) as HttpsAgent; + // vsCode wasn't convinced HttpsProxyAgent is an https.Agent, so we convinced it return { httpAgent, httpsAgent }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts index d78237beb98a1..51a4e3f857153 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts @@ -6,23 +6,24 @@ import axios, { AxiosResponse } from 'axios'; import { Logger } from '../../../../../../src/core/server'; -import { Services, ProxySettings } from '../../types'; +import { Services } from '../../types'; import { request } from './axios_utils'; +import { ActionsConfigurationUtilities } from '../../actions_config'; interface PostPagerdutyOptions { apiUrl: string; data: unknown; headers: Record; services: Services; - proxySettings?: ProxySettings; } // post an event to pagerduty export async function postPagerduty( options: PostPagerdutyOptions, - logger: Logger + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities ): Promise { - const { apiUrl, data, headers, proxySettings } = options; + const { apiUrl, data, headers } = options; const axiosInstance = axios.create(); return await request({ @@ -31,8 +32,8 @@ export async function postPagerduty( method: 'post', logger, data, - proxySettings, headers, + configurationUtilities, validateStatus: () => true, }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index a1c4041628bd5..bd23aba618544 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -13,6 +13,7 @@ import { sendEmail } from './send_email'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import nodemailer from 'nodemailer'; import { ProxySettings } from '../../types'; +import { actionsConfigMock } from '../../actions_config.mock'; const createTransportMock = nodemailer.createTransport as jest.Mock; const sendMailMockResult = { result: 'does not matter' }; @@ -136,7 +137,7 @@ describe('send_email module', () => { "port": 1025, "secure": false, "tls": Object { - "rejectUnauthorized": undefined, + "rejectUnauthorized": true, }, }, ] @@ -223,6 +224,10 @@ function getSendEmailOptions( { content = {}, routing = {}, transport = {} } = {}, proxySettings?: ProxySettings ) { + const configurationUtilities = actionsConfigMock.create(); + if (proxySettings) { + configurationUtilities.getProxySettings.mockReturnValue(proxySettings); + } return { content: { ...content, @@ -242,8 +247,8 @@ function getSendEmailOptions( user: 'elastic', password: 'changeme', }, - proxySettings, hasAuth: true, + configurationUtilities, }; } @@ -251,6 +256,10 @@ function getSendEmailOptionsNoAuth( { content = {}, routing = {}, transport = {} } = {}, proxySettings?: ProxySettings ) { + const configurationUtilities = actionsConfigMock.create(); + if (proxySettings) { + configurationUtilities.getProxySettings.mockReturnValue(proxySettings); + } return { content: { ...content, @@ -267,7 +276,7 @@ function getSendEmailOptionsNoAuth( transport: { ...transport, }, - proxySettings, hasAuth: false, + configurationUtilities, }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index f3cdf82bfe8cd..2ade4f9e9dcb4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -9,7 +9,7 @@ import nodemailer from 'nodemailer'; import { default as MarkdownIt } from 'markdown-it'; import { Logger } from '../../../../../../src/core/server'; -import { ProxySettings } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -18,9 +18,8 @@ export interface SendEmailOptions { transport: Transport; routing: Routing; content: Content; - proxySettings?: ProxySettings; - rejectUnauthorized?: boolean; hasAuth: boolean; + configurationUtilities: ActionsConfigurationUtilities; } // config validation ensures either service is set or host/port are set @@ -47,12 +46,14 @@ export interface Content { // send an email export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { - const { transport, routing, content, proxySettings, rejectUnauthorized, hasAuth } = options; + const { transport, routing, content, configurationUtilities, hasAuth } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; const transportConfig: Record = {}; + const proxySettings = configurationUtilities.getProxySettings(); + const rejectUnauthorized = configurationUtilities.isRejectUnauthorizedCertificatesEnabled(); if (hasAuth && user != null && password != null) { transportConfig.auth = { 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 ccd25da2397bb..688e75aece43c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -139,7 +139,7 @@ export function getActionType({ secrets: SecretsSchema, params: ParamsSchema, }, - executor: curry(executor)({ logger }), + executor: curry(executor)({ logger, configurationUtilities }), }; } @@ -166,7 +166,10 @@ function getPagerDutyApiUrl(config: ActionTypeConfigType): string { // action executor async function executor( - { logger }: { logger: Logger }, + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: PagerDutyActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; @@ -174,7 +177,6 @@ async function executor( const secrets = execOptions.secrets; const params = execOptions.params; const services = execOptions.services; - const proxySettings = execOptions.proxySettings; const apiUrl = getPagerDutyApiUrl(config); const headers = { @@ -185,7 +187,11 @@ async function executor( let response; try { - response = await postPagerduty({ apiUrl, data, headers, services, proxySettings }, logger); + response = await postPagerduty( + { apiUrl, data, headers, services }, + logger, + configurationUtilities + ); } catch (err) { const message = i18n.translate('xpack.actions.builtin.pagerduty.postingErrorMessage', { defaultMessage: 'error posting pagerduty event', diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index fca99f81d62bd..a14a1c7d4c8af 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -63,13 +63,16 @@ export function getActionType( }), params: ExecutorParamsSchema, }, - executor: curry(executor)({ logger }), + executor: curry(executor)({ logger, configurationUtilities }), }; } // action executor async function executor( - { logger }: { logger: Logger }, + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: ActionTypeExecutorOptions< ResilientPublicConfigurationType, ResilientSecretConfigurationType, @@ -86,7 +89,7 @@ async function executor( secrets, }, logger, - execOptions.proxySettings + configurationUtilities ); if (!api[subAction]) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index 97d8b64fb6535..326f0a6ed5f8b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -12,6 +12,7 @@ import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { incidentTypes, resilientFields, severity } from './mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -28,6 +29,7 @@ axios.create = jest.fn(() => axios); const requestMock = utils.request as jest.Mock; const now = Date.now; const TIMESTAMP = 1589391874472; +const configurationUtilities = actionsConfigMock.create(); // Incident update makes three calls to the API. // The function below mocks this calls. @@ -86,7 +88,8 @@ describe('IBM Resilient service', () => { config: { apiUrl: 'https://resilient.elastic.co/', orgId: '201' }, secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' }, }, - logger + logger, + configurationUtilities ); }); @@ -155,7 +158,8 @@ describe('IBM Resilient service', () => { config: { apiUrl: null, orgId: '201' }, secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -167,7 +171,8 @@ describe('IBM Resilient service', () => { config: { apiUrl: 'test.com', orgId: null }, secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -179,7 +184,8 @@ describe('IBM Resilient service', () => { config: { apiUrl: 'test.com', orgId: '201' }, secrets: { apiKeyId: '', apiKeySecret: 'secret' }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -191,7 +197,8 @@ describe('IBM Resilient service', () => { config: { apiUrl: 'test.com', orgId: '201' }, secrets: { apiKeyId: '', apiKeySecret: undefined }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -226,6 +233,7 @@ describe('IBM Resilient service', () => { params: { text_content_output_format: 'objects_convert', }, + configurationUtilities, }); }); @@ -294,6 +302,7 @@ describe('IBM Resilient service', () => { 'https://resilient.elastic.co/rest/orgs/201/incidents?text_content_output_format=objects_convert', logger, method: 'post', + configurationUtilities, data: { name: 'title', description: { @@ -367,6 +376,7 @@ describe('IBM Resilient service', () => { axios, logger, method: 'patch', + configurationUtilities, url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', data: { changes: [ @@ -480,7 +490,7 @@ describe('IBM Resilient service', () => { axios, logger, method: 'post', - proxySettings: undefined, + configurationUtilities, url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments', data: { text: { @@ -584,6 +594,7 @@ describe('IBM Resilient service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, logger, + configurationUtilities, url: 'https://resilient.elastic.co/rest/orgs/201/types/incident/fields', }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index a13204f8bb1d8..ec31de4f2afd5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -24,7 +24,7 @@ import { import * as i18n from './translations'; import { getErrorMessage, request } from '../lib/axios_utils'; -import { ProxySettings } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; const VIEW_INCIDENT_URL = `#incidents`; @@ -93,7 +93,7 @@ export const formatUpdateRequest = ({ export const createExternalService = ( { config, secrets }: ExternalServiceCredentials, logger: Logger, - proxySettings?: ProxySettings + configurationUtilities: ActionsConfigurationUtilities ): ExternalService => { const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType; const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType; @@ -130,7 +130,7 @@ export const createExternalService = ( params: { text_content_output_format: 'objects_convert', }, - proxySettings, + configurationUtilities, }); return { ...res.data, description: res.data.description?.content ?? '' }; @@ -178,7 +178,7 @@ export const createExternalService = ( method: 'post', logger, data, - proxySettings, + configurationUtilities, }); return { @@ -208,7 +208,7 @@ export const createExternalService = ( url: `${incidentUrl}/${incidentId}`, logger, data, - proxySettings, + configurationUtilities, }); if (!res.data.success) { @@ -241,7 +241,7 @@ export const createExternalService = ( url: getCommentsURL(incidentId), logger, data: { text: { format: 'text', content: comment.comment } }, - proxySettings, + configurationUtilities, }); return { @@ -266,7 +266,7 @@ export const createExternalService = ( method: 'get', url: incidentTypesUrl, logger, - proxySettings, + configurationUtilities, }); const incidentTypes = res.data?.values ?? []; @@ -288,7 +288,7 @@ export const createExternalService = ( method: 'get', url: severityUrl, logger, - proxySettings, + configurationUtilities, }); const incidentTypes = res.data?.values ?? []; @@ -309,7 +309,7 @@ export const createExternalService = ( axios: axiosInstance, url: incidentFieldsUrl, logger, - proxySettings, + configurationUtilities, }); return res.data ?? []; } catch (error) { 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 1f75d439200e3..107d86f111deb 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 @@ -60,14 +60,17 @@ export function getActionType( }), params: ExecutorParamsSchema, }, - executor: curry(executor)({ logger }), + executor: curry(executor)({ logger, configurationUtilities }), }; } // action executor const supportedSubActions: string[] = ['getFields', 'pushToService']; async function executor( - { logger }: { logger: Logger }, + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: ActionTypeExecutorOptions< ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, @@ -84,7 +87,7 @@ async function executor( secrets, }, logger, - execOptions.proxySettings + configurationUtilities ); if (!api[subAction]) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 1a6412f9ceb5b..4ef0e7da166e6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -11,6 +11,7 @@ import * as utils from '../lib/axios_utils'; import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; import { serviceNowCommonFields } from './mocks'; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -27,6 +28,7 @@ jest.mock('../lib/axios_utils', () => { axios.create = jest.fn(() => axios); const requestMock = utils.request as jest.Mock; const patchMock = utils.patch as jest.Mock; +const configurationUtilities = actionsConfigMock.create(); describe('ServiceNow service', () => { let service: ExternalService; @@ -39,7 +41,8 @@ describe('ServiceNow service', () => { config: { apiUrl: 'https://dev102283.service-now.com/' }, secrets: { username: 'admin', password: 'admin' }, }, - logger + logger, + configurationUtilities ); }); @@ -55,7 +58,8 @@ describe('ServiceNow service', () => { config: { apiUrl: null }, secrets: { username: 'admin', password: 'admin' }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -67,7 +71,8 @@ describe('ServiceNow service', () => { config: { apiUrl: 'test.com' }, secrets: { username: '', password: 'admin' }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -79,7 +84,8 @@ describe('ServiceNow service', () => { config: { apiUrl: 'test.com' }, secrets: { username: '', password: undefined }, }, - logger + logger, + configurationUtilities ) ).toThrow(); }); @@ -103,6 +109,7 @@ describe('ServiceNow service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, logger, + configurationUtilities, url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', }); }); @@ -147,6 +154,7 @@ describe('ServiceNow service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, logger, + configurationUtilities, url: 'https://dev102283.service-now.com/api/now/v2/table/incident', method: 'post', data: { short_description: 'title', description: 'desc' }, @@ -200,6 +208,7 @@ describe('ServiceNow service', () => { expect(patchMock).toHaveBeenCalledWith({ axios, logger, + configurationUtilities, url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', data: { short_description: 'title', description: 'desc' }, }); @@ -248,6 +257,7 @@ describe('ServiceNow service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, logger, + configurationUtilities, url: 'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory', }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 96faf6d338b90..108fe06bcbcaa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -12,7 +12,7 @@ import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils'; -import { ProxySettings } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; @@ -24,7 +24,7 @@ const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; export const createExternalService = ( { config, secrets }: ExternalServiceCredentials, logger: Logger, - proxySettings?: ProxySettings + configurationUtilities: ActionsConfigurationUtilities ): ExternalService => { const { apiUrl: url } = config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; @@ -58,7 +58,7 @@ export const createExternalService = ( axios: axiosInstance, url: `${incidentUrl}/${id}`, logger, - proxySettings, + configurationUtilities, }); checkInstance(res); return { ...res.data.result }; @@ -75,8 +75,8 @@ export const createExternalService = ( axios: axiosInstance, url: incidentUrl, logger, - proxySettings, params, + configurationUtilities, }); checkInstance(res); return res.data.result.length > 0 ? { ...res.data.result } : undefined; @@ -93,9 +93,9 @@ export const createExternalService = ( axios: axiosInstance, url: `${incidentUrl}`, logger, - proxySettings, method: 'post', data: { ...(incident as Record) }, + configurationUtilities, }); checkInstance(res); return { @@ -118,7 +118,7 @@ export const createExternalService = ( url: `${incidentUrl}/${incidentId}`, logger, data: { ...(incident as Record) }, - proxySettings, + configurationUtilities, }); checkInstance(res); return { @@ -143,7 +143,7 @@ export const createExternalService = ( axios: axiosInstance, url: fieldsUrl, logger, - proxySettings, + configurationUtilities, }); checkInstance(res); return res.data.result.length > 0 ? res.data.result : []; 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 e73f8d91b0847..cfac23e624a04 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 @@ -165,10 +165,6 @@ describe('execute()', () => { config: {}, secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, - proxySettings: { - proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, - }, }); expect(response).toMatchInlineSnapshot(` Object { @@ -194,9 +190,14 @@ describe('execute()', () => { }); test('calls the mock executor with success proxy', async () => { + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + }); const actionTypeProxy = getActionType({ logger: mockedLogger, - configurationUtilities: actionsConfigMock.create(), + configurationUtilities, }); await actionTypeProxy.executor({ actionId: 'some-id', @@ -204,10 +205,6 @@ describe('execute()', () => { config: {}, secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, - proxySettings: { - proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, - }, }); expect(mockedLogger.debug).toHaveBeenCalledWith( 'IncomingWebhook was called with proxyUrl https://someproxyhost' 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 02026eb729727..5d2c5a24b3edd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -6,7 +6,6 @@ import { URL } from 'url'; import { curry } from 'lodash'; -import { Agent } from 'http'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; @@ -56,7 +55,7 @@ export const ActionTypeId = '.slack'; export function getActionType({ logger, configurationUtilities, - executor = curry(slackExecutor)({ logger }), + executor = curry(slackExecutor)({ logger, configurationUtilities }), }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; @@ -116,7 +115,10 @@ function validateActionTypeConfig( // action executor async function slackExecutor( - { logger }: { logger: Logger }, + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: SlackActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; @@ -126,15 +128,15 @@ async function slackExecutor( let result: IncomingWebhookResult; const { webhookUrl } = secrets; const { message } = params; + const proxySettings = configurationUtilities.getProxySettings(); - let httpProxyAgent: Agent | undefined; - if (execOptions.proxySettings) { - const httpProxyAgents = getProxyAgents(execOptions.proxySettings, logger); - httpProxyAgent = webhookUrl.toLowerCase().startsWith('https') - ? httpProxyAgents.httpsAgent - : httpProxyAgents.httpAgent; + const proxyAgents = getProxyAgents(configurationUtilities, logger); + const httpProxyAgent = webhookUrl.toLowerCase().startsWith('https') + ? proxyAgents.httpsAgent + : proxyAgents.httpAgent; - logger.debug(`IncomingWebhook was called with proxyUrl ${execOptions.proxySettings.proxyUrl}`); + if (proxySettings) { + logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`); } try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts index a9fce2f0a9ebf..4ca25013e9691 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -160,38 +160,47 @@ describe('execute()', () => { params: { message: 'this invocation should succeed' }, }); expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "axios": undefined, - "data": Object { - "text": "this invocation should succeed", - }, - "logger": Object { - "context": Array [], - "debug": [MockFunction] { - "calls": Array [ - Array [ - "response from teams action \\"some-id\\": [HTTP 200] ", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], + Object { + "axios": undefined, + "configurationUtilities": Object { + "ensureActionTypeEnabled": [MockFunction], + "ensureHostnameAllowed": [MockFunction], + "ensureUriAllowed": [MockFunction], + "getProxySettings": [MockFunction], + "isActionTypeEnabled": [MockFunction], + "isHostnameAllowed": [MockFunction], + "isRejectUnauthorizedCertificatesEnabled": [MockFunction], + "isUriAllowed": [MockFunction], + }, + "data": Object { + "text": "this invocation should succeed", + }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from teams action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, }, - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - "method": "post", - "proxySettings": undefined, - "url": "http://example.com", - } + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "method": "post", + "url": "http://example.com", + } `); expect(response).toMatchInlineSnapshot(` Object { @@ -211,47 +220,49 @@ describe('execute()', () => { config: {}, secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, - proxySettings: { - proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, - }, }); expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "axios": undefined, - "data": Object { - "text": "this invocation should succeed", - }, - "logger": Object { - "context": Array [], - "debug": [MockFunction] { - "calls": Array [ - Array [ - "response from teams action \\"some-id\\": [HTTP 200] ", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], + Object { + "axios": undefined, + "configurationUtilities": Object { + "ensureActionTypeEnabled": [MockFunction], + "ensureHostnameAllowed": [MockFunction], + "ensureUriAllowed": [MockFunction], + "getProxySettings": [MockFunction], + "isActionTypeEnabled": [MockFunction], + "isHostnameAllowed": [MockFunction], + "isRejectUnauthorizedCertificatesEnabled": [MockFunction], + "isUriAllowed": [MockFunction], + }, + "data": Object { + "text": "this invocation should succeed", + }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from teams action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, }, - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - "method": "post", - "proxySettings": Object { - "proxyRejectUnauthorizedCertificates": false, - "proxyUrl": "https://someproxyhost", - }, - "url": "http://example.com", - } + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "method": "post", + "url": "http://example.com", + } `); expect(response).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.ts index 088e30db4e3ce..857110d2f53c4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.ts @@ -63,7 +63,7 @@ export function getActionType({ }), params: ParamsSchema, }, - executor: curry(teamsExecutor)({ logger }), + executor: curry(teamsExecutor)({ logger, configurationUtilities }), }; } @@ -95,7 +95,10 @@ function validateActionTypeConfig( // action executor async function teamsExecutor( - { logger }: { logger: Logger }, + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: TeamsActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; @@ -114,7 +117,7 @@ async function teamsExecutor( url: webhookUrl, logger, data, - proxySettings: execOptions.proxySettings, + configurationUtilities, }) ); 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 dbbd2a029caa9..80614e6b1336d 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 @@ -279,43 +279,52 @@ describe('execute()', () => { }); expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "auth": Object { - "password": "123", - "username": "abc", - }, - "axios": undefined, - "data": "some data", - "headers": Object { - "aheader": "a value", - }, - "logger": Object { - "context": Array [], - "debug": [MockFunction] { - "calls": Array [ - Array [ - "response from webhook action \\"some-id\\": [HTTP 200] ", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], + Object { + "auth": Object { + "password": "123", + "username": "abc", + }, + "axios": undefined, + "configurationUtilities": Object { + "ensureActionTypeEnabled": [MockFunction], + "ensureHostnameAllowed": [MockFunction], + "ensureUriAllowed": [MockFunction], + "getProxySettings": [MockFunction], + "isActionTypeEnabled": [MockFunction], + "isHostnameAllowed": [MockFunction], + "isRejectUnauthorizedCertificatesEnabled": [MockFunction], + "isUriAllowed": [MockFunction], + }, + "data": "some data", + "headers": Object { + "aheader": "a value", + }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from webhook action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, }, - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - "method": "post", - "proxySettings": undefined, - "url": "https://abc.def/my-webhook", - } + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "method": "post", + "url": "https://abc.def/my-webhook", + } `); }); @@ -338,39 +347,48 @@ describe('execute()', () => { }); expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "axios": undefined, - "data": "some data", - "headers": Object { - "aheader": "a value", - }, - "logger": Object { - "context": Array [], - "debug": [MockFunction] { - "calls": Array [ - Array [ - "response from webhook action \\"some-id\\": [HTTP 200] ", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], + Object { + "axios": undefined, + "configurationUtilities": Object { + "ensureActionTypeEnabled": [MockFunction], + "ensureHostnameAllowed": [MockFunction], + "ensureUriAllowed": [MockFunction], + "getProxySettings": [MockFunction], + "isActionTypeEnabled": [MockFunction], + "isHostnameAllowed": [MockFunction], + "isRejectUnauthorizedCertificatesEnabled": [MockFunction], + "isUriAllowed": [MockFunction], + }, + "data": "some data", + "headers": Object { + "aheader": "a value", + }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from webhook action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, }, - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - "method": "post", - "proxySettings": undefined, - "url": "https://abc.def/my-webhook", - } + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "method": "post", + "url": "https://abc.def/my-webhook", + } `); }); 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 fa6d2663c94ab..76063deee0f5e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -94,7 +94,7 @@ export function getActionType({ params: ParamsSchema, }, renderParameterTemplates, - executor: curry(executor)({ logger }), + executor: curry(executor)({ logger, configurationUtilities }), }; } @@ -138,7 +138,10 @@ function validateActionTypeConfig( // action executor export async function executor( - { logger }: { logger: Logger }, + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: WebhookActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; @@ -162,7 +165,7 @@ export async function executor( ...basicAuth, headers, data, - proxySettings: execOptions.proxySettings, + configurationUtilities, }) ); @@ -202,7 +205,7 @@ export async function executor( ); } return errorResultInvalid(actionId, message); - } else if (error.isAxiosError) { + } else if (error.code) { const message = `[${error.code}] ${error.message}`; logger.error(`error on ${actionId} webhook event: ${message}`); return errorResultRequestFailed(actionId, message); diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 6c4857bff4e81..4e59dfd099811 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -6,10 +6,9 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { ActionsPlugin } from './plugin'; -import { configSchema } from './config'; +import { configSchema, ActionsConfig } from './config'; import { ActionsClient as ActionsClientClass } from './actions_client'; import { ActionsAuthorization as ActionsAuthorizationClass } from './authorization/actions_authorization'; -import { ActionsConfigType } from './types'; export type ActionsClient = PublicMethodsOf; export type ActionsAuthorization = PublicMethodsOf; @@ -52,7 +51,7 @@ export { asSavedObjectExecutionSource, asHttpRequestExecutionSource } from './li export const plugin = (initContext: PluginInitializerContext) => new ActionsPlugin(initContext); -export const config: PluginConfigDescriptor = { +export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot }) => [ renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts'), diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 695613a59eff1..fa33c1226ec9e 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,7 +12,6 @@ import { GetServicesFunction, RawAction, PreConfiguredAction, - ProxySettings, } from '../types'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { SpacesServiceStart } from '../../../spaces/server'; @@ -33,7 +32,6 @@ export interface ActionExecutorContext { actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; preconfiguredActions: PreConfiguredAction[]; - proxySettings?: ProxySettings; } export interface ExecuteOptions { @@ -87,7 +85,6 @@ export class ActionExecutor { eventLogger, preconfiguredActions, getActionsClientWithRequest, - proxySettings, } = this.actionExecutorContext!; const services = getServices(request); @@ -145,7 +142,6 @@ export class ActionExecutor { params: validatedParams, config: validatedConfig, secrets: validatedSecrets, - proxySettings, }); } catch (err) { rawResult = { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1543f8d7a07ce..22400a08a2a09 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -357,15 +357,6 @@ export class ActionsPlugin implements Plugin, Plugi encryptedSavedObjectsClient, actionTypeRegistry: actionTypeRegistry!, preconfiguredActions, - proxySettings: - this.actionsConfig && this.actionsConfig.proxyUrl - ? { - proxyUrl: this.actionsConfig.proxyUrl, - proxyHeaders: this.actionsConfig.proxyHeaders, - proxyRejectUnauthorizedCertificates: this.actionsConfig - .proxyRejectUnauthorizedCertificates, - } - : undefined, }); const spaceIdToNamespace = (spaceId?: string) => { diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index f545c0fc96633..0bcf02c6f83ae 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -55,12 +55,6 @@ export interface ActionsPlugin { start: PluginStartContract; } -export interface ActionsConfigType { - enabled: boolean; - allowedHosts: string[]; - enabledActionTypes: string[]; -} - // the parameters passed to an action type executor function export interface ActionTypeExecutorOptions { actionId: string; @@ -68,7 +62,6 @@ export interface ActionTypeExecutorOptions { config: Config; secrets: Secrets; params: Params; - proxySettings?: ProxySettings; } export interface ActionResult { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index cf944008c08d6..4193843c63bce 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -17,6 +17,7 @@ interface CreateTestConfigOptions { disabledPlugins?: string[]; ssl?: boolean; enableActionsProxy: boolean; + rejectUnauthorized?: boolean; } // test.not-enabled is specifically not enabled @@ -39,7 +40,12 @@ const enabledActionTypes = [ ]; export function createTestConfig(name: string, options: CreateTestConfigOptions) { - const { license = 'trial', disabledPlugins = [], ssl = false } = options; + const { + license = 'trial', + disabledPlugins = [], + ssl = false, + rejectUnauthorized = true, + } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackApiIntegrationTestsConfig = await readConfigFile( @@ -95,6 +101,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', '--xpack.alerts.invalidateApiKeysTask.interval="15s"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + `--xpack.actions.rejectUnauthorized=${rejectUnauthorized}`, ...actionsProxyUrl, '--xpack.eventLog.logEntries=true', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index e7ce0638c6319..18f3c83b00141 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -5,6 +5,7 @@ */ import http from 'http'; +import https from 'https'; import { Plugin, CoreSetup, IRouter } from 'kibana/server'; import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; @@ -47,7 +48,13 @@ export function getAllExternalServiceSimulatorPaths(): string[] { } export async function getWebhookServer(): Promise { - return await initWebhook(); + const { httpServer } = await initWebhook(); + return httpServer; +} + +export async function getHttpsWebhookServer(): Promise { + const { httpsServer } = await initWebhook(); + return httpsServer; } export async function getSlackServer(): Promise { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts index a34293090d7af..116f0604a37c9 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts @@ -3,16 +3,35 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import fs from 'fs'; import expect from '@kbn/expect'; import http from 'http'; +import https from 'https'; +import { promisify } from 'util'; import { fromNullable, map, filter, getOrElse } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; import { constant } from 'fp-ts/lib/function'; +import { KBN_KEY_PATH, KBN_CERT_PATH } from '@kbn/dev-utils'; export async function initPlugin() { - const payloads: string[] = []; + const httpsServerKey = await promisify(fs.readFile)(KBN_KEY_PATH, 'utf8'); + const httpsServerCert = await promisify(fs.readFile)(KBN_CERT_PATH, 'utf8'); + + return { + httpServer: http.createServer(createServerCallback()), + httpsServer: https.createServer( + { + key: httpsServerKey, + cert: httpsServerCert, + }, + createServerCallback() + ), + }; +} - return http.createServer((request, response) => { +function createServerCallback() { + const payloads: string[] = []; + return (request: http.IncomingMessage, response: http.ServerResponse) => { const credentials = pipe( fromNullable(request.headers.authorization), map((authorization) => authorization.split(/\s+/)), @@ -77,7 +96,7 @@ export async function initPlugin() { return; }); } - }); + }; } function validateAuthentication(credentials: any, res: any) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index f9860b642f13a..2b770395786b3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -11,4 +11,5 @@ export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'trial', enableActionsProxy: false, + rejectUnauthorized: false, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts index acfbad007d722..1748e770929d6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts @@ -5,11 +5,15 @@ */ import http from 'http'; +import https from 'https'; import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getWebhookServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { + getWebhookServer, + getHttpsWebhookServer, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { @@ -43,32 +47,65 @@ export default function webhookTest({ getService }: FtrProviderContext) { } describe('webhook action', () => { - let webhookSimulatorURL: string = ''; - let webhookServer: http.Server; - before(async () => { - webhookServer = await getWebhookServer(); - const availablePort = await getPort({ port: 9000 }); - webhookServer.listen(availablePort); - webhookSimulatorURL = `http://localhost:${availablePort}`; - }); + describe('with http endpoint', () => { + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; + }); + + it('webhook can be executed without username and password', async () => { + const webhookActionId = await createWebhookAction(webhookSimulatorURL); + const { body: result } = await supertest + .post(`/api/actions/action/${webhookActionId}/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'success', + }, + }) + .expect(200); - it('webhook can be executed without username and password', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL); - const { body: result } = await supertest - .post(`/api/actions/action/${webhookActionId}/_execute`) - .set('kbn-xsrf', 'test') - .send({ - params: { - body: 'success', - }, - }) - .expect(200); + expect(result.status).to.eql('ok'); + }); - expect(result.status).to.eql('ok'); + after(() => { + webhookServer.close(); + }); }); - after(() => { - webhookServer.close(); + describe('with https endpoint and rejectUnauthorized=false', () => { + let webhookSimulatorURL: string = ''; + let webhookServer: https.Server; + + before(async () => { + webhookServer = await getHttpsWebhookServer(); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `https://localhost:${availablePort}`; + }); + + it('should support the POST method against webhook target', async () => { + const webhookActionId = await createWebhookAction(webhookSimulatorURL, { method: 'post' }); + const { body: result } = await supertest + .post(`/api/actions/action/${webhookActionId}/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'success_post_method', + }, + }) + .expect(200); + + expect(result.status).to.eql('ok'); + }); + + after(() => { + webhookServer.close(); + }); }); }); } From af88e024fa167b8f5e9f0a16a02156c3828cdacb Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 28 Jan 2021 12:55:29 -0600 Subject: [PATCH 091/163] [Workplace Search] Add i18n sources components (#89571) * Refactor Cancel button constant to shared * Add i18n for DisplaySettings section * Moves more shared constants to correct files Also fixes duplicate text in ConnectInstance (was left alongside translated text below it) * Add source overview i18n * More refactoring of shared constants * Add i18n to remaining shared sources components * Fix failing test * Fix duplicate i18n id * Remove unused translations --- .../workplace_search/constants.ts | 80 +++++ .../add_source/connect_instance.tsx | 4 +- .../components/add_source/constants.ts | 28 -- .../components/add_source/save_config.tsx | 10 +- .../components/add_source/save_custom.tsx | 11 +- .../components/display_settings/constants.ts | 131 +++++++- .../display_settings/display_settings.tsx | 30 +- .../example_result_detail_card.tsx | 4 +- .../example_search_result_group.tsx | 4 +- .../example_standout_result.tsx | 4 +- .../display_settings/field_editor_modal.tsx | 12 +- .../display_settings/result_detail.tsx | 17 +- .../display_settings/search_results.tsx | 35 +- .../display_settings/subtitle_field.tsx | 4 +- .../display_settings/title_field.tsx | 4 +- .../content_sources/components/overview.tsx | 141 +++++--- .../components/source_added.tsx | 10 +- .../components/source_content.tsx | 55 ++- .../components/source_info_card.tsx | 10 +- .../components/source_settings.tsx | 56 +++- .../views/content_sources/constants.ts | 313 ++++++++++++++++++ .../groups/components/add_group_modal.tsx | 10 +- .../groups/components/group_manager_modal.tsx | 9 +- .../groups/components/group_overview.tsx | 10 +- .../views/overview/recent_activity.tsx | 11 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 27 files changed, 804 insertions(+), 205 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 6eedc9270b83f..e72e28aa47d9b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -500,6 +500,21 @@ export const CONFIGURE_BUTTON = i18n.translate( } ); +export const SAVE_BUTTON = i18n.translate('xpack.enterpriseSearch.workplaceSearch.save.button', { + defaultMessage: 'Save', +}); + +export const CANCEL_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.cancel.button', + { + defaultMessage: 'Cancel', + } +); + +export const OK_BUTTON = i18n.translate('xpack.enterpriseSearch.workplaceSearch.ok.button', { + defaultMessage: 'Ok', +}); + export const PRIVATE_PLATINUM_LICENSE_CALLOUT = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.privatePlatinumCallout.text', { @@ -527,3 +542,68 @@ export const CONNECTORS_HEADER_DESCRIPTION = i18n.translate( defaultMessage: 'All of your configurable connectors.', } ); + +export const URL_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.url.label', { + defaultMessage: 'URL', +}); + +export const FIELD_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.field.label', { + defaultMessage: 'Field', +}); + +export const DESCRIPTION_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.description.label', + { + defaultMessage: 'Description', + } +); + +export const UPDATE_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.update.label', { + defaultMessage: 'Update', +}); + +export const ADD_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.add.label', { + defaultMessage: 'Add', +}); + +export const ADD_FIELD_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.addField.label', + { + defaultMessage: 'Add field', + } +); + +export const EDIT_FIELD_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.editField.label', + { + defaultMessage: 'Edit field', + } +); + +export const REMOVE_FIELD_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.removeField.label', + { + defaultMessage: 'Remove field', + } +); + +export const RECENT_ACTIVITY_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.recentActivity.title', + { + defaultMessage: 'Recent activity', + } +); + +export const CONFIRM_MODAL_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.comfirmModal.title', + { + defaultMessage: 'Please confirm', + } +); + +export const REMOVE_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.remove.button', + { + defaultMessage: 'Remove', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index 6fe87290737f5..61cf1ed34fdcc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -35,6 +35,7 @@ import { FeatureIds, Configuration, Features } from '../../../../types'; import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes'; import { SourceFeatures } from './source_features'; +import { LEARN_MORE_LINK } from '../../constants'; import { CONNECT_REMOTE, CONNECT_PRIVATE, @@ -206,7 +207,7 @@ export const ConnectInstance: React.FC = ({ values={{ link: ( - Learn more + {LEARN_MORE_LINK} ), }} @@ -242,7 +243,6 @@ export const ConnectInstance: React.FC = ({ - Connect {name} {i18n.translate('xpack.enterpriseSearch.workplaceSearch.contentSource.connect.button', { defaultMessage: 'Connect {name}', values: { name }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts index 7d891953e618b..8ac3edeb0f308 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts @@ -236,13 +236,6 @@ export const OAUTH_SAVE_CONFIG_BUTTON = i18n.translate( } ); -export const OAUTH_REMOVE_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.remove.button', - { - defaultMessage: 'Remove', - } -); - export const OAUTH_BACK_BUTTON = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.back.button', { @@ -292,20 +285,6 @@ export const SAVE_CUSTOM_API_KEYS_BODY = i18n.translate( } ); -export const SAVE_CUSTOM_ACCESS_TOKEN_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.accessToken.label', - { - defaultMessage: 'Access Token', - } -); - -export const SAVE_CUSTOM_ID_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.id.label', - { - defaultMessage: 'ID', - } -); - export const SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.title', { @@ -327,13 +306,6 @@ export const SAVE_CUSTOM_DOC_PERMISSIONS_TITLE = i18n.translate( } ); -export const SAVE_CUSTOM_FEATURES_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.features.button', - { - defaultMessage: 'Learn about Platinum features', - } -); - export const SOURCE_FEATURES_SEARCHABLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.searchable.text', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx index e6e428ecab115..1bab035b8f379 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx @@ -28,14 +28,10 @@ import { BASE_URL_LABEL, CLIENT_ID_LABEL, CLIENT_SECRET_LABEL, + REMOVE_BUTTON, } from '../../../../constants'; -import { - OAUTH_SAVE_CONFIG_BUTTON, - OAUTH_REMOVE_BUTTON, - OAUTH_BACK_BUTTON, - OAUTH_STEP_2, -} from './constants'; +import { OAUTH_SAVE_CONFIG_BUTTON, OAUTH_BACK_BUTTON, OAUTH_STEP_2 } from './constants'; import { LicensingLogic } from '../../../../../../applications/shared/licensing'; @@ -99,7 +95,7 @@ export const SaveConfig: React.FC = ({ const deleteButton = ( - {OAUTH_REMOVE_BUTTON} + {REMOVE_BUTTON} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index 28aeaec2b47df..8e3bd1c6ab2b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -36,18 +36,17 @@ import { getSourcesPath, } from '../../../../routes'; +import { ACCESS_TOKEN_LABEL, ID_LABEL, LEARN_CUSTOM_FEATURES_BUTTON } from '../../constants'; + import { SAVE_CUSTOM_BODY1, SAVE_CUSTOM_BODY2, SAVE_CUSTOM_RETURN_BUTTON, SAVE_CUSTOM_API_KEYS_TITLE, SAVE_CUSTOM_API_KEYS_BODY, - SAVE_CUSTOM_ACCESS_TOKEN_LABEL, - SAVE_CUSTOM_ID_LABEL, SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE, SAVE_CUSTOM_STYLING_RESULTS_TITLE, SAVE_CUSTOM_DOC_PERMISSIONS_TITLE, - SAVE_CUSTOM_FEATURES_BUTTON, } from './constants'; interface SaveCustomProps { @@ -109,10 +108,10 @@ export const SaveCustom: React.FC = ({

{SAVE_CUSTOM_API_KEYS_BODY}

- + @@ -200,7 +199,7 @@ export const SaveCustom: React.FC = ({ - {SAVE_CUSTOM_FEATURES_BUTTON} + {LEARN_CUSTOM_FEATURES_BUTTON}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/constants.ts index 0e6cbb2560128..3b04456b1f59c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/constants.ts @@ -7,15 +7,142 @@ import { i18n } from '@kbn/i18n'; export const LEAVE_UNASSIGNED_FIELD = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.leaveUnassignedField', + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.leaveUnassigned.field', { defaultMessage: 'Leave unassigned', } ); export const SUCCESS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.successMessage', + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.success.message', { defaultMessage: 'Display Settings have been successfuly updated.', } ); + +export const UNSAVED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.unsaved.message', + { + defaultMessage: 'Your display settings have not been saved. Are you sure you want to leave?', + } +); + +export const DISPLAY_SETTINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.displaySettings.title', + { + defaultMessage: 'Display Settings', + } +); + +export const DISPLAY_SETTINGS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.displaySettings.description', + { + defaultMessage: + 'Customize the content and appearance of your Custom API Source search results.', + } +); + +export const DISPLAY_SETTINGS_EMPTY_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.displaySettingsEmpty.title', + { + defaultMessage: 'You have no content yet', + } +); + +export const DISPLAY_SETTINGS_EMPTY_BODY = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.displaySettingsEmpty.body', + { + defaultMessage: 'You need some content to display in order to configure the display settings.', + } +); + +export const SEARCH_RESULTS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.searchResults.label', + { + defaultMessage: 'Search Results', + } +); + +export const RESULT_DETAIL_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.resultDetail.label', + { + defaultMessage: 'Result Detail', + } +); + +export const SUBTITLE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.subtitle.label', + { + defaultMessage: 'Subtitle', + } +); + +export const TITLE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.title.label', + { + defaultMessage: 'Title', + } +); + +export const EMPTY_FIELDS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.emptyFields.description', + { + defaultMessage: 'Add fields and move them into the order you want them to appear.', + } +); + +export const VISIBLE_FIELDS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.visibleFields.title', + { + defaultMessage: 'Visible fields', + } +); + +export const PREVIEW_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.preview.title', + { + defaultMessage: 'Preview', + } +); + +export const SEARCH_RESULTS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.searchResults.title', + { + defaultMessage: 'Search Results settings', + } +); + +export const FEATURED_RESULTS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.featuredResults.title', + { + defaultMessage: 'Featured Results', + } +); + +export const FEATURED_RESULTS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.featuredResults.description', + { + defaultMessage: 'A matching document will appear as a single bold card.', + } +); + +export const STANDARD_RESULTS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.standardResults.title', + { + defaultMessage: 'Standard Results', + } +); + +export const STANDARD_RESULTS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.standardResults.description', + { + defaultMessage: 'Somewhat matching documents will appear as a set.', + } +); + +export const SEARCH_RESULTS_ROW_HELP_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.searchResultsRow.helpText', + { + defaultMessage: 'This area is optional', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index cf066f3157e39..19ccfab11a729 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -32,15 +32,23 @@ import { AppLogic } from '../../../../app_logic'; import { Loading } from '../../../../../shared/loading'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { SAVE_BUTTON } from '../../../../constants'; +import { + UNSAVED_MESSAGE, + DISPLAY_SETTINGS_TITLE, + DISPLAY_SETTINGS_DESCRIPTION, + DISPLAY_SETTINGS_EMPTY_TITLE, + DISPLAY_SETTINGS_EMPTY_BODY, + SEARCH_RESULTS_LABEL, + RESULT_DETAIL_LABEL, +} from './constants'; + import { DisplaySettingsLogic } from './display_settings_logic'; import { FieldEditorModal } from './field_editor_modal'; import { ResultDetail } from './result_detail'; import { SearchResults } from './search_results'; -const UNSAVED_MESSAGE = - 'Your display settings have not been saved. Are you sure you want to leave?'; - interface DisplaySettingsProps { tabId: number; } @@ -77,12 +85,12 @@ export const DisplaySettings: React.FC = ({ tabId }) => { const tabs = [ { id: 'search_results', - name: 'Search Results', + name: SEARCH_RESULTS_LABEL, content: , }, { id: 'result_detail', - name: 'Result Detail', + name: RESULT_DETAIL_LABEL, content: , }, ] as EuiTabbedContentTab[]; @@ -105,12 +113,12 @@ export const DisplaySettings: React.FC = ({ tabId }) => { <>
- Save + {SAVE_BUTTON} ) : null } @@ -125,10 +133,8 @@ export const DisplaySettings: React.FC = ({ tabId }) => { You have no content yet} - body={ -

You need some content to display in order to configure the display settings.

- } + title={

{DISPLAY_SETTINGS_EMPTY_TITLE}

} + body={

{DISPLAY_SETTINGS_EMPTY_BODY}

} />
)} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx index 3278140a2dfe6..3ca70979cc247 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx @@ -11,6 +11,8 @@ import { useValues } from 'kea'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { URL_LABEL } from '../../../../constants'; + import { DisplaySettingsLogic } from './display_settings_logic'; import { CustomSourceIcon } from './custom_source_icon'; @@ -50,7 +52,7 @@ export const ExampleResultDetailCard: React.FC = () => {
{result[urlField]}
) : ( - URL + {URL_LABEL} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx index aa7bc4d917886..7f033d8f8d97a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx @@ -10,6 +10,8 @@ import { isColorDark, hexToRgb } from '@elastic/eui'; import classNames from 'classnames'; import { useValues } from 'kea'; +import { DESCRIPTION_LABEL } from '../../../../constants'; + import { DisplaySettingsLogic } from './display_settings_logic'; import { CustomSourceIcon } from './custom_source_icon'; @@ -65,7 +67,7 @@ export const ExampleSearchResultGroup: React.FC = () => { className="example-result-content-placeholder" data-test-subj="DefaultDescriptionLabel" > - Description + {DESCRIPTION_LABEL} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx index a80680d219aef..cdd883413481d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx @@ -11,6 +11,8 @@ import { useValues } from 'kea'; import { isColorDark, hexToRgb } from '@elastic/eui'; +import { DESCRIPTION_LABEL } from '../../../../constants'; + import { DisplaySettingsLogic } from './display_settings_logic'; import { CustomSourceIcon } from './custom_source_icon'; @@ -60,7 +62,7 @@ export const ExampleStandoutResult: React.FC = () => { className="example-result-content-placeholder" data-test-subj="DefaultDescriptionLabel" > - Description + {DESCRIPTION_LABEL} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx index 587916a741d66..e220e07153867 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx @@ -23,6 +23,8 @@ import { EuiSelect, } from '@elastic/eui'; +import { CANCEL_BUTTON, FIELD_LABEL, UPDATE_LABEL, ADD_LABEL } from '../../../../constants'; + import { DisplaySettingsLogic } from './display_settings_logic'; const emptyField = { fieldName: '', label: '' }; @@ -53,14 +55,16 @@ export const FieldEditorModal: React.FC = () => { } }; - const ACTION_LABEL = isEditing ? 'Update' : 'Add'; + const ACTION_LABEL = isEditing ? UPDATE_LABEL : ADD_LABEL; return ( - {ACTION_LABEL} Field + + {ACTION_LABEL} {FIELD_LABEL} + @@ -89,9 +93,9 @@ export const FieldEditorModal: React.FC = () => { - Cancel + {CANCEL_BUTTON} - {ACTION_LABEL} Field + {ACTION_LABEL} {FIELD_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx index 5ee484250ca62..48e285cdcc778 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx @@ -26,6 +26,9 @@ import { EuiTitle, } from '@elastic/eui'; +import { ADD_FIELD_LABEL, EDIT_FIELD_LABEL, REMOVE_FIELD_LABEL } from '../../../../constants'; +import { VISIBLE_FIELDS_TITLE, EMPTY_FIELDS_DESCRIPTION, PREVIEW_TITLE } from './constants'; + import { DisplaySettingsLogic } from './display_settings_logic'; import { ExampleResultDetailCard } from './example_result_detail_card'; @@ -55,7 +58,7 @@ export const ResultDetail: React.FC = () => { -

Visible Fields

+

{VISIBLE_FIELDS_TITLE}

@@ -65,7 +68,7 @@ export const ResultDetail: React.FC = () => { disabled={availableFieldOptions.length < 1} data-test-subj="AddFieldButton" > - Add Field + {ADD_FIELD_LABEL}
@@ -106,13 +109,13 @@ export const ResultDetail: React.FC = () => {
openEditDetailField(index)} /> removeDetailField(index)} /> @@ -127,9 +130,7 @@ export const ResultDetail: React.FC = () => { ) : ( -

- Add fields and move them into the order you want them to appear. -

+

{EMPTY_FIELDS_DESCRIPTION}

)} @@ -138,7 +139,7 @@ export const ResultDetail: React.FC = () => { -

Preview

+

{PREVIEW_TITLE}

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx index c1a65d1c52b65..95096331a49d3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -21,7 +21,18 @@ import { } from '@elastic/eui'; import { DisplaySettingsLogic } from './display_settings_logic'; -import { LEAVE_UNASSIGNED_FIELD } from './constants'; + +import { DESCRIPTION_LABEL } from '../../../../constants'; +import { + LEAVE_UNASSIGNED_FIELD, + SEARCH_RESULTS_TITLE, + SEARCH_RESULTS_ROW_HELP_TEXT, + PREVIEW_TITLE, + FEATURED_RESULTS_TITLE, + FEATURED_RESULTS_DESCRIPTION, + STANDARD_RESULTS_TITLE, + STANDARD_RESULTS_DESCRIPTION, +} from './constants'; import { ExampleSearchResultGroup } from './example_search_result_group'; import { ExampleStandoutResult } from './example_standout_result'; @@ -51,7 +62,7 @@ export const SearchResults: React.FC = () => { -

Search Result Settings

+

{SEARCH_RESULTS_TITLE}

@@ -89,7 +100,7 @@ export const SearchResults: React.FC = () => { { /> { -

Preview

+

{PREVIEW_TITLE}

-

Featured Results

+

{FEATURED_RESULTS_TITLE}

-

- A matching document will appear as a single bold card. -

+

{FEATURED_RESULTS_DESCRIPTION}

-

Standard Results

+

{STANDARD_RESULTS_TITLE}

-

- Somewhat matching documents will appear as a set. -

+

{STANDARD_RESULTS_DESCRIPTION}

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx index d2f26cd6726df..77f77ad3d3cb9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx @@ -10,6 +10,8 @@ import classNames from 'classnames'; import { Result } from '../../../../types'; +import { SUBTITLE_LABEL } from './constants'; + interface SubtitleFieldProps { result: Result; subtitleField: string | null; @@ -31,7 +33,7 @@ export const SubtitleField: React.FC = ({
{result[subtitleField]}
) : ( - Subtitle + {SUBTITLE_LABEL} )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx index fa975c8b11ce0..00b548043aae5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx @@ -10,6 +10,8 @@ import classNames from 'classnames'; import { Result } from '../../../../types'; +import { TITLE_LABEL } from './constants'; + interface TitleFieldProps { result: Result; titleField: string | null; @@ -32,7 +34,7 @@ export const TitleField: React.FC = ({ result, titleField, titl ) : ( - Title + {TITLE_LABEL} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index a0797305de6ca..be20eefa1b481 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -8,6 +8,8 @@ import React from 'react'; import { useValues } from 'kea'; +import { FormattedMessage } from '@kbn/i18n/react'; + import { EuiEmptyPrompt, EuiFlexGroup, @@ -36,6 +38,38 @@ import { getGroupPath, } from '../../../routes'; +import { + RECENT_ACTIVITY_TITLE, + CREDENTIALS_TITLE, + DOCUMENTATION_LINK_TITLE, +} from '../../../constants'; +import { + SOURCES_NO_CONTENT_TITLE, + CONTENT_SUMMARY_TITLE, + CONTENT_TYPE_HEADER, + ITEMS_HEADER, + EVENT_HEADER, + STATUS_HEADER, + TIME_HEADER, + TOTAL_DOCUMENTS_LABEL, + EMPTY_ACTIVITY_TITLE, + GROUP_ACCESS_TITLE, + CONFIGURATION_TITLE, + DOCUMENT_PERMISSIONS_TITLE, + DOCUMENT_PERMISSIONS_TEXT, + DOCUMENT_PERMISSIONS_DISABLED_TEXT, + LEARN_MORE_LINK, + STATUS_HEADING, + STATUS_TEXT, + ADDITIONAL_CONFIG_HEADING, + EXTERNAL_IDENTITIES_LINK, + ACCESS_TOKEN_LABEL, + ID_LABEL, + LEARN_CUSTOM_FEATURES_BUTTON, + DOC_PERMISSIONS_DESCRIPTION, + CUSTOM_CALLOUT_TITLE, +} from '../constants'; + import { AppLogic } from '../../../app_logic'; import { ComponentLoader } from '../../../components/shared/component_loader'; @@ -88,7 +122,7 @@ export const Overview: React.FC = () => { No content yet} + title={

{SOURCES_NO_CONTENT_TITLE}

} iconType="documents" iconColor="subdued" /> @@ -100,7 +134,7 @@ export const Overview: React.FC = () => {
-

Content summary

+

{CONTENT_SUMMARY_TITLE}

@@ -111,14 +145,14 @@ export const Overview: React.FC = () => { ) : ( - Content Type - Items + {CONTENT_TYPE_HEADER} + {ITEMS_HEADER} {tableContent} - Total documents + {TOTAL_DOCUMENTS_LABEL} {totalDocuments.toLocaleString('en-US')} @@ -137,7 +171,7 @@ export const Overview: React.FC = () => { There is no recent activity} + title={

{EMPTY_ACTIVITY_TITLE}

} iconType="clock" iconColor="subdued" /> @@ -148,9 +182,9 @@ export const Overview: React.FC = () => { const activitiesTable = ( - Event - {!custom && Status} - Time + {EVENT_HEADER} + {!custom && {STATUS_HEADER}} + {TIME_HEADER} {activities.map(({ details: activityDetails, event, time, status }, i) => ( @@ -186,7 +220,7 @@ export const Overview: React.FC = () => {
-

Recent activity

+

{RECENT_ACTIVITY_TITLE}

@@ -198,7 +232,7 @@ export const Overview: React.FC = () => { const groupsSummary = ( <> -

Group Access

+

{GROUP_ACCESS_TITLE}

@@ -223,7 +257,7 @@ export const Overview: React.FC = () => { <> -

Configuration

+

{CONFIGURATION_TITLE}

@@ -251,7 +285,7 @@ export const Overview: React.FC = () => { <> -

Document-level permissions

+

{DOCUMENT_PERMISSIONS_TITLE}

@@ -261,7 +295,7 @@ export const Overview: React.FC = () => { - Using document-level permissions + {DOCUMENT_PERMISSIONS_TEXT}
@@ -273,7 +307,7 @@ export const Overview: React.FC = () => { <> -

Document-level permissions

+

{DOCUMENT_PERMISSIONS_TITLE}

@@ -284,13 +318,20 @@ export const Overview: React.FC = () => { - Disabled for this source + {DOCUMENT_PERMISSIONS_DISABLED_TEXT} - - Learn more - {' '} - about permissions + + {LEARN_MORE_LINK} + + ), + }} + /> @@ -303,7 +344,7 @@ export const Overview: React.FC = () => {
- Status + {STATUS_HEADER}
@@ -313,10 +354,10 @@ export const Overview: React.FC = () => { - Everything looks good + {STATUS_HEADING} -

Your endpoints are ready to accept requests.

+

{STATUS_TEXT}

@@ -327,7 +368,7 @@ export const Overview: React.FC = () => {
- Status + {STATUS_HEADING}
@@ -337,15 +378,21 @@ export const Overview: React.FC = () => { - Requires additional configuration + {ADDITIONAL_CONFIG_HEADING}

- The{' '} - - External Identities API - {' '} - must be used to configure user access mappings. Read the guide to learn more. + + {EXTERNAL_IDENTITIES_LINK} + + ), + }} + />

@@ -357,13 +404,13 @@ export const Overview: React.FC = () => {
- Credentials + {CREDENTIALS_TITLE}
- + - +
); @@ -377,7 +424,7 @@ export const Overview: React.FC = () => {
- Documentation + {DOCUMENTATION_LINK_TITLE}
@@ -393,18 +440,15 @@ export const Overview: React.FC = () => { -

Document-level permissions

+

{DOCUMENT_PERMISSIONS_TITLE}

-

- Document-level permissions manage content access content on individual or group - attributes. Allow or deny access to specific documents. -

+

{DOC_PERMISSIONS_DESCRIPTION}

- Learn about Platinum features + {LEARN_CUSTOM_FEATURES_BUTTON}
@@ -449,13 +493,20 @@ export const Overview: React.FC = () => {

- - Learn more - {' '} - about custom sources. + + {LEARN_MORE_LINK} + + ), + }} + />

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx index 16aceacbddcd5..3f7d99629ca4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx @@ -10,6 +10,8 @@ import { Location } from 'history'; import { useActions, useValues } from 'kea'; import { Redirect, useLocation } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + import { setErrorMessage } from '../../../../shared/flash_messages'; import { parseQueryParams } from '../../../../../applications/shared/query_params'; @@ -37,7 +39,13 @@ export const SourceAdded: React.FC = () => { const decodedName = decodeURIComponent(name); if (hasError) { - const defaultError = `${decodedName} failed to connect.`; + const defaultError = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAdded.error', + { + defaultMessage: '{decodedName} failed to connect.', + values: { decodedName }, + } + ); setErrorMessage(errorMessages ? errorMessages.join(' ') : defaultError); } else { setAddedSource(decodedName, indexPermissions, serviceType); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index 728d21eb1530f..cac74d37f9f66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -10,6 +10,9 @@ import { useActions, useValues } from 'kea'; import { startCase } from 'lodash'; import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + import { EuiButton, EuiButtonEmpty, @@ -41,6 +44,16 @@ import { TablePaginationBar } from '../../../components/shared/table_pagination_ import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { CUSTOM_SERVICE_TYPE } from '../../../constants'; +import { + NO_CONTENT_MESSAGE, + CUSTOM_DOCUMENTATION_LINK, + TITLE_HEADING, + LAST_UPDATED_HEADING, + GO_BUTTON, + RESET_BUTTON, + SOURCE_CONTENT_TITLE, + CONTENT_LOADING_TEXT, +} from '../constants'; import { SourceLogic } from '../source_logic'; @@ -78,8 +91,11 @@ export const SourceContent: React.FC = () => { const showPagination = totalPages > 1; const hasItems = totalItems > 0; const emptyMessage = contentFilterValue - ? `No results for '${contentFilterValue}'` - : "This source doesn't have any content yet"; + ? i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.noContentForValue.message', { + defaultMessage: "No results for '{contentFilterValue}'", + values: { contentFilterValue }, + }) + : NO_CONTENT_MESSAGE; const paginationOptions = { totalPages, @@ -101,10 +117,17 @@ export const SourceContent: React.FC = () => { body={ isCustomSource ? (

- Learn more about adding content in our{' '} - - documentation - + + {CUSTOM_DOCUMENTATION_LINK} + + ), + }} + />

) : null } @@ -143,9 +166,9 @@ export const SourceContent: React.FC = () => { - Title + {TITLE_HEADING} {startCase(urlField)} - Last Updated + {LAST_UPDATED_HEADING} {contentItems.map(contentItem)} @@ -167,12 +190,12 @@ export const SourceContent: React.FC = () => { color="primary" onClick={() => setContentFilterValue(searchTerm)} > - Go + {GO_BUTTON} - Reset + {RESET_BUTTON} @@ -180,12 +203,18 @@ export const SourceContent: React.FC = () => { return ( <> - + { {isFederatedSource && federatedSearchControls} - {sectionLoading && } + {sectionLoading && } {!sectionLoading && (hasItems ? contentTable : emptyState)} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index 8e3a116e3ac33..ee877e8f61ad6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -18,6 +18,8 @@ import { import { SourceIcon } from '../../../components/shared/source_icon'; +import { REMOTE_SOURCE_LABEL, CREATED_LABEL, STATUS_LABEL, READY_TEXT } from '../constants'; + interface SourceInfoCardProps { sourceName: string; sourceType: string; @@ -54,7 +56,7 @@ export const SourceInfoCard: React.FC = ({ - Remote Source + {REMOTE_SOURCE_LABEL} @@ -63,7 +65,7 @@ export const SourceInfoCard: React.FC = ({ - Created: + {CREATED_LABEL} {dateCreated} @@ -71,12 +73,12 @@ export const SourceInfoCard: React.FC = ({ - Status: + {STATUS_LABEL} - Ready to search + {READY_TEXT} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 8d3219be9b02a..5f47fa2d5927b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -10,6 +10,8 @@ import { useActions, useValues } from 'kea'; import { isEmpty } from 'lodash'; import { Link } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; + import { EuiButton, EuiButtonEmpty, @@ -21,6 +23,24 @@ import { EuiFormRow, } from '@elastic/eui'; +import { + CANCEL_BUTTON, + OK_BUTTON, + CONFIRM_MODAL_TITLE, + SAVE_CHANGES_BUTTON, + REMOVE_BUTTON, +} from '../../../constants'; +import { + SOURCE_SETTINGS_TITLE, + SOURCE_SETTINGS_DESCRIPTION, + SOURCE_NAME_LABEL, + SOURCE_CONFIG_TITLE, + SOURCE_CONFIG_DESCRIPTION, + SOURCE_CONFIG_LINK, + SOURCE_REMOVE_TITLE, + SOURCE_REMOVE_DESCRIPTION, +} from '../constants'; + import { ContentSection } from '../../../components/shared/content_section'; import { SourceConfigFields } from '../../../components/shared/source_config_fields'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; @@ -85,16 +105,22 @@ export const SourceSettings: React.FC = () => { const confirmModal = ( - Your source documents will be deleted from Workplace Search.
- Are you sure you want to remove {name}? + , + }} + />
); @@ -102,10 +128,7 @@ export const SourceSettings: React.FC = () => { return ( <> - + @@ -114,7 +137,7 @@ export const SourceSettings: React.FC = () => { value={inputValue} size={64} onChange={handleNameChange} - aria-label="Source Name" + aria-label={SOURCE_NAME_LABEL} disabled={buttonLoading} data-test-subj="SourceNameInput" /> @@ -127,17 +150,14 @@ export const SourceSettings: React.FC = () => { onClick={submitNameChange} data-test-subj="SaveChangesButton" > - Save changes + {SAVE_CHANGES_BUTTON} {showConfig && ( - + { /> - Edit content source connector settings + {SOURCE_CONFIG_LINK} )} - + { color="danger" onClick={showConfirm} > - Remove + {REMOVE_BUTTON} {confirmModalVisible && confirmModal} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts new file mode 100644 index 0000000000000..48b8a06b2549c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -0,0 +1,313 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const SOURCES_NO_CONTENT_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.noContent.title', + { + defaultMessage: 'No content yet', + } +); + +export const CONTENT_SUMMARY_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.contentSummary.title', + { + defaultMessage: 'Content summary', + } +); + +export const CONTENT_TYPE_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.contentType.header', + { + defaultMessage: 'Content type', + } +); + +export const ITEMS_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.items.header', + { + defaultMessage: 'Items', + } +); + +export const EVENT_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.event.header', + { + defaultMessage: 'Event', + } +); + +export const STATUS_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.status.header', + { + defaultMessage: 'Status', + } +); + +export const TIME_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.time.header', + { + defaultMessage: 'Time', + } +); + +export const TOTAL_DOCUMENTS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.totalDocuments.label', + { + defaultMessage: 'Total documents', + } +); + +export const EMPTY_ACTIVITY_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.emptyActivity.title', + { + defaultMessage: 'There is no recent activity', + } +); + +export const GROUP_ACCESS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.groupAccess.title', + { + defaultMessage: 'Group access', + } +); + +export const CONFIGURATION_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.configuration.title', + { + defaultMessage: 'Configuration', + } +); + +export const DOCUMENT_PERMISSIONS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.documentPermissions.title', + { + defaultMessage: 'Document-level permissions', + } +); + +export const DOCUMENT_PERMISSIONS_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.documentPermissions.text', + { + defaultMessage: 'Using document-level permissions', + } +); + +export const DOCUMENT_PERMISSIONS_DISABLED_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.documentPermissionsDisabled.text', + { + defaultMessage: 'Disabled for this sources', + } +); + +export const LEARN_MORE_LINK = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.learnMore.link', + { + defaultMessage: 'Learn more', + } +); + +export const STATUS_HEADING = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.status.heading', + { + defaultMessage: 'Everything looks good', + } +); + +export const STATUS_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.status.text', + { + defaultMessage: 'Your endpoints are ready to accept requests.', + } +); + +export const ADDITIONAL_CONFIG_HEADING = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.additionalConfig.heading', + { + defaultMessage: 'Requires additional configuration', + } +); + +export const EXTERNAL_IDENTITIES_LINK = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.externalIdentities.link', + { + defaultMessage: 'External Identities API', + } +); + +export const ACCESS_TOKEN_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.accessToken.label', + { + defaultMessage: 'Access Token', + } +); + +export const ID_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.id.label', { + defaultMessage: 'ID', +}); + +export const LEARN_CUSTOM_FEATURES_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.learnCustom.features.button', + { + defaultMessage: 'Learn about Platinum features', + } +); + +export const DOC_PERMISSIONS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.docPermissions.description', + { + defaultMessage: + 'Document-level permissions manage content access content on individual or group attributes. Allow or deny access to specific documents.', + } +); + +export const CUSTOM_CALLOUT_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.customCallout.title', + { + defaultMessage: 'Getting started with custom sources?', + } +); + +export const NO_CONTENT_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.noContentEmpty.message', + { + defaultMessage: "This source doesn't have any content yet", + } +); + +export const CUSTOM_DOCUMENTATION_LINK = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.customSourceDocs.link', + { + defaultMessage: 'documentation', + } +); + +export const TITLE_HEADING = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.title.heading', + { + defaultMessage: 'Title', + } +); + +export const LAST_UPDATED_HEADING = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.lastUpdated.heading', + { + defaultMessage: 'Last updated', + } +); + +export const GO_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.go.button', + { + defaultMessage: 'Go', + } +); + +export const RESET_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.reset.button', + { + defaultMessage: 'Reset', + } +); + +export const SOURCE_CONTENT_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceContent.title', + { + defaultMessage: 'Source content', + } +); + +export const CONTENT_LOADING_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.contentLoading.text', + { + defaultMessage: 'Loading content...', + } +); + +export const REMOTE_SOURCE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.remoteSource.label', + { + defaultMessage: 'Remote source', + } +); + +export const CREATED_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.created.label', + { + defaultMessage: 'Created: ', + } +); + +export const STATUS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.status.label', + { + defaultMessage: 'Status: ', + } +); + +export const READY_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.ready.text', + { + defaultMessage: 'Ready to search', + } +); + +export const SOURCE_SETTINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.settings.title', + { + defaultMessage: 'Content source name', + } +); + +export const SOURCE_SETTINGS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.settings.description', + { + defaultMessage: 'Customize the name of this content source.', + } +); + +export const SOURCE_CONFIG_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.config.title', + { + defaultMessage: 'Content source configuration', + } +); + +export const SOURCE_CONFIG_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.config.description', + { + defaultMessage: 'Edit content source connector settings to change.', + } +); + +export const SOURCE_CONFIG_LINK = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.config.link', + { + defaultMessage: 'Edit content source connector settings', + } +); + +export const SOURCE_REMOVE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.remove.title', + { + defaultMessage: 'Remove this source', + } +); + +export const SOURCE_REMOVE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.config.description', + { + defaultMessage: 'Edit content source connector settings to change.', + } +); + +export const SOURCE_NAME_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceName.label', + { + defaultMessage: 'Source name', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx index 766aa511ebb2d..2402a862a62e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx @@ -22,6 +22,8 @@ import { EuiOverlayMask, } from '@elastic/eui'; +import { CANCEL_BUTTON } from '../../../constants'; + import { GroupsLogic } from '../groups_logic'; const ADD_GROUP_HEADER = i18n.translate( @@ -30,12 +32,6 @@ const ADD_GROUP_HEADER = i18n.translate( defaultMessage: 'Add a group', } ); -const ADD_GROUP_CANCEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.addGroup.cancel.action', - { - defaultMessage: 'Cancel', - } -); const ADD_GROUP_SUBMIT = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.addGroup.submit.action', { @@ -72,7 +68,7 @@ export const AddGroupModal: React.FC<{}> = () => { - {ADD_GROUP_CANCEL} + {CANCEL_BUTTON} = ({ - {CANCEL_BUTTON_TEXT} + {CANCEL_BUTTON} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index 6f55c03746aa8..a1cf7b2ca0a25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -22,6 +22,8 @@ import { EuiHorizontalRule, } from '@elastic/eui'; +import { CANCEL_BUTTON } from '../../../constants'; + import { AppLogic } from '../../../app_logic'; import { TruncatedContent } from '../../../../shared/truncate'; import { ContentSection } from '../../../components/shared/content_section'; @@ -99,12 +101,6 @@ const REMOVE_BUTTON_TEXT = i18n.translate( defaultMessage: 'Remove group', } ); -const CANCEL_REMOVE_BUTTON_TEXT = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.overview.cancelRemoveButtonText', - { - defaultMessage: 'Cancel', - } -); const CONFIRM_TITLE_TEXT = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText', { @@ -238,7 +234,7 @@ export const GroupOverview: React.FC = () => { onConfirm={deleteGroup} confirmButtonText={CONFIRM_REMOVE_BUTTON_TEXT} title={CONFIRM_TITLE_TEXT} - cancelButtonText={CANCEL_REMOVE_BUTTON_TEXT} + cancelButtonText={CANCEL_BUTTON} defaultFocusedButton="confirm" > {CONFIRM_REMOVE_DESCRIPTION} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 6911196afa81d..a81df1aab83bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -16,6 +16,7 @@ import { ContentSection } from '../../components/shared/content_section'; import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; +import { RECENT_ACTIVITY_TITLE } from '../../constants'; import { AppLogic } from '../../app_logic'; import { OverviewLogic } from './overview_logic'; @@ -38,15 +39,7 @@ export const RecentActivity: React.FC = () => { const { activityFeed } = useValues(OverviewLogic); return ( - - } - headerSpacer="m" - > + {activityFeed.length > 0 ? ( <> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a20864c8c369f..1e81795eb2328 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7369,7 +7369,6 @@ "xpack.enterpriseSearch.troubleshooting.standardAuth.title": "標準認証の{productName}はサポートされていません", "xpack.enterpriseSearch.workplaceSearch.activityFeedEmptyDefault.title": "組織には最近のアクティビティがありません", "xpack.enterpriseSearch.workplaceSearch.activityFeedNamedDefault.title": "{name}には最近のアクティビティがありません", - "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.cancel.action": "キャンセル", "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.heading": "グループを追加", "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.submit.action": "グループを追加", "xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action": "グループを作成", @@ -7381,7 +7380,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.buttonText": "ユーザー", "xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.placeholder": "ユーザーをフィルター...", "xpack.enterpriseSearch.workplaceSearch.groups.groupDeleted": "グループ「{groupName}」が正常に削除されました。", - "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerCancel": "キャンセル", "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerHeaderTitle": "{label}を管理", "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSelectAllToggle": "{action}すべて", "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSourceEmpty.body": "まだ共有コンテンツソースが追加されていない可能性があります。", @@ -7406,7 +7404,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "共有コンテンツソースがありません", "xpack.enterpriseSearch.workplaceSearch.groups.noUsersFound": "ユーザーが見つかりません", "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "ユーザーがありません", - "xpack.enterpriseSearch.workplaceSearch.groups.overview.cancelRemoveButtonText": "キャンセル", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "{name}を削除", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "グループはWorkplace Searchから削除されます。{name}を削除してよろしいですか?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "確認", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 38beab0e5d931..4d21a05cab09a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7388,7 +7388,6 @@ "xpack.enterpriseSearch.troubleshooting.standardAuth.title": "不支持使用标准身份验证的 {productName}", "xpack.enterpriseSearch.workplaceSearch.activityFeedEmptyDefault.title": "您的组织最近无活动", "xpack.enterpriseSearch.workplaceSearch.activityFeedNamedDefault.title": "{name} 最近无活动", - "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.cancel.action": "取消", "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.heading": "添加组", "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.submit.action": "添加组", "xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action": "创建组", @@ -7400,7 +7399,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.buttonText": "用户", "xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.placeholder": "筛选用户......", "xpack.enterpriseSearch.workplaceSearch.groups.groupDeleted": "组“{groupName}”已成功删除。", - "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerCancel": "取消", "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerHeaderTitle": "管理 {label}", "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSelectAllToggle": "全部{action}", "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSourceEmpty.body": "可能您尚未添加任何共享内容源。", @@ -7425,7 +7423,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "无共享内容源", "xpack.enterpriseSearch.workplaceSearch.groups.noUsersFound": "找不到用户", "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "无用户", - "xpack.enterpriseSearch.workplaceSearch.groups.overview.cancelRemoveButtonText": "取消", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "删除 {name}", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "您的组将从 Workplace Search 中删除。确定要移除 {name}?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "确认", From ac39321fc5f2e1a9b1075893731aa438d80c8ade Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 28 Jan 2021 13:58:37 -0500 Subject: [PATCH 092/163] Tinymath is now a Kibana package (#89383) * Tinymath is now a Kibana package * Rename to @kbn/tinymath * Update import style * Update README * Use commonjs import syntax * Fix to commonjs export * More commonjs fixes --- .eslintignore | 1 + package.json | 4 +- .../elastic-eslint-config-kibana/.eslintrc.js | 5 + .../src/worker/webpack.config.ts | 1 - packages/kbn-tinymath/README.md | 72 + packages/kbn-tinymath/babel.config.js | 19 + packages/kbn-tinymath/docs/functions.md | 687 ++++++++ .../kbn-tinymath/docs/template/functions.hbs | 15 + packages/kbn-tinymath/jest.config.js | 13 + packages/kbn-tinymath/package.json | 12 + packages/kbn-tinymath/src/functions/abs.js | 27 + packages/kbn-tinymath/src/functions/add.js | 37 + packages/kbn-tinymath/src/functions/cbrt.js | 27 + packages/kbn-tinymath/src/functions/ceil.js | 27 + packages/kbn-tinymath/src/functions/clamp.js | 75 + packages/kbn-tinymath/src/functions/cos.js | 26 + packages/kbn-tinymath/src/functions/count.js | 28 + packages/kbn-tinymath/src/functions/cube.js | 25 + .../kbn-tinymath/src/functions/degtorad.js | 26 + packages/kbn-tinymath/src/functions/divide.js | 37 + packages/kbn-tinymath/src/functions/exp.js | 26 + packages/kbn-tinymath/src/functions/first.js | 28 + packages/kbn-tinymath/src/functions/fix.js | 34 + packages/kbn-tinymath/src/functions/floor.js | 27 + packages/kbn-tinymath/src/functions/index.js | 89 ++ packages/kbn-tinymath/src/functions/last.js | 28 + .../src/functions/lib/transpose.js | 34 + packages/kbn-tinymath/src/functions/log.js | 38 + packages/kbn-tinymath/src/functions/log10.js | 27 + packages/kbn-tinymath/src/functions/max.js | 38 + packages/kbn-tinymath/src/functions/mean.js | 36 + packages/kbn-tinymath/src/functions/median.js | 50 + packages/kbn-tinymath/src/functions/min.js | 38 + packages/kbn-tinymath/src/functions/mod.js | 37 + packages/kbn-tinymath/src/functions/mode.js | 57 + .../kbn-tinymath/src/functions/multiply.js | 34 + packages/kbn-tinymath/src/functions/pi.js | 21 + packages/kbn-tinymath/src/functions/pow.js | 28 + .../kbn-tinymath/src/functions/radtodeg.js | 26 + packages/kbn-tinymath/src/functions/random.js | 35 + packages/kbn-tinymath/src/functions/range.js | 28 + packages/kbn-tinymath/src/functions/round.js | 32 + packages/kbn-tinymath/src/functions/sin.js | 26 + packages/kbn-tinymath/src/functions/size.js | 27 + packages/kbn-tinymath/src/functions/sqrt.js | 32 + packages/kbn-tinymath/src/functions/square.js | 25 + .../kbn-tinymath/src/functions/subtract.js | 32 + packages/kbn-tinymath/src/functions/sum.js | 32 + packages/kbn-tinymath/src/functions/tan.js | 26 + packages/kbn-tinymath/src/functions/unique.js | 30 + packages/kbn-tinymath/src/grammar.js | 1385 +++++++++++++++++ packages/kbn-tinymath/src/grammar.pegjs | 100 ++ packages/kbn-tinymath/src/index.js | 86 + .../kbn-tinymath/test/functions/abs.test.js | 22 + .../kbn-tinymath/test/functions/add.test.js | 33 + .../kbn-tinymath/test/functions/cbrt.test.js | 22 + .../kbn-tinymath/test/functions/ceil.test.js | 22 + .../kbn-tinymath/test/functions/clamp.test.js | 50 + .../kbn-tinymath/test/functions/cos.test.js | 20 + .../kbn-tinymath/test/functions/cube.test.js | 21 + .../test/functions/degtorad.test.js | 25 + .../test/functions/divide.test.js | 32 + .../kbn-tinymath/test/functions/exp.test.js | 23 + .../kbn-tinymath/test/functions/first.test.js | 27 + .../kbn-tinymath/test/functions/fix.test.js | 22 + .../kbn-tinymath/test/functions/floor.test.js | 22 + .../kbn-tinymath/test/functions/last.test.js | 27 + .../kbn-tinymath/test/functions/log.test.js | 39 + .../kbn-tinymath/test/functions/log10.test.js | 35 + .../kbn-tinymath/test/functions/max.test.js | 33 + .../kbn-tinymath/test/functions/mean.test.js | 38 + .../test/functions/median.test.js | 32 + .../kbn-tinymath/test/functions/min.test.js | 34 + .../kbn-tinymath/test/functions/mod.test.js | 32 + .../kbn-tinymath/test/functions/mode.test.js | 37 + .../test/functions/multiply.test.js | 32 + .../kbn-tinymath/test/functions/pi.test.js | 15 + .../kbn-tinymath/test/functions/pow.test.js | 26 + .../test/functions/radtodeg.test.js | 25 + .../test/functions/random.test.js | 31 + .../kbn-tinymath/test/functions/range.test.js | 33 + .../kbn-tinymath/test/functions/round.test.js | 22 + .../kbn-tinymath/test/functions/sin.test.js | 20 + .../kbn-tinymath/test/functions/size.test.js | 30 + .../kbn-tinymath/test/functions/sqrt.test.js | 26 + .../test/functions/square.test.js | 21 + .../test/functions/subtract.test.js | 32 + .../kbn-tinymath/test/functions/sum.test.js | 27 + .../kbn-tinymath/test/functions/tan.test.js | 20 + .../test/functions/transpose.test.js | 41 + .../test/functions/unique.test.js | 27 + packages/kbn-tinymath/test/library.test.js | 273 ++++ .../application/components/aggs/math.js | 2 +- .../response_processors/series/math.js | 2 +- .../functions/common/math.ts | 2 +- .../functions/server/get_field_names.test.ts | 2 +- .../functions/server/pointseries/index.ts | 2 +- .../pointseries/lib/get_expression_type.js | 2 +- .../pointseries/lib/is_column_reference.ts | 2 +- .../arguments/datacolumn/get_form_object.js | 2 +- .../plugins/canvas/common/lib/handlebars.js | 2 +- yarn.lock | 9 +- 102 files changed, 5115 insertions(+), 17 deletions(-) create mode 100644 packages/kbn-tinymath/README.md create mode 100644 packages/kbn-tinymath/babel.config.js create mode 100644 packages/kbn-tinymath/docs/functions.md create mode 100644 packages/kbn-tinymath/docs/template/functions.hbs create mode 100644 packages/kbn-tinymath/jest.config.js create mode 100644 packages/kbn-tinymath/package.json create mode 100644 packages/kbn-tinymath/src/functions/abs.js create mode 100644 packages/kbn-tinymath/src/functions/add.js create mode 100644 packages/kbn-tinymath/src/functions/cbrt.js create mode 100644 packages/kbn-tinymath/src/functions/ceil.js create mode 100644 packages/kbn-tinymath/src/functions/clamp.js create mode 100644 packages/kbn-tinymath/src/functions/cos.js create mode 100644 packages/kbn-tinymath/src/functions/count.js create mode 100644 packages/kbn-tinymath/src/functions/cube.js create mode 100644 packages/kbn-tinymath/src/functions/degtorad.js create mode 100644 packages/kbn-tinymath/src/functions/divide.js create mode 100644 packages/kbn-tinymath/src/functions/exp.js create mode 100644 packages/kbn-tinymath/src/functions/first.js create mode 100644 packages/kbn-tinymath/src/functions/fix.js create mode 100644 packages/kbn-tinymath/src/functions/floor.js create mode 100644 packages/kbn-tinymath/src/functions/index.js create mode 100644 packages/kbn-tinymath/src/functions/last.js create mode 100644 packages/kbn-tinymath/src/functions/lib/transpose.js create mode 100644 packages/kbn-tinymath/src/functions/log.js create mode 100644 packages/kbn-tinymath/src/functions/log10.js create mode 100644 packages/kbn-tinymath/src/functions/max.js create mode 100644 packages/kbn-tinymath/src/functions/mean.js create mode 100644 packages/kbn-tinymath/src/functions/median.js create mode 100644 packages/kbn-tinymath/src/functions/min.js create mode 100644 packages/kbn-tinymath/src/functions/mod.js create mode 100644 packages/kbn-tinymath/src/functions/mode.js create mode 100644 packages/kbn-tinymath/src/functions/multiply.js create mode 100644 packages/kbn-tinymath/src/functions/pi.js create mode 100644 packages/kbn-tinymath/src/functions/pow.js create mode 100644 packages/kbn-tinymath/src/functions/radtodeg.js create mode 100644 packages/kbn-tinymath/src/functions/random.js create mode 100644 packages/kbn-tinymath/src/functions/range.js create mode 100644 packages/kbn-tinymath/src/functions/round.js create mode 100644 packages/kbn-tinymath/src/functions/sin.js create mode 100644 packages/kbn-tinymath/src/functions/size.js create mode 100644 packages/kbn-tinymath/src/functions/sqrt.js create mode 100644 packages/kbn-tinymath/src/functions/square.js create mode 100644 packages/kbn-tinymath/src/functions/subtract.js create mode 100644 packages/kbn-tinymath/src/functions/sum.js create mode 100644 packages/kbn-tinymath/src/functions/tan.js create mode 100644 packages/kbn-tinymath/src/functions/unique.js create mode 100644 packages/kbn-tinymath/src/grammar.js create mode 100644 packages/kbn-tinymath/src/grammar.pegjs create mode 100644 packages/kbn-tinymath/src/index.js create mode 100644 packages/kbn-tinymath/test/functions/abs.test.js create mode 100644 packages/kbn-tinymath/test/functions/add.test.js create mode 100644 packages/kbn-tinymath/test/functions/cbrt.test.js create mode 100644 packages/kbn-tinymath/test/functions/ceil.test.js create mode 100644 packages/kbn-tinymath/test/functions/clamp.test.js create mode 100644 packages/kbn-tinymath/test/functions/cos.test.js create mode 100644 packages/kbn-tinymath/test/functions/cube.test.js create mode 100644 packages/kbn-tinymath/test/functions/degtorad.test.js create mode 100644 packages/kbn-tinymath/test/functions/divide.test.js create mode 100644 packages/kbn-tinymath/test/functions/exp.test.js create mode 100644 packages/kbn-tinymath/test/functions/first.test.js create mode 100644 packages/kbn-tinymath/test/functions/fix.test.js create mode 100644 packages/kbn-tinymath/test/functions/floor.test.js create mode 100644 packages/kbn-tinymath/test/functions/last.test.js create mode 100644 packages/kbn-tinymath/test/functions/log.test.js create mode 100644 packages/kbn-tinymath/test/functions/log10.test.js create mode 100644 packages/kbn-tinymath/test/functions/max.test.js create mode 100644 packages/kbn-tinymath/test/functions/mean.test.js create mode 100644 packages/kbn-tinymath/test/functions/median.test.js create mode 100644 packages/kbn-tinymath/test/functions/min.test.js create mode 100644 packages/kbn-tinymath/test/functions/mod.test.js create mode 100644 packages/kbn-tinymath/test/functions/mode.test.js create mode 100644 packages/kbn-tinymath/test/functions/multiply.test.js create mode 100644 packages/kbn-tinymath/test/functions/pi.test.js create mode 100644 packages/kbn-tinymath/test/functions/pow.test.js create mode 100644 packages/kbn-tinymath/test/functions/radtodeg.test.js create mode 100644 packages/kbn-tinymath/test/functions/random.test.js create mode 100644 packages/kbn-tinymath/test/functions/range.test.js create mode 100644 packages/kbn-tinymath/test/functions/round.test.js create mode 100644 packages/kbn-tinymath/test/functions/sin.test.js create mode 100644 packages/kbn-tinymath/test/functions/size.test.js create mode 100644 packages/kbn-tinymath/test/functions/sqrt.test.js create mode 100644 packages/kbn-tinymath/test/functions/square.test.js create mode 100644 packages/kbn-tinymath/test/functions/subtract.test.js create mode 100644 packages/kbn-tinymath/test/functions/sum.test.js create mode 100644 packages/kbn-tinymath/test/functions/tan.test.js create mode 100644 packages/kbn-tinymath/test/functions/transpose.test.js create mode 100644 packages/kbn-tinymath/test/functions/unique.test.js create mode 100644 packages/kbn-tinymath/test/library.test.js diff --git a/.eslintignore b/.eslintignore index e74a3d6deaa8b..5d25f3a78c1ec 100644 --- a/.eslintignore +++ b/.eslintignore @@ -36,6 +36,7 @@ snapshots.js # package overrides /packages/elastic-eslint-config-kibana /packages/kbn-interpreter/src/common/lib/grammar.js +/packages/kbn-tinymath/src/grammar.js /packages/kbn-plugin-generator/template /packages/kbn-pm/dist /packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ diff --git a/package.json b/package.json index 4e83d4e1aa45c..d6850a50c046f 100644 --- a/package.json +++ b/package.json @@ -312,7 +312,7 @@ "tabbable": "1.1.3", "tar": "4.4.13", "tinygradient": "0.4.3", - "tinymath": "1.2.1", + "@kbn/tinymath": "link:packages/kbn-tinymath", "tree-kill": "^1.2.2", "ts-easing": "^0.2.0", "tslib": "^2.0.0", @@ -847,4 +847,4 @@ "yargs": "^15.4.1", "zlib": "^1.0.5" } -} +} \ No newline at end of file diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index c0f8bf0ecb508..2e978c543cc69 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -65,6 +65,11 @@ module.exports = { to: false, disallowedMessage: `Don't import monaco directly, use or add exports to @kbn/monaco` }, + { + from: 'tinymath', + to: '@kbn/tinymath', + disallowedMessage: `Don't use 'tinymath', use '@kbn/tinymath'` + }, ], ], }, diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 96356e01f8c04..089ff163a692d 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -216,7 +216,6 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: extensions: ['.js', '.ts', '.tsx', '.json'], mainFields: ['browser', 'main'], alias: { - tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), core_app_image_assets: Path.resolve(worker.repoRoot, 'src/core/public/core_app/images'), }, }, diff --git a/packages/kbn-tinymath/README.md b/packages/kbn-tinymath/README.md new file mode 100644 index 0000000000000..1094c4286c851 --- /dev/null +++ b/packages/kbn-tinymath/README.md @@ -0,0 +1,72 @@ +# kbn-tinymath + +kbn-tinymath is a tiny arithmetic and function evaluator for simple numbers and arrays. Named properties can be accessed from an optional scope parameter. +It's available as an expression function called `math` in Canvas, and the grammar/AST structure is available +for use by Kibana plugins that want to use math. + +See [Function Documentation](/docs/functions.md) for details on built-in functions available in Tinymath. + +```javascript +const { evaluate } = require('@kbn/tinymath'); + +// Simple math +evaluate('10 + 20'); // 30 +evaluate('round(3.141592)') // 3 + +// Named properties +evaluate('foo + 20', {foo: 5}); // 25 + +// Arrays +evaluate('bar + 20', {bar: [1, 2, 3]}); // [21, 22, 23] +evaluate('bar + baz', {bar: [1, 2, 3], baz: [4, 5, 6]}); // [5, 7, 9] +evaluate('multiply(bar, baz) / 10', {bar: [1, 2, 3], baz: [4, 5, 6]}); // [0.4, 1, 1.8] +``` + +### Adding Functions + +Functions can be injected, and built in function overwritten, via the 3rd argument to `evaluate`: + +```javascript +const { evaluate } = require('@kbn/tinymath'); + +evaluate('plustwo(foo)', {foo: 5}, { + plustwo: function(a) { + return a + 2; + } +}); // 7 +``` + +### Parsing + +You can get to the parsed AST by importing `parse` + +```javascript +const { parse } = require('@kbn/tinymath'); + +parse('1 + random()') +/* +{ + "name": "add", + "args": [ + 1, + { + "name": "random", + "args": [] + } + ] +} +*/ +``` + +#### Notes + +* Floating point operations have the normal Javascript limitations + +### Building kbn-tinymath + +This package is rebuilt when running `yarn kbn bootstrap`, but can also be build directly +using `yarn build` from the `packages/kbn-tinymath` directory. +### Running tests + +To test `@kbn/tinymath` from Kibana, run `yarn run jest --watch packages/kbn-tinymath` from +the top level of Kibana. diff --git a/packages/kbn-tinymath/babel.config.js b/packages/kbn-tinymath/babel.config.js new file mode 100644 index 0000000000000..c578a02ede1fb --- /dev/null +++ b/packages/kbn-tinymath/babel.config.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +module.exports = { + env: { + web: { + presets: ['@kbn/babel-preset/webpack_preset'], + }, + node: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + ignore: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/packages/kbn-tinymath/docs/functions.md b/packages/kbn-tinymath/docs/functions.md new file mode 100644 index 0000000000000..0c7460a8189dd --- /dev/null +++ b/packages/kbn-tinymath/docs/functions.md @@ -0,0 +1,687 @@ +# TinyMath Functions +This document provides detailed information about the functions available in Tinymath and lists what parameters each function accepts, the return value of that function, and examples of how each function behaves. Most of the functions below accept arrays and apply JavaScript Math methods to each element of that array. For the functions that accept multiple arrays as parameters, the function generally does calculation index by index. Any function below can be wrapped by another function as long as the return type of the inner function matches the acceptable parameter type of the outer function. + +## _abs(_ _a_ _)_ +Calculates the absolute value of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The absolute value of `a`. Returns an array with the the absolute values of each element if `a` is an array. +**Example** +```js +abs(-1) // returns 1 +abs(2) // returns 2 +abs([-1 , -2, 3, -4]) // returns [1, 2, 3, 4] +``` +*** +## _add(_ ..._args_ _)_ +Calculates the sum of one or more numbers/arrays passed into the function. If at least one array of numbers is passed into the function, the function will calculate the sum by index. + + +| Param | Type | Description | +| --- | --- | --- | +| ...args | number \| Array.<number> | one or more numbers or arrays of numbers | + +**Returns**: number \| Array.<number> - The sum of all numbers in `args` if `args` contains only numbers. Returns an array of sums of the elements at each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. +**Throws**: + +- `'Array length mismatch'` if `args` contains arrays of different lengths + +**Example** +```js +add(1, 2, 3) // returns 6 +add([10, 20, 30, 40], 10, 20, 30) // returns [70, 80, 90, 100] +add([1, 2], 3, [4, 5], 6) // returns [(1 + 3 + 4 + 6), (2 + 3 + 5 + 6)] = [14, 16] +``` +*** +## _cbrt(_ _a_ _)_ +Calculates the cube root of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The cube root of `a`. Returns an array with the the cube roots of each element if `a` is an array. +**Example** +```js +cbrt(-27) // returns -3 +cbrt(94) // returns 4.546835943776344 +cbrt([27, 64, 125]) // returns [3, 4, 5] +``` +*** +## _ceil(_ _a_ _)_ +Calculates the ceiling of a number, i.e. rounds a number towards positive infinity. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The ceiling of `a`. Returns an array with the the ceilings of each element if `a` is an array. +**Example** +```js +ceil(1.2) // returns 2 +ceil(-1.8) // returns -1 +ceil([1.1, 2.2, 3.3]) // returns [2, 3, 4] +``` +*** +## _clamp(_ ..._a_, _min_, _max_ _)_ +Restricts value to a given range and returns closed available value. If only min is provided, values are restricted to only a lower bound. + + +| Param | Type | Description | +| --- | --- | --- | +| ...a | number \| Array.<number> | one or more numbers or arrays of numbers | +| min | number \| Array.<number> | The minimum value this function will return. | +| max | number \| Array.<number> | The maximum value this function will return. | + +**Returns**: number \| Array.<number> - The closest value between `min` (inclusive) and `max` (inclusive). Returns an array with values greater than or equal to `min` and less than or equal to `max` (if provided) at each index. +**Throws**: + +- `'Array length mismatch'` if `a`, `min`, and/or `max` are arrays of different lengths +- `'Min must be less than max'` if `max` is less than `min` +- `'Missing minimum value. You may want to use the 'max' function instead'` if min is not provided +- `'Missing maximum value. You may want to use the 'min' function instead'` if max is not provided + +**Example** +```js +clamp(1, 2, 3) // returns 2 +clamp([10, 20, 30, 40], 15, 25) // returns [15, 20, 25, 25] +clamp(10, [15, 2, 4, 20], 25) // returns [15, 10, 10, 20] +clamp(35, 10, [20, 30, 40, 50]) // returns [20, 30, 35, 35] +clamp([1, 9], 3, [4, 5]) // returns [clamp([1, 3, 4]), clamp([9, 3, 5])] = [3, 5] +``` +*** +## _cos(_ _a_ _)_ +Calculates the the cosine of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers, `a` is expected to be given in radians. | + +**Returns**: number \| Array.<number> - The cosine of `a`. Returns an array with the the cosine of each element if `a` is an array. +**Example** +```js +cos(0) // returns 1 +cos(1.5707963267948966) // returns 6.123233995736766e-17 +cos([0, 1.5707963267948966]) // returns [1, 6.123233995736766e-17] +``` +*** +## _count(_ _a_ _)_ +Returns the length of an array. Alias for size + + +| Param | Type | Description | +| --- | --- | --- | +| a | Array.<any> | array of any values | + +**Returns**: number - The length of the array. Returns 1 if `a` is not an array. +**Throws**: + +- `'Must pass an array'` if `a` is not an array + +**Example** +```js +count([]) // returns 0 +count([-1, -2, -3, -4]) // returns 4 +count(100) // returns 1 +``` +*** +## _cube(_ _a_ _)_ +Calculates the cube of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The cube of `a`. Returns an array with the the cubes of each element if `a` is an array. +**Example** +```js +cube(-3) // returns -27 +cube([3, 4, 5]) // returns [27, 64, 125] +``` +*** +## _degtorad(_ _a_ _)_ +Converts degrees to radians for a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers, `a` is expected to be given in degrees. | + +**Returns**: number \| Array.<number> - The radians of `a`. Returns an array with the the radians of each element if `a` is an array. +**Example** +```js +degtorad(0) // returns 0 +degtorad(90) // returns 1.5707963267948966 +degtorad([0, 90, 180, 360]) // returns [0, 1.5707963267948966, 3.141592653589793, 6.283185307179586] +``` +*** +## _divide(_ _a_, _b_ _)_ +Divides two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | dividend, a number or an array of numbers | +| b | number \| Array.<number> | divisor, a number or an array of numbers, `b` != 0 | + +**Returns**: number \| Array.<number> - The quotient of `a` and `b` if both are numbers. Returns an array with the quotients applied index-wise to each element if `a` or `b` is an array. +**Throws**: + +- `'Array length mismatch'` if `a` and `b` are arrays with different lengths +- `'Cannot divide by 0'` if `b` equals 0 or contains 0 + +**Example** +```js +divide(6, 3) // returns 2 +divide([10, 20, 30, 40], 10) // returns [1, 2, 3, 4] +divide(10, [1, 2, 5, 10]) // returns [10, 5, 2, 1] +divide([14, 42, 65, 108], [2, 7, 5, 12]) // returns [7, 6, 13, 9] +``` +*** +## _exp(_ _a_ _)_ +Calculates _e^x_ where _e_ is Euler's number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - `e^a`. Returns an array with the values of `e^x` evaluated where `x` is each element of `a` if `a` is an array. +**Example** +```js +exp(2) // returns e^2 = 7.3890560989306495 +exp([1, 2, 3]) // returns [e^1, e^2, e^3] = [2.718281828459045, 7.3890560989306495, 20.085536923187668] +``` +*** +## _first(_ _a_ _)_ +Returns the first element of an array. If anything other than an array is passed in, the input is returned. + + +| Param | Type | Description | +| --- | --- | --- | +| a | Array.<any> | array of any values | + +**Returns**: \* - The first element of `a`. Returns `a` if `a` is not an array. +**Example** +```js +first(2) // returns 2 +first([1, 2, 3]) // returns 1 +``` +*** +## _fix(_ _a_ _)_ +Calculates the fix of a number, i.e. rounds a number towards 0. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The fix of `a`. Returns an array with the the fixes for each element if `a` is an array. +**Example** +```js +fix(1.2) // returns 1 +fix(-1.8) // returns -1 +fix([1.8, 2.9, -3.7, -4.6]) // returns [1, 2, -3, -4] +``` +*** +## _floor(_ _a_ _)_ +Calculates the floor of a number, i.e. rounds a number towards negative infinity. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The floor of `a`. Returns an array with the the floor of each element if `a` is an array. +**Example** +```js +floor(1.8) // returns 1 +floor(-1.2) // returns -2 +floor([1.7, 2.8, 3.9]) // returns [1, 2, 3] +``` +*** +## _last(_ _a_ _)_ +Returns the last element of an array. If anything other than an array is passed in, the input is returned. + + +| Param | Type | Description | +| --- | --- | --- | +| a | Array.<any> | array of any values | + +**Returns**: \* - The last element of `a`. Returns `a` if `a` is not an array. +**Example** +```js +last(2) // returns 2 +last([1, 2, 3]) // returns 3 +``` +*** +## _log(_ _a_, _b_ _)_ +Calculates the logarithm of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers, `a` must be greater than 0 | +| b | Object | (optional) base for the logarithm. If not provided a value, the default base is e, and the natural log is calculated. | + +**Returns**: number \| Array.<number> - The logarithm of `a`. Returns an array with the the logarithms of each element if `a` is an array. +**Throws**: + +- `'Base out of range'` if `b` <= 0 +- 'Must be greater than 0' if `a` > 0 + +**Example** +```js +log(1) // returns 0 +log(64, 8) // returns 2 +log(42, 5) // returns 2.322344707681546 +log([2, 4, 8, 16, 32], 2) // returns [1, 2, 3, 4, 5] +``` +*** +## _log10(_ _a_ _)_ +Calculates the logarithm base 10 of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers, `a` must be greater than 0 | + +**Returns**: number \| Array.<number> - The logarithm of `a`. Returns an array with the the logarithms base 10 of each element if `a` is an array. +**Throws**: + +- `'Must be greater than 0'` if `a` < 0 + +**Example** +```js +log(10) // returns 1 +log(100) // returns 2 +log(80) // returns 1.9030899869919433 +log([10, 100, 1000, 10000, 100000]) // returns [1, 2, 3, 4, 5] +``` +*** +## _max(_ ..._args_ _)_ +Finds the maximum value of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the maximum by index. + + +| Param | Type | Description | +| --- | --- | --- | +| ...args | number \| Array.<number> | one or more numbers or arrays of numbers | + +**Returns**: number \| Array.<number> - The maximum value of all numbers if `args` contains only numbers. Returns an array with the the maximum values at each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. +**Throws**: + +- `'Array length mismatch'` if `args` contains arrays of different lengths + +**Example** +```js +max(1, 2, 3) // returns 3 +max([10, 20, 30, 40], 15) // returns [15, 20, 30, 40] +max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9] +``` +*** +## _mean(_ ..._args_ _)_ +Finds the mean value of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the mean by index. + + +| Param | Type | Description | +| --- | --- | --- | +| ...args | number \| Array.<number> | one or more numbers or arrays of numbers | + +**Returns**: number \| Array.<number> - The mean value of all numbers if `args` contains only numbers. Returns an array with the the mean values of each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. +**Example** +```js +mean(1, 2, 3) // returns 2 +mean([10, 20, 30, 40], 20) // returns [15, 20, 25, 30] +mean([1, 9], 5, [3, 4]) // returns [mean([1, 5, 3]), mean([9, 5, 4])] = [3, 6] +``` +*** +## _median(_ ..._args_ _)_ +Finds the median value(s) of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the median by index. + + +| Param | Type | Description | +| --- | --- | --- | +| ...args | number \| Array.<number> | one or more numbers or arrays of numbers | + +**Returns**: number \| Array.<number> - The median value of all numbers if `args` contains only numbers. Returns an array with the the median values of each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. +**Example** +```js +median(1, 1, 2, 3) // returns 1.5 +median(1, 1, 2, 2, 3) // returns 2 +median([10, 20, 30, 40], 10, 20, 30) // returns [15, 20, 25, 25] +median([1, 9], 2, 4, [3, 5]) // returns [median([1, 2, 4, 3]), median([9, 2, 4, 5])] = [2.5, 4.5] +``` +*** +## _min(_ ..._args_ _)_ +Finds the minimum value of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the minimum by index. + + +| Param | Type | Description | +| --- | --- | --- | +| ...args | number \| Array.<number> | one or more numbers or arrays of numbers | + +**Returns**: number \| Array.<number> - The minimum value of all numbers if `args` contains only numbers. Returns an array with the the minimum values of each index, including all scalar numbers in `args` in the calculation at each index if `a` is an array. +**Throws**: + +- `'Array length mismatch'` if `args` contains arrays of different lengths + +**Example** +```js +min(1, 2, 3) // returns 1 +min([10, 20, 30, 40], 25) // returns [10, 20, 25, 25] +min([1, 9], 4, [3, 5]) // returns [min([1, 4, 3]), min([9, 4, 5])] = [1, 4] +``` +*** +## _mod(_ _a_, _b_ _)_ +Remainder after dividing two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | dividend, a number or an array of numbers | +| b | number \| Array.<number> | divisor, a number or an array of numbers, `b` != 0 | + +**Returns**: number \| Array.<number> - The remainder of `a` divided by `b` if both are numbers. Returns an array with the the remainders applied index-wise to each element if `a` or `b` is an array. +**Throws**: + +- `'Array length mismatch'` if `a` and `b` are arrays with different lengths +- `'Cannot divide by 0'` if `b` equals 0 or contains 0 + +**Example** +```js +mod(10, 7) // returns 3 +mod([11, 22, 33, 44], 10) // returns [1, 2, 3, 4] +mod(100, [3, 7, 11, 23]) // returns [1, 2, 1, 8] +mod([14, 42, 65, 108], [5, 4, 14, 2]) // returns [5, 2, 9, 0] +``` +*** +## _mode(_ ..._args_ _)_ +Finds the mode value(s) of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the mode by index. + + +| Param | Type | Description | +| --- | --- | --- | +| ...args | number \| Array.<number> | one or more numbers or arrays of numbers | + +**Returns**: Array.<number> \| Array.<Array.<number>> - An array mode value(s) of all numbers if `args` contains only numbers. Returns an array of arrays with mode value(s) of each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. +**Example** +```js +mode(1, 1, 2, 3) // returns [1] +mode(1, 1, 2, 2, 3) // returns [1,2] +mode([10, 20, 30, 40], 10, 20, 30) // returns [[10], [20], [30], [10, 20, 30, 40]] +mode([1, 9], 1, 4, [3, 5]) // returns [mode([1, 1, 4, 3]), mode([9, 1, 4, 5])] = [[1], [4, 5, 9]] +``` +*** +## _multiply(_ _a_, _b_ _)_ +Multiplies two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | +| b | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The product of `a` and `b` if both are numbers. Returns an array with the the products applied index-wise to each element if `a` or `b` is an array. +**Throws**: + +- `'Array length mismatch'` if `a` and `b` are arrays with different lengths + +**Example** +```js +multiply(6, 3) // returns 18 +multiply([10, 20, 30, 40], 10) // returns [100, 200, 300, 400] +multiply(10, [1, 2, 5, 10]) // returns [10, 20, 50, 100] +multiply([1, 2, 3, 4], [2, 7, 5, 12]) // returns [2, 14, 15, 48] +``` +*** +## _pi(__)_ +Returns the mathematical constant PI + +**Returns**: number - The mathematical constant PI +**Example** +```js +pi() // 3.141592653589793 +``` +*** +## _pow(_ _a_, _b_ _)_ +Raises a number to a given exponent. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | +| b | number | the power that `a` is raised to | + +**Returns**: number \| Array.<number> - `a` raised to the power of `b`. Returns an array with the each element raised to the power of `b` if `a` is an array. +**Throws**: + +- `'Missing exponent'` if `b` is not provided + +**Example** +```js +pow(2,3) // returns 8 +pow([1, 2, 3], 4) // returns [1, 16, 81] +``` +*** +## _radtodeg(_ _a_ _)_ +Converts radians to degrees for a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers, `a` is expected to be given in radians. | + +**Returns**: number \| Array.<number> - The degrees of `a`. Returns an array with the the degrees of each element if `a` is an array. +**Example** +```js +radtodeg(0) // returns 0 +radtodeg(1.5707963267948966) // returns 90 +radtodeg([0, 1.5707963267948966, 3.141592653589793, 6.283185307179586]) // returns [0, 90, 180, 360] +``` +*** +## _random(_ _a_, _b_ _)_ +Generates a random number within the given range where the lower bound is inclusive and the upper bound is exclusive. If no numbers are passed in, it will return a number between 0 and 1. If only one number is passed in, it will return . + + +| Param | Type | Description | +| --- | --- | --- | +| a | number | (optional) must be greater than 0 if `b` is not provided | +| b | number | (optional) must be greater | + +**Returns**: number - A random number between 0 and 1 if no numbers are passed in. Returns a random number between 0 and `a` if only one number is passed in. Returns a random number between `a` and `b` if two numbers are passed in. +**Throws**: + +- `'Min is be greater than max'` if `a` < 0 when only `a` is passed in or if `a` > `b` when both `a` and `b` are passed in + +**Example** +```js +random() // returns a random number between 0 (inclusive) and 1 (exclusive) +random(10) // returns a random number between 0 (inclusive) and 10 (exclusive) +random(-10,10) // returns a random number between -10 (inclusive) and 10 (exclusive) +``` +*** +## _range(_ ..._args_ _)_ +Finds the range of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the range by index. + + +| Param | Type | Description | +| --- | --- | --- | +| ...args | number \| Array.<number> | one or more numbers or arrays of numbers | + +**Returns**: number \| Array.<number> - The range value of all numbers if `args` contains only numbers. Returns an array with the the range values at each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. +**Example** +```js +range(1, 2, 3) // returns 2 +range([10, 20, 30, 40], 15) // returns [5, 5, 15, 25] +range([1, 9], 4, [3, 5]) // returns [range([1, 4, 3]), range([9, 4, 5])] = [3, 5] +``` +*** +## _round(_ _a_, _b_ _)_ +Rounds a number towards the nearest integer by default or decimal place if specified. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | +| b | number | (optional) number of decimal places, default value: 0 | + +**Returns**: number \| Array.<number> - The rounded value of `a`. Returns an array with the the rounded values of each element if `a` is an array. +**Example** +```js +round(1.2) // returns 2 +round(-10.51) // returns -11 +round(-10.1, 2) // returns -10.1 +round(10.93745987, 4) // returns 10.9375 +round([2.9234, 5.1234, 3.5234, 4.49234324], 2) // returns [2.92, 5.12, 3.52, 4.49] +``` +*** +## _sin(_ _a_ _)_ +Calculates the the sine of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers, `a` is expected to be given in radians. | + +**Returns**: number \| Array.<number> - The sine of `a`. Returns an array with the the sine of each element if `a` is an array. +**Example** +```js +sin(0) // returns 0 +sin(1.5707963267948966) // returns 1 +sin([0, 1.5707963267948966]) // returns [0, 1] +``` +*** +## _size(_ _a_ _)_ +Returns the length of an array. Alias for count + + +| Param | Type | Description | +| --- | --- | --- | +| a | Array.<any> | array of any values | + +**Returns**: number - The length of the array. Returns 1 if `a` is not an array. +**Throws**: + +- `'Must pass an array'` if `a` is not an array + +**Example** +```js +size([]) // returns 0 +size([-1, -2, -3, -4]) // returns 4 +size(100) // returns 1 +``` +*** +## _sqrt(_ _a_ _)_ +Calculates the square root of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The square root of `a`. Returns an array with the the square roots of each element if `a` is an array. +**Throws**: + +- `'Unable find the square root of a negative number'` if `a` < 0 + +**Example** +```js +sqrt(9) // returns 3 +sqrt(30) //5.477225575051661 +sqrt([9, 16, 25]) // returns [3, 4, 5] +``` +*** +## _square(_ _a_ _)_ +Calculates the square of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The square of `a`. Returns an array with the the squares of each element if `a` is an array. +**Example** +```js +square(-3) // returns 9 +square([3, 4, 5]) // returns [9, 16, 25] +``` +*** +## _subtract(_ _a_, _b_ _)_ +Subtracts two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers | +| b | number \| Array.<number> | a number or an array of numbers | + +**Returns**: number \| Array.<number> - The difference of `a` and `b` if both are numbers or an array of differences applied index-wise to each element. +**Throws**: + +- `'Array length mismatch'` if `a` and `b` are arrays with different lengths + +**Example** +```js +subtract(6, 3) // returns 3 +subtract([10, 20, 30, 40], 10) // returns [0, 10, 20, 30] +subtract(10, [1, 2, 5, 10]) // returns [9, 8, 5, 0] +subtract([14, 42, 65, 108], [2, 7, 5, 12]) // returns [12, 35, 52, 96] +``` +*** +## _sum(_ ..._args_ _)_ +Calculates the sum of one or more numbers/arrays passed into the function. If at least one array is passed, the function will sum up one or more numbers/arrays of numbers and distinct values of an array. Sum accepts arrays of different lengths. + + +| Param | Type | Description | +| --- | --- | --- | +| ...args | number \| Array.<number> | one or more numbers or arrays of numbers | + +**Returns**: number - The sum of one or more numbers/arrays of numbers including distinct values in arrays +**Example** +```js +sum(1, 2, 3) // returns 6 +sum([10, 20, 30, 40], 10, 20, 30) // returns 160 +sum([1, 2], 3, [4, 5], 6) // returns sum(1, 2, 3, 4, 5, 6) = 21 +sum([10, 20, 30, 40], 10, [1, 2, 3], 22) // returns sum(10, 20, 30, 40, 10, 1, 2, 3, 22) = 138 +``` +*** +## _tan(_ _a_ _)_ +Calculates the the tangent of a number. For arrays, the function will be applied index-wise to each element. + + +| Param | Type | Description | +| --- | --- | --- | +| a | number \| Array.<number> | a number or an array of numbers, `a` is expected to be given in radians. | + +**Returns**: number \| Array.<number> - The tangent of `a`. Returns an array with the the tangent of each element if `a` is an array. +**Example** +```js +tan(0) // returns 0 +tan(1) // returns 1.5574077246549023 +tan([0, 1, -1]) // returns [0, 1.5574077246549023, -1.5574077246549023] +``` +*** +## _unique(_ _a_ _)_ +Counts the number of unique values in an array + + +| Param | Type | Description | +| --- | --- | --- | +| a | Array.<any> | array of any values | + +**Returns**: number - The number of unique values in the array. Returns 1 if `a` is not an array. +**Example** +```js +unique(100) // returns 1 +unique([]) // returns 0 +unique([1, 2, 3, 4]) // returns 4 +unique([1, 2, 3, 4, 2, 2, 2, 3, 4, 2, 4, 5, 2, 1, 4, 2]) // returns 5 +``` diff --git a/packages/kbn-tinymath/docs/template/functions.hbs b/packages/kbn-tinymath/docs/template/functions.hbs new file mode 100644 index 0000000000000..60f821e6d15bf --- /dev/null +++ b/packages/kbn-tinymath/docs/template/functions.hbs @@ -0,0 +1,15 @@ +# TinyMath Functions +This document provides detailed information about the functions available in Tinymath and lists what parameters each function accepts, the return value of that function, and examples of how each function behaves. Most of the functions below accept arrays and apply JavaScript Math methods to each element of that array. For the functions that accept multiple arrays as parameters, the function generally does calculation index by index. Any function below can be wrapped by another function as long as the return type of the inner function matches the acceptable parameter type of the outer function. + +{{#functions}} +## _{{name}}(_{{#each params}} {{#if variable}}...{{/if}}_{{name}}_{{#unless @last}},{{/unless}} {{/each}}_)_ +{{description}} + +{{>params~}} +{{>returns~}} +{{>throws~}} +{{>examples~}} +{{#unless @last}} +*** +{{/unless}} +{{/functions}} diff --git a/packages/kbn-tinymath/jest.config.js b/packages/kbn-tinymath/jest.config.js new file mode 100644 index 0000000000000..2fb97d8aa416a --- /dev/null +++ b/packages/kbn-tinymath/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-tinymath'], +}; diff --git a/packages/kbn-tinymath/package.json b/packages/kbn-tinymath/package.json new file mode 100644 index 0000000000000..34fd593672b5a --- /dev/null +++ b/packages/kbn-tinymath/package.json @@ -0,0 +1,12 @@ +{ + "name": "@kbn/tinymath", + "version": "2.0.0", + "license": "SSPL-1.0 OR Elastic License", + "private": true, + "main": "src/index.js", + "scripts": { + "kbn:bootstrap": "yarn build", + "build": "../../node_modules/.bin/pegjs -o src/grammar.js src/grammar.pegjs" + }, + "dependencies": {} +} \ No newline at end of file diff --git a/packages/kbn-tinymath/src/functions/abs.js b/packages/kbn-tinymath/src/functions/abs.js new file mode 100644 index 0000000000000..aa9eaba1ce3b2 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/abs.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the absolute value of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} The absolute value of `a`. Returns an array with the the absolute values of each element if `a` is an array. + * + * @example + * abs(-1) // returns 1 + * abs(2) // returns 2 + * abs([-1 , -2, 3, -4]) // returns [1, 2, 3, 4] + */ + +module.exports = { abs }; + +function abs(a) { + if (Array.isArray(a)) { + return a.map((a) => Math.abs(a)); + } + return Math.abs(a); +} diff --git a/packages/kbn-tinymath/src/functions/add.js b/packages/kbn-tinymath/src/functions/add.js new file mode 100644 index 0000000000000..5a4d6802a85ea --- /dev/null +++ b/packages/kbn-tinymath/src/functions/add.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the sum of one or more numbers/arrays passed into the function. If at least one array of numbers is passed into the function, the function will calculate the sum by index. + * @param {...(number|number[])} args one or more numbers or arrays of numbers + * @return {(number|number[])} The sum of all numbers in `args` if `args` contains only numbers. Returns an array of sums of the elements at each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. + * @throws `'Array length mismatch'` if `args` contains arrays of different lengths + * @example + * add(1, 2, 3) // returns 6 + * add([10, 20, 30, 40], 10, 20, 30) // returns [70, 80, 90, 100] + * add([1, 2], 3, [4, 5], 6) // returns [(1 + 3 + 4 + 6), (2 + 3 + 5 + 6)] = [14, 16] + */ + +module.exports = { add }; + +function add(...args) { + if (args.length === 1) { + if (Array.isArray(args[0])) return args[0].reduce((result, current) => result + current); + return args[0]; + } + + return args.reduce((result, current) => { + if (Array.isArray(result) && Array.isArray(current)) { + if (current.length !== result.length) throw new Error('Array length mismatch'); + return result.map((val, i) => val + current[i]); + } + if (Array.isArray(result)) return result.map((val) => val + current); + if (Array.isArray(current)) return current.map((val) => val + result); + return result + current; + }); +} diff --git a/packages/kbn-tinymath/src/functions/cbrt.js b/packages/kbn-tinymath/src/functions/cbrt.js new file mode 100644 index 0000000000000..017a661702761 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/cbrt.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the cube root of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} The cube root of `a`. Returns an array with the the cube roots of each element if `a` is an array. + * + * @example + * cbrt(-27) // returns -3 + * cbrt(94) // returns 4.546835943776344 + * cbrt([27, 64, 125]) // returns [3, 4, 5] + */ + +module.exports = { cbrt }; + +function cbrt(a) { + if (Array.isArray(a)) { + return a.map((a) => Math.cbrt(a)); + } + return Math.cbrt(a); +} diff --git a/packages/kbn-tinymath/src/functions/ceil.js b/packages/kbn-tinymath/src/functions/ceil.js new file mode 100644 index 0000000000000..7fbbabe481073 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/ceil.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the ceiling of a number, i.e. rounds a number towards positive infinity. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} The ceiling of `a`. Returns an array with the the ceilings of each element if `a` is an array. + * + * @example + * ceil(1.2) // returns 2 + * ceil(-1.8) // returns -1 + * ceil([1.1, 2.2, 3.3]) // returns [2, 3, 4] + */ + +module.exports = { ceil }; + +function ceil(a) { + if (Array.isArray(a)) { + return a.map((a) => Math.ceil(a)); + } + return Math.ceil(a); +} diff --git a/packages/kbn-tinymath/src/functions/clamp.js b/packages/kbn-tinymath/src/functions/clamp.js new file mode 100644 index 0000000000000..66b9e9eaf4f0d --- /dev/null +++ b/packages/kbn-tinymath/src/functions/clamp.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const findClamp = (a, min, max) => { + if (min > max) throw new Error('Min must be less than max'); + return Math.min(Math.max(a, min), max); +}; + +/** + * Restricts value to a given range and returns closed available value. If only min is provided, values are restricted to only a lower bound. + * @param {...(number|number[])} a one or more numbers or arrays of numbers + * @param {(number|number[])} min The minimum value this function will return. + * @param {(number|number[])} max The maximum value this function will return. + * @return {(number|number[])} The closest value between `min` (inclusive) and `max` (inclusive). Returns an array with values greater than or equal to `min` and less than or equal to `max` (if provided) at each index. + * @throws `'Array length mismatch'` if `a`, `min`, and/or `max` are arrays of different lengths + * @throws `'Min must be less than max'` if `max` is less than `min` + * @throws `'Missing minimum value. You may want to use the 'max' function instead'` if min is not provided + * @throws `'Missing maximum value. You may want to use the 'min' function instead'` if max is not provided + * + * @example + * clamp(1, 2, 3) // returns 2 + * clamp([10, 20, 30, 40], 15, 25) // returns [15, 20, 25, 25] + * clamp(10, [15, 2, 4, 20], 25) // returns [15, 10, 10, 20] + * clamp(35, 10, [20, 30, 40, 50]) // returns [20, 30, 35, 35] + * clamp([1, 9], 3, [4, 5]) // returns [clamp([1, 3, 4]), clamp([9, 3, 5])] = [3, 5] + */ + +module.exports = { clamp }; + +function clamp(a, min, max) { + if (max === null) + throw new Error("Missing maximum value. You may want to use the 'min' function instead"); + if (min === null) + throw new Error("Missing minimum value. You may want to use the 'max' function instead"); + + if (Array.isArray(max)) { + if (Array.isArray(a) && Array.isArray(min)) { + if (a.length !== max.length || a.length !== min.length) + throw new Error('Array length mismatch'); + return max.map((max, i) => findClamp(a[i], min[i], max)); + } + + if (Array.isArray(a)) { + if (a.length !== max.length) throw new Error('Array length mismatch'); + return max.map((max, i) => findClamp(a[i], min, max)); + } + + if (Array.isArray(min)) { + if (min.length !== max.length) throw new Error('Array length mismatch'); + return max.map((max, i) => findClamp(a, min[i], max)); + } + + return max.map((max) => findClamp(a, min, max)); + } + + if (Array.isArray(a) && Array.isArray(min)) { + if (a.length !== min.length) throw new Error('Array length mismatch'); + return a.map((a, i) => findClamp(a, min[i])); + } + + if (Array.isArray(a)) { + return a.map((a) => findClamp(a, min, max)); + } + + if (Array.isArray(min)) { + return min.map((min) => findClamp(a, min, max)); + } + + return findClamp(a, min, max); +} diff --git a/packages/kbn-tinymath/src/functions/cos.js b/packages/kbn-tinymath/src/functions/cos.js new file mode 100644 index 0000000000000..0385f52793c27 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/cos.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the the cosine of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers, `a` is expected to be given in radians. + * @return {(number|number[])} The cosine of `a`. Returns an array with the the cosine of each element if `a` is an array. + * @example + * cos(0) // returns 1 + * cos(1.5707963267948966) // returns 6.123233995736766e-17 + * cos([0, 1.5707963267948966]) // returns [1, 6.123233995736766e-17] + */ + +module.exports = { cos }; + +function cos(a) { + if (Array.isArray(a)) { + return a.map((a) => Math.cos(a)); + } + return Math.cos(a); +} diff --git a/packages/kbn-tinymath/src/functions/count.js b/packages/kbn-tinymath/src/functions/count.js new file mode 100644 index 0000000000000..b037999b7ac8a --- /dev/null +++ b/packages/kbn-tinymath/src/functions/count.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { size } = require('./size.js'); + +/** + * Returns the length of an array. Alias for size + * @param {any[]} a array of any values + * @return {(number)} The length of the array. Returns 1 if `a` is not an array. + * @throws `'Must pass an array'` if `a` is not an array + * @example + * count([]) // returns 0 + * count([-1, -2, -3, -4]) // returns 4 + * count(100) // returns 1 + */ + +module.exports = { count }; + +function count(a) { + return size(a); +} + +count.skipNumberValidation = true; diff --git a/packages/kbn-tinymath/src/functions/cube.js b/packages/kbn-tinymath/src/functions/cube.js new file mode 100644 index 0000000000000..de14ac8749ae1 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/cube.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { pow } = require('./pow.js'); + +/** + * Calculates the cube of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} The cube of `a`. Returns an array with the the cubes of each element if `a` is an array. + * + * @example + * cube(-3) // returns -27 + * cube([3, 4, 5]) // returns [27, 64, 125] + */ + +module.exports = { cube }; + +function cube(a) { + return pow(a, 3); +} diff --git a/packages/kbn-tinymath/src/functions/degtorad.js b/packages/kbn-tinymath/src/functions/degtorad.js new file mode 100644 index 0000000000000..20fd8ac9e2060 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/degtorad.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Converts degrees to radians for a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers, `a` is expected to be given in degrees. + * @return {(number|number[])} The radians of `a`. Returns an array with the the radians of each element if `a` is an array. + * @example + * degtorad(0) // returns 0 + * degtorad(90) // returns 1.5707963267948966 + * degtorad([0, 90, 180, 360]) // returns [0, 1.5707963267948966, 3.141592653589793, 6.283185307179586] + */ + +module.exports = { degtorad }; + +function degtorad(a) { + if (Array.isArray(a)) { + return a.map((a) => (a * Math.PI) / 180); + } + return (a * Math.PI) / 180; +} diff --git a/packages/kbn-tinymath/src/functions/divide.js b/packages/kbn-tinymath/src/functions/divide.js new file mode 100644 index 0000000000000..889e2305cbd9e --- /dev/null +++ b/packages/kbn-tinymath/src/functions/divide.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Divides two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. + * @param {(number|number[])} a dividend, a number or an array of numbers + * @param {(number|number[])} b divisor, a number or an array of numbers, `b` != 0 + * @return {(number|number[])} The quotient of `a` and `b` if both are numbers. Returns an array with the quotients applied index-wise to each element if `a` or `b` is an array. + * @throws `'Array length mismatch'` if `a` and `b` are arrays with different lengths + * - `'Cannot divide by 0'` if `b` equals 0 or contains 0 + * @example + * divide(6, 3) // returns 2 + * divide([10, 20, 30, 40], 10) // returns [1, 2, 3, 4] + * divide(10, [1, 2, 5, 10]) // returns [10, 5, 2, 1] + * divide([14, 42, 65, 108], [2, 7, 5, 12]) // returns [7, 6, 13, 9] + */ + +module.exports = { divide }; + +function divide(a, b) { + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) throw new Error('Array length mismatch'); + return a.map((val, i) => { + if (b[i] === 0) throw new Error('Cannot divide by 0'); + return val / b[i]; + }); + } + if (Array.isArray(b)) return b.map((b) => a / b); + if (b === 0) throw new Error('Cannot divide by 0'); + if (Array.isArray(a)) return a.map((a) => a / b); + return a / b; +} diff --git a/packages/kbn-tinymath/src/functions/exp.js b/packages/kbn-tinymath/src/functions/exp.js new file mode 100644 index 0000000000000..d7fd3877001c9 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/exp.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates _e^x_ where _e_ is Euler's number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} `e^a`. Returns an array with the values of `e^x` evaluated where `x` is each element of `a` if `a` is an array. + * + * @example + * exp(2) // returns e^2 = 7.3890560989306495 + * exp([1, 2, 3]) // returns [e^1, e^2, e^3] = [2.718281828459045, 7.3890560989306495, 20.085536923187668] + */ + +module.exports = { exp }; + +function exp(a) { + if (Array.isArray(a)) { + return a.map((a) => Math.exp(a)); + } + return Math.exp(a); +} diff --git a/packages/kbn-tinymath/src/functions/first.js b/packages/kbn-tinymath/src/functions/first.js new file mode 100644 index 0000000000000..911482541b1d3 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/first.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Returns the first element of an array. If anything other than an array is passed in, the input is returned. + * @param {any[]} a array of any values + * @return {*} The first element of `a`. Returns `a` if `a` is not an array. + * + * @example + * first(2) // returns 2 + * first([1, 2, 3]) // returns 1 + */ + +module.exports = { first }; + +function first(a) { + if (Array.isArray(a)) { + return a[0]; + } + return a; +} + +first.skipNumberValidation = true; diff --git a/packages/kbn-tinymath/src/functions/fix.js b/packages/kbn-tinymath/src/functions/fix.js new file mode 100644 index 0000000000000..16ed2d0dcb54f --- /dev/null +++ b/packages/kbn-tinymath/src/functions/fix.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const fixer = (a) => { + if (a > 0) { + return Math.floor(a); + } + return Math.ceil(a); +}; + +/** + * Calculates the fix of a number, i.e. rounds a number towards 0. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} The fix of `a`. Returns an array with the the fixes for each element if `a` is an array. + * + * @example + * fix(1.2) // returns 1 + * fix(-1.8) // returns -1 + * fix([1.8, 2.9, -3.7, -4.6]) // returns [1, 2, -3, -4] + */ + +module.exports = { fix }; + +function fix(a) { + if (Array.isArray(a)) { + return a.map((a) => fixer(a)); + } + return fixer(a); +} diff --git a/packages/kbn-tinymath/src/functions/floor.js b/packages/kbn-tinymath/src/functions/floor.js new file mode 100644 index 0000000000000..db90697edc346 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/floor.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the floor of a number, i.e. rounds a number towards negative infinity. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} The floor of `a`. Returns an array with the the floor of each element if `a` is an array. + * + * @example + * floor(1.8) // returns 1 + * floor(-1.2) // returns -2 + * floor([1.7, 2.8, 3.9]) // returns [1, 2, 3] + */ + +module.exports = { floor }; + +function floor(a) { + if (Array.isArray(a)) { + return a.map((a) => Math.floor(a)); + } + return Math.floor(a); +} diff --git a/packages/kbn-tinymath/src/functions/index.js b/packages/kbn-tinymath/src/functions/index.js new file mode 100644 index 0000000000000..ab5805cc0a77e --- /dev/null +++ b/packages/kbn-tinymath/src/functions/index.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { abs } = require('./abs'); +const { add } = require('./add'); +const { cbrt } = require('./cbrt'); +const { ceil } = require('./ceil'); +const { clamp } = require('./clamp'); +const { cos } = require('./cos'); +const { count } = require('./count'); +const { cube } = require('./cube'); +const { degtorad } = require('./degtorad'); +const { divide } = require('./divide'); +const { exp } = require('./exp'); +const { first } = require('./first'); +const { fix } = require('./fix'); +const { floor } = require('./floor'); +const { last } = require('./last'); +const { log } = require('./log'); +const { log10 } = require('./log10'); +const { max } = require('./max'); +const { mean } = require('./mean'); +const { median } = require('./median'); +const { min } = require('./min'); +const { mod } = require('./mod'); +const { mode } = require('./mode'); +const { multiply } = require('./multiply'); +const { pi } = require('./pi'); +const { pow } = require('./pow'); +const { radtodeg } = require('./radtodeg'); +const { random } = require('./random'); +const { range } = require('./range'); +const { round } = require('./round'); +const { sin } = require('./sin'); +const { size } = require('./size'); +const { sqrt } = require('./sqrt'); +const { square } = require('./square'); +const { subtract } = require('./subtract'); +const { sum } = require('./sum'); +const { tan } = require('./tan'); +const { unique } = require('./unique'); + +module.exports = { + functions: { + abs, + add, + cbrt, + ceil, + clamp, + cos, + count, + cube, + degtorad, + divide, + exp, + first, + fix, + floor, + last, + log, + log10, + max, + mean, + median, + min, + mod, + mode, + multiply, + pi, + pow, + radtodeg, + random, + range, + round, + sin, + size, + sqrt, + square, + subtract, + sum, + tan, + unique, + }, +}; diff --git a/packages/kbn-tinymath/src/functions/last.js b/packages/kbn-tinymath/src/functions/last.js new file mode 100644 index 0000000000000..08964c784ba88 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/last.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Returns the last element of an array. If anything other than an array is passed in, the input is returned. + * @param {any[]} a array of any values + * @return {*} The last element of `a`. Returns `a` if `a` is not an array. + * + * @example + * last(2) // returns 2 + * last([1, 2, 3]) // returns 3 + */ + +module.exports = { last }; + +function last(a) { + if (Array.isArray(a)) { + return a[a.length - 1]; + } + return a; +} + +last.skipNumberValidation = true; diff --git a/packages/kbn-tinymath/src/functions/lib/transpose.js b/packages/kbn-tinymath/src/functions/lib/transpose.js new file mode 100644 index 0000000000000..6a771f4f54336 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/lib/transpose.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Transposes a 2D array, i.e. turns the rows into columns and vice versa. Scalar values are also included in the transpose. + * @param {any[][]} args an array or an array that contains arrays + * @param {number} index index of the first array element in args + * @return {any[][]} transpose of args + * @throws `'Array length mismatch'` if `args` contains arrays of different lengths + * @example + * transpose([[1,2], [3,4], [5,6]], 0) // returns [[1, 3, 5], [2, 4, 6]] + * transpose([10, 20, [10, 20, 30, 40], 30], 2) // returns [[10, 20, 10, 30], [10, 20, 20, 30], [10, 20, 30, 30], [10, 20, 40, 30]] + * transpose([4, [1, 9], [3, 5]], 1) // returns [[4, 1, 3], [4, 9, 5]] + */ + +module.exports = { transpose }; + +function transpose(args, index) { + const len = args[index].length; + return args[index].map((col, i) => + args.map((row) => { + if (Array.isArray(row)) { + if (row.length !== len) throw new Error('Array length mismatch'); + return row[i]; + } + return row; + }) + ); +} diff --git a/packages/kbn-tinymath/src/functions/log.js b/packages/kbn-tinymath/src/functions/log.js new file mode 100644 index 0000000000000..07fb8376438d6 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/log.js @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const changeOfBase = (a, b) => Math.log(a) / Math.log(b); + +/** + * Calculates the logarithm of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers, `a` must be greater than 0 + * @param {{number}} b (optional) base for the logarithm. If not provided a value, the default base is e, and the natural log is calculated. + * @return {(number|number[])} The logarithm of `a`. Returns an array with the the logarithms of each element if `a` is an array. + * @throws `'Base out of range'` if `b` <= 0 + * - 'Must be greater than 0' if `a` > 0 + * @example + * log(1) // returns 0 + * log(64, 8) // returns 2 + * log(42, 5) // returns 2.322344707681546 + * log([2, 4, 8, 16, 32], 2) // returns [1, 2, 3, 4, 5] + */ + +module.exports = { log }; + +function log(a, b = Math.E) { + if (b <= 0) throw new Error('Base out of range'); + + if (Array.isArray(a)) { + return a.map((a) => { + if (a < 0) throw new Error('Must be greater than 0'); + return changeOfBase(a, b); + }); + } + if (a < 0) throw new Error('Must be greater than 0'); + return changeOfBase(a, b); +} diff --git a/packages/kbn-tinymath/src/functions/log10.js b/packages/kbn-tinymath/src/functions/log10.js new file mode 100644 index 0000000000000..79417031d5ed8 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/log10.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { log } = require('./log.js'); + +/** + * Calculates the logarithm base 10 of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers, `a` must be greater than 0 + * @return {(number|number[])} The logarithm of `a`. Returns an array with the the logarithms base 10 of each element if `a` is an array. + * @throws `'Must be greater than 0'` if `a` < 0 + * @example + * log(10) // returns 1 + * log(100) // returns 2 + * log(80) // returns 1.9030899869919433 + * log([10, 100, 1000, 10000, 100000]) // returns [1, 2, 3, 4, 5] + */ + +module.exports = { log10 }; + +function log10(a) { + return log(a, 10); +} diff --git a/packages/kbn-tinymath/src/functions/max.js b/packages/kbn-tinymath/src/functions/max.js new file mode 100644 index 0000000000000..13cebbfdf662a --- /dev/null +++ b/packages/kbn-tinymath/src/functions/max.js @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Finds the maximum value of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the maximum by index. + * @param {...(number|number[])} args one or more numbers or arrays of numbers + * @return {(number|number[])} The maximum value of all numbers if `args` contains only numbers. Returns an array with the the maximum values at each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. + * @throws `'Array length mismatch'` if `args` contains arrays of different lengths + * @example + * max(1, 2, 3) // returns 3 + * max([10, 20, 30, 40], 15) // returns [15, 20, 30, 40] + * max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9] + */ + +module.exports = { max }; + +function max(...args) { + if (args.length === 1) { + if (Array.isArray(args[0])) + return args[0].reduce((result, current) => Math.max(result, current)); + return args[0]; + } + + return args.reduce((result, current) => { + if (Array.isArray(result) && Array.isArray(current)) { + if (current.length !== result.length) throw new Error('Array length mismatch'); + return result.map((val, i) => Math.max(val, current[i])); + } + if (Array.isArray(result)) return result.map((val) => Math.max(val, current)); + if (Array.isArray(current)) return current.map((val) => Math.max(val, result)); + return Math.max(result, current); + }); +} diff --git a/packages/kbn-tinymath/src/functions/mean.js b/packages/kbn-tinymath/src/functions/mean.js new file mode 100644 index 0000000000000..ee37d77b10e71 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/mean.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { add } = require('./add.js'); + +/** + * Finds the mean value of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the mean by index. + * @param {...(number|number[])} args one or more numbers or arrays of numbers + * @return {(number|number[])} The mean value of all numbers if `args` contains only numbers. Returns an array with the the mean values of each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. + * + * @example + * mean(1, 2, 3) // returns 2 + * mean([10, 20, 30, 40], 20) // returns [15, 20, 25, 30] + * mean([1, 9], 5, [3, 4]) // returns [mean([1, 5, 3]), mean([9, 5, 4])] = [3, 6] + */ + +module.exports = { mean }; + +function mean(...args) { + if (args.length === 1) { + if (Array.isArray(args[0])) return add(args[0]) / args[0].length; + return args[0]; + } + const sum = add(...args); + + if (Array.isArray(sum)) { + return sum.map((val) => val / args.length); + } + + return sum / args.length; +} diff --git a/packages/kbn-tinymath/src/functions/median.js b/packages/kbn-tinymath/src/functions/median.js new file mode 100644 index 0000000000000..6f1e3cd4972e5 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/median.js @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { transpose } = require('./lib/transpose'); + +const findMedian = (a) => { + const len = a.length; + const half = Math.floor(len / 2); + + a.sort((a, b) => b - a); + + if (len % 2 === 0) { + return (a[half] + a[half - 1]) / 2; + } + + return a[half]; +}; + +/** + * Finds the median value(s) of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the median by index. + * @param {...(number|number[])} args one or more numbers or arrays of numbers + * @return {(number|number[])} The median value of all numbers if `args` contains only numbers. Returns an array with the the median values of each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. + * + * @example + * median(1, 1, 2, 3) // returns 1.5 + * median(1, 1, 2, 2, 3) // returns 2 + * median([10, 20, 30, 40], 10, 20, 30) // returns [15, 20, 25, 25] + * median([1, 9], 2, 4, [3, 5]) // returns [median([1, 2, 4, 3]), median([9, 2, 4, 5])] = [2.5, 4.5] + */ + +module.exports = { median }; + +function median(...args) { + if (args.length === 1) { + if (Array.isArray(args[0])) return findMedian(args[0]); + return args[0]; + } + + const firstArray = args.findIndex((element) => Array.isArray(element)); + if (firstArray !== -1) { + const result = transpose(args, firstArray); + return result.map((val) => findMedian(val)); + } + return findMedian(args); +} diff --git a/packages/kbn-tinymath/src/functions/min.js b/packages/kbn-tinymath/src/functions/min.js new file mode 100644 index 0000000000000..44509bedfd088 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/min.js @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Finds the minimum value of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the minimum by index. + * @param {...(number|number[])} args one or more numbers or arrays of numbers + * @return {(number|number[])} The minimum value of all numbers if `args` contains only numbers. Returns an array with the the minimum values of each index, including all scalar numbers in `args` in the calculation at each index if `a` is an array. + * @throws `'Array length mismatch'` if `args` contains arrays of different lengths + * @example + * min(1, 2, 3) // returns 1 + * min([10, 20, 30, 40], 25) // returns [10, 20, 25, 25] + * min([1, 9], 4, [3, 5]) // returns [min([1, 4, 3]), min([9, 4, 5])] = [1, 4] + */ + +module.exports = { min }; + +function min(...args) { + if (args.length === 1) { + if (Array.isArray(args[0])) + return args[0].reduce((result, current) => Math.min(result, current)); + return args[0]; + } + + return args.reduce((result, current) => { + if (Array.isArray(result) && Array.isArray(current)) { + if (current.length !== result.length) throw new Error('Array length mismatch'); + return result.map((val, i) => Math.min(val, current[i])); + } + if (Array.isArray(result)) return result.map((val) => Math.min(val, current)); + if (Array.isArray(current)) return current.map((val) => Math.min(val, result)); + return Math.min(result, current); + }); +} diff --git a/packages/kbn-tinymath/src/functions/mod.js b/packages/kbn-tinymath/src/functions/mod.js new file mode 100644 index 0000000000000..93c23077a9d69 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/mod.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Remainder after dividing two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. + * @param {(number|number[])} a dividend, a number or an array of numbers + * @param {(number|number[])} b divisor, a number or an array of numbers, `b` != 0 + * @return {(number|number[])} The remainder of `a` divided by `b` if both are numbers. Returns an array with the the remainders applied index-wise to each element if `a` or `b` is an array. + * @throws `'Array length mismatch'` if `a` and `b` are arrays with different lengths + * - `'Cannot divide by 0'` if `b` equals 0 or contains 0 + * @example + * mod(10, 7) // returns 3 + * mod([11, 22, 33, 44], 10) // returns [1, 2, 3, 4] + * mod(100, [3, 7, 11, 23]) // returns [1, 2, 1, 8] + * mod([14, 42, 65, 108], [5, 4, 14, 2]) // returns [5, 2, 9, 0] + */ + +module.exports = { mod }; + +function mod(a, b) { + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) throw new Error('Array length mismatch'); + return a.map((val, i) => { + if (b[i] === 0) throw new Error('Cannot divide by 0'); + return val % b[i]; + }); + } + if (Array.isArray(b)) return b.map((b) => a % b); + if (b === 0) throw new Error('Cannot divide by 0'); + if (Array.isArray(a)) return a.map((a) => a % b); + return a % b; +} diff --git a/packages/kbn-tinymath/src/functions/mode.js b/packages/kbn-tinymath/src/functions/mode.js new file mode 100644 index 0000000000000..4c7d8414602df --- /dev/null +++ b/packages/kbn-tinymath/src/functions/mode.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { transpose } = require('./lib/transpose'); + +const findMode = (a) => { + let maxFreq = 0; + const mapping = {}; + + a.map((val) => { + if (mapping[val] === undefined) { + mapping[val] = 0; + } + mapping[val] += 1; + if (mapping[val] > maxFreq) { + maxFreq = mapping[val]; + } + }); + + return Object.keys(mapping) + .filter((key) => mapping[key] === maxFreq) + .map((val) => parseFloat(val)) + .sort((a, b) => a - b); +}; + +/** + * Finds the mode value(s) of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the mode by index. + * @param {...(number|number[])} args one or more numbers or arrays of numbers + * @return {(number[]|number[][])} An array mode value(s) of all numbers if `args` contains only numbers. Returns an array of arrays with mode value(s) of each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. + * + * @example + * mode(1, 1, 2, 3) // returns [1] + * mode(1, 1, 2, 2, 3) // returns [1,2] + * mode([10, 20, 30, 40], 10, 20, 30) // returns [[10], [20], [30], [10, 20, 30, 40]] + * mode([1, 9], 1, 4, [3, 5]) // returns [mode([1, 1, 4, 3]), mode([9, 1, 4, 5])] = [[1], [4, 5, 9]] + */ + +module.exports = { mode }; + +function mode(...args) { + if (args.length === 1) { + if (Array.isArray(args[0])) return findMode(args[0]); + return args[0]; + } + + const firstArray = args.findIndex((element) => Array.isArray(element)); + if (firstArray !== -1) { + const result = transpose(args, firstArray); + return result.map((val) => findMode(val)); + } + return findMode(args); +} diff --git a/packages/kbn-tinymath/src/functions/multiply.js b/packages/kbn-tinymath/src/functions/multiply.js new file mode 100644 index 0000000000000..6334b510e550b --- /dev/null +++ b/packages/kbn-tinymath/src/functions/multiply.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Multiplies two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @param {(number|number[])} b a number or an array of numbers + * @return {(number|number[])} The product of `a` and `b` if both are numbers. Returns an array with the the products applied index-wise to each element if `a` or `b` is an array. + * @throws `'Array length mismatch'` if `a` and `b` are arrays with different lengths + * @example + * multiply(6, 3) // returns 18 + * multiply([10, 20, 30, 40], 10) // returns [100, 200, 300, 400] + * multiply(10, [1, 2, 5, 10]) // returns [10, 20, 50, 100] + * multiply([1, 2, 3, 4], [2, 7, 5, 12]) // returns [2, 14, 15, 48] + */ + +module.exports = { multiply }; + +function multiply(...args) { + return args.reduce((result, current) => { + if (Array.isArray(result) && Array.isArray(current)) { + if (current.length !== result.length) throw new Error('Array length mismatch'); + return result.map((val, i) => val * current[i]); + } + if (Array.isArray(result)) return result.map((val) => val * current); + if (Array.isArray(current)) return current.map((val) => val * result); + return result * current; + }); +} diff --git a/packages/kbn-tinymath/src/functions/pi.js b/packages/kbn-tinymath/src/functions/pi.js new file mode 100644 index 0000000000000..5dd625cf7f0d3 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/pi.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Returns the mathematical constant PI + * @return {(number)} The mathematical constant PI + * + * @example + * pi() // 3.141592653589793 + */ + +module.exports = { pi }; + +function pi() { + return Math.PI; +} diff --git a/packages/kbn-tinymath/src/functions/pow.js b/packages/kbn-tinymath/src/functions/pow.js new file mode 100644 index 0000000000000..b44b9679fc7f8 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/pow.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Raises a number to a given exponent. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @param {(number)} b the power that `a` is raised to + * @return {(number|number[])} `a` raised to the power of `b`. Returns an array with the each element raised to the power of `b` if `a` is an array. + * @throws `'Missing exponent'` if `b` is not provided + * @example + * pow(2,3) // returns 8 + * pow([1, 2, 3], 4) // returns [1, 16, 81] + */ + +module.exports = { pow }; + +function pow(a, b) { + if (b == null) throw new Error('Missing exponent'); + if (Array.isArray(a)) { + return a.map((a) => Math.pow(a, b)); + } + return Math.pow(a, b); +} diff --git a/packages/kbn-tinymath/src/functions/radtodeg.js b/packages/kbn-tinymath/src/functions/radtodeg.js new file mode 100644 index 0000000000000..51f911e2dcad0 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/radtodeg.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Converts radians to degrees for a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers, `a` is expected to be given in radians. + * @return {(number|number[])} The degrees of `a`. Returns an array with the the degrees of each element if `a` is an array. + * @example + * radtodeg(0) // returns 0 + * radtodeg(1.5707963267948966) // returns 90 + * radtodeg([0, 1.5707963267948966, 3.141592653589793, 6.283185307179586]) // returns [0, 90, 180, 360] + */ + +module.exports = { radtodeg }; + +function radtodeg(a) { + if (Array.isArray(a)) { + return a.map((a) => (a * 180) / Math.PI); + } + return (a * 180) / Math.PI; +} diff --git a/packages/kbn-tinymath/src/functions/random.js b/packages/kbn-tinymath/src/functions/random.js new file mode 100644 index 0000000000000..ffe5c3a9cb8e2 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/random.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Generates a random number within the given range where the lower bound is inclusive and the upper bound is exclusive. If no numbers are passed in, it will return a number between 0 and 1. If only one number is passed in, it will return . + * @param {number} a (optional) must be greater than 0 if `b` is not provided + * @param {number} b (optional) must be greater + * @return {number} A random number between 0 and 1 if no numbers are passed in. Returns a random number between 0 and `a` if only one number is passed in. Returns a random number between `a` and `b` if two numbers are passed in. + * @throws `'Min is be greater than max'` if `a` < 0 when only `a` is passed in or if `a` > `b` when both `a` and `b` are passed in + * @example + * random() // returns a random number between 0 (inclusive) and 1 (exclusive) + * random(10) // returns a random number between 0 (inclusive) and 10 (exclusive) + * random(-10,10) // returns a random number between -10 (inclusive) and 10 (exclusive) + */ + +module.exports = { random }; + +function random(a, b) { + if (a == null) return Math.random(); + + // a: max, generate random number between 0 and a + if (b == null) { + if (a < 0) throw new Error(`Min is greater than max`); + return Math.random() * a; + } + + // a: min, b: max, generate random number between a and b + if (a > b) throw new Error(`Min is greater than max`); + return Math.random() * (b - a) + a; +} diff --git a/packages/kbn-tinymath/src/functions/range.js b/packages/kbn-tinymath/src/functions/range.js new file mode 100644 index 0000000000000..31f9b618bb1db --- /dev/null +++ b/packages/kbn-tinymath/src/functions/range.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { max } = require('./max.js'); +const { min } = require('./min.js'); +const { subtract } = require('./subtract.js'); + +/** + * Finds the range of one of more numbers/arrays of numbers into the function. If at least one array of numbers is passed into the function, the function will find the range by index. + * @param {...(number|number[])} args one or more numbers or arrays of numbers + * @return {(number|number[])} The range value of all numbers if `args` contains only numbers. Returns an array with the the range values at each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. + * + * @example + * range(1, 2, 3) // returns 2 + * range([10, 20, 30, 40], 15) // returns [5, 5, 15, 25] + * range([1, 9], 4, [3, 5]) // returns [range([1, 4, 3]), range([9, 4, 5])] = [3, 5] + */ + +module.exports = { range }; + +function range(...args) { + return subtract(max(...args), min(...args)); +} diff --git a/packages/kbn-tinymath/src/functions/round.js b/packages/kbn-tinymath/src/functions/round.js new file mode 100644 index 0000000000000..1e8847e6dfd2b --- /dev/null +++ b/packages/kbn-tinymath/src/functions/round.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const rounder = (a, b) => Math.round(a * Math.pow(10, b)) / Math.pow(10, b); + +/** + * Rounds a number towards the nearest integer by default or decimal place if specified. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @param {(number)} b (optional) number of decimal places, default value: 0 + * @return {(number|number[])} The rounded value of `a`. Returns an array with the the rounded values of each element if `a` is an array. + * + * @example + * round(1.2) // returns 2 + * round(-10.51) // returns -11 + * round(-10.1, 2) // returns -10.1 + * round(10.93745987, 4) // returns 10.9375 + * round([2.9234, 5.1234, 3.5234, 4.49234324], 2) // returns [2.92, 5.12, 3.52, 4.49] + */ + +module.exports = { round }; + +function round(a, b = 0) { + if (Array.isArray(a)) { + return a.map((a) => rounder(a, b)); + } + return rounder(a, b); +} diff --git a/packages/kbn-tinymath/src/functions/sin.js b/packages/kbn-tinymath/src/functions/sin.js new file mode 100644 index 0000000000000..f08ffa8bdc197 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/sin.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the the sine of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers, `a` is expected to be given in radians. + * @return {(number|number[])} The sine of `a`. Returns an array with the the sine of each element if `a` is an array. + * @example + * sin(0) // returns 0 + * sin(1.5707963267948966) // returns 1 + * sin([0, 1.5707963267948966]) // returns [0, 1] + */ + +module.exports = { sin }; + +function sin(a) { + if (Array.isArray(a)) { + return a.map((a) => Math.sin(a)); + } + return Math.sin(a); +} diff --git a/packages/kbn-tinymath/src/functions/size.js b/packages/kbn-tinymath/src/functions/size.js new file mode 100644 index 0000000000000..5156a70b38d69 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/size.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Returns the length of an array. Alias for count + * @param {any[]} a array of any values + * @return {(number)} The length of the array. Returns 1 if `a` is not an array. + * @throws `'Must pass an array'` if `a` is not an array + * @example + * size([]) // returns 0 + * size([-1, -2, -3, -4]) // returns 4 + * size(100) // returns 1 + */ + +module.exports = { size }; + +function size(a) { + if (Array.isArray(a)) return a.length; + throw new Error('Must pass an array'); +} + +size.skipNumberValidation = true; diff --git a/packages/kbn-tinymath/src/functions/sqrt.js b/packages/kbn-tinymath/src/functions/sqrt.js new file mode 100644 index 0000000000000..2c55b2256e0f6 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/sqrt.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the square root of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} The square root of `a`. Returns an array with the the square roots of each element if `a` is an array. + * @throws `'Unable find the square root of a negative number'` if `a` < 0 + * @example + * sqrt(9) // returns 3 + * sqrt(30) //5.477225575051661 + * sqrt([9, 16, 25]) // returns [3, 4, 5] + */ + +module.exports = { sqrt }; + +function sqrt(a) { + if (Array.isArray(a)) { + return a.map((a) => { + if (a < 0) throw new Error('Unable find the square root of a negative number'); + return Math.sqrt(a); + }); + } + + if (a < 0) throw new Error('Unable find the square root of a negative number'); + return Math.sqrt(a); +} diff --git a/packages/kbn-tinymath/src/functions/square.js b/packages/kbn-tinymath/src/functions/square.js new file mode 100644 index 0000000000000..a5bccdef7661b --- /dev/null +++ b/packages/kbn-tinymath/src/functions/square.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { pow } = require('./pow.js'); + +/** + * Calculates the square of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @return {(number|number[])} The square of `a`. Returns an array with the the squares of each element if `a` is an array. + * + * @example + * square(-3) // returns 9 + * square([3, 4, 5]) // returns [9, 16, 25] + */ + +module.exports = { square }; + +function square(a) { + return pow(a, 2); +} diff --git a/packages/kbn-tinymath/src/functions/subtract.js b/packages/kbn-tinymath/src/functions/subtract.js new file mode 100644 index 0000000000000..8e5fd256bf158 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/subtract.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Subtracts two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers + * @param {(number|number[])} b a number or an array of numbers + * @return {(number|number[])} The difference of `a` and `b` if both are numbers or an array of differences applied index-wise to each element. + * @throws `'Array length mismatch'` if `a` and `b` are arrays with different lengths + * @example + * subtract(6, 3) // returns 3 + * subtract([10, 20, 30, 40], 10) // returns [0, 10, 20, 30] + * subtract(10, [1, 2, 5, 10]) // returns [9, 8, 5, 0] + * subtract([14, 42, 65, 108], [2, 7, 5, 12]) // returns [12, 35, 52, 96] + */ + +module.exports = { subtract }; + +function subtract(a, b) { + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) throw new Error('Array length mismatch'); + return a.map((val, i) => val - b[i]); + } + if (Array.isArray(a)) return a.map((a) => a - b); + if (Array.isArray(b)) return b.map((b) => a - b); + return a - b; +} diff --git a/packages/kbn-tinymath/src/functions/sum.js b/packages/kbn-tinymath/src/functions/sum.js new file mode 100644 index 0000000000000..b13a86d5c2122 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/sum.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const findSum = (total, current) => total + current; + +/** + * Calculates the sum of one or more numbers/arrays passed into the function. If at least one array is passed, the function will sum up one or more numbers/arrays of numbers and distinct values of an array. Sum accepts arrays of different lengths. + * @param {...(number|number[])} args one or more numbers or arrays of numbers + * @return {number} The sum of one or more numbers/arrays of numbers including distinct values in arrays + * + * @example + * sum(1, 2, 3) // returns 6 + * sum([10, 20, 30, 40], 10, 20, 30) // returns 160 + * sum([1, 2], 3, [4, 5], 6) // returns sum(1, 2, 3, 4, 5, 6) = 21 + * sum([10, 20, 30, 40], 10, [1, 2, 3], 22) // returns sum(10, 20, 30, 40, 10, 1, 2, 3, 22) = 138 + */ + +module.exports = { sum }; + +function sum(...args) { + return args.reduce((total, current) => { + if (Array.isArray(current)) { + return total + current.reduce(findSum, 0); + } + return total + current; + }, 0); +} diff --git a/packages/kbn-tinymath/src/functions/tan.js b/packages/kbn-tinymath/src/functions/tan.js new file mode 100644 index 0000000000000..56ea4c35f1459 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/tan.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Calculates the the tangent of a number. For arrays, the function will be applied index-wise to each element. + * @param {(number|number[])} a a number or an array of numbers, `a` is expected to be given in radians. + * @return {(number|number[])} The tangent of `a`. Returns an array with the the tangent of each element if `a` is an array. + * @example + * tan(0) // returns 0 + * tan(1) // returns 1.5574077246549023 + * tan([0, 1, -1]) // returns [0, 1.5574077246549023, -1.5574077246549023] + */ + +module.exports = { tan }; + +function tan(a) { + if (Array.isArray(a)) { + return a.map((a) => Math.tan(a)); + } + return Math.tan(a); +} diff --git a/packages/kbn-tinymath/src/functions/unique.js b/packages/kbn-tinymath/src/functions/unique.js new file mode 100644 index 0000000000000..60196e8568855 --- /dev/null +++ b/packages/kbn-tinymath/src/functions/unique.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * Counts the number of unique values in an array + * @param {any[]} a array of any values + * @return {number} The number of unique values in the array. Returns 1 if `a` is not an array. + * + * @example + * unique(100) // returns 1 + * unique([]) // returns 0 + * unique([1, 2, 3, 4]) // returns 4 + * unique([1, 2, 3, 4, 2, 2, 2, 3, 4, 2, 4, 5, 2, 1, 4, 2]) // returns 5 + */ + +module.exports = { unique }; + +function unique(a) { + if (Array.isArray(a)) { + return a.filter((val, i) => a.indexOf(val) === i).length; + } + return 1; +} + +unique.skipNumberValidation = true; diff --git a/packages/kbn-tinymath/src/grammar.js b/packages/kbn-tinymath/src/grammar.js new file mode 100644 index 0000000000000..60dfcf4800631 --- /dev/null +++ b/packages/kbn-tinymath/src/grammar.js @@ -0,0 +1,1385 @@ +/* + * Generated by PEG.js 0.10.0. + * + * http://pegjs.org/ + */ + +"use strict"; + +function peg$subclass(child, parent) { + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); +} + +function peg$SyntaxError(message, expected, found, location) { + this.message = message; + this.expected = expected; + this.found = found; + this.location = location; + this.name = "SyntaxError"; + + if (typeof Error.captureStackTrace === "function") { + Error.captureStackTrace(this, peg$SyntaxError); + } +} + +peg$subclass(peg$SyntaxError, Error); + +peg$SyntaxError.buildMessage = function(expected, found) { + var DESCRIBE_EXPECTATION_FNS = { + literal: function(expectation) { + return "\"" + literalEscape(expectation.text) + "\""; + }, + + "class": function(expectation) { + var escapedParts = "", + i; + + for (i = 0; i < expectation.parts.length; i++) { + escapedParts += expectation.parts[i] instanceof Array + ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1]) + : classEscape(expectation.parts[i]); + } + + return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]"; + }, + + any: function(expectation) { + return "any character"; + }, + + end: function(expectation) { + return "end of input"; + }, + + other: function(expectation) { + return expectation.description; + } + }; + + function hex(ch) { + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + + function literalEscape(s) { + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); + } + + function classEscape(s) { + return s + .replace(/\\/g, '\\\\') + .replace(/\]/g, '\\]') + .replace(/\^/g, '\\^') + .replace(/-/g, '\\-') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); + } + + function describeExpectation(expectation) { + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + + function describeExpected(expected) { + var descriptions = new Array(expected.length), + i, j; + + for (i = 0; i < expected.length; i++) { + descriptions[i] = describeExpectation(expected[i]); + } + + descriptions.sort(); + + if (descriptions.length > 0) { + for (i = 1, j = 1; i < descriptions.length; i++) { + if (descriptions[i - 1] !== descriptions[i]) { + descriptions[j] = descriptions[i]; + j++; + } + } + descriptions.length = j; + } + + switch (descriptions.length) { + case 1: + return descriptions[0]; + + case 2: + return descriptions[0] + " or " + descriptions[1]; + + default: + return descriptions.slice(0, -1).join(", ") + + ", or " + + descriptions[descriptions.length - 1]; + } + } + + function describeFound(found) { + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; +}; + +function peg$parse(input, options) { + options = options !== void 0 ? options : {}; + + var peg$FAILED = {}, + + peg$startRuleFunctions = { start: peg$parsestart }, + peg$startRuleFunction = peg$parsestart, + + peg$c0 = peg$otherExpectation("whitespace"), + peg$c1 = /^[ \t\n\r]/, + peg$c2 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false), + peg$c3 = /^[ ]/, + peg$c4 = peg$classExpectation([" "], false, false), + peg$c5 = /^["']/, + peg$c6 = peg$classExpectation(["\"", "'"], false, false), + peg$c7 = /^[A-Za-z_@.[\]\-]/, + peg$c8 = peg$classExpectation([["A", "Z"], ["a", "z"], "_", "@", ".", "[", "]", "-"], false, false), + peg$c9 = /^[0-9A-Za-z._@[\]\-]/, + peg$c10 = peg$classExpectation([["0", "9"], ["A", "Z"], ["a", "z"], ".", "_", "@", "[", "]", "-"], false, false), + peg$c11 = peg$otherExpectation("literal"), + peg$c12 = function(literal) { + return literal; + }, + peg$c13 = function(first, rest) { // We can open this up later. Strict for now. + return first + rest.join(''); + }, + peg$c14 = function(first, mid) { + return first + mid.map(m => m[0].join('') + m[1].join('')).join('') + }, + peg$c15 = "+", + peg$c16 = peg$literalExpectation("+", false), + peg$c17 = "-", + peg$c18 = peg$literalExpectation("-", false), + peg$c19 = function(left, rest) { + return rest.reduce((acc, curr) => ({ + name: curr[0] === '+' ? 'add' : 'subtract', + args: [acc, curr[1]] + }), left) + }, + peg$c20 = "*", + peg$c21 = peg$literalExpectation("*", false), + peg$c22 = "/", + peg$c23 = peg$literalExpectation("/", false), + peg$c24 = function(left, rest) { + return rest.reduce((acc, curr) => ({ + name: curr[0] === '*' ? 'multiply' : 'divide', + args: [acc, curr[1]] + }), left) + }, + peg$c25 = "(", + peg$c26 = peg$literalExpectation("(", false), + peg$c27 = ")", + peg$c28 = peg$literalExpectation(")", false), + peg$c29 = function(expr) { + return expr + }, + peg$c30 = peg$otherExpectation("arguments"), + peg$c31 = ",", + peg$c32 = peg$literalExpectation(",", false), + peg$c33 = function(first, arg) {return arg}, + peg$c34 = function(first, rest) { + return [first].concat(rest); + }, + peg$c35 = peg$otherExpectation("function"), + peg$c36 = /^[a-z]/, + peg$c37 = peg$classExpectation([["a", "z"]], false, false), + peg$c38 = function(name, args) { + return {name: name.join(''), args: args || []}; + }, + peg$c39 = peg$otherExpectation("number"), + peg$c40 = function() { return parseFloat(text()); }, + peg$c41 = /^[eE]/, + peg$c42 = peg$classExpectation(["e", "E"], false, false), + peg$c43 = peg$otherExpectation("exponent"), + peg$c44 = ".", + peg$c45 = peg$literalExpectation(".", false), + peg$c46 = "0", + peg$c47 = peg$literalExpectation("0", false), + peg$c48 = /^[1-9]/, + peg$c49 = peg$classExpectation([["1", "9"]], false, false), + peg$c50 = /^[0-9]/, + peg$c51 = peg$classExpectation([["0", "9"]], false, false), + + peg$currPos = 0, + peg$savedPos = 0, + peg$posDetailsCache = [{ line: 1, column: 1 }], + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + + peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$savedPos, peg$currPos); + } + + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); + } + + function expected(description, location) { + location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) + + throw peg$buildStructuredError( + [peg$otherExpectation(description)], + input.substring(peg$savedPos, peg$currPos), + location + ); + } + + function error(message, location) { + location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) + + throw peg$buildSimpleError(message, location); + } + + function peg$literalExpectation(text, ignoreCase) { + return { type: "literal", text: text, ignoreCase: ignoreCase }; + } + + function peg$classExpectation(parts, inverted, ignoreCase) { + return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + + function peg$anyExpectation() { + return { type: "any" }; + } + + function peg$endExpectation() { + return { type: "end" }; + } + + function peg$otherExpectation(description) { + return { type: "other", description: description }; + } + + function peg$computePosDetails(pos) { + var details = peg$posDetailsCache[pos], p; + + if (details) { + return details; + } else { + p = pos - 1; + while (!peg$posDetailsCache[p]) { + p--; + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column + }; + + while (p < pos) { + if (input.charCodeAt(p) === 10) { + details.line++; + details.column = 1; + } else { + details.column++; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + return details; + } + } + + function peg$computeLocation(startPos, endPos) { + var startPosDetails = peg$computePosDetails(startPos), + endPosDetails = peg$computePosDetails(endPos); + + return { + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column + } + }; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildSimpleError(message, location) { + return new peg$SyntaxError(message, null, null, location); + } + + function peg$buildStructuredError(expected, found, location) { + return new peg$SyntaxError( + peg$SyntaxError.buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parsestart() { + var s0; + + s0 = peg$parseAddSubtract(); + + return s0; + } + + function peg$parse_() { + var s0, s1; + + peg$silentFails++; + s0 = []; + if (peg$c1.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + while (s1 !== peg$FAILED) { + s0.push(s1); + if (peg$c1.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + + return s0; + } + + function peg$parseSpace() { + var s0; + + if (peg$c3.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + + return s0; + } + + function peg$parseQuote() { + var s0; + + if (peg$c5.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + + return s0; + } + + function peg$parseStartChar() { + var s0; + + if (peg$c7.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c8); } + } + + return s0; + } + + function peg$parseValidChar() { + var s0; + + if (peg$c9.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c10); } + } + + return s0; + } + + function peg$parseLiteral() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = peg$parseNumber(); + if (s2 === peg$FAILED) { + s2 = peg$parseVariableWithQuote(); + if (s2 === peg$FAILED) { + s2 = peg$parseVariable(); + } + } + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c12(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c11); } + } + + return s0; + } + + function peg$parseVariable() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = peg$parseStartChar(); + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$parseValidChar(); + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseValidChar(); + } + if (s3 !== peg$FAILED) { + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c13(s2, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseVariableWithQuote() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = peg$parseQuote(); + if (s2 !== peg$FAILED) { + s3 = peg$parseStartChar(); + if (s3 !== peg$FAILED) { + s4 = []; + s5 = peg$currPos; + s6 = []; + s7 = peg$parseSpace(); + while (s7 !== peg$FAILED) { + s6.push(s7); + s7 = peg$parseSpace(); + } + if (s6 !== peg$FAILED) { + s7 = []; + s8 = peg$parseValidChar(); + if (s8 !== peg$FAILED) { + while (s8 !== peg$FAILED) { + s7.push(s8); + s8 = peg$parseValidChar(); + } + } else { + s7 = peg$FAILED; + } + if (s7 !== peg$FAILED) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$currPos; + s6 = []; + s7 = peg$parseSpace(); + while (s7 !== peg$FAILED) { + s6.push(s7); + s7 = peg$parseSpace(); + } + if (s6 !== peg$FAILED) { + s7 = []; + s8 = peg$parseValidChar(); + if (s8 !== peg$FAILED) { + while (s8 !== peg$FAILED) { + s7.push(s8); + s8 = peg$parseValidChar(); + } + } else { + s7 = peg$FAILED; + } + if (s7 !== peg$FAILED) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } + if (s4 !== peg$FAILED) { + s5 = peg$parseQuote(); + if (s5 !== peg$FAILED) { + s6 = peg$parse_(); + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c14(s3, s4); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseAddSubtract() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = peg$parseMultiplyDivide(); + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 43) { + s5 = peg$c15; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s5 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 45) { + s5 = peg$c17; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c18); } + } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseMultiplyDivide(); + if (s6 !== peg$FAILED) { + s5 = [s5, s6]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 43) { + s5 = peg$c15; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s5 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 45) { + s5 = peg$c17; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c18); } + } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseMultiplyDivide(); + if (s6 !== peg$FAILED) { + s5 = [s5, s6]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c19(s2, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseMultiplyDivide() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = peg$parseFactor(); + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 42) { + s5 = peg$c20; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + if (s5 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 47) { + s5 = peg$c22; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c23); } + } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseFactor(); + if (s6 !== peg$FAILED) { + s5 = [s5, s6]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 42) { + s5 = peg$c20; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + if (s5 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 47) { + s5 = peg$c22; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c23); } + } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseFactor(); + if (s6 !== peg$FAILED) { + s5 = [s5, s6]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c24(s2, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseFactor() { + var s0; + + s0 = peg$parseGroup(); + if (s0 === peg$FAILED) { + s0 = peg$parseFunction(); + if (s0 === peg$FAILED) { + s0 = peg$parseLiteral(); + } + } + + return s0; + } + + function peg$parseGroup() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 40) { + s2 = peg$c25; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c26); } + } + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); + if (s3 !== peg$FAILED) { + s4 = peg$parseAddSubtract(); + if (s4 !== peg$FAILED) { + s5 = peg$parse_(); + if (s5 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s6 = peg$c27; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c28); } + } + if (s6 !== peg$FAILED) { + s7 = peg$parse_(); + if (s7 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c29(s4); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseArguments() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = peg$parseAddSubtract(); + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$currPos; + s5 = peg$parse_(); + if (s5 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s6 = peg$c31; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s6 !== peg$FAILED) { + s7 = peg$parse_(); + if (s7 !== peg$FAILED) { + s8 = peg$parseAddSubtract(); + if (s8 !== peg$FAILED) { + peg$savedPos = s4; + s5 = peg$c33(s2, s8); + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$currPos; + s5 = peg$parse_(); + if (s5 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s6 = peg$c31; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s6 !== peg$FAILED) { + s7 = peg$parse_(); + if (s7 !== peg$FAILED) { + s8 = peg$parseAddSubtract(); + if (s8 !== peg$FAILED) { + peg$savedPos = s4; + s5 = peg$c33(s2, s8); + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c31; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s5 === peg$FAILED) { + s5 = null; + } + if (s5 !== peg$FAILED) { + s6 = peg$parse_(); + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c34(s2, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + + return s0; + } + + function peg$parseFunction() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = []; + if (peg$c36.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c37); } + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + if (peg$c36.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c37); } + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 40) { + s3 = peg$c25; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c26); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + s5 = peg$parseArguments(); + if (s5 === peg$FAILED) { + s5 = null; + } + if (s5 !== peg$FAILED) { + s6 = peg$parse_(); + if (s6 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s7 = peg$c27; + peg$currPos++; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c28); } + } + if (s7 !== peg$FAILED) { + s8 = peg$parse_(); + if (s8 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c38(s2, s5); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c35); } + } + + return s0; + } + + function peg$parseNumber() { + var s0, s1, s2, s3, s4; + + peg$silentFails++; + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 45) { + s1 = peg$c17; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c18); } + } + if (s1 === peg$FAILED) { + s1 = null; + } + if (s1 !== peg$FAILED) { + s2 = peg$parseInteger(); + if (s2 !== peg$FAILED) { + s3 = peg$parseFraction(); + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + s4 = peg$parseExp(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c40(); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c39); } + } + + return s0; + } + + function peg$parseE() { + var s0; + + if (peg$c41.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c42); } + } + + return s0; + } + + function peg$parseExp() { + var s0, s1, s2, s3, s4; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parseE(); + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 45) { + s2 = peg$c17; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c18); } + } + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$parseDigit(); + if (s4 !== peg$FAILED) { + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseDigit(); + } + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + + return s0; + } + + function peg$parseFraction() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s1 = peg$c44; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c45); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseDigit(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseDigit(); + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseInteger() { + var s0, s1, s2, s3; + + if (input.charCodeAt(peg$currPos) === 48) { + s0 = peg$c46; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c47); } + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (peg$c48.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c49); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseDigit(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseDigit(); + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parseDigit() { + var s0; + + if (peg$c50.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c51); } + } + + return s0; + } + + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail(peg$endExpectation()); + } + + throw peg$buildStructuredError( + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } +} + +module.exports = { + SyntaxError: peg$SyntaxError, + parse: peg$parse +}; diff --git a/packages/kbn-tinymath/src/grammar.pegjs b/packages/kbn-tinymath/src/grammar.pegjs new file mode 100644 index 0000000000000..cab8e024e60b3 --- /dev/null +++ b/packages/kbn-tinymath/src/grammar.pegjs @@ -0,0 +1,100 @@ +// tinymath parsing grammar + +start + = Expression + +// characters + +_ "whitespace" + = [ \t\n\r]* + +Space + = [ ] + +Quote + = [\"\'] + +StartChar + = [A-Za-z_@.\[\]-] + +ValidChar + = [0-9A-Za-z._@\[\]-] + +// literals and variables + +Literal "literal" + = _ literal:(Number / VariableWithQuote / Variable) _ { + return literal; + } + +Variable + = _ first:StartChar rest:ValidChar* _ { // We can open this up later. Strict for now. + return first + rest.join(''); + } + +VariableWithQuote + = _ Quote first:StartChar mid:(Space* ValidChar+)* Quote _ { + return first + mid.map(m => m[0].join('') + m[1].join('')).join('') + } + +// expressions + +Expression + = AddSubtract + +AddSubtract + = _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)* _ { + return rest.reduce((acc, curr) => ({ + name: curr[0] === '+' ? 'add' : 'subtract', + args: [acc, curr[1]] + }), left) + } + +MultiplyDivide + = _ left:Factor rest:(('*' / '/') Factor)* _ { + return rest.reduce((acc, curr) => ({ + name: curr[0] === '*' ? 'multiply' : 'divide', + args: [acc, curr[1]] + }), left) + } + +Factor + = Group + / Function + / Literal + +Group + = _ '(' _ expr:Expression _ ')' _ { + return expr + } + +Arguments "arguments" + = _ first:Expression rest:(_ ',' _ arg:Expression {return arg})* _ ','? _ { + return [first].concat(rest); + } + +Function "function" + = _ name:[a-z]+ '(' _ args:Arguments? _ ')' _ { + return {name: name.join(''), args: args || []}; + } + +// Numbers. Lol. + +Number "number" + = '-'? Integer Fraction? Exp? { return parseFloat(text()); } + +E + = [eE] + +Exp "exponent" + = E '-'? Digit+ + +Fraction + = '.' Digit+ + +Integer + = '0' + / ([1-9] Digit*) + +Digit + = [0-9] diff --git a/packages/kbn-tinymath/src/index.js b/packages/kbn-tinymath/src/index.js new file mode 100644 index 0000000000000..e61956bd63e55 --- /dev/null +++ b/packages/kbn-tinymath/src/index.js @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { get } = require('lodash'); +const { parse: parseFn } = require('./grammar'); +const { functions: includedFunctions } = require('./functions'); + +module.exports = { parse, evaluate, interpret }; + +function parse(input, options) { + if (input == null) { + throw new Error('Missing expression'); + } + + if (typeof input !== 'string') { + throw new Error('Expression must be a string'); + } + + try { + return parseFn(input, options); + } catch (e) { + throw new Error(`Failed to parse expression. ${e.message}`); + } +} + +function evaluate(expression, scope = {}, injectedFunctions = {}) { + scope = scope || {}; + return interpret(parse(expression), scope, injectedFunctions); +} + +function interpret(node, scope, injectedFunctions) { + const functions = Object.assign({}, includedFunctions, injectedFunctions); // eslint-disable-line + return exec(node); + + function exec(node) { + const type = getType(node); + + if (type === 'function') return invoke(node); + + if (type === 'string') { + const val = getValue(scope, node); + if (typeof val === 'undefined') throw new Error(`Unknown variable: ${node}`); + return val; + } + + return node; // Can only be a number at this point + } + + function invoke(node) { + const { name, args } = node; + const fn = functions[name]; + if (!fn) throw new Error(`No such function: ${name}`); + const execOutput = args.map(exec); + if (fn.skipNumberValidation || isOperable(execOutput)) return fn(...execOutput); + return NaN; + } +} + +function getValue(scope, node) { + // attempt to read value from nested object first, check for exact match if value is undefined + const val = get(scope, node); + return typeof val !== 'undefined' ? val : scope[node]; +} + +function getType(x) { + const type = typeof x; + if (type === 'object') { + const keys = Object.keys(x); + if (keys.length !== 2 || !x.name || !x.args) throw new Error('Invalid AST object'); + return 'function'; + } + if (type === 'string' || type === 'number') return type; + throw new Error(`Unknown AST property type: ${type}`); +} + +function isOperable(args) { + return args.every((arg) => { + if (Array.isArray(arg)) return isOperable(arg); + return typeof arg === 'number' && !isNaN(arg); + }); +} diff --git a/packages/kbn-tinymath/test/functions/abs.test.js b/packages/kbn-tinymath/test/functions/abs.test.js new file mode 100644 index 0000000000000..09ae042d23de6 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/abs.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { abs } = require('../../src/functions/abs.js'); + +describe('Abs', () => { + it('numbers', () => { + expect(abs(-10)).toEqual(10); + expect(abs(10)).toEqual(10); + }); + + it('arrays', () => { + expect(abs([-1])).toEqual([1]); + expect(abs([-10, -20, -30, -40])).toEqual([10, 20, 30, 40]); + expect(abs([-13, 30, -90, 200])).toEqual([13, 30, 90, 200]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/add.test.js b/packages/kbn-tinymath/test/functions/add.test.js new file mode 100644 index 0000000000000..56b4fc48a62ad --- /dev/null +++ b/packages/kbn-tinymath/test/functions/add.test.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { add } = require('../../src/functions/add.js'); + +describe('Add', () => { + it('numbers', () => { + expect(add(1)).toEqual(1); + expect(add(10, 2, 5, 8)).toEqual(25); + expect(add(0.1, 0.2, 0.4, 0.3)).toEqual(0.1 + 0.2 + 0.3 + 0.4); + }); + + it('arrays & numbers', () => { + expect(add([10, 20, 30, 40], 10, 20, 30)).toEqual([70, 80, 90, 100]); + expect(add(10, [10, 20, 30, 40], [1, 2, 3, 4], 22)).toEqual([43, 54, 65, 76]); + }); + + it('arrays', () => { + expect(add([1, 2, 3, 4])).toEqual(10); + expect(add([1, 2, 3, 4], [1, 2, 5, 10])).toEqual([2, 4, 8, 14]); + expect(add([1, 2, 3, 4], [1, 2, 5, 10], [10, 20, 30, 40])).toEqual([12, 24, 38, 54]); + expect(add([11, 48, 60, 72], [1, 2, 3, 4])).toEqual([12, 50, 63, 76]); + }); + + it('array length mismatch', () => { + expect(() => add([1, 2], [3])).toThrow('Array length mismatch'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/cbrt.test.js b/packages/kbn-tinymath/test/functions/cbrt.test.js new file mode 100644 index 0000000000000..8b8b57c5a1ba1 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/cbrt.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { cbrt } = require('../../src/functions/cbrt.js'); + +describe('Cbrt', () => { + it('numbers', () => { + expect(cbrt(27)).toEqual(3); + expect(cbrt(-1)).toEqual(-1); + expect(cbrt(94)).toEqual(4.546835943776344); + }); + + it('arrays', () => { + expect(cbrt([27, 64, 125])).toEqual([3, 4, 5]); + expect(cbrt([1, 8, 1000])).toEqual([1, 2, 10]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/ceil.test.js b/packages/kbn-tinymath/test/functions/ceil.test.js new file mode 100644 index 0000000000000..0809c9ba1e9d5 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/ceil.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { ceil } = require('../../src/functions/ceil.js'); + +describe('Ceil', () => { + it('numbers', () => { + expect(ceil(-10.5)).toEqual(-10); + expect(ceil(-10.1)).toEqual(-10); + expect(ceil(10.9)).toEqual(11); + }); + + it('arrays', () => { + expect(ceil([-10.5, -20.9, -30.1, -40.2])).toEqual([-10, -20, -30, -40]); + expect(ceil([2.9, 5.1, 3.5, 4.3])).toEqual([3, 6, 4, 5]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/clamp.test.js b/packages/kbn-tinymath/test/functions/clamp.test.js new file mode 100644 index 0000000000000..7e6015bf304cf --- /dev/null +++ b/packages/kbn-tinymath/test/functions/clamp.test.js @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { clamp } = require('../../src/functions/clamp.js'); + +describe('Clamp', () => { + it('numbers', () => { + expect(clamp(10, 5, 8)).toEqual(8); + expect(clamp(1, 2, 3)).toEqual(2); + expect(clamp(0.5, 0.2, 0.4)).toEqual(0.4); + expect(clamp(3.58, 0, 1)).toEqual(1); + expect(clamp(-0.48, 0, 1)).toEqual(0); + expect(clamp(1.38, -1, 0)).toEqual(0); + }); + + it('arrays & numbers', () => { + expect(clamp([10, 20, 30, 40], 15, 25)).toEqual([15, 20, 25, 25]); + expect(clamp(10, [15, 2, 4, 20], 25)).toEqual([15, 10, 10, 20]); + expect(clamp(5, 10, [20, 30, 40, 50])).toEqual([10, 10, 10, 10]); + expect(clamp(35, 10, [20, 30, 40, 50])).toEqual([20, 30, 35, 35]); + expect(clamp([1, 9], 3, [4, 5])).toEqual([3, 5]); + }); + + it('arrays', () => { + expect(clamp([6, 28, 32, 10], [11, 2, 5, 10], [20, 21, 22, 23])).toEqual([11, 21, 22, 10]); + }); + + it('errors', () => { + expect(() => clamp(1, 4, 3)).toThrow('Min must be less than max'); + expect(() => clamp([1, 2], [3], 3)).toThrow('Array length mismatch'); + expect(() => clamp([1, 2], [3], 3)).toThrow('Array length mismatch'); + expect(() => clamp(10, 20, null)).toThrow( + "Missing maximum value. You may want to use the 'min' function instead" + ); + expect(() => clamp([10, 20, 30, 40], 15, null)).toThrow( + "Missing maximum value. You may want to use the 'min' function instead" + ); + expect(() => clamp(10, null, 30)).toThrow( + "Missing minimum value. You may want to use the 'max' function instead" + ); + expect(() => clamp([11, 28, 60, 10], null, [1, 48, 3, -17])).toThrow( + "Missing minimum value. You may want to use the 'max' function instead" + ); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/cos.test.js b/packages/kbn-tinymath/test/functions/cos.test.js new file mode 100644 index 0000000000000..9e4461512fe06 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/cos.test.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { cos } = require('../../src/functions/cos.js'); + +describe('Cosine', () => { + it('numbers', () => { + expect(cos(0)).toEqual(1); + expect(cos(1.5707963267948966)).toEqual(6.123233995736766e-17); + }); + + it('arrays', () => { + expect(cos([0, 1.5707963267948966])).toEqual([1, 6.123233995736766e-17]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/cube.test.js b/packages/kbn-tinymath/test/functions/cube.test.js new file mode 100644 index 0000000000000..f91cbd3c58059 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/cube.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { cube } = require('../../src/functions/cube.js'); + +describe('Cube', () => { + it('numbers', () => { + expect(cube(3)).toEqual(27); + expect(cube(-1)).toEqual(-1); + }); + + it('arrays', () => { + expect(cube([3, 4, 5])).toEqual([27, 64, 125]); + expect(cube([1, 2, 10])).toEqual([1, 8, 1000]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/degtorad.test.js b/packages/kbn-tinymath/test/functions/degtorad.test.js new file mode 100644 index 0000000000000..8ce78851e7844 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/degtorad.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { degtorad } = require('../../src/functions/degtorad.js'); + +describe('Degrees to Radians', () => { + it('numbers', () => { + expect(degtorad(0)).toEqual(0); + expect(degtorad(90)).toEqual(1.5707963267948966); + }); + + it('arrays', () => { + expect(degtorad([0, 90, 180, 360])).toEqual([ + 0, + 1.5707963267948966, + 3.141592653589793, + 6.283185307179586, + ]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/divide.test.js b/packages/kbn-tinymath/test/functions/divide.test.js new file mode 100644 index 0000000000000..f3eea83c3fb80 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/divide.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { divide } = require('../../src/functions/divide.js'); + +describe('Divide', () => { + it('number, number', () => { + expect(divide(10, 2)).toEqual(5); + expect(divide(0.1, 0.02)).toEqual(0.1 / 0.02); + }); + + it('array, number', () => { + expect(divide([10, 20, 30, 40], 10)).toEqual([1, 2, 3, 4]); + }); + + it('number, array', () => { + expect(divide(10, [1, 2, 5, 10])).toEqual([10, 5, 2, 1]); + }); + + it('array, array', () => { + expect(divide([11, 48, 60, 72], [1, 2, 3, 4])).toEqual([11, 24, 20, 18]); + }); + + it('array length mismatch', () => { + expect(() => divide([1, 2], [3])).toThrow('Array length mismatch'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/exp.test.js b/packages/kbn-tinymath/test/functions/exp.test.js new file mode 100644 index 0000000000000..0bb25d772ae2c --- /dev/null +++ b/packages/kbn-tinymath/test/functions/exp.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { exp } = require('../../src/functions/exp.js'); + +describe('Exp', () => { + it('numbers', () => { + expect(exp(3)).toEqual(Math.exp(3)); + expect(exp(0)).toEqual(Math.exp(0)); + expect(exp(5)).toEqual(Math.exp(5)); + }); + + it('arrays', () => { + expect(exp([3, 4, 5])).toEqual([Math.exp(3), Math.exp(4), Math.exp(5)]); + expect(exp([1, 2, 10])).toEqual([Math.exp(1), Math.exp(2), Math.exp(10)]); + expect(exp([10])).toEqual([Math.exp(10)]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/first.test.js b/packages/kbn-tinymath/test/functions/first.test.js new file mode 100644 index 0000000000000..c977f68117724 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/first.test.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { first } = require('../../src/functions/first.js'); + +describe('First', () => { + it('numbers', () => { + expect(first(-10)).toEqual(-10); + expect(first(10)).toEqual(10); + }); + + it('arrays', () => { + expect(first([])).toEqual(undefined); + expect(first([-1])).toEqual(-1); + expect(first([-10, -20, -30, -40])).toEqual(-10); + expect(first([-13, 30, -90, 200])).toEqual(-13); + }); + + it('skips number validation', () => { + expect(first).toHaveProperty('skipNumberValidation', true); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/fix.test.js b/packages/kbn-tinymath/test/functions/fix.test.js new file mode 100644 index 0000000000000..59a71352ac680 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/fix.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { fix } = require('../../src/functions/fix.js'); + +describe('Fix', () => { + it('numbers', () => { + expect(fix(-10.5)).toEqual(-10); + expect(fix(-10.1)).toEqual(-10); + expect(fix(10.9)).toEqual(10); + }); + + it('arrays', () => { + expect(fix([-10.5, -20.9, -30.1, -40.2])).toEqual([-10, -20, -30, -40]); + expect(fix([2.9, 5.1, 3.5, 4.3])).toEqual([2, 5, 3, 4]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/floor.test.js b/packages/kbn-tinymath/test/functions/floor.test.js new file mode 100644 index 0000000000000..19f80e9bb7b06 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/floor.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { floor } = require('../../src/functions/floor.js'); + +describe('Floor', () => { + it('numbers', () => { + expect(floor(-10.5)).toEqual(-11); + expect(floor(-10.1)).toEqual(-11); + expect(floor(10.9)).toEqual(10); + }); + + it('arrays', () => { + expect(floor([-10.5, -20.9, -30.1, -40.2])).toEqual([-11, -21, -31, -41]); + expect(floor([2.9, 5.1, 3.5, 4.3])).toEqual([2, 5, 3, 4]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/last.test.js b/packages/kbn-tinymath/test/functions/last.test.js new file mode 100644 index 0000000000000..a333541b147ea --- /dev/null +++ b/packages/kbn-tinymath/test/functions/last.test.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { last } = require('../../src/functions/last.js'); + +describe('Last', () => { + it('numbers', () => { + expect(last(-10)).toEqual(-10); + expect(last(10)).toEqual(10); + }); + + it('arrays', () => { + expect(last([])).toEqual(undefined); + expect(last([-1])).toEqual(-1); + expect(last([-10, -20, -30, -40])).toEqual(-40); + expect(last([-13, 30, -90, 200])).toEqual(200); + }); + + it('skips number validation', () => { + expect(last).toHaveProperty('skipNumberValidation', true); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/log.test.js b/packages/kbn-tinymath/test/functions/log.test.js new file mode 100644 index 0000000000000..de142b997039b --- /dev/null +++ b/packages/kbn-tinymath/test/functions/log.test.js @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { log } = require('../../src/functions/log.js'); + +describe('Log', () => { + it('numbers', () => { + expect(log(1)).toEqual(Math.log(1)); + expect(log(3, 2)).toEqual(Math.log(3) / Math.log(2)); + expect(log(11, 3)).toEqual(Math.log(11) / Math.log(3)); + expect(log(42, 5)).toEqual(2.322344707681546); + }); + + it('arrays', () => { + expect(log([3, 4, 5], 3)).toEqual([ + Math.log(3) / Math.log(3), + Math.log(4) / Math.log(3), + Math.log(5) / Math.log(3), + ]); + expect(log([1, 2, 10], 10)).toEqual([ + Math.log(1) / Math.log(10), + Math.log(2) / Math.log(10), + Math.log(10) / Math.log(10), + ]); + }); + + it('number less than 1', () => { + expect(() => log(-1)).toThrow('Must be greater than 0'); + }); + + it('base out of range', () => { + expect(() => log(1, -1)).toThrow('Base out of range'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/log10.test.js b/packages/kbn-tinymath/test/functions/log10.test.js new file mode 100644 index 0000000000000..e0edfaa8388f0 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/log10.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { log10 } = require('../../src/functions/log10.js'); + +describe('Log10', () => { + it('numbers', () => { + expect(log10(1)).toEqual(Math.log(1) / Math.log(10)); + expect(log10(3)).toEqual(Math.log(3) / Math.log(10)); + expect(log10(11)).toEqual(Math.log(11) / Math.log(10)); + expect(log10(80)).toEqual(1.9030899869919433); + }); + + it('arrays', () => { + expect(log10([3, 4, 5], 3)).toEqual([ + Math.log(3) / Math.log(10), + Math.log(4) / Math.log(10), + Math.log(5) / Math.log(10), + ]); + expect(log10([1, 2, 10], 10)).toEqual([ + Math.log(1) / Math.log(10), + Math.log(2) / Math.log(10), + Math.log(10) / Math.log(10), + ]); + }); + + it('number less than 1', () => { + expect(() => log10(-1)).toThrow('Must be greater than 0'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/max.test.js b/packages/kbn-tinymath/test/functions/max.test.js new file mode 100644 index 0000000000000..ab4de7b958e68 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/max.test.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { max } = require('../../src/functions/max.js'); + +describe('Max', () => { + it('numbers', () => { + expect(max(1)).toEqual(1); + expect(max(10, 2, 5, 8)).toEqual(10); + expect(max(0.1, 0.2, 0.4, 0.3)).toEqual(0.4); + }); + + it('arrays & numbers', () => { + expect(max([88, 20, 30, 40], 60, [30, 10, 70, 90])).toEqual([88, 60, 70, 90]); + expect(max(10, [10, 20, 30, 40], [1, 2, 3, 4], 22)).toEqual([22, 22, 30, 40]); + }); + + it('arrays', () => { + expect(max([1, 2, 3, 4])).toEqual(4); + expect(max([6, 2, 3, 10], [11, 2, 5, 10])).toEqual([11, 2, 5, 10]); + expect(max([30, 55, 9, 4], [72, 24, 48, 10], [10, 20, 30, 40])).toEqual([72, 55, 48, 40]); + expect(max([11, 28, 60, 10], [1, 48, 3, -17])).toEqual([11, 48, 60, 10]); + }); + + it('array length mismatch', () => { + expect(() => max([1, 2], [3])).toThrow('Array length mismatch'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/mean.test.js b/packages/kbn-tinymath/test/functions/mean.test.js new file mode 100644 index 0000000000000..6fb1c1fa18b98 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/mean.test.js @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { mean } = require('../../src/functions/mean.js'); + +describe('Mean', () => { + it('numbers', () => { + expect(mean(1)).toEqual(1); + expect(mean(10, 2, 5, 8)).toEqual(25 / 4); + expect(mean(0.1, 0.2, 0.4, 0.3)).toEqual((0.1 + 0.2 + 0.3 + 0.4) / 4); + }); + + it('arrays & numbers', () => { + expect(mean([10, 20, 30, 40], 10, 20, 30)).toEqual([70 / 4, 80 / 4, 90 / 4, 100 / 4]); + expect(mean(10, [10, 20, 30, 40], [1, 2, 3, 4], 22)).toEqual([43 / 4, 54 / 4, 65 / 4, 76 / 4]); + }); + + it('arrays', () => { + expect(mean([1, 2, 3, 4])).toEqual(10 / 4); + expect(mean([1, 2, 3, 4], [1, 2, 5, 10])).toEqual([2 / 2, 4 / 2, 8 / 2, 14 / 2]); + expect(mean([1, 2, 3, 4], [1, 2, 5, 10], [10, 20, 30, 40])).toEqual([ + 12 / 3, + 24 / 3, + 38 / 3, + 54 / 3, + ]); + expect(mean([11, 48, 60, 72], [1, 2, 3, 4])).toEqual([12 / 2, 50 / 2, 63 / 2, 76 / 2]); + }); + + it('array length mismatch', () => { + expect(() => mean([1, 2], [3])).toThrow(); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/median.test.js b/packages/kbn-tinymath/test/functions/median.test.js new file mode 100644 index 0000000000000..e7dd56b4c6fc4 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/median.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { median } = require('../../src/functions/median.js'); + +describe('Median', () => { + it('numbers', () => { + expect(median(1)).toEqual(1); + expect(median(10, 2, 5, 8)).toEqual((8 + 5) / 2); + expect(median(0.1, 0.2, 0.4, 0.3)).toEqual((0.2 + 0.3) / 2); + }); + + it('arrays & numbers', () => { + expect(median([10, 20, 30, 40], 10, 20, 30)).toEqual([15, 20, 25, 25]); + expect(median(10, [10, 20, 30, 40], [1, 2, 3, 4], 22)).toEqual([10, 15, 16, 16]); + }); + + it('arrays', () => { + expect(median([1, 2, 3, 4], [1, 2, 5, 10])).toEqual([1, 2, 4, 7]); + expect(median([1, 2, 3, 4], [1, 2, 5, 10], [10, 20, 30, 40])).toEqual([1, 2, 5, 10]); + expect(median([11, 48, 60, 72], [1, 2, 3, 4])).toEqual([12 / 2, 50 / 2, 63 / 2, 76 / 2]); + }); + + it('array length mismatch', () => { + expect(() => median([1, 2], [3])).toThrow(); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/min.test.js b/packages/kbn-tinymath/test/functions/min.test.js new file mode 100644 index 0000000000000..9612ce4274d11 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/min.test.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { min } = require('../../src/functions/min.js'); + +describe('Min', () => { + it('numbers', () => { + expect(min(1)).toEqual(1); + expect(min(10, 2, 5, 8)).toEqual(2); + expect(min(0.1, 0.2, 0.4, 0.3)).toEqual(0.1); + }); + + it('arrays & numbers', () => { + expect(min([88, 20, 30, 100], 60, [30, 10, 70, 90])).toEqual([30, 10, 30, 60]); + expect(min([50, 20, 3, 40], 10, [13, 2, 34, 4], 22)).toEqual([10, 2, 3, 4]); + expect(min(10, [50, 20, 3, 40], [13, 2, 34, 4], 22)).toEqual([10, 2, 3, 4]); + }); + + it('arrays', () => { + expect(min([1, 2, 3, 4])).toEqual(1); + expect(min([6, 2, 30, 10], [11, 2, 5, 15])).toEqual([6, 2, 5, 10]); + expect(min([30, 55, 9, 4], [72, 24, 48, 10], [10, 20, 30, 40])).toEqual([10, 20, 9, 4]); + expect(min([11, 28, 60, 10], [1, 48, 3, -17])).toEqual([1, 28, 3, -17]); + }); + + it('array length mismatch', () => { + expect(() => min([1, 2], [3])).toThrow('Array length mismatch'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/mod.test.js b/packages/kbn-tinymath/test/functions/mod.test.js new file mode 100644 index 0000000000000..ba3fc35b7e70c --- /dev/null +++ b/packages/kbn-tinymath/test/functions/mod.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { mod } = require('../../src/functions/mod.js'); + +describe('Mod', () => { + it('number, number', () => { + expect(mod(13, 8)).toEqual(5); + expect(mod(0.1, 0.02)).toEqual(0.1 % 0.02); + }); + + it('array, number', () => { + expect(mod([13, 26, 34, 42], 10)).toEqual([3, 6, 4, 2]); + }); + + it('number, array', () => { + expect(mod(10, [3, 7, 2, 4])).toEqual([1, 3, 0, 2]); + }); + + it('array, array', () => { + expect(mod([11, 48, 60, 72], [4, 13, 9, 5])).toEqual([3, 9, 6, 2]); + }); + + it('array length mismatch', () => { + expect(() => mod([1, 2], [3])).toThrow('Array length mismatch'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/mode.test.js b/packages/kbn-tinymath/test/functions/mode.test.js new file mode 100644 index 0000000000000..6f33140d41ef0 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/mode.test.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { mode } = require('../../src/functions/mode.js'); + +describe('Mode', () => { + it('numbers', () => { + expect(mode(1)).toEqual(1); + expect(mode(10, 2, 5, 8)).toEqual([2, 5, 8, 10]); + expect(mode(0.1, 0.2, 0.4, 0.3)).toEqual([0.1, 0.2, 0.3, 0.4]); + expect(mode(1, 1, 2, 3, 1, 4, 3, 2, 4)).toEqual([1]); + }); + + it('arrays & numbers', () => { + expect(mode([10, 20, 30, 40], 10, 20, 30)).toEqual([[10], [20], [30], [10, 20, 30, 40]]); + expect(mode([1, 2, 3, 4], 2, 3, [3, 2, 4, 3])).toEqual([[3], [2], [3], [3]]); + }); + + it('arrays', () => { + expect(mode([1, 2, 3, 4], [1, 2, 5, 10])).toEqual([[1], [2], [3, 5], [4, 10]]); + expect(mode([1, 2, 3, 4], [1, 2, 1, 2], [2, 3, 2, 3], [4, 3, 2, 3])).toEqual([ + [1], + [2, 3], + [2], + [3], + ]); + }); + + it('array length mismatch', () => { + expect(() => mode([1, 2], [3])).toThrow(); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/multiply.test.js b/packages/kbn-tinymath/test/functions/multiply.test.js new file mode 100644 index 0000000000000..f3a35d1f45695 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/multiply.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { multiply } = require('../../src/functions/multiply.js'); + +describe('Multiply', () => { + it('number, number', () => { + expect(multiply(10, 2)).toEqual(20); + expect(multiply(0.1, 0.2)).toEqual(0.1 * 0.2); + }); + + it('array, number', () => { + expect(multiply([10, 20, 30, 40], 10)).toEqual([100, 200, 300, 400]); + }); + + it('number, array', () => { + expect(multiply(10, [1, 2, 5, 10])).toEqual([10, 20, 50, 100]); + }); + + it('array, array', () => { + expect(multiply([11, 48, 60, 72], [1, 2, 3, 4])).toEqual([11, 96, 180, 288]); + }); + + it('array length mismatch', () => { + expect(() => multiply([1, 2], [3])).toThrow('Array length mismatch'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/pi.test.js b/packages/kbn-tinymath/test/functions/pi.test.js new file mode 100644 index 0000000000000..7f1cdd019401d --- /dev/null +++ b/packages/kbn-tinymath/test/functions/pi.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { pi } = require('../../src/functions/pi.js'); + +describe('PI', () => { + it('constant', () => { + expect(pi()).toEqual(Math.PI); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/pow.test.js b/packages/kbn-tinymath/test/functions/pow.test.js new file mode 100644 index 0000000000000..05193aa2177a6 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/pow.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { pow } = require('../../src/functions/pow.js'); + +describe('Pow', () => { + it('numbers', () => { + expect(pow(3, 2)).toEqual(9); + expect(pow(-1, -1)).toEqual(-1); + expect(pow(5, 0)).toEqual(1); + }); + + it('arrays', () => { + expect(pow([3, 4, 5], 3)).toEqual([Math.pow(3, 3), Math.pow(4, 3), Math.pow(5, 3)]); + expect(pow([1, 2, 10], 10)).toEqual([Math.pow(1, 10), Math.pow(2, 10), Math.pow(10, 10)]); + }); + + it('missing exponent', () => { + expect(() => pow(1)).toThrow('Missing exponent'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/radtodeg.test.js b/packages/kbn-tinymath/test/functions/radtodeg.test.js new file mode 100644 index 0000000000000..0b97d3d2695be --- /dev/null +++ b/packages/kbn-tinymath/test/functions/radtodeg.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { radtodeg } = require('../../src/functions/radtodeg.js'); + +describe('Radians to Degrees', () => { + it('numbers', () => { + expect(radtodeg(0)).toEqual(0); + expect(radtodeg(1.5707963267948966)).toEqual(90); + }); + + it('arrays', () => { + expect(radtodeg([0, 1.5707963267948966, 3.141592653589793, 6.283185307179586])).toEqual([ + 0, + 90, + 180, + 360, + ]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/random.test.js b/packages/kbn-tinymath/test/functions/random.test.js new file mode 100644 index 0000000000000..2b259f2b84771 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/random.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { random } = require('../../src/functions/random.js'); + +describe('Random', () => { + it('numbers', () => { + const random1 = random(); + expect(random1).toBeGreaterThanOrEqual(0); + expect(random1).toBeLessThan(1); + expect(random(0)).toEqual(0); + const random3 = random(3); + expect(random3).toBeGreaterThanOrEqual(0); + expect(random3).toBeLessThan(3); + const random100 = random(-100, 100); + expect(random100).toBeGreaterThanOrEqual(-100); + expect(random100).toBeLessThan(100); + expect(random(1, 1)).toEqual(1); + expect(random(100, 100)).toEqual(100); + }); + + it('min greater than max', () => { + expect(() => random(-1)).toThrow('Min is greater than max'); + expect(() => random(3, 1)).toThrow('Min is greater than max'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/range.test.js b/packages/kbn-tinymath/test/functions/range.test.js new file mode 100644 index 0000000000000..920986d5e1368 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/range.test.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { range } = require('../../src/functions/range.js'); + +describe('Range', () => { + it('numbers', () => { + expect(range(1)).toEqual(0); + expect(range(10, 2, 5, 8)).toEqual(8); + expect(range(0.1, 0.2, 0.4, 0.3)).toEqual(0.4 - 0.1); + }); + + it('arrays & numbers', () => { + expect(range([88, 20, 30, 40], 60, [30, 10, 70, 90])).toEqual([58, 50, 40, 50]); + expect(range(10, [10, 20, 30, 40], [1, 2, 3, 4], 22)).toEqual([21, 20, 27, 36]); + }); + + it('arrays', () => { + expect(range([1, 2, 3, 4])).toEqual(3); + expect(range([6, 2, 3, 10], [11, 2, 5, 10])).toEqual([5, 0, 2, 0]); + expect(range([30, 55, 9, 4], [72, 24, 48, 10], [10, 20, 30, 40])).toEqual([62, 35, 39, 36]); + expect(range([11, 28, 60, 10], [1, 48, 3, -17])).toEqual([10, 20, 57, 27]); + }); + + it('array length mismatch', () => { + expect(() => range([1, 2], [3])).toThrow(); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/round.test.js b/packages/kbn-tinymath/test/functions/round.test.js new file mode 100644 index 0000000000000..bea0a9a8377d7 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/round.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { round } = require('../../src/functions/round.js'); + +describe('Round', () => { + it('numbers', () => { + expect(round(-10.51)).toEqual(-11); + expect(round(-10.1, 2)).toEqual(-10.1); + expect(round(10.93745987, 4)).toEqual(10.9375); + }); + + it('arrays', () => { + expect(round([-10.51, -20.9, -30.1, -40.2])).toEqual([-11, -21, -30, -40]); + expect(round([2.9234, 5.1234, 3.5234, 4.49234324], 2)).toEqual([2.92, 5.12, 3.52, 4.49]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/sin.test.js b/packages/kbn-tinymath/test/functions/sin.test.js new file mode 100644 index 0000000000000..35a37abe35a68 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/sin.test.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { sin } = require('../../src/functions/sin.js'); + +describe('Sine', () => { + it('numbers', () => { + expect(sin(0)).toEqual(0); + expect(sin(1.5707963267948966)).toEqual(1); + }); + + it('arrays', () => { + expect(sin([0, 1.5707963267948966])).toEqual([0, 1]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/size.test.js b/packages/kbn-tinymath/test/functions/size.test.js new file mode 100644 index 0000000000000..b4db587f30230 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/size.test.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { size } = require('../../src/functions/size.js'); + +describe('Size (also Count)', () => { + it('array', () => { + expect(size([])).toEqual(0); + expect(size([10, 20, 30, 40])).toEqual(4); + }); + + it('not an array', () => { + expect(() => size(null)).toThrow('Must pass an array'); + expect(() => size(undefined)).toThrow('Must pass an array'); + expect(() => size('string')).toThrow('Must pass an array'); + expect(() => size(10)).toThrow('Must pass an array'); + expect(() => size(true)).toThrow('Must pass an array'); + expect(() => size({})).toThrow('Must pass an array'); + expect(() => size(function () {})).toThrow('Must pass an array'); + }); + + it('skips number validation', () => { + expect(size).toHaveProperty('skipNumberValidation', true); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/sqrt.test.js b/packages/kbn-tinymath/test/functions/sqrt.test.js new file mode 100644 index 0000000000000..a170140598d06 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/sqrt.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { sqrt } = require('../../src/functions/sqrt.js'); + +describe('Sqrt', () => { + it('numbers', () => { + expect(sqrt(9)).toEqual(3); + expect(sqrt(0)).toEqual(0); + expect(sqrt(30)).toEqual(5.477225575051661); + }); + + it('arrays', () => { + expect(sqrt([49, 64, 81])).toEqual([7, 8, 9]); + expect(sqrt([1, 4, 100])).toEqual([1, 2, 10]); + }); + + it('Invalid negative number', () => { + expect(() => sqrt(-1)).toThrow('Unable find the square root of a negative number'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/square.test.js b/packages/kbn-tinymath/test/functions/square.test.js new file mode 100644 index 0000000000000..3b91a5f79c8d3 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/square.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { square } = require('../../src/functions/square.js'); + +describe('Square', () => { + it('numbers', () => { + expect(square(3)).toEqual(9); + expect(square(-1)).toEqual(1); + }); + + it('arrays', () => { + expect(square([3, 4, 5])).toEqual([9, 16, 25]); + expect(square([1, 2, 10])).toEqual([1, 4, 100]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/subtract.test.js b/packages/kbn-tinymath/test/functions/subtract.test.js new file mode 100644 index 0000000000000..9cdc1fb85a562 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/subtract.test.js @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { subtract } = require('../../src/functions/subtract.js'); + +describe('Subtract', () => { + it('number, number', () => { + expect(subtract(10, 2)).toEqual(8); + expect(subtract(0.1, 0.2)).toEqual(0.1 - 0.2); + }); + + it('array, number', () => { + expect(subtract([10, 20, 30, 40], 10)).toEqual([0, 10, 20, 30]); + }); + + it('number, array', () => { + expect(subtract(10, [1, 2, 5, 10])).toEqual([9, 8, 5, 0]); + }); + + it('array, array', () => { + expect(subtract([11, 48, 60, 72], [1, 2, 3, 4])).toEqual([10, 46, 57, 68]); + }); + + it('array length mismatch', () => { + expect(() => subtract([1, 2], [3])).toThrow('Array length mismatch'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/sum.test.js b/packages/kbn-tinymath/test/functions/sum.test.js new file mode 100644 index 0000000000000..a7d8c3d135253 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/sum.test.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { sum } = require('../../src/functions/sum.js'); + +describe('Sum', () => { + it('numbers', () => { + expect(sum(10, 2, 5, 8)).toEqual(25); + expect(sum(0.1, 0.2, 0.4, 0.3)).toEqual(0.1 + 0.2 + 0.3 + 0.4); + }); + + it('arrays & numbers', () => { + expect(sum([10, 20, 30, 40], 10, 20, 30)).toEqual(160); + expect(sum([10, 20, 30, 40], 10, [1, 2, 3], 22)).toEqual(138); + }); + + it('arrays', () => { + expect(sum([1, 2, 3, 4], [1, 2, 5, 10])).toEqual(28); + expect(sum([1, 2, 3, 4], [1, 2, 5, 10], [10, 20, 30, 40])).toEqual(128); + expect(sum([11, 48, 60, 72], [1, 2, 3, 4])).toEqual(201); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/tan.test.js b/packages/kbn-tinymath/test/functions/tan.test.js new file mode 100644 index 0000000000000..ba6960c0c1d8a --- /dev/null +++ b/packages/kbn-tinymath/test/functions/tan.test.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { tan } = require('../../src/functions/tan.js'); + +describe('Tangent', () => { + it('numbers', () => { + expect(tan(0)).toEqual(0); + expect(tan(1)).toEqual(1.5574077246549023); + }); + + it('arrays', () => { + expect(tan([0, 1])).toEqual([0, 1.5574077246549023]); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/transpose.test.js b/packages/kbn-tinymath/test/functions/transpose.test.js new file mode 100644 index 0000000000000..eb0b8d0c7a0e2 --- /dev/null +++ b/packages/kbn-tinymath/test/functions/transpose.test.js @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { transpose } = require('../../src/functions/lib/transpose'); + +describe('transpose', () => { + it('2D arrays', () => { + expect( + transpose( + [ + [1, 2], + [3, 4], + [5, 6], + ], + 0 + ) + ).toEqual([ + [1, 3, 5], + [2, 4, 6], + ]); + expect(transpose([10, 20, [10, 20, 30, 40], 30], 2)).toEqual([ + [10, 20, 10, 30], + [10, 20, 20, 30], + [10, 20, 30, 30], + [10, 20, 40, 30], + ]); + expect(transpose([4, [1, 9], [3, 5]], 1)).toEqual([ + [4, 1, 3], + [4, 9, 5], + ]); + }); + + it('array length mismatch', () => { + expect(() => transpose([[1], [2, 3]], 0)).toThrow('Array length mismatch'); + }); +}); diff --git a/packages/kbn-tinymath/test/functions/unique.test.js b/packages/kbn-tinymath/test/functions/unique.test.js new file mode 100644 index 0000000000000..d58c190876e2c --- /dev/null +++ b/packages/kbn-tinymath/test/functions/unique.test.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const { unique } = require('../../src/functions/unique.js'); + +describe('Unique', () => { + it('numbers', () => { + expect(unique(1)).toEqual(1); + expect(unique(10000)).toEqual(1); + }); + + it('arrays', () => { + expect(unique([])).toEqual(0); + expect(unique([-10, -20, -30, -40])).toEqual(4); + expect(unique([-13, 30, -90, 200])).toEqual(4); + expect(unique([1, 2, 3, 4, 2, 2, 2, 3, 4, 2, 4, 5, 2, 1, 4, 2])).toEqual(5); + }); + + it('skips number validation', () => { + expect(unique).toHaveProperty('skipNumberValidation', true); + }); +}); diff --git a/packages/kbn-tinymath/test/library.test.js b/packages/kbn-tinymath/test/library.test.js new file mode 100644 index 0000000000000..7569cf90b2e35 --- /dev/null +++ b/packages/kbn-tinymath/test/library.test.js @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/* + TODO: These tests are wildly imcomplete + Need tests for spacing, etc +*/ + +const { evaluate, parse } = require('..'); + +describe('Parser', () => { + describe('Numbers', () => { + it('integers', () => { + expect(parse('10')).toEqual(10); + }); + + it('floats', () => { + expect(parse('10.5')).toEqual(10.5); + }); + + it('negatives', () => { + expect(parse('-10')).toEqual(-10); + expect(parse('-10.5')).toEqual(-10.5); + }); + }); + + describe('Variables', () => { + it('strings', () => { + expect(parse('f')).toEqual('f'); + expect(parse('foo')).toEqual('foo'); + }); + + it('allowed characters', () => { + expect(parse('_foo')).toEqual('_foo'); + expect(parse('@foo')).toEqual('@foo'); + expect(parse('.foo')).toEqual('.foo'); + expect(parse('-foo')).toEqual('-foo'); + expect(parse('_foo0')).toEqual('_foo0'); + expect(parse('@foo0')).toEqual('@foo0'); + expect(parse('.foo0')).toEqual('.foo0'); + expect(parse('-foo0')).toEqual('-foo0'); + }); + }); + + describe('quoted variables', () => { + it('strings with double quotes', () => { + expect(parse('"foo"')).toEqual('foo'); + expect(parse('"f b"')).toEqual('f b'); + expect(parse('"foo bar"')).toEqual('foo bar'); + expect(parse('"foo bar fizz buzz"')).toEqual('foo bar fizz buzz'); + expect(parse('"foo bar baby"')).toEqual('foo bar baby'); + }); + + it('strings with single quotes', () => { + /* eslint-disable prettier/prettier */ + expect(parse("'foo'")).toEqual('foo'); + expect(parse("'f b'")).toEqual('f b'); + expect(parse("'foo bar'")).toEqual('foo bar'); + expect(parse("'foo bar fizz buzz'")).toEqual('foo bar fizz buzz'); + expect(parse("'foo bar baby'")).toEqual('foo bar baby'); + /* eslint-enable prettier/prettier */ + }); + + it('allowed characters', () => { + expect(parse('"_foo bar"')).toEqual('_foo bar'); + expect(parse('"@foo bar"')).toEqual('@foo bar'); + expect(parse('".foo bar"')).toEqual('.foo bar'); + expect(parse('"-foo bar"')).toEqual('-foo bar'); + expect(parse('"_foo0 bar1"')).toEqual('_foo0 bar1'); + expect(parse('"@foo0 bar1"')).toEqual('@foo0 bar1'); + expect(parse('".foo0 bar1"')).toEqual('.foo0 bar1'); + expect(parse('"-foo0 bar1"')).toEqual('-foo0 bar1'); + }); + + it('invalid characters in double quotes', () => { + const check = (str) => () => parse(str); + expect(check('" foo bar"')).toThrow('but "\\"" found'); + expect(check('"foo bar "')).toThrow('but "\\"" found'); + expect(check('"0foo"')).toThrow('but "\\"" found'); + expect(check('" foo bar"')).toThrow('but "\\"" found'); + expect(check('"foo bar "')).toThrow('but "\\"" found'); + expect(check('"0foo"')).toThrow('but "\\"" found'); + }); + + it('invalid characters in single quotes', () => { + const check = (str) => () => parse(str); + /* eslint-disable prettier/prettier */ + expect(check("' foo bar'")).toThrow('but "\'" found'); + expect(check("'foo bar '")).toThrow('but "\'" found'); + expect(check("'0foo'")).toThrow('but "\'" found'); + expect(check("' foo bar'")).toThrow('but "\'" found'); + expect(check("'foo bar '")).toThrow('but "\'" found'); + expect(check("'0foo'")).toThrow('but "\'" found'); + /* eslint-enable prettier/prettier */ + }); + }); + + describe('Functions', () => { + it('no arguments', () => { + expect(parse('foo()')).toEqual({ name: 'foo', args: [] }); + }); + + it('arguments', () => { + expect(parse('foo(5,10)')).toEqual({ name: 'foo', args: [5, 10] }); + }); + + it('arguments with strings', () => { + expect(parse('foo("string with spaces")')).toEqual({ + name: 'foo', + args: ['string with spaces'], + }); + + /* eslint-disable prettier/prettier */ + expect(parse("foo('string with spaces')")).toEqual({ + name: 'foo', + args: ['string with spaces'], + }); + /* eslint-enable prettier/prettier */ + }); + }); + + it('Missing expression', () => { + expect(() => parse(undefined)).toThrow('Missing expression'); + expect(() => parse(null)).toThrow('Missing expression'); + }); + + it('Failed parse', () => { + expect(() => parse('')).toThrow('Failed to parse expression'); + }); + + it('Not a string', () => { + expect(() => parse(3)).toThrow('Expression must be a string'); + }); +}); + +describe('Evaluate', () => { + it('numbers', () => { + expect(evaluate('10')).toEqual(10); + }); + + it('variables', () => { + expect(evaluate('foo', { foo: 10 })).toEqual(10); + expect(evaluate('bar', { bar: [1, 2] })).toEqual([1, 2]); + }); + + it('variables with spaces', () => { + expect(evaluate('"foo bar"', { 'foo bar': 10 })).toEqual(10); + expect(evaluate('"key with many spaces in it"', { 'key with many spaces in it': 10 })).toEqual( + 10 + ); + }); + + it('valiables with dots', () => { + expect(evaluate('foo.bar', { 'foo.bar': 20 })).toEqual(20); + expect(evaluate('"is.null"', { 'is.null': null })).toEqual(null); + expect(evaluate('"is.false"', { 'is.null': null, 'is.false': false })).toEqual(false); + expect(evaluate('"with space.val"', { 'with space.val': 42 })).toEqual(42); + }); + + it('variables with dot notation', () => { + expect(evaluate('foo.bar', { foo: { bar: 20 } })).toEqual(20); + expect(evaluate('foo.bar[0].baz', { foo: { bar: [{ baz: 30 }, { beer: 40 }] } })).toEqual(30); + expect(evaluate('"is.false"', { is: { null: null, false: false } })).toEqual(false); + }); + + it('equations', () => { + expect(evaluate('3 + 4')).toEqual(7); + expect(evaluate('10 - 2')).toEqual(8); + expect(evaluate('8 + 6 / 3')).toEqual(10); + expect(evaluate('10 * (1 + 2)')).toEqual(30); + expect(evaluate('(3 - 4) * 10')).toEqual(-10); + expect(evaluate('-1 - -12')).toEqual(11); + expect(evaluate('5/20')).toEqual(0.25); + expect(evaluate('1 + 1 + 2 + 3 + 12')).toEqual(19); + expect(evaluate('100 / 10 / 10')).toEqual(1); + }); + + it('equations with functions', () => { + expect(evaluate('3 + multiply(10, 4)')).toEqual(43); + expect(evaluate('3 + multiply(10, 4, 5)')).toEqual(203); + }); + + it('equations with trigonometry', () => { + expect(evaluate('pi()')).toEqual(Math.PI); + expect(evaluate('sin(degtorad(0))')).toEqual(0); + expect(evaluate('sin(degtorad(180))')).toEqual(1.2246467991473532e-16); + expect(evaluate('cos(degtorad(0))')).toEqual(1); + expect(evaluate('cos(degtorad(180))')).toEqual(-1); + expect(evaluate('tan(degtorad(0))')).toEqual(0); + expect(evaluate('tan(degtorad(180))')).toEqual(-1.2246467991473532e-16); + }); + + it('equations with variables', () => { + expect(evaluate('3 + foo', { foo: 5 })).toEqual(8); + expect(evaluate('3 + foo', { foo: [5, 10] })).toEqual([8, 13]); + expect(evaluate('3 + foo', { foo: 5 })).toEqual(8); + expect(evaluate('sum(foo)', { foo: [5, 10, 15] })).toEqual(30); + expect(evaluate('90 / sum(foo)', { foo: [5, 10, 15] })).toEqual(3); + expect(evaluate('multiply(foo, bar)', { foo: [1, 2, 3], bar: [4, 5, 6] })).toEqual([4, 10, 18]); + }); + + it('equations with quoted variables', () => { + expect(evaluate('"b" * 7', { b: 3 })).toEqual(21); + expect(evaluate('"space name" * 2', { 'space name': [1, 2, 21] })).toEqual([2, 4, 42]); + expect(evaluate('sum("space name")', { 'space name': [1, 2, 21] })).toEqual(24); + }); + + it('equations with injected functions', () => { + expect( + evaluate( + 'plustwo(foo)', + { foo: 5 }, + { + plustwo: function (a) { + return a + 2; + }, + } + ) + ).toEqual(7); + expect( + evaluate('negate(1)', null, { + negate: function (a) { + return -a; + }, + }) + ).toEqual(-1); + expect( + evaluate('stringify(2)', null, { + stringify: function (a) { + return '' + a; + }, + }) + ).toEqual('2'); + }); + + it('equations with arrays using special operator functions', () => { + expect(evaluate('foo + bar', { foo: [1, 2, 3], bar: [4, 5, 6] })).toEqual([5, 7, 9]); + expect(evaluate('foo - bar', { foo: [1, 2, 3], bar: [4, 5, 6] })).toEqual([-3, -3, -3]); + expect(evaluate('foo * bar', { foo: [1, 2, 3], bar: [4, 5, 6] })).toEqual([4, 10, 18]); + expect(evaluate('foo / bar', { foo: [1, 2, 3], bar: [4, 5, 6] })).toEqual([ + 1 / 4, + 2 / 5, + 3 / 6, + ]); + }); + + it('missing expression', () => { + expect(() => evaluate('')).toThrow('Failed to parse expression'); + }); + + it('missing referenced scope when used in injected function', () => { + expect(() => + evaluate('increment(foo)', null, { + increment: function (a) { + return a + 1; + }, + }) + ).toThrow('Unknown variable: foo'); + }); + + it('invalid context datatypes', () => { + expect(evaluate('mean(foo)', { foo: [true, true, false] })).toBeNaN(); + expect(evaluate('mean(foo + bar)', { foo: [true, true, false], bar: [1, 2, 3] })).toBeNaN(); + expect(evaluate('mean(foo)', { foo: ['dog', 'cat', 'mouse'] })).toBeNaN(); + expect(evaluate('mean(foo + 2)', { foo: ['dog', 'cat', 'mouse'] })).toBeNaN(); + expect(evaluate('foo + bar', { foo: NaN, bar: [4, 5, 6] })).toBeNaN(); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js index 1dab41566da67..f66c011d1f928 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js @@ -114,7 +114,7 @@ export function MathAgg(props) { values={{ link: ( async (results) => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts index e36644530eae8..0f200a92a41f1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts @@ -5,7 +5,7 @@ */ // @ts-expect-error no @typed def; Elastic library -import { evaluate } from 'tinymath'; +import { evaluate } from '@kbn/tinymath'; import { pivotObjectArray } from '../../../common/lib/pivot_object_array'; import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts index 7dee587895485..a04f39c66bc26 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts @@ -5,7 +5,7 @@ */ // @ts-expect-error untyped library -import { parse } from 'tinymath'; +import { parse } from '@kbn/tinymath'; import { getFieldNames } from './pointseries/lib/get_field_names'; describe('getFieldNames', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts index f79f189f363d4..b528eb63ef2b6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts @@ -5,7 +5,7 @@ */ // @ts-expect-error Untyped Elastic library -import { evaluate } from 'tinymath'; +import { evaluate } from '@kbn/tinymath'; import { groupBy, zipObject, omit, uniqBy } from 'lodash'; import moment from 'moment'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js index 1cbfafe8103ed..ed1f1d5e6c706 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'tinymath'; +import { parse } from '@kbn/tinymath'; import { getFieldType } from '../../../../../common/lib/get_field_type'; import { isColumnReference } from './is_column_reference'; import { getFieldNames } from './get_field_names'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts index aed9861e1250c..54e1adbeddd78 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts @@ -5,7 +5,7 @@ */ // @ts-expect-error untyped library -import { parse } from 'tinymath'; +import { parse } from '@kbn/tinymath'; export function isColumnReference(mathExpression: string | null): boolean { if (mathExpression == null) { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js index 7f5fe8b2cce12..fbde9f7f63f41 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'tinymath'; +import { parse } from '@kbn/tinymath'; import { unquoteString } from '../../../../common/lib/unquote_string'; // break out into separate function, write unit tests first diff --git a/x-pack/plugins/canvas/common/lib/handlebars.js b/x-pack/plugins/canvas/common/lib/handlebars.js index 0b7ef38fe8f6d..ae5063e173525 100644 --- a/x-pack/plugins/canvas/common/lib/handlebars.js +++ b/x-pack/plugins/canvas/common/lib/handlebars.js @@ -5,7 +5,7 @@ */ import Hbars from 'handlebars/dist/handlebars'; -import { evaluate } from 'tinymath'; +import { evaluate } from '@kbn/tinymath'; import { pivotObjectArray } from './pivot_object_array'; // example use: {{math rows 'mean(price - cost)' 2}} diff --git a/yarn.lock b/yarn.lock index fcafc23e42d73..befb729569945 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3517,6 +3517,10 @@ version "0.0.0" uid "" +"@kbn/tinymath@link:packages/kbn-tinymath": + version "0.0.0" + uid "" + "@kbn/ui-framework@link:packages/kbn-ui-framework": version "0.0.0" uid "" @@ -28056,11 +28060,6 @@ tinygradient@0.4.3: "@types/tinycolor2" "^1.4.0" tinycolor2 "^1.0.0" -tinymath@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/tinymath/-/tinymath-1.2.1.tgz#f97ed66c588cdbf3c19dfba2ae266ee323db7e47" - integrity sha512-8CYutfuHR3ywAJus/3JUhaJogZap1mrUQGzNxdBiQDhP3H0uFdQenvaXvqI8lMehX4RsanRZzxVfjMBREFdQaA== - tinyqueue@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.3.tgz#b6a61de23060584da29f82362e45df1ec7353f3d" From 62a4266ab615570d913e09111141f7a0a75df9e4 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 28 Jan 2021 13:13:56 -0600 Subject: [PATCH 093/163] skip "Should pass the query language to the language switcher". #89603 --- .../public/ui/query_string_input/query_string_input.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index 9784ab7116cfb..eca9d90a7500e 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -102,7 +102,7 @@ describe('QueryStringInput', () => { await waitFor(() => getByText('KQL')); }); - it('Should pass the query language to the language switcher', () => { + it.skip('Should pass the query language to the language switcher', () => { const component = mount( wrapQueryStringInputInContext({ query: luceneQuery, From 4de729f3c3ac6fe88a8aeda69709c63fdbe12b29 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 28 Jan 2021 11:19:59 -0800 Subject: [PATCH 094/163] [Event Log] Added KQL queries support for Event Log API. (#89394) * [Event Log] Added KQL queries support for Event Log API. * refactored to use core.elasticsearch.client * Fixed tests * removed get index pattern for event log * Fixed tests * Fixed due to comments. --- .../server/es/cluster_client_adapter.test.ts | 174 ++++++++++-------- .../server/es/cluster_client_adapter.ts | 102 +++++----- .../event_log/server/es/context.test.ts | 31 ++-- x-pack/plugins/event_log/server/es/context.ts | 8 +- x-pack/plugins/event_log/server/es/index.ts | 2 +- .../event_log/server/event_log_client.ts | 7 +- .../event_log/server/event_log_service.ts | 4 +- .../server/event_log_start_service.ts | 4 +- x-pack/plugins/event_log/server/plugin.ts | 8 +- .../common/lib/get_event_log.ts | 5 +- .../tests/actions/execute.ts | 1 + .../spaces_only/tests/alerting/event_log.ts | 17 ++ 12 files changed, 199 insertions(+), 164 deletions(-) diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 545b3b1517145..32f08e685c75d 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyClusterClient } from 'src/core/server'; +import { ElasticsearchClient } from 'src/core/server'; import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { ClusterClientAdapter, @@ -15,20 +15,21 @@ import { contextMock } from './context.mock'; import { findOptionsSchema } from '../event_log_client'; import { delay } from '../lib/delay'; import { times } from 'lodash'; +import { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import { RequestEvent } from '@elastic/elasticsearch'; -type EsClusterClient = Pick, 'callAsInternalUser' | 'asScoped'>; type MockedLogger = ReturnType; let logger: MockedLogger; -let clusterClient: EsClusterClient; +let clusterClient: DeeplyMockedKeys; let clusterClientAdapter: IClusterClientAdapter; beforeEach(() => { logger = loggingSystemMock.createLogger(); - clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; clusterClientAdapter = new ClusterClientAdapter({ logger, - clusterClientPromise: Promise.resolve(clusterClient), + elasticsearchClientPromise: Promise.resolve(clusterClient), context: contextMock.create(), }); }); @@ -38,16 +39,16 @@ describe('indexDocument', () => { clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' }); await retryUntil('cluster client bulk called', () => { - return clusterClient.callAsInternalUser.mock.calls.length !== 0; + return clusterClient.bulk.mock.calls.length !== 0; }); - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('bulk', { + expect(clusterClient.bulk).toHaveBeenCalledWith({ body: [{ create: { _index: 'event-log' } }, { message: 'foo' }], }); }); test('should log an error when cluster client throws an error', async () => { - clusterClient.callAsInternalUser.mockRejectedValue(new Error('expected failure')); + clusterClient.bulk.mockRejectedValue(new Error('expected failure')); clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' }); await retryUntil('cluster client bulk called', () => { return logger.error.mock.calls.length !== 0; @@ -69,7 +70,7 @@ describe('shutdown()', () => { const resultPromise = clusterClientAdapter.shutdown(); await retryUntil('cluster client bulk called', () => { - return clusterClient.callAsInternalUser.mock.calls.length !== 0; + return clusterClient.bulk.mock.calls.length !== 0; }); const result = await resultPromise; @@ -85,7 +86,7 @@ describe('buffering documents', () => { } await retryUntil('cluster client bulk called', () => { - return clusterClient.callAsInternalUser.mock.calls.length !== 0; + return clusterClient.bulk.mock.calls.length !== 0; }); const expectedBody = []; @@ -93,7 +94,7 @@ describe('buffering documents', () => { expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` }); } - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('bulk', { + expect(clusterClient.bulk).toHaveBeenCalledWith({ body: expectedBody, }); }); @@ -105,7 +106,7 @@ describe('buffering documents', () => { } await retryUntil('cluster client bulk called', () => { - return clusterClient.callAsInternalUser.mock.calls.length >= 2; + return clusterClient.bulk.mock.calls.length >= 2; }); const expectedBody = []; @@ -113,18 +114,18 @@ describe('buffering documents', () => { expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` }); } - expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(1, 'bulk', { + expect(clusterClient.bulk).toHaveBeenNthCalledWith(1, { body: expectedBody, }); - expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(2, 'bulk', { + expect(clusterClient.bulk).toHaveBeenNthCalledWith(2, { body: [{ create: { _index: 'event-log' } }, { message: `foo 100` }], }); }); test('should handle lots of docs correctly with a delay in the bulk index', async () => { // @ts-ignore - clusterClient.callAsInternalUser.mockImplementation = async () => await delay(100); + clusterClient.bulk.mockImplementation = async () => await delay(100); const docs = times(EVENT_BUFFER_LENGTH * 10, (i) => ({ body: { message: `foo ${i}` }, @@ -137,7 +138,7 @@ describe('buffering documents', () => { } await retryUntil('cluster client bulk called', () => { - return clusterClient.callAsInternalUser.mock.calls.length >= 10; + return clusterClient.bulk.mock.calls.length >= 10; }); for (let i = 0; i < 10; i++) { @@ -149,7 +150,7 @@ describe('buffering documents', () => { ); } - expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(i + 1, 'bulk', { + expect(clusterClient.bulk).toHaveBeenNthCalledWith(i + 1, { body: expectedBody, }); } @@ -164,19 +165,19 @@ describe('doesIlmPolicyExist', () => { test('should call cluster with proper arguments', async () => { await clusterClientAdapter.doesIlmPolicyExist('foo'); - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { + expect(clusterClient.transport.request).toHaveBeenCalledWith({ method: 'GET', path: '/_ilm/policy/foo', }); }); test('should return false when 404 error is returned by Elasticsearch', async () => { - clusterClient.callAsInternalUser.mockRejectedValue(notFoundError); + clusterClient.transport.request.mockRejectedValue(notFoundError); await expect(clusterClientAdapter.doesIlmPolicyExist('foo')).resolves.toEqual(false); }); test('should throw error when error is not 404', async () => { - clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); + clusterClient.transport.request.mockRejectedValue(new Error('Fail')); await expect( clusterClientAdapter.doesIlmPolicyExist('foo') ).rejects.toThrowErrorMatchingInlineSnapshot(`"error checking existance of ilm policy: Fail"`); @@ -189,9 +190,9 @@ describe('doesIlmPolicyExist', () => { describe('createIlmPolicy', () => { test('should call cluster client with given policy', async () => { - clusterClient.callAsInternalUser.mockResolvedValue({ success: true }); + clusterClient.transport.request.mockResolvedValue(asApiResponse({ success: true })); await clusterClientAdapter.createIlmPolicy('foo', { args: true }); - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { + expect(clusterClient.transport.request).toHaveBeenCalledWith({ method: 'PUT', path: '/_ilm/policy/foo', body: { args: true }, @@ -199,7 +200,7 @@ describe('createIlmPolicy', () => { }); test('should throw error when call cluster client throws', async () => { - clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); + clusterClient.transport.request.mockRejectedValue(new Error('Fail')); await expect( clusterClientAdapter.createIlmPolicy('foo', { args: true }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating ilm policy: Fail"`); @@ -209,23 +210,23 @@ describe('createIlmPolicy', () => { describe('doesIndexTemplateExist', () => { test('should call cluster with proper arguments', async () => { await clusterClientAdapter.doesIndexTemplateExist('foo'); - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.existsTemplate', { + expect(clusterClient.indices.existsTemplate).toHaveBeenCalledWith({ name: 'foo', }); }); test('should return true when call cluster returns true', async () => { - clusterClient.callAsInternalUser.mockResolvedValue(true); + clusterClient.indices.existsTemplate.mockResolvedValue(asApiResponse(true)); await expect(clusterClientAdapter.doesIndexTemplateExist('foo')).resolves.toEqual(true); }); test('should return false when call cluster returns false', async () => { - clusterClient.callAsInternalUser.mockResolvedValue(false); + clusterClient.indices.existsTemplate.mockResolvedValue(asApiResponse(false)); await expect(clusterClientAdapter.doesIndexTemplateExist('foo')).resolves.toEqual(false); }); test('should throw error when call cluster throws an error', async () => { - clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); + clusterClient.indices.existsTemplate.mockRejectedValue(new Error('Fail')); await expect( clusterClientAdapter.doesIndexTemplateExist('foo') ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -237,7 +238,7 @@ describe('doesIndexTemplateExist', () => { describe('createIndexTemplate', () => { test('should call cluster with given template', async () => { await clusterClientAdapter.createIndexTemplate('foo', { args: true }); - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.putTemplate', { + expect(clusterClient.indices.putTemplate).toHaveBeenCalledWith({ name: 'foo', create: true, body: { args: true }, @@ -245,16 +246,16 @@ describe('createIndexTemplate', () => { }); test(`should throw error if index template still doesn't exist after error is thrown`, async () => { - clusterClient.callAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - clusterClient.callAsInternalUser.mockResolvedValueOnce(false); + clusterClient.indices.putTemplate.mockRejectedValueOnce(new Error('Fail')); + clusterClient.indices.existsTemplate.mockResolvedValueOnce(asApiResponse(false)); await expect( clusterClientAdapter.createIndexTemplate('foo', { args: true }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating index template: Fail"`); }); test('should not throw error if index template exists after error is thrown', async () => { - clusterClient.callAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - clusterClient.callAsInternalUser.mockResolvedValueOnce(true); + clusterClient.indices.putTemplate.mockRejectedValueOnce(new Error('Fail')); + clusterClient.indices.existsTemplate.mockResolvedValueOnce(asApiResponse(true)); await clusterClientAdapter.createIndexTemplate('foo', { args: true }); }); }); @@ -262,23 +263,23 @@ describe('createIndexTemplate', () => { describe('doesAliasExist', () => { test('should call cluster with proper arguments', async () => { await clusterClientAdapter.doesAliasExist('foo'); - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.existsAlias', { + expect(clusterClient.indices.existsAlias).toHaveBeenCalledWith({ name: 'foo', }); }); test('should return true when call cluster returns true', async () => { - clusterClient.callAsInternalUser.mockResolvedValueOnce(true); + clusterClient.indices.existsAlias.mockResolvedValueOnce(asApiResponse(true)); await expect(clusterClientAdapter.doesAliasExist('foo')).resolves.toEqual(true); }); test('should return false when call cluster returns false', async () => { - clusterClient.callAsInternalUser.mockResolvedValueOnce(false); + clusterClient.indices.existsAlias.mockResolvedValueOnce(asApiResponse(false)); await expect(clusterClientAdapter.doesAliasExist('foo')).resolves.toEqual(false); }); test('should throw error when call cluster throws an error', async () => { - clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); + clusterClient.indices.existsAlias.mockRejectedValue(new Error('Fail')); await expect( clusterClientAdapter.doesAliasExist('foo') ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -290,14 +291,14 @@ describe('doesAliasExist', () => { describe('createIndex', () => { test('should call cluster with proper arguments', async () => { await clusterClientAdapter.createIndex('foo'); - expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.create', { + expect(clusterClient.indices.create).toHaveBeenCalledWith({ index: 'foo', body: {}, }); }); test('should throw error when not getting an error of type resource_already_exists_exception', async () => { - clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail')); + clusterClient.indices.create.mockRejectedValue(new Error('Fail')); await expect( clusterClientAdapter.createIndex('foo') ).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating initial index: Fail"`); @@ -312,7 +313,7 @@ describe('createIndex', () => { type: 'resource_already_exists_exception', }, }; - clusterClient.callAsInternalUser.mockRejectedValue(err); + clusterClient.indices.create.mockRejectedValue(err); await clusterClientAdapter.createIndex('foo'); }); }); @@ -321,12 +322,14 @@ describe('queryEventsBySavedObject', () => { const DEFAULT_OPTIONS = findOptionsSchema.validate({}); test('should call cluster with proper arguments with non-default namespace', async () => { - clusterClient.callAsInternalUser.mockResolvedValue({ - hits: { - hits: [], - total: { value: 0 }, - }, - }); + clusterClient.search.mockResolvedValue( + asApiResponse({ + hits: { + hits: [], + total: { value: 0 }, + }, + }) + ); await clusterClientAdapter.queryEventsBySavedObjects( 'index-name', 'namespace', @@ -335,14 +338,14 @@ describe('queryEventsBySavedObject', () => { DEFAULT_OPTIONS ); - const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; - expect(method).toEqual('search'); + const [query] = clusterClient.search.mock.calls[0]; expect(query).toMatchInlineSnapshot(` Object { "body": Object { "from": 0, "query": Object { "bool": Object { + "filter": Array [], "must": Array [ Object { "nested": Object { @@ -400,12 +403,14 @@ describe('queryEventsBySavedObject', () => { }); test('should call cluster with proper arguments with default namespace', async () => { - clusterClient.callAsInternalUser.mockResolvedValue({ - hits: { - hits: [], - total: { value: 0 }, - }, - }); + clusterClient.search.mockResolvedValue( + asApiResponse({ + hits: { + hits: [], + total: { value: 0 }, + }, + }) + ); await clusterClientAdapter.queryEventsBySavedObjects( 'index-name', undefined, @@ -414,14 +419,14 @@ describe('queryEventsBySavedObject', () => { DEFAULT_OPTIONS ); - const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; - expect(method).toEqual('search'); + const [query] = clusterClient.search.mock.calls[0]; expect(query).toMatchInlineSnapshot(` Object { "body": Object { "from": 0, "query": Object { "bool": Object { + "filter": Array [], "must": Array [ Object { "nested": Object { @@ -481,12 +486,14 @@ describe('queryEventsBySavedObject', () => { }); test('should call cluster with sort', async () => { - clusterClient.callAsInternalUser.mockResolvedValue({ - hits: { - hits: [], - total: { value: 0 }, - }, - }); + clusterClient.search.mockResolvedValue( + asApiResponse({ + hits: { + hits: [], + total: { value: 0 }, + }, + }) + ); await clusterClientAdapter.queryEventsBySavedObjects( 'index-name', 'namespace', @@ -495,8 +502,7 @@ describe('queryEventsBySavedObject', () => { { ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' } ); - const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; - expect(method).toEqual('search'); + const [query] = clusterClient.search.mock.calls[0]; expect(query).toMatchObject({ index: 'index-name', body: { @@ -506,12 +512,14 @@ describe('queryEventsBySavedObject', () => { }); test('supports open ended date', async () => { - clusterClient.callAsInternalUser.mockResolvedValue({ - hits: { - hits: [], - total: { value: 0 }, - }, - }); + clusterClient.search.mockResolvedValue( + asApiResponse({ + hits: { + hits: [], + total: { value: 0 }, + }, + }) + ); const start = '2020-07-08T00:52:28.350Z'; @@ -523,14 +531,14 @@ describe('queryEventsBySavedObject', () => { { ...DEFAULT_OPTIONS, start } ); - const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; - expect(method).toEqual('search'); + const [query] = clusterClient.search.mock.calls[0]; expect(query).toMatchInlineSnapshot(` Object { "body": Object { "from": 0, "query": Object { "bool": Object { + "filter": Array [], "must": Array [ Object { "nested": Object { @@ -595,12 +603,14 @@ describe('queryEventsBySavedObject', () => { }); test('supports optional date range', async () => { - clusterClient.callAsInternalUser.mockResolvedValue({ - hits: { - hits: [], - total: { value: 0 }, - }, - }); + clusterClient.search.mockResolvedValue( + asApiResponse({ + hits: { + hits: [], + total: { value: 0 }, + }, + }) + ); const start = '2020-07-08T00:52:28.350Z'; const end = '2020-07-08T00:00:00.000Z'; @@ -613,14 +623,14 @@ describe('queryEventsBySavedObject', () => { { ...DEFAULT_OPTIONS, start, end } ); - const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; - expect(method).toEqual('search'); + const [query] = clusterClient.search.mock.calls[0]; expect(query).toMatchInlineSnapshot(` Object { "body": Object { "from": 0, "query": Object { "bool": Object { + "filter": Array [], "must": Array [ Object { "nested": Object { @@ -697,6 +707,12 @@ type RetryableFunction = () => boolean; const RETRY_UNTIL_DEFAULT_COUNT = 20; const RETRY_UNTIL_DEFAULT_WAIT = 1000; // milliseconds +function asApiResponse(body: T): RequestEvent { + return { + body, + } as RequestEvent; +} + async function retryUntil( label: string, fn: RetryableFunction, diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 5d4c33f319fcc..4488dc74556ca 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -5,20 +5,18 @@ */ import { Subject } from 'rxjs'; -import { bufferTime, filter, switchMap } from 'rxjs/operators'; +import { bufferTime, filter as rxFilter, switchMap } from 'rxjs/operators'; import { reject, isUndefined } from 'lodash'; -import { Client } from 'elasticsearch'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Logger, LegacyClusterClient } from 'src/core/server'; -import { ESSearchResponse } from '../../../../typings/elasticsearch'; +import { Logger, ElasticsearchClient } from 'src/core/server'; import { EsContext } from '.'; import { IEvent, IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; import { FindOptionsType } from '../event_log_client'; +import { esKuery } from '../../../../../src/plugins/data/server'; export const EVENT_BUFFER_TIME = 1000; // milliseconds export const EVENT_BUFFER_LENGTH = 100; -export type EsClusterClient = Pick; export type IClusterClientAdapter = PublicMethodsOf; export interface Doc { @@ -28,7 +26,7 @@ export interface Doc { export interface ConstructorOpts { logger: Logger; - clusterClientPromise: Promise; + elasticsearchClientPromise: Promise; context: EsContext; } @@ -41,14 +39,14 @@ export interface QueryEventsBySavedObjectResult { export class ClusterClientAdapter { private readonly logger: Logger; - private readonly clusterClientPromise: Promise; + private readonly elasticsearchClientPromise: Promise; private readonly docBuffer$: Subject; private readonly context: EsContext; private readonly docsBufferedFlushed: Promise; constructor(opts: ConstructorOpts) { this.logger = opts.logger; - this.clusterClientPromise = opts.clusterClientPromise; + this.elasticsearchClientPromise = opts.elasticsearchClientPromise; this.context = opts.context; this.docBuffer$ = new Subject(); @@ -58,7 +56,7 @@ export class ClusterClientAdapter { this.docsBufferedFlushed = this.docBuffer$ .pipe( bufferTime(EVENT_BUFFER_TIME, null, EVENT_BUFFER_LENGTH), - filter((docs) => docs.length > 0), + rxFilter((docs) => docs.length > 0), switchMap(async (docs) => await this.indexDocuments(docs)) ) .toPromise(); @@ -97,7 +95,8 @@ export class ClusterClientAdapter { } try { - await this.callEs>('bulk', { body: bulkBody }); + const esClient = await this.elasticsearchClientPromise; + await esClient.bulk({ body: bulkBody }); } catch (err) { this.logger.error( `error writing bulk events: "${err.message}"; docs: ${JSON.stringify(bulkBody)}` @@ -111,7 +110,8 @@ export class ClusterClientAdapter { path: `/_ilm/policy/${policyName}`, }; try { - await this.callEs('transport.request', request); + const esClient = await this.elasticsearchClientPromise; + await esClient.transport.request(request); } catch (err) { if (err.statusCode === 404) return false; throw new Error(`error checking existance of ilm policy: ${err.message}`); @@ -119,14 +119,15 @@ export class ClusterClientAdapter { return true; } - public async createIlmPolicy(policyName: string, policy: unknown): Promise { + public async createIlmPolicy(policyName: string, policy: Record): Promise { const request = { method: 'PUT', path: `/_ilm/policy/${policyName}`, body: policy, }; try { - await this.callEs('transport.request', request); + const esClient = await this.elasticsearchClientPromise; + await esClient.transport.request(request); } catch (err) { throw new Error(`error creating ilm policy: ${err.message}`); } @@ -135,27 +136,18 @@ export class ClusterClientAdapter { public async doesIndexTemplateExist(name: string): Promise { let result; try { - result = await this.callEs>( - 'indices.existsTemplate', - { name } - ); + const esClient = await this.elasticsearchClientPromise; + result = (await esClient.indices.existsTemplate({ name })).body; } catch (err) { throw new Error(`error checking existance of index template: ${err.message}`); } return result as boolean; } - public async createIndexTemplate(name: string, template: unknown): Promise { - const addTemplateParams = { - name, - create: true, - body: template, - }; + public async createIndexTemplate(name: string, template: Record): Promise { try { - await this.callEs>( - 'indices.putTemplate', - addTemplateParams - ); + const esClient = await this.elasticsearchClientPromise; + await esClient.indices.putTemplate({ name, body: template, create: true }); } catch (err) { // The error message doesn't have a type attribute we can look to guarantee it's due // to the template already existing (only long message) so we'll check ourselves to see @@ -171,19 +163,21 @@ export class ClusterClientAdapter { public async doesAliasExist(name: string): Promise { let result; try { - result = await this.callEs>( - 'indices.existsAlias', - { name } - ); + const esClient = await this.elasticsearchClientPromise; + result = (await esClient.indices.existsAlias({ name })).body; } catch (err) { throw new Error(`error checking existance of initial index: ${err.message}`); } return result as boolean; } - public async createIndex(name: string, body: unknown = {}): Promise { + public async createIndex( + name: string, + body: string | Record = {} + ): Promise { try { - await this.callEs>('indices.create', { + const esClient = await this.elasticsearchClientPromise; + await esClient.indices.create({ index: name, body, }); @@ -200,7 +194,7 @@ export class ClusterClientAdapter { type: string, ids: string[], // eslint-disable-next-line @typescript-eslint/naming-convention - { page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType + { page, per_page: perPage, start, end, sort_field, sort_order, filter }: FindOptionsType ): Promise { const defaultNamespaceQuery = { bool: { @@ -220,12 +214,26 @@ export class ClusterClientAdapter { }; const namespaceQuery = namespace === undefined ? defaultNamespaceQuery : namedNamespaceQuery; + const esClient = await this.elasticsearchClientPromise; + let dslFilterQuery; + try { + dslFilterQuery = filter + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(filter)) + : []; + } catch (err) { + this.debug(`Invalid kuery syntax for the filter (${filter}) error:`, { + message: err.message, + statusCode: err.statusCode, + }); + throw err; + } const body = { size: perPage, from: (page - 1) * perPage, sort: { [sort_field]: { order: sort_order } }, query: { bool: { + filter: dslFilterQuery, must: reject( [ { @@ -283,8 +291,10 @@ export class ClusterClientAdapter { try { const { - hits: { hits, total }, - }: ESSearchResponse = await this.callEs('search', { + body: { + hits: { hits, total }, + }, + } = await esClient.search({ index, track_total_hits: true, body, @@ -293,7 +303,7 @@ export class ClusterClientAdapter { page, per_page: perPage, total: total.value, - data: hits.map((hit) => hit._source) as IValidatedEvent[], + data: hits.map((hit: { _source: unknown }) => hit._source) as IValidatedEvent[], }; } catch (err) { throw new Error( @@ -302,24 +312,6 @@ export class ClusterClientAdapter { } } - // We have a common problem typing ES-DSL Queries - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async callEs(operation: string, body?: any) { - try { - this.debug(`callEs(${operation}) calls:`, body); - const clusterClient = await this.clusterClientPromise; - const result = await clusterClient.callAsInternalUser(operation, body); - this.debug(`callEs(${operation}) result:`, result); - return result as ESQueryResult; - } catch (err) { - this.debug(`callEs(${operation}) error:`, { - message: err.message, - statusCode: err.statusCode, - }); - throw err; - } - } - private debug(message: string, object?: unknown) { const objectString = object == null ? '' : JSON.stringify(object); this.logger.debug(`esContext: ${message} ${objectString}`); diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts index 5f26399618e38..fc137b4e45b13 100644 --- a/x-pack/plugins/event_log/server/es/context.test.ts +++ b/x-pack/plugins/event_log/server/es/context.test.ts @@ -5,27 +5,28 @@ */ import { createEsContext } from './context'; -import { LegacyClusterClient, Logger } from '../../../../../src/core/server'; +import { ElasticsearchClient, Logger } from '../../../../../src/core/server'; import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import { RequestEvent } from '@elastic/elasticsearch'; jest.mock('../lib/../../../../package.json', () => ({ version: '1.2.3' })); jest.mock('./init'); -type EsClusterClient = Pick, 'callAsInternalUser' | 'asScoped'>; let logger: Logger; -let clusterClient: EsClusterClient; +let elasticsearchClient: DeeplyMockedKeys; beforeEach(() => { logger = loggingSystemMock.createLogger(); - clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; }); describe('createEsContext', () => { test('should return is ready state as falsy if not initialized', () => { const context = createEsContext({ logger, - clusterClientPromise: Promise.resolve(clusterClient), indexNameRoot: 'test0', kibanaVersion: '1.2.3', + elasticsearchClientPromise: Promise.resolve(elasticsearchClient), }); expect(context.initialized).toBeFalsy(); @@ -37,9 +38,9 @@ describe('createEsContext', () => { test('should return esNames', () => { const context = createEsContext({ logger, - clusterClientPromise: Promise.resolve(clusterClient), indexNameRoot: 'test-index', kibanaVersion: '1.2.3', + elasticsearchClientPromise: Promise.resolve(elasticsearchClient), }); const esNames = context.esNames; @@ -57,12 +58,12 @@ describe('createEsContext', () => { test('should return exist false for esAdapter ilm policy, index template and alias before initialize', async () => { const context = createEsContext({ logger, - clusterClientPromise: Promise.resolve(clusterClient), indexNameRoot: 'test1', kibanaVersion: '1.2.3', + elasticsearchClientPromise: Promise.resolve(elasticsearchClient), }); - clusterClient.callAsInternalUser.mockResolvedValue(false); - + elasticsearchClient.indices.existsTemplate.mockResolvedValue(asApiResponse(false)); + elasticsearchClient.indices.existsAlias.mockResolvedValue(asApiResponse(false)); const doesAliasExist = await context.esAdapter.doesAliasExist(context.esNames.alias); expect(doesAliasExist).toBeFalsy(); @@ -75,11 +76,11 @@ describe('createEsContext', () => { test('should return exist true for esAdapter ilm policy, index template and alias after initialize', async () => { const context = createEsContext({ logger, - clusterClientPromise: Promise.resolve(clusterClient), indexNameRoot: 'test2', kibanaVersion: '1.2.3', + elasticsearchClientPromise: Promise.resolve(elasticsearchClient), }); - clusterClient.callAsInternalUser.mockResolvedValue(true); + elasticsearchClient.indices.existsTemplate.mockResolvedValue(asApiResponse(true)); context.initialize(); const doesIlmPolicyExist = await context.esAdapter.doesIlmPolicyExist( @@ -100,12 +101,18 @@ describe('createEsContext', () => { jest.requireMock('./init').initializeEs.mockResolvedValue(false); const context = createEsContext({ logger, - clusterClientPromise: Promise.resolve(clusterClient), indexNameRoot: 'test2', kibanaVersion: '1.2.3', + elasticsearchClientPromise: Promise.resolve(elasticsearchClient), }); context.initialize(); const success = await context.waitTillReady(); expect(success).toBe(false); }); }); + +function asApiResponse(body: T): RequestEvent { + return { + body, + } as RequestEvent; +} diff --git a/x-pack/plugins/event_log/server/es/context.ts b/x-pack/plugins/event_log/server/es/context.ts index c1777d6979c5c..26f249d3b2c06 100644 --- a/x-pack/plugins/event_log/server/es/context.ts +++ b/x-pack/plugins/event_log/server/es/context.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, LegacyClusterClient } from 'src/core/server'; +import { Logger, ElasticsearchClient } from 'src/core/server'; import { EsNames, getEsNames } from './names'; import { initializeEs } from './init'; import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter'; import { createReadySignal, ReadySignal } from '../lib/ready_signal'; -export type EsClusterClient = Pick; - export interface EsContext { logger: Logger; esNames: EsNames; @@ -34,9 +32,9 @@ export function createEsContext(params: EsContextCtorParams): EsContext { export interface EsContextCtorParams { logger: Logger; - clusterClientPromise: Promise; indexNameRoot: string; kibanaVersion: string; + elasticsearchClientPromise: Promise; } class EsContextImpl implements EsContext { @@ -53,7 +51,7 @@ class EsContextImpl implements EsContext { this.initialized = false; this.esAdapter = new ClusterClientAdapter({ logger: params.logger, - clusterClientPromise: params.clusterClientPromise, + elasticsearchClientPromise: params.elasticsearchClientPromise, context: this, }); } diff --git a/x-pack/plugins/event_log/server/es/index.ts b/x-pack/plugins/event_log/server/es/index.ts index ad1409e33589f..adc7ed011aa14 100644 --- a/x-pack/plugins/event_log/server/es/index.ts +++ b/x-pack/plugins/event_log/server/es/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EsClusterClient, EsContext, createEsContext } from './context'; +export { EsContext, createEsContext } from './context'; diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 63453c6327da2..091f997fe62ea 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -6,14 +6,14 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; -import { LegacyClusterClient, KibanaRequest } from 'src/core/server'; +import { IClusterClient, KibanaRequest } from 'src/core/server'; import { SpacesServiceStart } from '../../spaces/server'; import { EsContext } from './es'; import { IEventLogClient } from './types'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; import { SavedObjectBulkGetterResult } from './saved_object_provider_registry'; -export type PluginClusterClient = Pick; +export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; const optionalDateFieldSchema = schema.maybe( @@ -48,12 +48,13 @@ export const findOptionsSchema = schema.object({ sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], { defaultValue: 'asc', }), + filter: schema.maybe(schema.string()), }); // page & perPage are required, other fields are optional // using schema.maybe allows us to set undefined, but not to make the field optional export type FindOptionsType = Pick< TypeOf, - 'page' | 'per_page' | 'sort_field' | 'sort_order' + 'page' | 'per_page' | 'sort_field' | 'sort_order' | 'filter' > & Partial>; diff --git a/x-pack/plugins/event_log/server/event_log_service.ts b/x-pack/plugins/event_log/server/event_log_service.ts index 9249288d33939..0bc675fee928d 100644 --- a/x-pack/plugins/event_log/server/event_log_service.ts +++ b/x-pack/plugins/event_log/server/event_log_service.ts @@ -5,14 +5,14 @@ */ import { Observable } from 'rxjs'; -import { LegacyClusterClient } from 'src/core/server'; +import { IClusterClient } from 'src/core/server'; import { Plugin } from './plugin'; import { EsContext } from './es'; import { IEvent, IEventLogger, IEventLogService, IEventLogConfig } from './types'; import { EventLogger } from './event_logger'; import { SavedObjectProvider, SavedObjectProviderRegistry } from './saved_object_provider_registry'; -export type PluginClusterClient = Pick; +export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; type SystemLogger = Plugin['systemLogger']; diff --git a/x-pack/plugins/event_log/server/event_log_start_service.ts b/x-pack/plugins/event_log/server/event_log_start_service.ts index 51dd7d6e95d15..82b8f06c251a3 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.ts @@ -5,14 +5,14 @@ */ import { Observable } from 'rxjs'; -import { LegacyClusterClient, KibanaRequest } from 'src/core/server'; +import { IClusterClient, KibanaRequest } from 'src/core/server'; import { SpacesServiceStart } from '../../spaces/server'; import { EsContext } from './es'; import { IEventLogClientService } from './types'; import { EventLogClient } from './event_log_client'; import { SavedObjectProviderRegistry } from './saved_object_provider_registry'; -export type PluginClusterClient = Pick; +export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; interface EventLogServiceCtorParams { diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 3bf726de71856..e2e31864eb31f 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -12,7 +12,7 @@ import { Logger, Plugin as CorePlugin, PluginInitializerContext, - LegacyClusterClient, + IClusterClient, SharedGlobalConfig, IContextProvider, } from 'src/core/server'; @@ -33,7 +33,7 @@ import { EventLogClientService } from './event_log_start_service'; import { SavedObjectProviderRegistry } from './saved_object_provider_registry'; import { findByIdsRoute } from './routes/find_by_ids'; -export type PluginClusterClient = Pick; +export type PluginClusterClient = Pick; const PROVIDER = 'eventLog'; @@ -77,9 +77,9 @@ export class Plugin implements CorePlugin elasticsearch.legacy.client), + .then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser), kibanaVersion: this.kibanaVersion, }); diff --git a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts index 6336d834c3943..5b093dfb28eab 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts @@ -28,6 +28,7 @@ interface GetEventLogParams { id: string; provider: string; actions: Map; + filter?: string; } // Return event log entries given the specified parameters; for the `actions` @@ -37,7 +38,9 @@ export async function getEventLog(params: GetEventLogParams): Promise { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([['new-instance', { equal: 1 }]]), + filter: 'event.action:(new-instance)', + }); + }); + + expect(getEventsByAction(filteredEvents, 'execute').length).equal(0); + expect(getEventsByAction(filteredEvents, 'execute-action').length).equal(0); + expect(getEventsByAction(events, 'new-instance').length).equal(1); + const executeEvents = getEventsByAction(events, 'execute'); const executeActionEvents = getEventsByAction(events, 'execute-action'); const newInstanceEvents = getEventsByAction(events, 'new-instance'); From ee2c74da44572aac7fab37405109d6cf46dba5cf Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 28 Jan 2021 14:30:45 -0500 Subject: [PATCH 095/163] [Upgrade Assistant] Use core doc links service (#89363) --- .../public/application/app.tsx | 4 +- .../components/latest_minor_banner.tsx | 77 ++-- .../application/components/tabs.test.tsx | 13 + .../__snapshots__/checkup_tab.test.tsx.snap | 9 +- .../tabs/checkup/checkup_tab.test.tsx | 13 + .../components/tabs/checkup/checkup_tab.tsx | 341 +++++++++--------- .../reindex/flyout/warning_step.test.tsx | 13 + .../reindex/flyout/warnings_step.tsx | 321 ++++++++--------- .../components/tabs/overview/steps.tsx | 17 +- 9 files changed, 419 insertions(+), 389 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx index 17eff71f1039b..2b245bceceb6c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/app.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx @@ -16,10 +16,10 @@ export interface AppDependencies extends ContextValue { i18n: I18nStart; } -export const RootComponent = ({ i18n, ...contexValue }: AppDependencies) => { +export const RootComponent = ({ i18n, ...contextValue }: AppDependencies) => { return ( - +
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx index 43d0364425cbb..d9ec183231739 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx @@ -10,40 +10,49 @@ import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../common/version'; +import { useAppContext } from '../app_context'; -export const LatestMinorBanner: React.FunctionComponent = () => ( - - } - color="warning" - iconType="help" - > -

- + } + color="warning" + iconType="help" + > +

+ - - - ), - nextEsVersion: `${NEXT_MAJOR_VERSION}.x`, - currentEsVersion: `${CURRENT_MAJOR_VERSION}.x`, - }} - /> -

-
-); + values={{ + breakingChangesDocButton: ( + + + + ), + nextEsVersion: `${NEXT_MAJOR_VERSION}.x`, + currentEsVersion: `${CURRENT_MAJOR_VERSION}.x`, + }} + /> +

+ + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx index 463c0c9d016b3..6a99bd24ef26b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx @@ -17,6 +17,19 @@ const promisesToResolve = () => new Promise((resolve) => setTimeout(resolve, 0)) const mockHttp = httpServiceMock.createSetupContract(); +jest.mock('../app_context', () => { + return { + useAppContext: () => { + return { + docLinks: { + DOC_LINK_VERSION: 'current', + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + }, + }; + }, + }; +}); + describe('UpgradeAssistantTabs', () => { test('renders loading state', async () => { mockHttp.get.mockReturnValue( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap index dda051e715234..5aa4a469e4f02 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap @@ -40,7 +40,8 @@ exports[`CheckupTab render with deprecations 1`] = ` values={ Object { "snapshotRestoreDocsButton": { + return { + useAppContext: () => { + return { + docLinks: { + DOC_LINK_VERSION: 'current', + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + }, + }; + }, + }; +}); + /** * Mostly a dumb container with copy, test the three main states. */ diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx index 5688903b8f7cd..02cbc87483e55 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx @@ -5,7 +5,7 @@ */ import { find } from 'lodash'; -import React, { Fragment } from 'react'; +import React, { FunctionComponent, useState } from 'react'; import { EuiCallOut, @@ -20,211 +20,65 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { NEXT_MAJOR_VERSION } from '../../../../../common/version'; import { LoadingErrorBanner } from '../../error_banner'; +import { useAppContext } from '../../../app_context'; import { GroupByOption, LevelFilterOption, LoadingState, - UpgradeAssistantTabComponent, UpgradeAssistantTabProps, } from '../../types'; import { CheckupControls } from './controls'; import { GroupedDeprecations } from './deprecations/grouped'; -interface CheckupTabProps extends UpgradeAssistantTabProps { +export interface CheckupTabProps extends UpgradeAssistantTabProps { checkupLabel: string; showBackupWarning?: boolean; } -interface CheckupTabState { - currentFilter: LevelFilterOption; - search: string; - currentGroupBy: GroupByOption; -} - /** * Displays a list of deprecations that filterable and groupable. Can be used for cluster, * nodes, or indices checkups. */ -export class CheckupTab extends UpgradeAssistantTabComponent { - constructor(props: CheckupTabProps) { - super(props); - - this.state = { - // initialize to all filters - currentFilter: LevelFilterOption.all, - search: '', - currentGroupBy: GroupByOption.message, - }; - } - - public render() { - const { - alertBanner, - checkupLabel, - deprecations, - loadingError, - loadingState, - refreshCheckupData, - setSelectedTabIndex, - showBackupWarning = false, - } = this.props; - const { currentFilter, currentGroupBy } = this.state; - - return ( - - - -

- {checkupLabel}, - nextEsVersion: `${NEXT_MAJOR_VERSION}.x`, - }} - /> -

-
- - - - {alertBanner && ( - - {alertBanner} - - - )} - - {showBackupWarning && ( - - - } - color="warning" - iconType="help" - > -

- - - - ), - }} - /> -

-
- -
- )} - - - - {loadingState === LoadingState.Error ? ( - - ) : deprecations && deprecations.length > 0 ? ( - - - - {this.renderCheckupData()} - - ) : ( - - - - } - body={ - -

- {checkupLabel}, - }} - /> -

-

- setSelectedTabIndex(0)}> - - - ), - }} - /> -

-
- } - /> - )} -
-
-
- ); - } - - private changeFilter = (filter: LevelFilterOption) => { - this.setState({ currentFilter: filter }); +export const CheckupTab: FunctionComponent = ({ + alertBanner, + checkupLabel, + deprecations, + loadingError, + loadingState, + refreshCheckupData, + setSelectedTabIndex, + showBackupWarning = false, +}) => { + const [currentFilter, setCurrentFilter] = useState(LevelFilterOption.all); + const [search, setSearch] = useState(''); + const [currentGroupBy, setCurrentGroupBy] = useState(GroupByOption.message); + + const { docLinks } = useAppContext(); + + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; + + const changeFilter = (filter: LevelFilterOption) => { + setCurrentFilter(filter); }; - private changeSearch = (search: string) => { - this.setState({ search }); + const changeSearch = (newSearch: string) => { + setSearch(newSearch); }; - private changeGroupBy = (groupBy: GroupByOption) => { - this.setState({ currentGroupBy: groupBy }); + const changeGroupBy = (groupBy: GroupByOption) => { + setCurrentGroupBy(groupBy); }; - private availableGroupByOptions() { - const { deprecations } = this.props; - + const availableGroupByOptions = () => { if (!deprecations) { return []; } return Object.keys(GroupByOption).filter((opt) => find(deprecations, opt)) as GroupByOption[]; - } - - private renderCheckupData() { - const { deprecations } = this.props; - const { currentFilter, currentGroupBy, search } = this.state; + }; + const renderCheckupData = () => { return ( ); - } -} + }; + + return ( + <> + + +

+ {checkupLabel}, + nextEsVersion: `${NEXT_MAJOR_VERSION}.x`, + }} + /> +

+
+ + + + {alertBanner && ( + <> + {alertBanner} + + + )} + + {showBackupWarning && ( + <> + + } + color="warning" + iconType="help" + > +

+ + + + ), + }} + /> +

+
+ + + )} + + + + {loadingState === LoadingState.Error ? ( + + ) : deprecations && deprecations.length > 0 ? ( + <> + + + {renderCheckupData()} + + ) : ( + + + + } + body={ + <> +

+ {checkupLabel}, + }} + /> +

+

+ setSelectedTabIndex(0)}> + + + ), + }} + /> +

+ + } + /> + )} +
+
+ + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx index 318d2bc7baffe..6428edfbe904d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx @@ -11,6 +11,19 @@ import React from 'react'; import { ReindexWarning } from '../../../../../../../../common/types'; import { idForWarning, WarningsFlyoutStep } from './warnings_step'; +jest.mock('../../../../../../app_context', () => { + return { + useAppContext: () => { + return { + docLinks: { + DOC_LINK_VERSION: 'current', + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + }, + }; + }, + }; +}); + describe('WarningsFlyoutStep', () => { const defaultProps = { advanceNextStep: jest.fn(), diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx index c3ef0fde6e749..9f48c77ec38e1 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { useState } from 'react'; import { EuiButton, @@ -21,6 +21,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useAppContext } from '../../../../../../app_context'; import { ReindexWarning } from '../../../../../../../../common/types'; interface CheckedIds { @@ -37,7 +38,7 @@ const WarningCheckbox: React.FunctionComponent<{ documentationUrl: string; onChange: (event: React.ChangeEvent) => void; }> = ({ checkedIds, warning, label, onChange, description, documentationUrl }) => ( - + <> {description}
- + -
+ ); interface WarningsConfirmationFlyoutProps { @@ -68,175 +69,169 @@ interface WarningsConfirmationFlyoutProps { advanceNextStep: () => void; } -interface WarningsConfirmationFlyoutState { - checkedIds: CheckedIds; -} - /** * Displays warning text about destructive changes required to reindex this index. The user * must acknowledge each change before being allowed to proceed. */ -export class WarningsFlyoutStep extends React.Component< - WarningsConfirmationFlyoutProps, - WarningsConfirmationFlyoutState -> { - constructor(props: WarningsConfirmationFlyoutProps) { - super(props); - - this.state = { - checkedIds: props.warnings.reduce((checkedIds, warning) => { - checkedIds[idForWarning(warning)] = false; - return checkedIds; - }, {} as { [id: string]: boolean }), - }; - } - - public render() { - const { warnings, closeFlyout, advanceNextStep, renderGlobalCallouts } = this.props; - const { checkedIds } = this.state; - - // Do not allow to proceed until all checkboxes are checked. - const blockAdvance = Object.values(checkedIds).filter((v) => v).length < warnings.length; - - return ( - - - {renderGlobalCallouts()} - = ({ + warnings, + renderGlobalCallouts, + closeFlyout, + advanceNextStep, +}) => { + const [checkedIds, setCheckedIds] = useState( + warnings.reduce((initialCheckedIds, warning) => { + initialCheckedIds[idForWarning(warning)] = false; + return initialCheckedIds; + }, {} as { [id: string]: boolean }) + ); + + // Do not allow to proceed until all checkboxes are checked. + const blockAdvance = Object.values(checkedIds).filter((v) => v).length < warnings.length; + + const onChange = (e: React.ChangeEvent) => { + const optionId = e.target.id; + + setCheckedIds((prev) => ({ + ...prev, + ...{ + [optionId]: !checkedIds[optionId], + }, + })); + }; + + const { docLinks } = useAppContext(); + const { ELASTIC_WEBSITE_URL } = docLinks; + const esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference`; + const observabilityDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/observability`; + + // TODO: Revisit warnings returned for 8.0 upgrade; many of these are likely obselete now + return ( + <> + + {renderGlobalCallouts()} + + } + color="danger" + iconType="alert" + > +

+ +

+
+ + + + {warnings.includes(ReindexWarning.allField) && ( + _all, + }} /> } - color="danger" - iconType="alert" - > -

+ description={ -

-
- - - - {warnings.includes(ReindexWarning.allField) && ( - _all, - }} - /> - } - description={ - _all, - }} - /> - } - documentationUrl="https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_the_literal__all_literal_meta_field_is_now_disabled_by_default" - /> - )} - - {warnings.includes(ReindexWarning.apmReindex) && ( - - } - description={ - + } + description={ + - } - documentationUrl="https://www.elastic.co/guide/en/apm/get-started/master/apm-release-notes.html" - /> - )} - - {warnings.includes(ReindexWarning.booleanFields) && ( - _source }} - /> - } - description={ - _source }} + /> + } + description={ + true, - false: false, - yes: "yes", - on: "on", - one: 1, - }} - /> - } - documentationUrl="https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_field" - /> - )} -
- - - - - - - - - - - - - - -
- ); - } - - private onChange = (e: React.ChangeEvent) => { - const optionId = e.target.id; - const nextCheckedIds = { - ...this.state.checkedIds, - ...{ - [optionId]: !this.state.checkedIds[optionId], - }, - }; - - this.setState({ checkedIds: nextCheckedIds }); - }; -} + values={{ + true: true, + false: false, + yes: "yes", + on: "on", + one: 1, + }} + /> + } + documentationUrl={`${esDocBasePath}/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_field`} + /> + )} + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx index 85d275b080e13..1a1ea48a350c8 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx @@ -54,7 +54,7 @@ const WAIT_FOR_RELEASE_STEP = { // Swap in this step for the one above it on the last minor release. // @ts-ignore -const START_UPGRADE_STEP = (isCloudEnabled: boolean) => ({ +const START_UPGRADE_STEP = (isCloudEnabled: boolean, esDocBasePath: string) => ({ title: i18n.translate('xpack.upgradeAssistant.overviewTab.steps.startUpgradeStep.stepTitle', { defaultMessage: 'Start your upgrade', }), @@ -73,10 +73,7 @@ const START_UPGRADE_STEP = (isCloudEnabled: boolean) => ({ defaultMessage="Follow {instructionButton} to start your upgrade." values={{ instructionButton: ( - + ); From b0f4c9831ee58f642abd095f5ac31d314630fc1f Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 28 Jan 2021 11:36:04 -0800 Subject: [PATCH 096/163] [DOCS] Updates intro doc (#88376) * [DOCS] Updates intro doc * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/introduction.asciidoc Co-authored-by: Kaarina Tungseth * [DOCS] Incorporates review comments * [DOCS] Add links to Security views * [DOCS] Minor tweaks to improve the flow of the doc Co-authored-by: Kaarina Tungseth Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/images/alerts-and-actions.png | Bin 0 -> 142368 bytes docs/user/images/app-navigation-search.png | Bin 0 -> 49936 bytes docs/user/images/features-control.png | Bin 0 -> 142443 bytes docs/user/images/home-page.png | Bin 0 -> 516322 bytes docs/user/images/kibana-main-menu.png | Bin 0 -> 369267 bytes docs/user/images/login-screen.png | Bin 0 -> 47292 bytes docs/user/images/roles-and-privileges.png | Bin 0 -> 329673 bytes docs/user/images/select-your-space.png | Bin 0 -> 105696 bytes docs/user/images/tags-search.png | Bin 0 -> 37555 bytes docs/user/images/visualization-journey.png | Bin 0 -> 152548 bytes docs/user/introduction.asciidoc | 479 +++++++++++++++++---- 11 files changed, 389 insertions(+), 90 deletions(-) create mode 100755 docs/user/images/alerts-and-actions.png create mode 100644 docs/user/images/app-navigation-search.png create mode 100755 docs/user/images/features-control.png create mode 100755 docs/user/images/home-page.png create mode 100755 docs/user/images/kibana-main-menu.png create mode 100755 docs/user/images/login-screen.png create mode 100755 docs/user/images/roles-and-privileges.png create mode 100755 docs/user/images/select-your-space.png create mode 100755 docs/user/images/tags-search.png create mode 100644 docs/user/images/visualization-journey.png diff --git a/docs/user/images/alerts-and-actions.png b/docs/user/images/alerts-and-actions.png new file mode 100755 index 0000000000000000000000000000000000000000..227abd9441e15a7ef250bf0919356d5323111259 GIT binary patch literal 142368 zcmeFZc|4T;`#!9cl2F=i3N4mO$ez7aBw32=OJ$2O2*X&16qO=-$e=6v#Y8{P#Tb@`u;V^}fvYUe5D4kK;JKG&3>aKOk~| zi;Iij@YaocTwMDITwL4<`+0zOZXSq~<>HFuGQ4rk0y;E55*unUMcmn81ckw<>SCG~ zU+q47x@YgMS&Sk5>5KDcKYVzq?D6#I-SY0kdoCP!x;nZ?FLHG>^p;*EGYjJHHV~?4 zDAg*FahXnnl+o0aT-M+til#a&%pz>$V#B$kH}C$-&#@M!yuj{%{pcnXe=Ya?s}KM8 z&whgX{(rd_aF=oQeNX>o!1-5awTk}l`}}^V$^YN}!vCuS7`Hz?wfpz)dw+Rlb7^ZE zKU(;&-|o}Ji$Dyg?Z^1@A-xPP68 z4;PCojCIP?tEc;e+0S)&yLRr64o=sp@kv}zu{-7J1n$ucD83;#MXpYItl zJUWY;o$pB~NE-QhAGf%2b;m0BDdYX{?w7_LZ^ypushj@PGGOu2sLglYLh9d__P*y2 zY1dmW_Mh$fOjE|Xpf3$?rOyv<9oRdwbCEVQojITx&G<6oVrWcA+-hGSf2VnB0I3)In@or=<}-c)3*F2 zq;zHDKbLYfCZ;60uFOB1yp!ycUJco7Yyb};z(dLRD)$q1{pY#}+%dSc>$@<`#(PbS zr({3#X2#K`|NfIZPo@W#@ke~W2n5(sQ-C~OV{G&I9nDMNBv(HZV8S5 z*Xo=e8xbV*^8WW@L=}5BGhF`n7eAx*U+c3+>%Tv|`t{uZ|Kb0qCt>Mp9qp55&|=#H zIVj((#7uX<40}-<9%9d2pL>E0O$Q15&!)WEX#fR&75gh_aadv8+uJcJKc9N*&ZA5q zYP45>*~5F_WpaBw;f%$Pxwbb~cm>|1pFC_RB%FSc_d-TeMz%D2?{g_L>05txqM7+& zVE;#oEG&-HOI2Yqf{c>PDV9 zd{vZ3pENj6Wll6Y|LT>H(IhOhkzF1C+%+=u+3&N~XFa59uj~9>(*4~bweu2=i06{G ztt~3POS;~&oi?t^uzKM3@yPT>jQRy{| zkBmAYvFu0aUG{>e%6S{IICs52F=5I5cX-JC*`>NLn^EUB0_JyvxK(C&;5*zJ&CSft zzihQ}3tw&D+1yH}udDqt$m;Jw`dwpMvf2jT&!jU^W4x`xg%HgxJAF%pO|+vfw?3{Y z!%XzgfUU#72^71E$&C?ey)Y-D6B_)3ZB5ByY zis9QozJG6H($r27O0w*~oUhD^CmcRpUMzG`*fI88}@?L^@+|QrpDzz--l~!?|ASrUTmO42ut+z z4xJ)cKQ}u>Xiq|7)9j8N+>N5Ik1#WL*z^fSw|OYSYfs98W?-8+L5q$SV zbjjXRoAZ9b5%z&tS>aEDM{21RndFpbXBmjYZWrFkmU{ktPwx4Vk@`nN)lPz@DP5l= zD!o<34lWxCyR8LV*k=yc`Tu8vgo6r zR7oRrSzJAI?fnHX-JPhC&*X+le*Wrn-~{;*=@%>MhY>wrLAsUPFfuUt6cf1UB;}L!FG~M z!NH+FnWNNBrxsDs+$~8hUH|F#9^H$xjSU~V#OEhCRZVGw(VUIE1&kLPl0_O1 z4!H9=fYyyW_P(*FG&O8&nW?B&m!%*_fa;u3*+Hq8N~Gj}ft71n=>LA|9%bt1pglZy$ye)&mMB9AP~Z;RseC zeh4pr{?lFHc{S_x#_M7xiwN(N>7=d4Zh7>P8mD{r?$tiNdy!-If%ZQ3rnHgCv3(`6 z0-|YDIIMhtLMDEujJa0YN6WcC=!boM;7Fr@fukVkQ z;>a5f-JZmT>i6pY#5Iw{@n9-KJAdcvH!)C;Cf(3)1#?Xj-PgWyti;QSKH?skA-fn> z7Aqd!6HrwiLHK&Dd9un`yp&ZahD(em>=D%7dKQaUGi`m&w_~wcM-} zj?AA29Roe1rrz3l9KPMOHyG>9-7Y@hq*5ub3tujlUD{r)%t(U69T5wM;78#5G+_G) zL3DO+xgf*~JOts4$7k2uY%kbU4qr9?RkC#Ie4HMRx}VdLyU>u_>-RZqb8)358;PC=qpjk}b|7wTI8{-OJ)h zpE-R5JmSsSczHzH^;dq71Csg6$oLK!LSyaO>!89aN!5uKQS)#+**9^CzmJNyGh3kA z?$dvELn@pv4jh}^? zW-2n$$hI1~7`$ zzkdC?lVwE}FUyIT4rV`E(>n5LwxfD9d|xfrarnTJ)h{4xLs5-_ z8Ikif3!0NHi64c+7SNNElkeVsxwN|J@8>6}aG}+rVuI=K?=yroa3A#e$)iyhhcE_sO~iP<1f^!MpLUHk zl>?J&9}S1KPRrVLLe^Q|t_ul?*t+Zhfu;W>+(2KywcXfuz%6KTWHX&@3x(EdtqgEz zRikllCXt*?8CPB9VZMYHDaq2NS+qb-%$sTZ>?=>&>`9~VWwJS+Nl}c<{rr5%%Ra^x z&!3?qg2VLm5<`un15FA0wcUyCSFfoP!%2~{=g#$ZbZP&BTFzXeIMr-V*UAZGx?A49 z{kaa_zKsr&ATN9QFXmwJomn@VZ}7`QG=YfqL@Ot-g{~Qtp*pg>ynK`R4zWL6HX9Q5 zYx2S%1Lufi?vzYmywjUEP6=S$@WwWDM&@}nW9mq|i47 zx4`wd&zf4KaN#SDKb5&|<~1ASgK8X60>T@cWg4?e=VZ^3N<+oczoY*tL_ePx50>mc zw6!{`wBl0c?f}d89^6n-Ut(QjFsrHpGE_kEG|;cIPv5HL6Q41Qkd#B4f(G^T5uS@(VRZ^#A$?uk~abM*3pukrVG)NV^ z%$#ycjT(w%=y8)t*(xC&>q6II99tKQlv4J%w6KET>#E=dNTvK65w+wKP9cNyaoemC z@K8HVw7%`b4=2MLH*VDNXr#mS}1%n=Iw@<7Go_pasp= zmb(*oHhq2Eg`^}U+k22!!@8Evxc2UkxK5Jt_g=2hwPAM8M|UTGjs}uXTUEok95dm* zu_`)ND`$BHG5czSG_9fKXCc+d!Um6|l{qR!RiUJbtk2|#YVbZBJIs80-5DvI@?=Ns z)9BI`E+-vW0tgE zpMlslUUT+X^;7ca%rz#Q(l#Ob3`o3uY4`(6`~j0|wL3z2;&3EHcLw9ePdKUKeR1Iw zDQgq6);UM_+V!y)r=;T_Zc5qqxq>J$a%`-z3lVihU&^_ZD%@h>AO)63_`QZ5W<&QY#>TevnZ_Q;*u z>G(~vgzLyr&Q$x}I0-2!{Ijj8#LP_3r+yd$(~+oB#T@m{CBd3E2@k__ayr(BPc^R` z)cbUy!k-~|<4}F zgKGZafj*f4$?b;w134RX$MGx7S+y?JWLx(o?7)~SiKJ4i6Enoswj0$nl{uo13o(q8`Jg|R=|^QeO)p*SO2jXhjQS%pI&-#7Xt>WgdBk;X0e%te+4eM~uYI)e zPW`^m@>- z`O_&4uY8Za4)?6*E@vQUupm%`Y_1hz!_x87W|Ec* z@0A8w?V=FuMQ4mP|LxDq;-ypXeM!K7-#>8OYgYkL&Iik zgcF$%%$o9f7|KMO9R9_l7kO;RJ0Rfo>rjTQzj*5Li=i4;8qu~$1y-(AG+4A{qJ7D} z{&eZh+r4yFl_w5wCiE&J(F;N8+q0D`^%};QK zMGP%353Ba7!wIi1t`5+Ls&NNDgbc(*X)YFU9|+Ko2n%cCW@#!bZ5HyHJ0IySaRsnq zup#$7Ta-5_O})FoxOBLgk%XW4>|>d}p_zASyiOqApMsSO&ts+O1l0%7JcJLeJ)Nh( za%~B$vL^C$7)^xr2lAO^?+pjIv9cAdG}H3rI!E z#)m=?QSJaF?>MLxddSu^WOYtHc{Jk@(Q2`)yo9aB@2B#h#~J;w*{#=ct$8-*T}WBD zJ6Xpg)PqVYt)47F#?7ZA>apG&{b1~((aom^!3m`cIaaVujE%XC`<{q8-}1qtSXoeM zR#)B{Up!~Xx=^*dpc}DQqf0L#v8c<;9SVB!L1~@2e#yG(hLkaUrIB7kh$*u}=Ghov zuK;)xPQ7+LChPJ58muj)krlKyEbqEFyjL-zNusxDsP|4d|06v;Jq+43UQAO?R1+ej zcy%l&#N6v`1ruk}xU$P>u2@07okJ0>o<^22~k4!>ku(H9{2P8}2^akYbM55$UY)H^EtoaF_64i{5U$#caQZ?$KEngC+9lwln zYfM1Dx0EaPE_=OKgXO9JM9U|H{I!}#iVkF+_wd+<*@mPw;_cU}C`V2;@>!Oy>#O=~ z-&fVZ#|k`~ReNBTXd;SMejyOp?JXuK1mh!U2F*TH@JXos@YxHT6AEj_)` zNM8gB{RELt5e3g}0-G^t*37xaZFJdp&IJ$@VdOpIPiAuxbhcCMv0*@Os#pQ>`w|vC!ifNbi{eDt?bHHw(Y7KO2^;`# zbGtn>FE5%`KX9%7PptYiU=5`%T(}_hwg0U)>F!3vuU`|!d=r*sPL-YC`$&JOgxHsl zmjgSNg8Y`Jl+wfOY<#C}D(A%O=cRUEQBg@!`&@G=LzF&HxEP%rBeW5M?-5QXj@{lN zZ$RAZ2s{?(7(c=&$7}o@Lbf#T1y3?spu$_TG56~;)`kf0%a;}2BRofvtea)B^Rj2w zGm$d~^)Uj&(f$rd+kLJxA^mv9N8Ak-z!=3oi)pPA)RpTov9;SREdhmR%!$Ei%#YlA z-jnnh$H5&TU3c%^m1^h)kB}W>1`~2%jvx@;+#XI3cDq`oMF3`_T9*;L&!8i{II7Ip%WT6^h_~5CH5cds+M1fju8BHz-4R@`es% z=~`ERiR-AFtu4Cl`0&0{kJF!~mS7WGTF(1N1W*Dn5RB!jyPN~|>=6pwK7R@epAc=N z`(dzfH5#UnHJ%?dTFDufZ!A?J`!s^oydgi>S-l zt>yq+`^f~D1vZ=SL?>O9)kCQxQXudY6|J2OTB;jYCNn5X&Ui%+6I1v%6$#oWTdQemx8JuSo#I}+xcsdTwi6^e*I@|S zZ*zZ~{kyidWD;42Ca;LWp%7qGpw!cy#$VNeJLEqJBYcl<*gDo)cXB*BBL zDEsIz_iB{SL6u!_X_4cgTo6Z?zr5X#5ONX@YH55ho^+O*yXxd~0nZEPYbAe-z@I;_ zF}Bv8sI-ZvJ`Goek`^&>2Os*rfO}$DP=OQ>i3PNTqgk01q~hA8O`8oiy})+;9U|lv=xb!}W5p};RBgq_ngbEK^8B%C%sMbO!4 zLg~^?GA`N-4mxrgVjeQ4<-c?eg9hiS&w>onV^J7l#xG)cmJ-Xz(8OdpWUXK^77`BC zruZBbn_CWz<0I5FXJm2lIZGE$WBjRzQyKv`JhfeOTCXg2zWm{M{e;}s1T+WiMLq?T zu62P6F~QnCi0j$>NFy$&Z8G|E6GKpEtA(gvA4<>>BW+oV=)u<5m&gipBSDd6o&FvH zm#?eUnBAfO$jvQrj2+bY7`&Y1>Rn09YTMw?dNo4-yq~~i1_WYJ#x>rcj)7&V_iJk# zB=^B84{@?vE=^?xsAjYxU{A(_vrx!(ao8erE_@D~tre^<(&&{FrSo{nH-WHMn~E+( zGeb2s@y@pBAlWvRJ4DHos||JV;&?C(vXtBMBGFb&I?PRIn{S;&xKlkcgddt3=4KcIWikQ6PI1;XYoX)q=*#MEOgv z-KD86#9<69H61A;T%cMsh$ZgoNhqb1JOlys{$~KcT5`~i<&b)juH$N_Z@8bNMed=o zXtuwGyvbzNCYxG0#cO1Zr2QqX1^qN&)BmIUkSWOv6{yzb`=yp~{}{lj&5|I!Bl~zx z$>#kCeWLuH2)1=HB5#HssL*~+Meq4qI_Tf7Tx&p&%21-VB&S`I2oVI)wm#sLaUE|P z8@+yF3ih?Fwri@}@Cb-_YGgI2kAnenL;zsNLLNk<&rxHR-W}zAb35(*@YY2NHWaG! zVC)d1LEUfilZ~w_d*%Mh=LunV%ulHXX1wg_AVC5ovTd6K3oV_9caILe;&VT3DePO* z4>w*eDhsRT`gtNabsoymvI8WN6Kw6nP8~wxsk8ADgK2M*0M+W36wJ}I<3(QBxo;CoK5DqZA}9n7S;x6gu36lVh4D13919$koGax&tzC%?Zm zpNeP{C#-jLTnXw#FLW2Ru%HJBnvmJ?WZ|SKeuB@lXPIkbEHbX~EQka?&sa0^oSNqo zaioCE9oE41jZc~bJ5**&*SdNJ&7nGeeOLTnx6RGh`J+^3n6`|gbAWKz0*EePQYoT% z0nFl-;rER_C~A}68Fc>{HLJ!ed4qqxGQYX0$wS8iWb1>GWT_1!nRokr!L=NL{k~Q}03Yxo+V%(tx(a`P$i#_J#o6WEs(r?sXIz&s8Cl%)9HUL&-Rm$PQf+HhM@`vl7`&N7_i7? zyXY}Mwiy~~O#7a-q^0d(ry^g?$%FMwUO48d5&;xE!E+fJsOn_MQew2s;c=@*&$B(TO>A<4{5{7eYOUosODx}j3#OE(+ zomaUv;!wMu&=-L~ludEqx@&2hn8gv6RpQ%{ie`UI1RxZi7PKRJiK}lICtNE|<>@xU zR!{`BLzzlowoxS@KYE`D2*+tdQTCfi!l`h%a7ML$PxaPtT^R;%%`ymAPi(U*+o>7^riI;+A*my z)aUpm`}D5I#I5Zfarr*bW1dUF+a3322I#3M_q7>?2^omlJL%^&^ml0sq~;?XJHH)+ zz$`m#3_h@v^;T$(XdOh`nEvrhoE zHIAtUv<`#kqeuj&3|y2wp2EXB6hUm$)*^keYqoJZ?*mjjOFaec;DtaZ5T&)$$9~iX zv3(<2kiV?#+nwkb?(Z>P#DjJxz$^E94qdcK_c*`sK4STYeekd! zCP7#Y+wvf_%?yZhJZ$j@<>o*wO4dr$;1|%nBntc}3Q6Ec1FT6}1 zm1S||0H{KRphqfo!ukKue%(jypvU`rhx5$Z(sg$l@b=g5i#l$U%nygviNVhU;_I~k zjr%>Sa~q-ekiw?@GgH%wk6ZR9*ktY9WX1H=NLuvcDOjNy~z<$~=}wedPc)<~Do z<+b?{Q^cOV_t(P{U^KAR6(%4hp9MH=RQQOt7HoR(#2p#8-wWml-9*g~SCgx5qu@v) z5~T#{@$}8zl_l?nvr+{QmOjivQTpGSYaW~#S|h5AvWn>vmEho+b%dO0Vy5_0NdJ2%*Ae6+9@=uEQ^xW`yW^nf?Z2qEdg-af=5tzd;?B(dOQY&QSL)2khjqQ!^>;| z!X%h7AqEGj@%!XR!e=V>?NkRytaDfnH=BGp^f4N#lQX@odA*+Naa;tyvv^dQ<9T#=BbqU*7AEtW$^n=PYmmI_a>1^5i>BSQL2e1O85D%PN+` ztN}!3jWpHr-+}=k<;p^ifA~GWU~h*Ud-qLvqwlwlfRbAnP1nz$<7HTP@J+9&gz>sp zadFAB;xOC%0t62@TO&xVl7-J{eBk!ob1W-8-Dz=f0Gms?8W9mS0d>m?>_Ize)Nie3 zC{=`S(%oPHH;VSywOB0qrDnViAJM;S&s6_McZyK6zeCREgm4a7?;iN1{auZ7y&Nw=1vCIPaePH4`bo?@AZcfPN1M{`5+? z?fzl7Q=O2619~kW+_Zizdn%G#Z7zmRAVzMyv?XNPG+6>Q*CM3?X_XS1^%2+L0ix0` zXf+0jPPV;DEdgJvuf&8^9!F+U^YIZxSE(vjR%cdlJVhz0zsEMg&J*o#>6badxRE#Z zEh#lwI$bn}4{0F5GtA`wpQ-+;3*_k+JdflXdzy|#&I}QHbu%CCHuW*$EI12}YA`%AtRc5ctBGhEkSboZ3r1d(44^|U!P zF`j+Dk+Ht!Lbja;3J<68iB%3uw=LOJ+fH(v4JfvqkcZmXMlfZ(R?e>hO@N$^aSS!> zLpSPd8NJEWh;NR0bzg;(YXGZA_FCjeUU{;9VTPxf;P5o9TJzlmS&~y|O3rWHeAqTDwM;=z1cccrTSj%lzq%hnFO|+X0{mXZk+Hoz@5FK0CXjwH=XD3IPM> z?c%~%-$eR_;j~5^4%c3k)0wovFVcQ4LHqtMOc}Qu;1ucBx-KZLcLNneu z|J!Gt(!}!O``9)0i>97{a|{x3PVw%649Fk%;8)W_@Xbh`(?_H+b{C?mcrF?wWS>{~ zbWv7|ltDYG2EL&9@vb|L_S(QH6(PMfnj+{$UZXAzUr@MUNGzkAz8({wQrKzRUPkZ;41|A_*kvmv`8^3&GQN-gvml7t=ypzJAYAI zfYaqNUt5!ZvjpX)9vy2W-0?A6_vdR-pZM%Row`jf(^mWrWrzxR!6YxJ7sluA}M^cE~IjzNt0#w%DxGYumqn&%PXRhsg;Iw9EeA-pOR5^(mz9ZGR ziS?*lEa_2YhHejl$brWD3E6)B{!_zs47ZU${vI-9#>-8gmf0$(J+FR;K2R46aNu7% zj67cY6GB=Ya3uL^8q$7BA%!Tw8@@3r zxIn`Uv{XeP4*=mX=Spx}1B}XA1zZ(?QEo1oBR*4myi|#x=2hiH***l2UD zZftXM3>+wGzFlT7oZQbyFDGt0I8Y^mX?^4QFX`&Z7=MjkF7Ff_R=ClyQ~YNUPuOVe*-)uxp|U~N<@+mnbZ+sENW1qH1@iA-;7EP^KkC9(x{EJ_SUQ!joE zz!qhT5az6pCcW7pK9S9LJe8*Nn={H-#UCZJiOf=#_!VmxvdE_dG0job0=N;3OkyHR z&T2IRmd=n>bYo#?yCpch2k%7Au-m6OvxD_r{;&|brJ@xS)V{n0ob2?R2uIM+kS?=> zf$~>udLpZT^QLx}xHV9@nSPgc&9tOEyxW?T5uVJ2(a&zwW3cIMZ>)Q-otCbNjQ+=J zN(Y>jX4cSJJJ7;V1waiQz_{IT$bX9n(nvl@`fsvE8q+|H4~T`NUCL7sw%^;? z$75l}{5Moy2MPXwTz)23xdam&fh8!pcdCNESfK#ksrlSlmyTNAv}@-eF5@30k7=Oh z29}S+yZ>@Vk>125)KFwpB(!RDqMt@1Tc5p5_TuK=-fTJIHwg$S+3A_LNV*9|ZI^eU z@mEa=4YRc-*4!hM+1Pe#+`di#Z{OMEl)l#z_}ydm`_*^@>Ugak>$rG8URk-6l3RYm z0B13yqH;MCkb^QH^I;3W)h6Ikpa&lUb9V0BN0+`CH3v$OEC#X$awXV@cQyG-gA=#( zFzH@Vzkrk~vzkdIIK9PkC|^3;h|I6;M;c~9Y-gyWnXgeGU`V?J_JxhCFZuzF4L=`7 zTk7dbo?0enLqm%s+(E~(qQE1wdCmHno1f1rZ^79=PLZ*KqB@^jXmcvLtVGm{pa>^G z(ldTx>!>kvdfkK89K z{8u_fs3PxUe>dl-e*35UV+U|}?Gm|H1txV1+3QN3Ge}^ziZ8JiIYzcNyQTS01}zRv zl&jPND4OR%i51M9&vt_;F7fT#x2aJBaC%l^hRvfFmZ{_H_jL@+FkKTct4XTq=}p}> zGb>lxFw$fpmBFrpM{B%Fx;EAQR^Ra^Wg|&w<52a0Phm7$dF!eH`DXtDS#(xS17+_u zQsPbsyX{fBQJOCOvgBYq{FnWz+ISC%Z%E7klki&%D_4Pd-%sCK(b~on@-|_JKlbfHZO@KRpa%6hJl}Tb=DKypK?-g|03axlj=! zI>BIma~?s_hoIilU6y{gyNP3?&!{5A$5%kvvSnuyV`N2srQoVTY8_tP;;@aI@#4B? zmU($^Vv8;|GcK4HrEVzM*bJ_aoc7)6(g_^iBdi|sUO2_f5|GT_WT9@|RLq)?Hn~mm zW^oT$B2mfFu>$FA$h}jf7d&5pghQ(aj}%(wL~t9sKd(5D@IpCi1&~p!f9JW~WgF9W zWXwmU5!3ftQ_05%uU)e$2+G-oAQ4r%bU9v@iJOY*>OZQMYfwdWUEjCE`*|<;e|-;! z%T;1)8`m1J({CT1lRWovrV^mv5UeoA2rP*#BdcX)bqaS4$xV6!txCI-t%0%sRw3}Y z%RP(9E_L9RbHd)63@rf;R@Cm0g-h18)_~v9hKFBRiW0taJ)>sgf|k=4-Jos%A1wS+bPV${kj>JQkrKPP38+1GO487>kZ%)EQ9g?rjbwbglN|X$+NxMR}p4EW!n1D;GcxmOJ ziHFDRG{%nA2sn9fK6S*P*q<&kB)#y`+xW$^EFm~$k6^1-bqUbeu7*NhlK*ef&9pY>Ox{XzP z=jQ!bD$susPjd1Ibz{vbeYJOYJuD(}5wO(PWCN+t)^72enTic|LMcY3l>cQiZJbx= zSo+1*cvyQ&o03v4v+_mE!sIMA4|jK8@$kh4mawT^Ws&RTJ3`RIyM|KAjR2XWusmC6 ztjR6>XRZ2hF+}zp#qN%#9v6MNQQhXCz14JC4C8RAmEpUWDES5i$ zmnXU1(V3<8+qA#3^pVvp%fF`F(aG|Y0^hq=k?|754`ae`-@jYVaK8We(LNbOsS)i} zaF%YVrjalQ#8%N=gLHzH9SPa&e@x`KRC7#1#lnP4r){e|h!om?z=2W%$LY(f)jS_c zJq!nAfB~YML%O>)ZNL+qaJFqA$aN$*-7+S!1@;|%c>vna?p=Rn`D?sveM-qpDwX1TiXyWjg~wv z?|2bHQ>hBj3APp33jbu^F0QqI@v%%8F7_|}?vIbmr);4gY@F@5hhMxq3iOq#(egnj zpK!!JKLYx$bbDv-z(hYC63LJN3Krw%Hzl(O!G&)k#=(F1vfn*d4ex=n)wSsW?O@jg zE@3m^q{uy>U*u;4%3l+hHr4U6RDfgYLD3l}E>%=(l1?McsbX%#M~1RVDy!XGmd$iD z^W=~l>(fV|2WS3h3+M>FU!z zy3H;Egk_jQC?Z&#xZugRzP0tLZ0hqk2iO>u{I{V%-K5Bw5H6gEEV8L&jrKYy@r4Xl z9CN!G8~S!0;E%9Y4m3MfJ32?CB4U#x^78eol>z>16i~_ph!RnwpQ+-rFt^oJX{BqI zJ72P7PhdCQH0lWU#RdY_>#6xcf1PZ@Vp8cS84sTE<2NrdmhWlRtEmfRiQgPr`E7W$ z0YIsL`0<+KbpltqT)BRlzMFv-4hyD1xLyTzw_JSK)x-h}P@rB)PO+nb8zEc%@ZrOb zW#NlJcv9AKtcmz2to1e`|{12y+~B3ov#Yy^liRG=6(Tk@oi|9qDQh-adQ&eBV~Ah&EBd{+Ld5L%N(p8Y6yz6TbBJx997isA|uim&7sqXsE-HA@g_E831Y-0cxxA zvY@iO!MAzmjK}I8HcJfxP>DDHku#lefb`b5x!g7_kh0;g(GCNQhQHzPN zs+gFV4B^?3c*|0Ia8oGl>TYd0;w~Ye5-Iu3T&aX2wC>Vu>|T7jwv=^5DiczjAgX)D z3dv&xxJ3N6zVou#>D7wkWrox5Gzs-`+POR10ShO>0M`nelrc(VEPty?kdTwZ7qNmZ zEm44w(S*ugjQ5B3Jc-Z_r~g8x4Z`{Gc)p=EC%O~Hi8uD!IifRP|=RU=WtbK@+Siqcf|$w;}`F+6!`*gjdId?mx# zDF?x6Aag|(#?yX*ah$EVQ`#XN!|7H?$K>Q9lXjrBr8C&0ADyk`5)8AAj(L5#UFB$z zMiXGj2_ySxdO4iko~VUGW(5~K%Yzxm8SjD8NUa?_le-^tA^^OqF!3{Mvv2509^jsA zOLMB(C_UK}tw(-!;^@&#m0AdRb+yC($YNo>yZNz8+VLL3b#8C<0==>9_g2s>)cT+8 z7BsYA#=ctPCz-8If32X z{-QN*`a=OVo2hW~2ggm2e zP|&Nm4?m3J4hnU#^3I@jg7yiD0s`9Wv|Ze>x7=})e)0g*(DKVa0vK21 zfwNlj`@j$9#WZMMi*31OX}C8zkNL0~fpc!45l6sS>0w={6yhGXQ762AthhhOqabP+ zht!0IeI#}t}x%^j0ngzN{G#=!X(1`!~+gi(Q zQ8iKn3FgtJ0!8Delu(C+XgSRX;R!yC4EXn-A@C`9Qj#b9W=QrH4vd0$6hz+$Ag%*q zy3w_7vdA_0iyF2%LBj?R$H!*BKs#XK2x{LpU}NgXtZj8yxU?#ItlVx_q-1GyxuPdV z1$cQcRAVB5*~GIxMb)6lB%7_NV4i9(W~OjMKB>i_aOBNc*Ij`j*^k3KODK62jO#U+z#$ZDKjPd83A5D#r*hQPM!kOQ1980p%f zq46<*=5GhugHl3}G-h{cFeqah>P9EC((?-oh2H=3FC50s0ZWxzX&bzU9%%6(4 znp;yI>l1FkFL-!7m_K;u{dip3B56=@F@a|X9m4>Os;-@ax~;>(d@gO9y^{-u^tPGu ziB64XK&3eomwqX=nnrA&1;hbu?U9nN`rLCZpB|I8Jq`sfZ1CrZpHoWKSZlW5E@tt7 z@DmYVpKzd_Du^?oOFQvd#BFYvQ9!_*Iu?729ft^YX2EsufINL>u`|J4Ih!FrZnFV? zQ>uZO)(@b-B*S>xa=ZMAR-m29@ZH2_DYs=$Pfv$>AXd0=f9n8u`s5qs4 ztt{lx!TQUCZ;yJ7AAE07n9l4Lo~ElWx?_+vV9FW5 zc9Hg42z@g}pJY$6oN4qPk>Wc6lLT5x?6L2Z2pzZo_9{LgfNLxnLzWaad{t{!soieF z79dRh)?4nf$8kBX_+hppdxX_ltq}J8uZ)>mIh`C8152RtvZWZ@yqP+CYL*rA{CWMF z55;M-Ggwak|FHL#QBg*H^r#{VDk4ailr)HdbcqtuA|zsjOG(}!>0zJ0W{w%;ni?`ftLu$p;!LGS5sboiH%_e+w@zq;!@29CTsF_;PT)ywBno9A(9FQDYH#h0Rb~$PH;qMV#lBYm3Cv+-X`NUw{J%DXG=CEA%RVG zaB#Cle=6rmfSDQeRee4#gtVr?+e|V2i!qV39`;L0vF|-}6TI`{+H^X|qeilC)cOB0 zcIm+VzYJDx$N$Iv;QxNe4*&l@-unL+0oV@z-xgu`AK)P3jvmc=uimcx@G!z*^2+${ z%_stqurvJzsJNGoWH;IkPEWhL**Db2^cw^4q(n>FTaub@H4=aUdI^?t2aD)d zL9(~F{wvh9=#?)iiV?ND>)7Zo`SwZTdx^e44|70ZynYc!76pwe8v{}OcNgcp1fqV8 zH|qBJ2}bhxDAq`6J5js+TO)>g5NPaD%$jeK01Y>XprAG|7e7K3!J9t~m@UwZcuGR{ zUrTzNrS0vAJBJO;CoZMZSl9#P+evCx^EJQshu;_T%=vDKh9eG|Sz@7Mo!YzexOz!0 zK~Z}iK$*%R?y^L3X7&DqkI8eh2g11C(pih4eOXB3JAzm_c!KWx5z)hcpy~~@h1lQ7 zD>=X2Fof&w0m2BgzW%E^EK-h|f`YFVPb!2&lrQ3kQAF{s?=D~IdBkd@UsOs8GD+i7 zMoa?NME+O2@jhDdl@u_m8>ga7F#v`+zw6|2rLoAevsbHKFBi@ajEp1$9A)*$Wk?7; z*b#Gl3}meCT|6LFxD2i#@F+NfGBqPSXIg9^Mh&0S_vmNq55Bx~Tp03iRp_R+9aC}M z*PO?#uP-!1Yoaq7ORHI=Tb8;#M|KGB=D4mpC=2bqI`6Q7$C3LLR4C zjn|wz2=Bz42YREn!6pZitvB{VrCS+_*-qr-I^~eOn~s$YKPDgo>KY-Fy^e}@72F*H z&w6W-Qm^8jpDg{sBz$)}04o_7cva#61}kFZ;I(X9#Y>2$?Zs?0>$@x&Pi6X6|C{Qe zlXy9Cq;gJiparGG^$mAG;MPr$BI-{r?vVehSI9l)O#Bn zhI0+qWF@R4z@+_i^6xsyPs6vEBRmT#)AJ{OrS6_vS;R@!?M%6d^}oYiWllOg0}{G) zEllkJax!1t(0aY-{&=np5ugsMFP;BA>*ci6q|YrPm$mo6fwXzVw0@kfrHpC1)fqJK zTgb!skOH7az8jQDXJE$bZ{DeDsF9votsbnLA5j$MtJeqG-pHLD>PE$DFTchLS$su* z`a~{lyYLJ{e}^dUo5QrT-gY1bJQWGdh&Ia^#s+{BD5;uGJ7{2Dcj=o& zy)%0GJoww#3-HoOwORyJ*5=e({zVb7zB19N^I~1MjJ&awoHnpeqW{Z)b=(0A5n!b& z=fE6C^0#j<4DS-5zP*WnW{CaH_LnkgI;H(c;DwG|s9%D!0mK8Jr?TjZq#i(wghv{N zgxGZVS&WF5h;Z%69|QnRlv|ziyz^*wc{b@y&LRW$@+fu9x5m8O?D5JM;uT3^Au<^=g?_Mv%+F z=X!EHxi=8CH}8#T8lSVQ^32pK|82W}siYJ#l(5BoG5n+s5A-fOV(Jh97kz!7{TSTl z@NKd-iKTsc3u7Pi44{p=c4fTUgBcWL)MV?|RxCM}u&p!-R!pwiY8kwg!KFHMPa{<< z%ozYE0fN5p~8i~{$#~k$>D-GKEP#= z%*8xDzvBOFHEw30Pd*lt@gDa)HmJB(0MrYhzye78?J9@qY>sOkcKP;KPbdP=2n;As{iNJ$*r z<=|>>&!9e4Z0GE_a^efK_oHztkoQEMRO(LNt-2CMvARX^`rpZzP`9F7H}tYw_$+hn7kNvsbpEH#~m z9vV$KpE4ewaG|a`iRP=sma?+dwdbAvKiw^Vd2gv}cWXokm27lV;d}1Hg1>NfxspUG1jvaq!|1gSwS8KB!Nn6}(T5IX7<$J?g@VaGous>|$c^rwn4f zUYm`^!sd7#8Rpn5xAb~;Z4q~L5Xr3v@tKd z5I@rI;1#gjk2lKn&zJ>`DwN%AR|gX`57lCAkm~nWYaON|>X(cYXQ8+}r>lSH>~0_f ztwG$zy`_EtYZ4q9-V5GNd{j#9k-U2g^W`Yq$;$sD>TLEPISaNchm2hmoa-My`{isU zx%x?aqI$w*hP97=uM;HxVk&c2bnOIBU491@fHPx6&4hdIi1pB*I=zJsNR?#z#SUS^ zhAmIxe=XBhV0{N5~mPiy_;ABI6rM^szhpq5MI)u8d$z1(c$rtpdVqrl85jm>8P zh$hn!xqL|>T<=%kTUOa^G(B}%UX>Dsw0U-Mo#lTsrE7Q~FZ(|4cAse+)O~L6@h)gv zX{{j-IO>eZZ|rW1fGkvsG#fmUHjs>H;KZ3XWyiUpA_G2YiP_(2*xz3y+O%!0hNsu} zo)Su%Km_Z=&EthS+VX2Qonx*{$`*5F2OK|drw$2KyP(>}=`-SdrwI2m?IlhG_d3$7 zN_&vNgu`%1X_Z)aU+U&o-)H%=pgD^9>o*Jafd)QH%%pbkvAP=7woz-`2>l?q-Wr-T zRqS$HyS;b10Vz4i^xdx^DrBBmYyv)xcGt+i6u zOs|{u<~$wMY61>YvC)8rc4%4~Tkv3;?Z3}Rp3f3X85ME`kPZ^3K5FFk_O}S?F4}rl zL0H%a$_}8Wes26kWU3jrl^kSzY z$Jzou{TtsnI&aGQ%Lm6_#|J%SYVz8l3nyvjl(`&O-#X-xqIYvfx$o>9T|=dsz)Q+1 zOcRnGJ4+(hm+QKtj@%`JQv2-}ye${o44+P%*77=nKPor7dTr*YR}kYCZ(Kh#WA~ak zjNKgB2(?W)D8vCPSz^(EUW&-AE;lz>^LTTZBa4)3wvfrIO`@8RH1#;jUkN5+j)SgC zdiH=}rQWga(sc*=hVikawm$#z4ZFVtzge46UyC#>9F;wG^Ej=YR*Cq$+Dk(d4ll&T zwP#9VW{YtMADgEd$UXqid{gZv4#~wM>J7@GEd9&eIxpX(F-q^3j~(PH_}a2v{n?2HSJkD{8dR7Y z8onsHOX{$XNh(FsdDkvKwH@&jYpZveiWkZDty62hVhrUi8gkvxXYcwodueT+yopv_ z86PeuLCT5??k-iMSqirbG)-1(wXGKPPvrOfdy~7XvD-{&*)^L#x%o$aDoo(l&!3Cf z_Q$4^E%}2z7wbAy+x}&Z%+5ggVZKx6zQ4(?+(&&=!-$yu_;;%JRse|6!Efndx2}k- zxOHgz)qeBmgQr2OgP$qVhhy``Www(>f`_}3cqY0dW@&%@%wLrhd%$(`w{($+gpnF5 z-3GaYc9Y7BWex{)iB2z(Q9n0*a!0iIew;+P_JOCD|I`u1eiUY-y!qg=1tZ%JR~5W&Rg$oJZvA#xgI)E@$J-kVYTZw@a5HOB zaa?xk6>Ptx&pKAGM$-x%rvMZd%5g+WD(K!6JHZe0p^MgxIHeQF)=K}H2r0e4?}x_Y zGH%iZ|KYur9_PaT(|FOa>LR)dz4tu#C)%ePvMnsX&F;=3BW{O=f3$Ta-+d1ZzN4ZW0Hur;$>!f)yxoRHOc>oh*=Xl)iB13uOe)w54 z>zj&b2MrE{XIMMX!`bfqdF1Aji2bbMEx`&yxxDf@spR>CoigsWNRyUdnjTEwGi&iN zGM(pK#JsaD#P=PY)CXO1H5*K)acJN7Rn!k_v1bWq9r$EHWZ|V`YoQf``6?O$VStYe za;O_BGK?AjaXdh0_h0E`WV%@I*a(o>&#D|QnK&~sgmM&@c~`U?8#g#1&Khv4JBUxf z$o)uRb@!R{L6m}c@><5#c#kSB^}=Rg(#?_jlQu%CPsY5(+PChyjb?vz2|1VSNN%;! z6?3m&GzDAE(NJ)87;-nJy3*Ck)Z|PipzB7YV1|$Vt>v_un2iRdC_{C2d(YJ+u(7`e z(vU_&oZI5@wiUi>ntAFw_8V^c*?Fyo)(|Ixpki5h@RxvlVj-8SW5LRqg zmKiw(XYDQ@y$M{OY#jb2RCLjM9_mRshfTe^OCgBwC^#4hb};6hCLoH+SQu ziORRGcZw%%OyWrH=?%JSezWfZrHcv`j87*TyxkQv_)}t4KC5d*Ggsez=tiV@njI=5 z*Y=#j-E%hmuRk=1{Q;Ls;E}Z3`}}M(*OeR}F|RTb1{j36{omE#O62dDwX$_G8BPec zb0yi(0$}|VNnqs*sGu9nm#r>{FLhv`HP%!DWO4_o>_x<;{O72*npKVJE*w1CDq;I* z^{_ZB`;WK8W%P7TSxndcim1e=+!Vgl!4 zR(y9AUSn-5;r5#@uI#7WTvsQE*>i70KD#J(vIz0DItj5YB6B;K+atnuPkqKM2@j;~ zL4xV->zubxv%c;2hih;e%d;2^je+Nrgbi`<6wGzo4wM`x4j|T9FQ-38^s1-SUhJAw z*-VI-C&l56U)1?P6As5YqOFnxG~ab68^#ZW^1lyqujswbc18a@JV$S6=@3>)Jgvg9 z22v_`$nNYu%&Zc^e}x=Co;|3Mk+ymQa&f!PUfO~E_V^cFjL855HIuB&*=siLPk&oPe#RD}8u_7ZGl1H`pKv|T2-&%EW)8J6?(7DxP zP7(Qzr|+9x%I+H!`1yl1E)A&xvG;H$X=ql4D_XX)^;-Y;U4+%X&};{tHu8CaJm5&;`lf&@t8(RQ<3mz< zd27-oIc)ja(=hoEgL|U0_b(FMKJ4oDuxt*Z@mT%J^Ze8GAxBxBIotBi)}Nx^D=~;& zGy^sAzK2A?n`w!7ST(D3ye*te&%mxZ zg90gkzFy7kKXKDiIImh5ai0E&-}biEFquo@o`zEwE$g|jcqNjfR}r&Nhv9iO$fFh8&iIr@CM6sn9T6p&_RCN z8Ce)SoGB%OG?}O*LvOu4gdFuMiK6hfw*50IN9F`dMixWU3xZVvU9hoA-FW{|*C4`r z#y^yo{YW}NLTTzf9;?t>f=ghvI$Mf${htj7J%MD_{W83Jr$hL!G`C+)5-_QEmNe(F znAAncmDaSsT}ylQwhf(d{bsdYa6iZhapEi>!`?|iXi9kyf55lDR++zxI>YCTbG~RZ z4+^kZ5J@>GlX_YcZXteI(C}RUp_XEfCWd@I4Qq^G>eH@#RM|;5NVR8jnJ4TZ9rDm} zOfOz!)%?Yt(6#T3kPW1@CZ3i1+i9J##cyr)7i@RM3P2?_`iMzTb|y`zqkY_3gvfk4 z0c-J<(-WKo2u5-Dsz&+f%763lH42Tt;bd?Rv=v|PT6W$WCwgofV8}OS z_}`GJm~qJ%jgI>h=|gI*mFQ;TZjXC_Y=6RF@s5id% z#)SIew0GZB`MNZ?>0Jtr4y_|(^KgwvlBB$?Z8s5H+XN2m0)f!A-`p4VghQd)fycitm)z*HX3>}2ylbU>ozd0& zQM|H{ET)v_0l#)ur;p;JZ4pr^ktZj zusI;uqIO1HJd9wUZIl=s$2$+hn~euu)a<~h!Srca26bzOI64tI+rU-D-o|v{d}BU% zV&m~vUZRy3tv>Pim8sXP{<%bkl#{ZNfu-yb4EvLC&zI%|nN=wwG|^upp6fOy-J<3-mXdZ(Gr{gJzNn3hI_G>z%_Q}`mp--Xog&|CxEj z*6YcDG}sE<1VXHpZP)07BeB*b`GK9CPzj?!Kp>HuC$UJRqRX-^Y*0k->NiIP6qr0Q+HS-jlWNHTr0nCq z^O5$%#s0Q3BaGLqX!NQiQgf3dIfYS-q7!}_di}caD$8HrTr_n`f$*b^jP}x1G7h$C~lRZ|uAZF&a5t)`g zAQw_ARwgpDV^W1LLtK_~!L880k9ZEDj)RS0bn4WfTj2OoU*ixJX9ku2LNLO~HGoN7oZqB-|u(G6$Z;<}@c1a8&i#09d=|>r?N_s*3>Tnu( zhTXef!L@PhaWl<_Ep{W%biF}8=oK7#z}S(Khgba)bjhcu8?zZ5OSQB~hg~h_M5hmf zIe;GdtsW5@)_>G|vPa`|ONbx-*UQojqs*rF0pmV8I0$lA%kR^<3(v?+%nMdt?e) z>B^7r|NABK|2zX0biJ5+c6u`XgKiTeR;y?!Mu;VUWm-p6L^p?xxc9cdJAbMwPv83T za(mKpeyP2Cn9jY1_XB;6UOQU}-I%cFg+0RclO2O$6v8&B)tufx8u!PN>|!V~gqMiD zc-Ws5E*MhTeBmicV?36~E^l}9;8#lDkK56Kr;#AmlN9}n_#M=!)t<2L2Q7d1x%Os% zw@L}xjMChTq79yS0P=A!KUz#m+LizK_Ib$4XI@+DvFoH0t@eINr8A_W<_<0y=#EA=kdi`;37#KN!6nxra>xe}Ez zt2#iuA@%7o9Np65fZ7qwiPo}OF8uwaoyyRL^g^cxN{9_^vuD0I%Eo|~z&FaCl3fQVZc!*5 ztu?Di8`V5aUX|=*JrT{`pr7fpPtU1ns}uHa(Y37=K4U)`U{3^_47TJj3UBNNjm%%h z?9Mt5^EsIYtrdw`JS0bqjyn1cc`+r9V4SFo&WImgW%MvDdB5ompzOsYQN`ZUI*syR z>Z4V8`8)7gXYdnb?|5eokHhwDA^e%EOIAx{bvYw0MON`)C z%AQ{GJ9e#wg2cabx>Sc)HX@1Px0{2kb!|lz*fUW975aO}no}0tDoe0bUfY0A0(^6i zdUE@@v`6kJk+tz?!y}Cv9j4Kcd%Jg6CeBu6^>`JCwIDA;s=NXW0`;_XWI!_Po0Xuq4kS30-*-Ey z><$zl6N6 zrD(4Z?ELSQET0x)EWKNDUd)p+a^yK_$UXDGjaQ!R(4uQ=JhM}6r4jv1qq<~;j7*Qn$()XQ}NhQCxXtGh?|;}{Ntk&qvB_skZAnN z5WdRBfAqvhjFY+x;=Qet6uQIMP;D8Zy&U!r2|jQ_%T4;Ahm1bh1FKsG58Rh!BWHSt z9w;5j+@(B%Th>VRU)hqW8QGKxi_=VIpgdfH3-d3BI_+jsvPl%0dr;DES zB6fWnESL|1$j8#Ufdf%9N%|$z$pIi88FU*Na#pYsZpqaUbn$KH$c6XwGlsXXc=QFM z^LHzq#8EPwlo1D*iHLF!d_jlqJK)F+ zMK~{?KlR%>x^#5zafD4wG9Fle#{*P6CQiC;M{RJ><7FE5=0xh+uG*7 zm*LuJpKHvlp-HA6Z~W0xK|IaMUZ`hWV@MfV5*Ks#VQNoo7oOSYC3cvZ6+RK6YMPs? zP|JS%j1H5fb~{PH`5A^0{Zeb+elIHP)uBc|qfEl0OQk7a`m(BNzmeAA>)LCZYYH+C z?p4Xv7b+^>lwv=IN50o2x!~F13t|3k*s@7AD(b8yqr#|=6mPUw2&3cE*2}cIjIGYs zZ9E=&-=TB_b9*Hkk)kK~`Iis$?OTpmy_D9u^S}PsZAYkxSG1*`NvBjt8pk~IZGUus zwn0Fjp?LuNfwwSw3}r8P?*8{jD~BI5u~t5o$0rL<6MGS}%SsHfhv|ghR%S2Wm(@rU z+zlrE)ir#;3ddxXiV!AO8YG+=#?4q5WfxZaRF)_eN1^@0!Qm54gMS_(<)rKRbDk-< zDr8VtznGFLOs@?tU8ddgUWg)^&EH&xS;A9K+2K~%d(6eNgiF(fzoH3^S`d5w%!#wg zP;phdW5yqcqtclra}Y(@zDG{3OXqi17x>1hS0#fzU`$ydQD zA482;U$E*>A}iCA9R$g44d`%jc^ynrYPgwuWr;s<=ofc0FFfk-9he-ls6hP|np4LX z6FxMU_x_t()&nk2bmXn@2F9mYq&qLv1|5s<%N%bdRW<&lKZ2Fog>~ESoT>}9Qj1LJ zw(s)I5G`e6N34}B##}lS^3l5O-#W8Hr0A)TM2A}7svG<|vUn)>6T!Y4`(qJJD~ zx&;(dA_u)}5A>Wy6aHlFgE~5fhpTeZv+XKg$$z&SX)BxaOv@`h71}xrOYDXruuKi8 zgS89zlT)n--nuG^|Co(8UW%yqQ=oO2@wrVttSX@k5kWs?_Vp@@D(smx22idR-y-ylb)JQii@FtLCnb{qf=a4KW3Gl8|Hf|EqnPI{G0o>K z9Czx@+JFyyq|UW2ex`Gu&5>7E^OxmZy962n8*f&9yZtoCa3_%L5qIY$BnnN`w;}im zkBzs#r}LuVeu!%F$KQ`OusNnOT*JrvsyK8AOYRH|VM{GS8UtpF%ACpfv$?FC2V0nu z^$Q*-H8}QAyE9>lY6;?JC>}MP49Wc7o<(m`PZh?yOKa+rIKC^p{@H@h?Hyjdc>ATJ zp9;MBt`~d}BYc^qwe{!av>-Itqc%tQu>zmnH8@N`Xt{_RUX2xgUK}ie^gZMm+xbdk z&d<>89h~P)D_LLFD zck39Qj?K`=q{P2!K&cdvO-Q`o4r?7Or|HD|75l2B{+)y?&KEzY(yaHtU5(BZNIk~C z*iRSDPFVTXb(?OoHpC*YY-q~7XW?X*aD}>EBnoXB-!Wm_(i-?C5T@P!kjMs7cPFSs zFB%P8Wl+JpN}#gBh_bFHF5+!_+&oro*FvU;QB%5GFx2{Zuj4uqf<6ehb`f*&6@2h_ z5N?UFzZ-w~H>oviu922_`u!2i{p{q8G5RSIuo!}IZH`!^&2#YH*^BuS%Qszvw2EFTSDqSj2-%+m{zf`Genb z=z}D>CB{K2#N;)w#5Fus)TF1dnO;^=ac)}bO)bYoZ z66q!}^`!N7p*`;~^woQhCNQ=AlPz+C?fX^|EIh#QXf#@a)TPBvVF~q8lMW*s@=Zo(xw`D1X24$2SQtRK)^?*4$@)JAff)>os(q$Ull6uhu2?j#?z9 z>6nwaluI}LKg~JLofLMfa^a#?cs`~^?-^^$sPL=J!eT*Td77DG(=}1ni-kE{7YE(1 zJ2Z4;3hkBDfg>k}ZfrbiS+~xXcoO#I8KL|TSe*XMjr(1O7Y;6~qo@4)r=Iwv$T)C2 zm3a)#fD%a_%%z=fKF{gmEB?5=)0PTZQt;DDiutVS$8?yielhp6vp|CUKo{UYb2cjU z-oKQ%MFDEE0=y4*=cj$5EOvQN+OKHn>IoH+2&M|cM$NQF(MSZ~1BoR2I902PcsAg&Rxt7? z=;lW>n%G(I?^`Er>d$2?KM0Wtz{PC?I7;6897l}vJ+<}<9N%i5CJc_Eqpc!?3WbmT zX(k298tkmFw52N%uT7%RC-NYtLXwZ8-a#)I<^@3z(j}G2NWZy20BIO~S3xb4kQ1c4 zOwdp8Q&u85Dq1CNx0rahH0=2DA8i8AYTdpQzR`tik=iqk0WPEAk996)R5H3&5uX2J z`h_r<)=txJ>wEujaUFm@)WRN!)v~L+doX;J#TneeytpK+gO3fIQR(Y5qTNv$nE zH4oltycX=;zpFfFhE(Zh*O|gEw+LZxehBn{s{~x@IF71NUqcy&nz}mUi#|M|K|VgH zTV9x6(F6(CiVWdDN;hg zu$hfn*wxvZcF@G>w`SBim98kU z`I~SvGwK$d`!*pHKGs?vA8*Gw?ej$K!C0@%lb=kD9QjVw*NDt6>z#cJSRWwS)tYuL zOpc3DJA$bZ*lGSyyQM%E=U%ibd4+w)diWh=_gv!Z8Z#Me;`~%esE!+M)o$Op&`_^R ztNbccEWmd5;68TsEVIU(^Ly(}$EVq+d(eiI_dmXR{`~&sH}C>hHVn;&H+Rn)KO$me zIaD@YSY(f01de3Xml)ve;(W9YHD!N%tIWdA!9wBK@RQTvstu4RL}NJ9rF;}e2B>+? zwTcygdV(=2IO9ce=BglvA$Qy7vKB>Z?_z*U&Db(Q91N11*drT-d#W-Z!4tJQ=yV)x-m}3kX&yK_dojWw*Bs}8fOl%@v9B>DGJWDycxGbh zYq7wu_c{n*!00Dp$&A{$(bEx8RQOUBA8bsO`M*kY{nmf`%j=bh`8?Y9?qS#gq)4w( z$ZsQ$XZ4$3XLNsZtey+DU(++q#RYO1DgJX*Qd2Jkv+YR!H(g284%(|jJNU}!m&RI` ziP;@wlk=1vok>S`wLp~A$QnTkBoS$upB6Yb#Yamy$bA>{dZ7%x+-rUmo z1$FmNyS39O$GS&6iopJRQr?DyXuQAJq>F&UX|P%2h>QH z<^w*Z(*{W&Y@p21KG3vSc3SdMZblj;(cWAc$ebfjbXC%lyRJ;9d4N$Zcg{rsE+Qu8 z=))DutLbx5r9T63~C?UmQCc8C38|2;s)-=c0&#p05R$UJqeKLsFFg0V;5=A)jLSY=r+P&;o*Bw}zc4G%YhupZ-l!Iu>jQPFPy(+??#PP;0-~+8D^3Ww zgW4<7Dl^t2B+l3*E>{3(TG;j0=u&14r*O|03u)>uLmm>@?j4OOZ1e*KUUfxTs+$F1 zJUi?0_u*Anz)R_ZDlhH+r?mz$g($7hb8ClllV6I|mldU^a;E9fil1sECB|8k)tH5j zH>+;t<@%TlS~UTJB4q#566#YRaouV>aC9ht00Y3|s+Z+}85d>pQ`5+}Y?>>Jqh-;yly z<+&tKl;x2hF-8!RvQ}v(zk8Z!@EH>C;y@7;-5IBGLPMaFd- zmo4kwnM9-W(RiVIH7pW~LV=bI$J^g^G_=xuLO>0zf6q*k-K^d;3+FJ0IU}y zK4`s;6JL5MWlOX6Qy@Ayat1IVc+No4Uh6ZOwobDBk`onuo2j-JljeKfk@qMD9m_@7 z>(K|6LZ>7295)v!4q`A7?v;ciKShv0uawxBNdS}Z3qGHEpDD44z5hq~(mWk~{<;mK zEz_PN;*(wx*_hNVV2ffWE;ua^_@rUUa5G!;RM>(nY2Zb{oT-OIN&aS$R#CZCG6fT{hNckI?(Fpd3M&D|V#)d26 z#Iml_Wo(QQDVjECd?H8-B%FO#eD2@7p+4qfAL!!Bz;``M`buo7MW7Q_dSZ4=@4@R^ zS^)nCcRU1i*bmfG%OfELE9@>%@Dm0j#IY?(p*MNTI;&1u2DI=WW(rv9Cc}_U;%`WTNy(6I=u%BtpsEcWtIAj;@^v@j#_@z$U(sS734szLQyk(M90%{ z)>?KT4%t%|Z02Q-Klzlm>f44}tQN=P(aE&SBFEt-YxJGy7)UP$|0q_4lRF16<^Rv~ zSNW(#fu0;W=)=M%Eh*wwN}2VC_%o007ZRzV<%HV#1jGD0Kl5$oK}cU#WQdLyYlN>FyoK<1`vEbqWM4WJ+0;?w< zr|UZ;)eJrjX+dnUm~-U(YZ;=xN4csp7E%r+e#oyP#~nhqgJONO8s|$Y=;(r$R%R%6 z;2@Gn`bbJWp-2p0^Hf{G_&+BBI2Ucd1gHF&s|i02FvUQ)PEN98NCd)u(TTwAlJmY} zd=uYJ;$ROQ+8$SK*3I{c^TC<75}DV0r(%`D+;<;?a=H4sSB<3Dp9VS{F@{r*%4ADA zXe}l*5^$a+$^7UgEH1dWJ<~^vbFV#<(D@=%xM?`0 zl`_;FYl~}a=OK%$#sBGtJAU`kx;4U(-ujwQQZTdK&ZYC{u4#Q$1^I`^4)pOYG_1e! ztr+k#8I^C|DLo;&0eciW$_tB?`ut1&3??R2Ta_cwG0x-d1&57LDt=wQy2dlVlraEm zKNXow;f+`MPqBt0HeKy$d(8WZ99XJ-0aLqp zkUR}|jcDVSa&_}?-?Zb$dG9?MNx{E)R=k0D4z$2#WbccEMKXm0TjF;G3ETaaV}BWN z!ZdOdxLp+yK9`^SM>D276vC~7Cbw&aC`i$+-vK0Nif#0_a*m=sZ&1vH_eN%kLmN#$ zEvdr(eK%*9#I2B-FJoCf_d*D_?}tKg`y)y&VG6Pq`4bR?cG)74O@;^u$N77U@8X?%TlEkW+{}3x%i{3zfy39&E zuDwXnsZZUrnR_o&XhDZe7W;xm`_jJY2RPyhV-K}6NAKZkmr7QO{U)x*oPL)_zeJtz zt+PHF9A^$u=-6p{eeyGz1Li+9*lOq_#+Up-1;DbZyxn)VMEW~r$fD1i_JrNWL(e^d z5S*Sj$hqLRGt=Ziq5vw%>=Tc{q*Ig`$8 zpa*#T9eWUq-{ie{kzKOKXT*FVcDLi$%%#-7#F>mNC~>SWHYVWfx$$#22%qApr57qo znZEw7@r!Wc=g&I$%Wm(+NE*NXUr}HD zf2Iw9JW;N&@EWED{q|6ho>9!1*W1)(=1hwcKJL2(%=yQYX4)^aFnIU(wD@0?Kj4yF zJumv>y-te!HBk)u*>ckvu`*PmX2a~pzvfZO5$K;KOJozwBdbnlPI$bY)7iPSmb65q zma&BSs82*i`W)t-q{9XA@bJ1xg7XV|ZpXTs_9u`@V9AE~S3fXLd3XuslBb_eR@GQV z4d^@MMj~X*07{noaOaXB0!zi$P}xRTi%?0B1~s)Uez%x&Gm7WKvz}J%U>t z;)!pPtNME1PdKr{!SYG=L1*8&fTBGXP;(hFU{2hA_1NJq5sNsKi?5yj1+K%WJj+sf zyz}7vxzrYU96+^5Y|#i9Q}@|k^-1(pnmFyQ=*j--r#r{F8MtNywMY20*^bQXoM=G3 z!+zxI%GGiq-)x#RA(eZ;j*kh*viqqgs~%gEj~|rDVqix?^3~I-pPO?~IlJdmlP$~y z*4*;`$kxYwV!RJ-0ebY_QnRYak|>YtfI2Rjo36`-d$Gf9H+l4Qp4LG(CEuJ7jnhs_ zB+uzzynOufl}FJ%tM8u2_0-6Jm?aQATYg6tC##oYWW*SA(1tfR`gik&9I1Qw5KH^L zjvgioBXBEk#9n0VonS8>{n@Wol<5!WfN03eUu-4GP++&LniS^^N?+d=U-CD3`p*4S zbej34c1&Fq!UyWUA|+!et5B7I--k#ny(wh@->{21?WPErV7A2pCO(+3)X2@>#^E8> z&zlv}Oq^`%JP?Sahe|a4%W^jv^hMceRks1R69Vt&Pl~P6w_@|LX+q+^L#u(9k;z%u z0nJ_aC3%;nuBtlchk)pSe(Le=@|(x91|rJ|pe^$c%xPQ#Uyt7SURlpeW;Y76OD9|Q z8{{7p0lo9h3+UDx#R2=I z16!~FVHFj6;6KN=+%;CqIL`=eKg%0JVD7WhOB?$_(t!&rWVi6bK$4d&px?}UQVw_7 zimTfU%6!MOI`R4oMNo`v;-2ONO2XP{x+xV%YomAQ%Ny*irz)8L`U?i7{u~m}d%{z0 z2+-u-2eXNLIEz@kB9`sMN%V7OnEaAaRTQJzrJ< z$`%zLy7#K?;L)Gkh5*}~PORD@orm-NG+GVLJ2WL!c}Cz54A884C#c3upYs z3J16%N!pOUpSxRV;vTv3fnM%7rD~UyrI|nZx+MqI!Hs?S-aZYxaoO=#TLKv6Gq9~< zmvS?3=LJC)4>O0s#j5PFLY znYdrGv8<5s)sf#I(+>)1V9kIMz!Y>D7Ip61B=OI|Mq}y;@^*TS@D>Nz80 zFHIZQ{Xt@8iI#}~13)Bor|UU5O^!AQElU*DdqPgSolAD6r66$ulta5r8&(q%toYQ@ znR?U`?Ln&C&*Sb#;?U@P{9`2O zg=!$*5o2fA?(nm>LI*tmEa*%X$36^^%IF<2vTlxZvPK>CRZ}bFEL?-uUPe8$jx8!hDnEaS&>gYhY|a zHl0AMElP-`S30})V4AB>88ZTQuPrKT51nB4=~BprSH#@N@Lzur8a;-K!Rh{A?EPn4 zQ(Mz8j2}CS2o^-7iHLL%P-!8EA_xjd?;yR`&_XB{q!;NuNEd0+r6f_14vByeI#NRq zA&>we@NUjI_j7wa|NrOr_kOtYAtalfz1EtUH8X3f|6v^vDxr1DLJSovTW;hlHvUcr zs8~i`IYqfrY5cjW!AH+6B$r@6@ z80|4kR}(9KR%UstoufE(*grC&PRX_i$#3 zNiICTbyfRU*D>x=iBsz9n10^#r^z+C?s7D3r)PCH&Q-3L?%1zY)qkF#Jp#^q8)@D8 zQO=9ciqJeAVuwjO&ie1ZxN8%C@12*nG)o$OUQWCd)O0@!h9--0kaUbgQL!W9`N-d2 z3-Po;s8^H`ts?5_lav^K#=09%Ce&krWIO4=kIygOEuW}&pVl5#A;vQWJ-O2_*Eb1h z(53dL>%N@hu{)okF^Y=fZdY?4TFSt0Ocjq13H5yIZf!bTE2OM{4vf!WkAM>+5Dq#! z?pz-mThVX15r#$q*&*REUjU)O6tnF1Ehj8hk)%{x)h=n$30JYTfe5qN&P;k!f>Wa- zpM+GuyzD5=9;6wl$R4w-!g>ANMRdjd(s%h*oFpWxs^h7x^d+=;idl+`VArjpOd(qk zq)=>jKP*l{NrOvjYTrcip0s!*`jtv@5v=3H``2p414XudF{Aee;pxn0&AhF5)e|f^ z&hf{jtAt9UdlI`sAsj{XyM%IQR5>VOe(_v4JRZBlVoZPhBfoQ3sZ&hd!UypjAN^!M;^S=4E!D9XZdsdwSrO|KeyaBM@Azs}jbe@rihz zr=zl?VIc7*55&18i`@P(<;cZjUTbJJQiNVoHHe-odC?xCk{QWU6EdsjPMtG>>m}a$7D7;5WXAbT0U_lCnaCDJN5GLFoBZ9Sdw3qa$9^(r zHwFjKW^%SaT7zjC*qG87wY@3=!VJceUq#>f?UpP#d#mo1R>Ie})LVlwl-LurX^5xu z*?@b$bog}u3xoo+^U_Gl+?K`)fh(qTDl2rU=(-8FBzOaS%SFG9OL@M)8nMz2+f zXgI&9HvcoS{6o@DC)C>y?}QYMXINqF#tu*3b|eeKifzsxI5+{>_4Lwt&c3E69T+SO z@7CO>{y9)1ezKTm~v3zU=nLjlTNsSi9RV!yP!*WOH9n?fkm2)NX8HcATd(i(D~#A9yS5s|UN& zUk#l)^_Y*v1|IYT+&N*|}uuDcC3B_o(9XX1^cu@(qA0 zWG_JCX#=c=YQwKc5v4(EoHVbLEwe4dBZ7(;fOFAv#|P_;N1^R8>kL5UQQ);F@DWIB z1LFI_C6{%sISIEFk4y9+8%IJ~K)Z<@UA6wq-QB`fMocTL>Wy4JPNNavBTKMhg;Xg;`T&7VYV) zeyM|N)vp~6crj=({8f^qo(}@W{Ji6|*=#jBH{a1enl9*>3DlA(z8~Nb;vvG5?O`K$ zl*#HK`FL+i{d4B|X3LHajWCt81wIOV?FxEWwMCiFDDcrW0e*WxfV<`P2QCN3e~Nvn z`0^z61%)iR5XCFV9^s3I@fyH0yAGJnNjz338~`2$=o+>H6^`CSn_S9lc)mz`$xETz zQkvEGI@(_8Vp(slBk(xRyi_&?BT=QU)VuG_;~YHl#e^9o!1ljOmt;qGg>7^TD-%wO z1)pudJDOXI1}VM%x<9rG>5#zb`Sk9mM_IMs!}6XTC@oIqLZ$ekeYQ-Zl5(8|k*Per zqE#j=fJsy)%vD+N8FtxwU|Cc$9Qp)y@A~DNhOkBG$^HvCKWU|#GC%yeKFt3pluC9I z#YFP9-Qbf(0YAK~z}q!duxh`zo4>Bj{CRHiWqD<8oZyUwrl5_~TY3R6&EUi5$>m{~?EO%rMe{31%|ecV zy-v#ndIQio^rsFRM)Iiwh&x`WWcRII{MvpH72#=C$`Mt3DtG2Q)J>Ud777ZvV}!X3 zjyJjW0Y9e>?&`Sv81t!rj=1_$?X+UQ;s`SLHOBAl645KGI#q{o_@-&6v z)@t{x;8DO>LGAjA@ZMG3JA89xL;HJ<+$c){MYgf^yy(4G9q8DS3)ItZ+4?w*jq<=&D6jKkG?MEMedy;}}wDCvC_(yI5BrB*fr>r?A=ZRG+@kS$V9oU&Kt z_=P8D@s%E;BDL$}nJ&Qg zr*w+SL_|Kp+A)2}vFGSbmBzn4th}OGu%rq^`x^51nT;8FMLIGOqoFz%gXL2zyo39* zRYA6yTPCea6PhC^j;tf+7BEs0S^1qc_N75t$+gsG(X;+{jK1Si)z{J*LVtk|4v^>A zrFb1#c1L&5Cv4&KvMMdNX;0j0!&|~H2WP&u8$l7sXCI!Vq>#Gpou`_iJ@UjVz>b?; zAbsgXcbj`e7HFXc>@LV=x-0X(*W+`}HS7#f1ZQ5!Ip3>vO!6OgaM@nuIEtXa*VI2L z`0HFAS0vHHl|`J>=EJS+xSb(Z8lGj=$?`KrYJ~SM6&|w9z7SI4Rg0s`m^v>pEqzzo zebDx@@?})yt-Bh+9s>yar?#5iA-qW56FtleR;EMJun#c#UL1(zDeOZ48W)vnpwaDo z#&oReVO8{UVp7t|ku9>h2DQ~^!3q$_D~Sw?NLSxU$-&+r&Iybs9^ckYR)x zF?X&cby@!I_@WSAXZ`9JcXZk?keX{(BGul)oto5i+;7 zfpBnRJx~$#E7{1UK~@Bxee!Vx?hQJ|b{`08vwsqt-}XGEt$Inzeqna?>s(VC|G9|{ ztPa{(;Y*vwC=hD77dB#WUjC+)mWN1juRG!Wt;Zpp2Fftfn*Gw!YtKTFb<=Bf@%I$H zubkjfW|))R4v35Q-F5+?8kFQ21gp=D5~Eps^lHu}$qVt><)!{$lw(JXvC=56KVp;chM>e-}i}`uP!?4vUBcZHAQoKtx1-_@r}SaQ2!%FoD%lR z3*E3l3K7K4j}%o%aZ!MTs5}T_Y~+ywT$1oc-vT*MXWpTuY~uueqf^G$m(|WQ7q-Hp z{qj@uxs9xy*L9zOlV&(~_;GCdGHyr*oo5^UsP~pY9dCI)(`q+R+H%gF2QV3N4Zk#m zZvGbNK6xETfQwJ5SHl}6Mi)(~rsXHeKcLajPLsb^!3~!q{HU3j(z}XzK1V4^jrRNa zVoyUuTev){_eqQeAA5p^{ZZ$Z;F*Jf(U(p4)QT7TOUP0>i9nno1W0Up3z*Eo97MygPa)0gv{rEyT|7 z%8l(rl|d%6FtyWSm0+#7qA|F|usFmbqec#j;z$t^E8rT| z_2qSxiSgUr!?h2KZoKBDTlUBtksZ3_ZH-PFOjVp&~ ze{p_ZVvy8skh}9~CRxMMF#^YGo!G7qgdxRTFJ9v~1Oi=v5chV-`gzkQtSGB*KU1C6 zf49nH)CUiZ;O?wK#c-&C#JenWr<> zD|qCjWOcPa*Qra@+GgwT%}$c*bY+X+NVQcx$Sl-PZm4I(bxXpc_Q6>KJFL}57k&A2 zUQ};8&FUFEPC9xG_HQ!%AwR%oXsoa5l^L>#3x7s1pf0nz?`mGCNPpMFE~&}e6@KsS zCfI9#u-`pu@DdLT^C>H52|WP5zwh7zc|~KmSmeE?ks>9?l#AD^n3+UO*$XkLjBL7A zzi)l{pxRfh4cWRj11J4}O<%(_%r}4UV9&TB{@Cr|ZGCN<5baa8w{Qebm0Q96m$HZ5 zqp+saxMe$7k`(6}6cm69VKK@5yoZgShXZo5q6UF@uSRKpgD~_T=}NiyrNq_M1 zZobEVdRTbF$Cw4SGX*meXvZ9u%1%e3icD7fo-cBxsWvlRE=;~ES1Wt_8r~A=_K-Il zhoey+d3Wch`{_qq_q;DVn*lk@KpmJ-w`pqZkHfs^G_x?38tbaTHgBPDd9(Ixah^)$ z9ppI}CetG7b_>_1m%{1zTvcYSAt%xGj788Jy0SATLP<|(;4(ge*WkQtM%UnjhTo-@ zqm0%b01;Dm87}<17MtZSK=!uWD6N@nO1Fy%t7Y4Dh%z z9iUK)VNFF13pEj5f5Yje{VCM$Y>89WSUg^x=DvlHVOlK^^Pz9&qW&W@pE~!obC{z$ zL@T+uDoDk0HY=UPF%vEt>()6HGUps`;=3?}t@eT%T|4DCi3ZAXd5qUUa6Aw2Dek@a z!HHLWpL|5D7^`_WKZbIh&>#P@ZWgekQ_{Vmhws79!LK*9z(ZHLs;Yu=vqzsGh-&r$ z?Jhb-+4e!hLEw)4ryL#%4Bqc#>9G|L7gLo^+awJi`dE1TUS3r0AI8FToxJ^A5j(PH z4ldVpfFrTxrCA9GTXuMW8lV2_7u$3XNX$aD9BWTgc@i%MZ4>w^If@l(`Nr$(tnfw- z<5>j=ojS}bkQ#bT2F5~asL??D=3TTL6Zlo~p^bcprFVpX<5UB9UHfJQi^B%r;8A+ymF>?G3dD!jQJ#UvNi10u)!hw?pKw2=Jp5vjO!c}>vpMCi)H)ZqM-NMzLgmnc8(FKrS)G1Bd#JaAA9&E$ z!s+^dmsFr)u+H=8lAqkF7{_t3!IT`7og-^T=$P8hZv$o}e_W0~9a<)HRdNCq6Hs|X z-{Ua@+Y8)}_ddv>Yd2i4Y?ec}DVCdMW)xRk<~|bqkfbAYs)+_Y&ys;Af=ORpteIz` zcP1;`g+0(RRP2>=#GI(h{MTQ}?4k!)cXxblX3HtC7-IU-c>8zBV<*PehWqEX^6v9j zOR?0_Z;RGdTA3YYYXP}=CXvu6$LsiPc}<(G^1uroKGHf<{HSXZ5h@-hwG zcj}XcKrEZN*D+(5ue*8VmkY$1q8pvV#=kq`3wnr7K`O3S!v=Io5tRxY55uzmsbFFm==|aIO^!ya zN4R-+V4#xD7H(F14HfpRsWKc!3em{JhjlPb`b@Vz5GZ}Q^G1A1YtOYiB%@g1-vPF; z@&q6k?b3M#J+$NE!K)vq(fI|eTR`br$mOi462R_E$CEYDsup!%!Vzp_=~P>dHI8P>^37{thh+S#`AoZx};R*ShcIaf&_NJczv3zx;%Q`9m@aQ$dpwJ@?7)OOx@|Nei#u}<4Ci3^ZssxKN|&$k_n!R z!Lv7d$;qCD?MOEG9FTz`MJ&^h-8i<^vb^??Sh+ff(0}(ur`h>7$mPpYR+18)qg5%7 zfx#9}La*HFe5QVM2vo1Sl(LNybeh`Q$X>}DC!MwZQVt|)Uql>?-Sg|+O?@wXdjOuJ z*GJ0F9{ zlRoByg?;9;`IO^|Xg~yh_T@(M5~J`}w^uhHl;C@g!w_@gqLAocf^4e)BKr32Ahv6m zrwaM1Gi!Qc8JDjI?#_hzstFz`mg?j)UXGKK37ai70E*RX8Mo^9=noDU7TO8_A0l~r zOeBB4#$=C4^Uv47h6ae~-{aXof8xAjXB+-q{Qs~a*zp|xeC1E(`Toy1|2}S={TNPv zzU;}zr1IzMS(DO#r}SSa{g;;h+$NX*yW{@Lm;PU8m4{5Tk*7$i~_xmYpA>>ach z5D6wz>E%Le@j>iz^}g#JGYJ?}wJ5hgP%6oMy>SP~oHM0~1xZW2_)3Jk0ogw-ma%fo zKt+=4b*|Hw1NWDCSXBNL5{FlK;YaUNDT^^bp+Da4O79GFo0Ce$4YwN*W_WrZ*(TAz za{O1unF6||Lv75?YQ=)co-@#^8Q&sMfnZmRZQwdip_uuxQFN?c&>ewFpGdB4&OHvD zQ@dCM_tw23xx01GvR!g|rZ4P2k_bCQqzm(Jr*yMb+b%-sIuIsy6vg0ddu+~q^t1VwbbwkobTxrb$$a!ps zES0X)Of$vIm|(!o_&$ik-FSC8NG5lqq$j-SJrF)}DLb(7`F9e12>)O8(aap>$i@BL zeS%WS$v>rE&Dl00ie?;4xqINeS>51!7-U-!gGJnU=;DejJ6fCFqZN?5kQIW7CHu@J zIJUq=$m27p6K4Jd>*KuXrLpyzO|u|5v4}agnAPjTC=XxXr5T=pHu?!u2)C#5d3d8_VjwmM%}3c3Y^-!cH{SPtwXUxHA?Khc!`06^1IU%T(g z#**q4N657XH~fT8I<<7R;Q&y1%n9+Sb|WtHh0K7J()lvh?*G(KTvbDC9wT@v?JSR-@~2?R7iG^C|4u9QGFmo zd841zE+vSA@jX`lT?GwL?f0(#5%nd3a0d9Ymc&`)^mlQm`t$gOuC(RJ>k{<AsqY2}L+?iPbeM{ZI8oV}_Grl^hm*QE`;)-v(o_e{Dj6?}zUQ79zT=j&pnj(P9kYAcJ8cPIwA3w~jOe6XFp z?XdK~&*0iVKG558*N!YfFeY|AuWu)lnVUcd)B#Q9eKoG@!JPEvQF6FiXh(`hr%(%u zPw(hQe3`id$=EiE&jhwftq6rJBCpo19Sm&EwvqN1QZ^@u#N6HJr7_}mb?e*r&s$>U z9tPDNz!BLUS2l`!DeIQ_Qogr`o2~RSef7GbGVSUYQ!X{|X9jGvWfUzAYUM9W3KkN@ToCSkj_mW^=77-_H1xTIWu!1NJ-|RA60};c+*qeG-NHRoXzSG%nTyFT{S7+Ag<>ZzCyHQjMnC1xfGxH~*anF3BbVq|%Zgz{Q3WE{O8Vn?q z-djRqTNc^Oe!y<-|7Iajj~u_P?KW*k9Zi3eBH)@$$G(&ZmShS}GK&N;NXP0kT*8!7 z&EDbWv3CWI%${V^7sZ<1EUehQWwZ9228gYu16q0YKE?LeN7El0d?LY--}dHz6?x21 zYX&LoiC%p$@Hj$d!m}sU&4LUkAQGWCYS_lnKDlycBxW+tj=BaRTC{s`a|9jOe|1{Y znfcNhOLtss8VIl-@X2uE0qzi4dS=tS)0U(2(-7 z+B#JC0Ip01F|hmXhX0(KpRxw(mJ)C{?(|C4=rB+f-Tf%AXmx=Of2oa8@gQ@??*xb9uj{u?_TOb?#8UWYuA zA3;*{-YE=EHjzVsTK)^S?XP$Pg&1T!H#|HJ#edAY@MN(U$~_RbDP|gNAkOi=BqC$7 z)Gi?WvNLQAIHvdVeFy22=LwGFpxc8ooO*5J}+!i&*&M!MM;^Z*BS)oUK4It5M6061y2` zGD}TT3O2>QS4|~;v25Wh%3@9yf=M~%X+ER?Ipi&X-jhgeRv!&;3zU_6GJ&)_n6#b_ zbvs-sakV0k${bysc6aJ}zU{cMekC;j~b z`LE_5-XR7Z1S^kH8WU^#_(l;V*(S3?$VPZ6CB@XQ?L7Y<3Ji+n81=+iBbnACr|!S- zVjl9Q53}NzY@-a=g93Yph%MC>t`*SeMW8Uiehxk2vN#foiPP()ixnKXGVD6e1RkZB z079hRd?SOSEj#7(>Zp0=JJ-Y{JdlR5{600HXS>$CZx~@tv7P?mv68((5x#}c^rHfb zVb~G8*~(Fpl+H9~TkbdC_hmUxWEKNmKU#h**&T3@r)-BaRqE4c8g52OAY)1T8C1E^ zHRVSf{?n`GdL$wV?O0J3HQExE90oMGbhWQ`cZDd)w^G(cNG9#uXjx+mcb zmrGZNzA8T-tqVx`(cbpF3XQYL7c{PE3A2KM9fw`wGbbimI-aV+ z(=sn#AM!%f3^lI$*ChG%yHC{$?KgU5^wFKM;TUqT8vEk-+|yY#J2la%t~#sMY;A{K z)}|^AB~`bzbw@}@b$0ej)VI2V8r`bZ!2WB_i1JvkwTZ!EZN0AYvhk}|5_-4$`$emu zJ(uiB2CHvZ2(eb(zYG?=fG9)wIPoUPs>ZnSn%%Iv>xHl{rc6)kDL~;$G~G%5ZYCiE zbC*Xyt$|iZU2TzX7H!TLeN@kSXtXiWHLpPbcVC!)-bBvKL+Ofe@x-kX_Ps6tG<@}n ze$tQt)kSjXH-B|@^1YBv;{4tevA$sAlRuQ&{w=`cj--YMUiNy*TUTPbW0RNj)rvuc zxG#$Jk_vAPxP0pXk%O#S`{vcva5Pe5sOPyeQI?A@khH(I_yIcgLnwt?cJs+3Dvc17<9BR_!|%6`N?Fg!k%Yy5tpv;QhLA76py zVhQ6?z01?Ix*2J_&vb=td;jo?C3eNrS)QGN!HWMYv0yn=%yJRb)#*EYI1K=4sFzdk zF>S2~7HVUQdxjH6WRX_2iu(9!)XKL&r~zcb?wZW2tUPwf)%+tJa~T-=vC@7li=58v z?9~f>j%0QYC0)nb{WNi0q;tj;=wr)BdCM?qiAN32cR&00Z-%`ZcUq$F-} zFxP?!bbG#d=YV3vPUZmf5kgL?KYstorF}Z1Ob5cl^~UR0X3Onp zJw)(Sn~RVv(+)ZM3&?{tgsB1?Khl7G_R5D<%CALGy}sCy9Qd9cl7!=L55363MaDM; z$rb-_9%|H*e|pH!uqbBJ;4=RD4HxT)veiI5oFaUdzX0fFpE7;(kh))GN7%CMcwb95 z?z84jI(q%4m0MJ_YkhZ?e-~ReEjZaoPEyN)b#*1!UI5e=T_`*~S$Gmg5@DE)?!B>D zKHL1Wfm7(o-4~e~+Q}Grb?3fOBk^kq2-{6Ds<)UZZ$fZ9be)=6gR!9yc8o{_Tca`J zjePz0$@_mm{V$ypTg_*yXY7KD`Sr{XhI$j)KG)UiKx5l*w1#Dr?nbW5E;y}pP^?OK zobGV5>d@ZnfGg}7d}b6;evFqB(LLjBLFrUu=@w|yCY=jswvKZi zL&ycb^%bCF%%YI?q*`>wt#FIFY7dYX+>?GwPt(ge+-dTLrSR$|rKXOHSCfXTb@3{Nc`(<&2{m z8lnXob3jM0Yc+Be-0#lDda`S2fW0TIi28u0C4L)G6OcbN&Y$u?J)*h3=QhS`YD{sG zJ*9aeh3$^mmTqxH-c-~_=Si=97uo$Z6+ad|=#=ZXd&=q-X@hMVUK;l@kH} z2c^z_EpKT4fZ@7J}q*GIpE!K2w9Cc@`WCJ- zGUen#HHP@Us?I7~A}GB00x-dpQqQ{QM)BiAzxXF%+pMn+r&dbTJT{t-XxH$#9th*e zA=9Z$`!Sz2bHkz(#b?hxd9Dv*UmQK{@%C6AsHXnr$8@Nt-c3s@$R4jf152-y2XxPO##1oW!{ehyo!KVO~Y>waRglyv{f1W9nXL zFGl#ZJN-=PVFdut{+{qH!mjP(Mv#VkmOqbKFXAG!+|K%d@`OIs9TQoO5eR55=vy0{ zUFUFYH!L!Tfu(hQ^3=iNVvwaTqE}d0%=m~pe2&itfL6ZGJv4sal#$*+1UVPM@$NIm`Gk&33~RtEtY$n_ZMFxX|U z!x6Zy_@3-zFyFu|L){d$b=o^T6)NY0tgLhwvl_80dO#`Gvdvx(_2`Ch;i;#3C0D$6 znjEJBQ1I;yas3zrVqZ^>KJ?N`A}YP<)D3iJZXdt zmnCYM)1;XWl4F8f@DJ})7AC}`1$E@lC-Q|eLkkhjTVe|h(a3YH7Gl;VwG-bvHaoaCk%G2n;h&{A&OcOJ0=71@6J{Qwq7qP^EIHMrA_J5vhDgLde!=Si#1w* zg5T9EkTf!9UO?l@Eg)lbUD$Mn=9f;1gygEm&p$2??p_p0 z(=rc(_dd!7K6hGbUSgM7qGbdF-<*HHsFx0+M`iH|6=u1$-$SK+*qQLgsZVN0AYVXT zVyjI(vt|c~y}P(0ty@3kGCYjY#;Sx}a)OFpQc9g0`#ra>6V1#9g*b5Ffr4U8=5HEI z;Hi-G;fV-cse1lTc1NE`9ka0CuZoEgk&!^MTA}1znCoccAhfixi@3SzIIz`2TCBXQ zib@=}w(~mr)ko2+H!xaQ_1At3gW^SM2Mh%$I+BjtMS3J1aMjJl8-I@rDy+$1``(&4PA6f_`868pp$&9q=_4*A41u?PV^AG`r3K)2t}V9Wnk$Ty25BD$)Y+^>4fk2~HT!b-WxAdhtFrHk25CLSL%+Rdi0?74H zv*_t=n8wvR5+a!OX6?)s{V|~n!8LVp*bDXv%_HvVvtJgWAn^;=v zR8Le^QHv8XT*TwfMyL0bo0z&-*X6D@dX72_Dw-5!OWKD(g`B#%FkW)`MYhj+*Y(fS zAy#xZ1oriDERez6}uZ%JQ>SIN2NR#*Us2a)ta28Ljj^~@)r-=quYN9EF>?%Xb|zGedvgm;NSj{Z7?ei6 z3ytZ<35_udR>!PO)Yku0J-HBvBjcwfC|a)QPg)g~teY~%PQHIp>$@RMs?BH?=-=%< z|0Q`5T7VZ=u6FT4OFmk|*5d*T#hL{tqA>9yg>YWqDf!Vr{;|tHF$uW-uF(PeDds)Z zih~v6B?zXIAfW6a^FU9t40qRRK5b8A2eA>wb7&Zp{n%pv>3l=PVe%jmOHBfsR?XPu zPkc%C5#Sr2xbaHd#_~(`0XcNPyDg)*@QSR^>Kg+L3&F>_AL*zz6iDpqSa@;6dEB|i zx-KT5++sMnfO_sC6Y8B()Qy<%2BFKB>*r_N{xWEEO8QkiK2q%AvP-b_*xe*pF!u7l z35~t6`U1O@fSF&ays8*dR8a6-MdfpFs96eg^`cB!qfY9^bkLWV8N?M{OkS=TXngkK%^ATGpJ`5;y{G*w3~PIK1wjh)LaSbpN!wAj5)VFo91bX_ z^lw}MD$L#+jFFX+YHJn_5AOo1NGf4Y*xY1XKXo?d9{!YGHxGuR^Oorh{CpA8xEm^XjfC#*#?c zMysxRYbfy%y~6dcnu{6qpc7@+=mAMM!`0q(j^lx6aD}44dz9U%ngiB(OJJ@SN&RYf(*2(Lb{xD{D==yJ27+w7N1e?yVxVZ)5b!WC5!xA)4mDTcRiz{`~ z-4JUOo^#M)$`r1v6N}VsgxpO78jEROSfMO_*H14 z2N8dF0e7r{km9C3{a#ow`b4Ev?@001u7Zqgs3EEafM##iKg;;Oa8Ps*fe26?_l6ld zqvSky^;f;pgsJ=VNq#%Tae`x8Gm^yL9XZ9S!456Q$ofp7eu_;)E8w21{py9P*<664 z?KMTa&pxXiAE{b0)^`zeH-wkg@bTi!woazXY8zSBMyDBbvY z%ecW;T-8qyEaFNxre`-guCB!Bz=>yJ;8Epa6Yt?|bV03DDw(e1xT1O3ALwjJD@tGE zk(O?!v0-B};%=NgJMnGniZW*=J=o&Q1f;r@pU%$RpEeEu7)?7M!#^^4JEFVzK-iVfxlG>=371z3}n#Q?P zhHW=xqE^&lMWh1}fcq8EG2VHUEAf1!=in2GXP_1)C*?n%YTsm@M$*1H(#5@8%&QHLM z%W;kwJ6+>XdWjuQ7vgCpH2bC@8*=ewMY+Axp%t`=-%b{9#AKW|sA|E7q}A<|CT#8f znky`5jLDmF?l&MH83cTY?AKv%-Pr7=WDB^&tA(coX%3@}_1I^f$$O?V1D0J-dl^b% zvHvve(3=xl9=NHp&6ZXx^FkUeV_7!Gx98EWOf*ZvC)B_1jO#hS^}GM-)XM_5qwlBA zpFf|pM@&(f4txSMs0I&|Kk-@qSs7|yP-f{17#H*g1_mMko$Zs=@WVww?M{Is&Z=~% z$*Nj=HIXLs0j&n8AaYR(2AnM))OwINObn3z(FQ5u357lDhbOqvJW^6Rx)oD)g<0aZaTASltVAYBytE`K5_7}ip#7E1*bEnUC^%~IT8<7JS5kIH=TxJE!<8Rc zqje=S*abi#7 za6N?CRaQo(gNFL~F|MrV*ZLpp3SA0R8kpH~0 z>H7iqpI3epZedRR$E9y`^6*ptapiT#JJ7lRxbj!*x&KfafPb>n|93)v!s!2t5HeL0 zo_dyWRWBYWmOT-Mv$^u;gkQVfoQF!;-{1IhphuPs);4T+x;Xpd^q;qE(!B#Zz;{A4 zN15vu<0tx%N;I6o43Gc!?OC+CE;?XdYAf?YL;-nL{0bmg{^JJV)?eGkO-aEZ@--;( z7rF7p*!9QH3amSdjp9@NQvQ4vaO;Pg99}J*5c;_Pi)c63PyLtt{y@xsH|T#^*#DBl z|9>WL26TP#vCdKd>X{y-3ugDxzZu0HtbK>B%wKfpYm`0{FGZwH*^l&2MMfuikGGCs-q4YTRJERO6*8V@CS#4#2P1vX%8A& z>4>yvmReJP?9l0xRug4B-@wyNJmH_AmH7fL8xy${41-fCs_`19~s@AJ(Hu%w^0^;=@3Q>{Vvxi`)Ax5mob0MH_ZXBEwcR4-a0x zpqqG(o}>EspYm|nN03?LQDgJ)^ttcK&yw_V@kud6bZ63%OwwJ9mbeE<*xtuup|Y0# zUsyg62V6*j=Fr0(tl4uvMI;E*bQDHadG;WtPI@R3?I)X;JHIG=)xY`jTTn%3o6Wow=L21MkjZyTcBmRnF?#D@^+I5ZJp8IA;Nrw~5?xUsvl{LB}E7{wNB>lW)@np&mtCb6N&Wy>YnU4=X$v z@#>8<{GA2a`q63Y54hl8tWbDnx=GF)8`SjFa@Ft5gF>un@Lf(3Q9KAbI1soX%1ctxW^$EbaWGX=$wq(KN%!FLnD^(!S&S| zk(y!=_va&$aq8TY`vI?k=iKcg0s`uBXZ=gKT-irZ(DAd|yNE@=8k;~^nL9V5WukiT z%(HZYk47%klOMs4M?7Am{P8CMB9CddG8x_i@Y6FKYDi^bhN^W+aAREccJD;~0n{LV z`{U^)f`YccT5K89z6O^4!GRBh2MALR`u2E1T@YE9INY=Y6dvTE2*0}3_oL&eZjTW9 z$5Q|#%`$HtxSv_r)9UI%#DaK&wVy!YdCP#(fyV2@I6Z zhY=XlxB)eNGQVdD{PTYie;Oc{KY{W$jQ$5?|BRc-@EBM;10eKSft&w+`x~IeZ~k2c zA0|7Q;(JVxpX)(EnkS%#*Pv?8qLXmy{~+;ye{ww1@v{KyjnL-*)8aU6+2|u_eMX?tKH>_A@C^Y@S09CMRFy-LMH8N6* z#<{q}6*reib)=<#C)+AuJI0vIOBMejYqn1sukL;ZcQlnNX|9+kV|wEzn$I<>{f(4k zumB)9UKKDoFARc0A8q_um??eoX$7;*!UWOx4RAw#)a*nATzq>%!vy2#hgR+Be5g}( z{CjBS{1oh!%U@Bdy`SnjnpHg4L2rP?{6j+kT>pMDfb-8Kl0loD2jD2xQsnG`L9I<; zhnXip#F}+W4Y`e;{k%ec8>)Ss4{;X9x}Nr?tmm{JeSwavy9*B|_h4Ayzr-L9H)>#a zH@d_Q<}SfHClv?`_v9Btth%?MRZa_M4N&$)tg`NEw{vHY(e*c`fGP6zJ3{zhmO$DX znl5Zp*ag!7^nm2(OEK*$#_p!2CuIpSR#uCCrQ1*82~1Ss1l=btCunN3brG=M|H&aDAmfvos9-308aa^4>NOC zd);(be5H;l_+M_2kn*xv2XZ=rjV*)i80h)=6Mg6sciaPKTqZ7O=u;vmUHlgRl8 zAA1z~W3Ebf`<8ol( z7NSp3^6_Fp2FpHA7GWkym?Cyt)Gr1oJ6ndJ0M;p9cyj;PSa!dqSt|uB7C8GNO(v+p zX{<`=q0<<5HBLCG-?B&rFguqm-&9_2%vQ+`Y_;mx2=I^rGybs%eq~)!z$)3zCOYAT zVdW0Z-zy1TlFBX2!fqS;ON@(K-`vV?&aUZy+4l4&N#=>386ovxKapP60J4zh!S+=>{ zfP+mL<9BQB;hAcT<7*H-U2LMV*5Wh7*}kaz9Mb-8*GO9l)l6@>4-{(@Ce_3S3-o!$ zj6}n)c&{gCf|+=Y>R`{O54m1E@Ecq#v;ID-bA>qz;O_s2@C%d{>{YM6@NP>swNyAJ zU-y)en|BA+KAk@Pro5Mn#X8Os;2%7gfCU4rTqh9QVYIk`;MeFHeIE=}UNUK5axLuo zFmz&C1&W-&hp=8!OXNzqT0kD41?;A#4+1j)K_LiOM|)R`c#`d9%C$4R23^iodxxe` z%j~6>Z)`O`%A{uzm7hBLl5_v4xeUOJk#OkLJzN7$R!{xw33)x$RZ=_Pu9DtK?~EFa zEP3(qf7s?Rf0F%bxa#5(-Tb@s_?y`sdJfs-D4DeQLc}P@N-WKdj{VImh-?aJQJy>r%X$BeGH%O>W%-{qi2npb~k7;4;3`1?AM;) zkn=JvpN^^mGqL+k#d@!X?$@%^zwmi`Dpc*6_a&dBe^@l&GB`FGk=S5XJL%C2Xtc?c zVh90#EP+c>K&Xr)E~VHken~aNo!erKH^Uk!9=o0H%VUV_i|o>dYMIg0n%oG8? zK~?Q-XGowC;IWBfk$c)6lkI@^3XRSDu#~Fr()$np2L%3OIK82lMs7ki&^)o>Xfymc zxT?$so@mH*shfowFrW3E674^Pa+bVl{-a^CZTI#VCToIU394?6$lOk*;0&tE8 zvlIADd*x?Htr=*s;B;fF5*Y9U-KUn;u1(At%`NOjhuNjo+to&8Ng9PjrQQxW8jw&ROrKE{j3;Pda8G7SsxDYs;XUn{jId@SRhks)^xRxaTT{XUzit)Gb zQ)z`8&deA|(9$e(j*O>qq<9)hrRbD_Vq=~sP=|4YKlsG+7gEX!`a7ZvY_kum?3Ve@ z|35^XiCdCs`~GKIr`2gS_bF2+*W6Q4r_9Vf%_UbVQ&U`W4-uJa%yP}#_sZM_a{)s| zD)$wY1QnG^1r?D56%m2oGw=I;kKf@R-~rsv_1x!qey;1n##-P1LCm1bDiS9Ad8?dE zS=@n}0G?qb!`R)TQP7LXm_X~|*i>762V)kKYrNp{hA|u;8#2!M%ADL*<_lk!WCY~A z9g6~PGN!R0{=w%1GyAOpKeL4PIFyLWv?(-jO)CL~jvV~&X_p-2h$YIr@;U#q;#k|e z`FrV};(vx7ozcLre6O&Y9XMzv*7q~&S5l#-sFVJ7(_Q+v7b+3i{KCE5eg^RSl6}un zcJy($g%*uTEw9{a&Y$*-P4R5HXf~q0{l0N!O)rZ?UW$PaVl@6uIJ9PPaszGlbJDLw z)A9ssf`RBRqqDh9qCSp~f|KHi;KwEIPf*u{ozrrIpU#&p4otj|R3Q+G#IpK{;*xH2 zWvrVLmm}ZVl%lWG)IAmE6^C^ZgChPuw~pARE`^KGk^T0JrbnFU&?bR=@5qOazut3v z{_A)=E&`dx|40Fn1L|*j3AYMn|ft}FGoCs;3{fn43RMeyw)8^?m%3&#yx1iN(6AH$}h=luoGRBVAdSBnRc zpO$9Vk_uU}TUcgt4zpbJ_xJG@>aBqBx@=By`Iu5^|2qYhqC3X8IPKdR<@cRGE~5;UFZV%wzAw) z!-h2XP)Dg*)7-MPJUoMA{dy8z=ZpH;cK!)T4~))<8FrroL}R$2s(A(L;|137Eo2tC zZlv#7Bi%D)s^wmb_G0XeS5y1nFLc~s7q1oA8SZ>x?-pO>+ZqWY$c{)sEraZQ8%khieUsU?>wn}9mjA53r{nn^%l@K9FVU zKQ5}Hwo`njrOy9PaH43QivFBq)UyXIklDoQkh_ahfuj#Q3 zZsza39}1tDZts;J7Hxt>wsVg;!Ms8|Pm0qbxBHE-KvW!FoV%>hhOGo;tGh0?t21V~jKLYG^$f;?6rjF;pY>e;Y@k5KVRw7#_WM!hfYk1yV&xVE;p!O#;%Ivcgx$icBI(ih;{K1EVI)+>f+^v1MC^I1uWOZG(ZTI zlJE0~o4Oj!+{%&C54H4+d-pP$GKuT$J?**-U3!)-*YF;US3^>(gf z3!^ zeIU&I{(DBAf*Um52FClsIf3=Z{W|X$;oFSvsNYzRE-`}pSG}LZfmh9(Mb!v?>s?rq z`t90r=XS<6%fL^n)L6vWC-m$RcHzqr=d#e2EkZD47L67XFEFZ$e+geR)%y~^ zRPguVK~6~W2=E9wK}5#QTj1}f($)wdh{*g7D?5X+H`&;=?XW8QkUsw}aObDKNFZi| zl+3q+FPv;blIOrQsFxasuu7)b0hnX8(axLRJ`UqVOF4uqB#G0~yIG@`7z?G&0Z)OU zeNy_Y9N;)V?25bY6qlMw?>K1R_xJfzDbjPys82KZ+&up<&5}Cdpml%ssP^pF{N&Wg z|MO7*o!KwF#RNaQcf(U^Vri=N;j9F~pxP|#K~l;2im6&ejK z0mTFtdp2J<4cv8FB6d9g*4f>@%_}qAk-rU}ZlnHT5Vu-agg z<4k#{+&(-mU;gw2B#Iu4U{+{G&zzQjkA`$Vd1i6ZNU3F#r>ev)*_?E!)ba|waIM5O z6e=5S?B0`7c#(=ui!!EB;jFz=jEGTbR;*WBcP?@<9nbgT&6^5+0?2QZw+}u)58R3R zH~`7we6s|fv!V)CK~t5@Ca^NW38*uR?qS~kU#~*ppE~^6aTDhL9m;mjMdByRNC3Y9#`UQ+kH`PTDXJFg!f0`^Y6$Gx^)~#-S8OaAopWP!( z+%0FL#;}KR-a9)BSf?(PSm~t>_(S-I*28PHE!wI3H0nVt|KMa3SFYRl#nagQ&sMP{ zIABl(X+k?D-R)s=jGgKwjacn3BTbqW1^*IWx2Nj)Tm8iPBf+^iLK%|$e(NUiHSmv1 zXZw|+HTM|*x!_)&ZX5q23*Q_<8QyuccD~o%t11lm3Z{eo;v%v;%h5iBk^WO){Jd7c z^d2`%cxKm3n#Nmm$DAUL*y~h}t-c?NQV)iAm?ZK1`HJZUD3Y|17*066E?A}skwX1hQo8~r!N*_)^rEM zTGIX}{@0e-o_N#HS$Lu%X zu|3VVbC@t&=L%qTtBID+FKw6zFgChrcUMk0cC(^&Q*t1mb9KV5ghfHz+}V)1U$Y;* z_e=5!Yff-5oK}S2STgw3UI}t2VX*jW9x-Q{)rFop>R|c32U2xTR{FE9o;)MtaicmK z41~WAn7vV20c@Lm!(dd$d&NK0fdx)u*nU66Bs92Ap@ zsBq3`4iTK4YKW+p5c~L^>`+T|t)IF(^6R{!O{RIWYadY1hqtmLcL60k6FYLl;9E?W zXW3{(o={YTP4<1ND;G?bmOkmC9#&73{b>}0DYI=u{fnjM!wo`(C!64sxPZS1_5i-G zDVl5d^{*!1yij)k0dr^AZ(7@5Fsv0G&RJNfZlZ;@7RQ3F&n2DE2nFYBMs%cMc+v~P~|w<#&v6>IZ@XSM@)^;FDk_>^I;%VO%0?69dqOzchisgBQ z`Uj55I{ygc0QT#lmzLu#qFWL1V}z9Pf0~oFKAZL4Y*`c3aRt)!xQc8>EU1|^**RpP zuzQrz_EZ+w-oAjs#kxeJike2A#qQb{>D&ft%>~X`)$DmCLLST1ywWswx7py}=CXcQ zky4~VX61<1;3#Jf&CoB7YX2s8SGd;1`kSY|5aRKrem2Y2Bl-?~ZZ>?-F!W;~&E;!w zM2W}4V9JtaCUV~Yt$}!Xg){ugE#E#)nbOE$D417lP$FWu;&XzH+AkWaPk<`si{!3E zUJIP4_Lmj4AVlSLl#jUS{s{OmY(w&)p-k4|?(S~dErUif3~!M5m@oYwT?*V|!^-Qs zeGR{$J7TNUdNyv>;ovYe$|RvDMQ+W%X05zW%VQW6K=SaY3cYeR zpeI3pe0rKtXg{Mhc!?nVs>fMY=hHuTDrtoc`ZgW)Qo_Bwx00qZ5*~S!4;MBpzNQvG}M7ay7)Hc&&?!7Ilw!Y!@HnxzG4o?Pw<+!eYOOZ=3;sJ=lXOG0lI*qJCE zLz1skA=5O0;B5zu+8QJUFI^InmO2_kAN|}1?ReB5$k@AaDj)x-d?v}VE)t6JDHfB{ zwUpG^vC#{iy*rY5`{OI`WbEdR28}9Z z8vnSYQiM1v#|tLbH~F$@n7zR(V776sdx};4rH;!Ps{w~jdtwWIfM#DtAp0I|B8{1T z3x6y362*?|%3D4e$*aLWx%y3@W(PuHciKb$heN=OVR-2b0}9E1BUw@4YNFPF3P z04n9On$xUpB{Scj;PwCj#jNa$hC=0J&YXhu*pl&#$6p}iU_Jet0M)b?3SjPRKpGGG z0R0xqz;+P5r_~o7-e*6QL+m63=$49R+({9Pf*Q(}OskY7`1Rx305)hPI;lB?IOb_1 z97`9D_f_1VD5O7o&+}Is(h&)hMXwYF$u36Fj_}O`ME4duoMK~eZ;5zofLmKlu>tI= zWc+S^yCB3%LNoBG#Xv;x6iQ!gDU8x;D=zQp)Ukv|AD)F#pJ++**R|pn9#?yDZ%6Dd zhgJ1{g~@%@C*K90Q`PE#n4)FpMn+^Sm8rMx9#xGX`amaNgpIJGjgO;a`L(3t#h)3c zx^rYoUp;()J^t2T!=sEt;yC1nz*82KW8r2AZGfNtm}UCt*ka&6s@Epvj#cl}Tg%51;W8{s#CaHy`fgymU@Jt3ii=L;Zyk~nA|Yt+&LlZVX6}VD%FYFB2-p3V#LG?kQ45Lhi7p zY23mctgM79asfN%^tQe{UNlhNh!DJ(h6yp%vO?SXxI;S(S(uCiICSGY02~fi2X)AnD*THWGDi{4 zmraYkJsKg47-Pc6W=|-(KIU`YJgmIC`4oxD2D&^XVZ+~llkf7PyoaE7ua!P(!uf$X z8=&RZH``ZMu4XLTd2G0~bq=+jF1|s>3Gcjbnww^OEw&2<_b-{ zyP1hQ=|fGz*g>3fUt*3isa;Fis|4cl*z37>l^ZS^0!Rst*d^+9_nT{ z>@-Cr&8c4de5EnN)q=FKbr9zh0U3SKP2=6P}&{* zzkymf-khKnx!^<(71Tkc4Z=v!v|11=7+HEave;$6U>^j2BeirH`dx}F?stW(P z>-9z0X9KhNYm@#cu88mStI(R5jl7y`1}LX^zt*>%y@t*{%3Fp}%RWNklsIzOJP=88 z3?KtAU&NDX)ZBQIJrbl6+f?uU7ioQJGp5j(X+esuz3lqr9{3_zL}j4y?kQu!ckEpk zo5ps*2<%3Rh)ER`2TXX^Az=^NZSr(xZ8*eIA#RfreFL3IpYFCX+P7rxpZ!$%q;<_F z6}6p`EB?5HPFht}=YW3SO}_r{`~vo{9P{U!*RG4J8xj%2QFlewPbv<3rSHvtRPNkU z{YX;RuW>3Jv+eNfWI05RVeDG@?M|KVLW_WZMQ61j!)8kpHR$jXSYDoiJVV7eZLkgQ zRsb3vvTpRad9qQ-Hh=NvV`HIFterU_t+K`a!4w8*hHj8q`ntb_-rxJ zbVyF**?ToD$rkAnOd)uEd}wlNQ!w2c+`YfGqotYgw;JuZjET#oY^1I&aP%L_$<^c5 zpQw5_jvF3)kY#q0Hn5S;8OFZ9S4Q(QiM3k}D%Re(;*!C1AC~<+a>|Ag+nG|*)C^0U zdtZqlZ;tw?(V~ku?PqF99<1SehFvw4+JwEB>#L;N4pI#i$&pt_Sxw3# z{sga!5;4j#UjK?$<(C7*tRX&6!;xmpyT7uooIy9tiN!Ap71B11;q)`|CE3!9-l)N4 zC95LL)^DbjT00?YW&!&AW=`H=w=)yNmD2h?)op41YXu!7pH{mht7viyA5-Z{TkURH4|)#k}<<>8r>Kz$^Y9v826@^TOUnk4c5O z5y*k7NTc@QT#(>i4S4dI*%x2-hIv|1N1Vb;^%_Yoog-c!*2VjpZ+%hAkOTsxR$@4L zXHQE+dgznYl0RVWVy)Tqt$84tbCY8S(0J1$4yJ=cVw*n;wSq|}n^Iek_u7}}J8!h6 z8P2{WiWe3B)~q}$+%vCCVD>f-w+a6qYAUI9z9hc)+Jnf?Cej8W&x;IU8Oh!MCp#VQ ziAQg?R;PVUMo&AEX7_h8jkXGoJTcS=Mg+PpejJG4@A-fUbXl*aE^0S5%4n8oq?)`o z_U0e%zZU##x_K&>myNb{ABd-R(pp|&%xwuD<=eKl&xKWR$Y=AEcLG~M=_!`4|22*^ z#b1kr{-(Je=E~a4=*lB!k(Y_PJixyZ95{}QW_wai-6g6f%6EjLNyfNGb>goE3zR# z9?B6fTsvmSeD$_$aVPbrcG?Ho^sx0q<4xSZ8ZzgAto#zt#X!{PvOD9sGTG0ld>OwXN?Qk~9ry7Oxn%$0gewr9j@SHNxGV1)Rn)SWG!tWJ1? zDjjAli~izajvXnR0Q5N|3z5oxqAh0Z2XqH4&(yu^EqBRdXjTzd81Lw}`JRzGL;L9! zCVbC^d&Fq}ATeo;gM9JgVZ_f!QP>Zpvf2-O%u1SO7J|0^Pk2rQnhKZ7&LCe zxdeO&?^~RlSUA587A}hVe)!hT+O@!Q(|3gvHv`8Xw;@g{a{{wb}se3upH^MrIy zyDXr4Ci@DKykGEUSo8;xG;0_iYe}s6m})f=_RnTY@!OC}70V6=r!oltBd8^<7wMCe z>snGjkxht6prdG5hrofIH!R8{@Hezr6$s zVu3CLQ{X$4hpPgxsok7qO|77>P8Pq{-d%IP<%@$B=KcdtS6%P)6veZf@weA;!Ss&% z;+h*~pg*pBbxLFGIyYCo_q~#y@O<`_FJNE2Up}HBu*>oITz_WCorinW@7}`pB*%2p zyl4ku4H`QnaWfg%?3{@VwSB4E&@0^;(ozsS8qf@fH4H>B8WILcXQcSn3ZjNn^x~D+ zdUzuCj?I4%jaPJY^`DgSN@vDMc=?MWQgWBF;cD`Pdj=<{Zp5GM*%ZW^zVEEJ1NZLT zkI{I5n0}v-T-T~X^aJ*4$B=TW)}{?Yr`uE#h^E5> zJOcBVL^UwlTe!fd>WdcvNB2RGRlOa120a}}DoUuBs7uLdQ{smc+nK^K{!NOv47B*hC^ z}bWzoouOhwPf$E8Xy9Qm|Ccf3UU@*;bJh`8Y~{6KI-E}QKs~^ z0Zsb90&`eU_vw4ij49_vNfwh(d(F41p|o*ZVX&@=;9ig4L396JnD$ zeZUBlqW^olDzt;b_`6-$<3TH$ct1rCXAdc~5Y;>y?BaLlOY@d@#3YqZj9CI^5{2)f zG6M9V1kW`so>+=iwJ!n@XMTULZ}`ylS&~y@eBlD{JTjm*iRK?dwZ;TBuiZULA>z_8 zW`?It5@)ifBE$>n5d8x&o#IQtLUEBb&GW6sYZ zX17;0lnEN%hr@I5_a|EKcizgoU$NiXcYS<f->(h^d(id6$>qySepq zK{TT%+I{T~7q|H93$79m2D_?{3~4FagO^^!xk62%e@Gk9T$$4{?Z?lp3)@V!!yNcOokmGzka+LtyPGpx=1`|$sk#&w_UT*cn?tk zCTW;iw?B^FoVHzdz3jaLSh544xv;2g`t)lrjSC365j8`^1>=>&TC*W~`85&6n5e?z z6_PlhpU~q@RngZYBg#t7syWf?cc@BNKcBzxk&&=s0#9_QTI9EwmoP$?iT;E_?njq2 z4bSFxqw~sM_=ovmv_as|#TMjUaaH#a<=V}2Dz+pq{E2(lgB?B#F0eg~9G4)~gL6$W z_!DDmIZT9RU-e9hVQ^Z+vCqJF;TcUHcT?f7-cxMO=8er)Dm#}jcuId5wXza-|K!j?ShVqT1I)uSXI%;_BMvmr(L(5Oq;c$K?u(_aKqUf` z8@GUb2=SOcmp|QMCU%?YIUS{dJ0Cpob?QbD1IGKCnxTAx4k!l7nDQeTA9pIfzC+kS zMEBTnOZKK-j&4Z@CZv?c$lNZYdaV}TQuL^I4s`Fa21MrCL}4zwCFZZs>q2(Db4L|O z?6;7w-2uP+^7k*6*RMKtC?Y!oA!+lXZM^7`NAzQC@!F^FnZT+KG{?yCYw)f67D=J* zC3G-iL{nceU=bf*u^jVld|zukcZvlYjq_XDh;0gR3z7Yo{sP|7Ec8PA_0!E7h`Y>A zQj=W=;6|SLVe#~ovZ6|9yqb`F`-t;1#<|&3EzfU8TG{2BNUH!!V>LRPa7N+cPFiH1 ztI5^6U0275-qy3EyS=c{V2`yIOx`zPHMy zzy03wU?J}k&3a&~*aV=hP5F*-K>lzv2S{=9H9rFmC8ODiEE>tjH;#%ed5kq^LFNu~ zws*~I6_&#DWK(~NX>-nj;u<#k7_7c3#JK{g=i1m&uRtsL%}{4cJgBski7btoP*^47{a!$7QMs%1FB={utmWUcL3P zLjJq&nj+EZT3$F}q4AE=7&5b6O?!%MTQaB5!4DP&pbKnjiGhtEvhpBaSSVL`q~DZ3 zMVPe;Lu!GWA7tm#_*BvD<*`~Y8bVn^1%Gt$3|M>=u*Zw)>aM%`f)j66<5Wc62l#H5 zlmd#^IrX!iA6fw9_I)LOo*Lbv*CD&Wp>Vx?_NVI)*qgZG#S-9LuY~gz_j^6}64X=TcZQ(1 zovZYJduZ)Vj(7{$By(l>sJX)$VaB`1Wptl#VZt~#4tyc3GU+Jqi<2t%1^tO7f;eSfr93`7{ka*B@2Xae1j5y5v%kE|Zo zEdaVE&oG+ZfWG4T#5I_)>GJRu!<4Rd1a9HA8-O7hrUI(dcoj}oBDG)MBwd~ z8{Ya)VPMTOP>X3Ldw*;&XD9pOD5slup5r|(b#QaC-YV=r0Hs?;UbLsWh3}O?xmLY5 zU149WYo5l#KbgqefaSi)e^7Axc7)eChE5Z-6cSkq#?;9H*qZ3{hmHc9)gzt6QU!5crudnUJe z+)RVi!n_bNN+#Omo%^q#_1`L}>{kk8+OI+6v(vVP z2w!-B#Kq69DA!hyDU|lExhY-;6F~M#E4ku2zX%;`8#k+;d*#f)dN>g_4QT|qI&7HM@6LJN6fWC~ zysx2dw4PZtJo~|pGhe!U9kA)0n2T7rrke*C;{4Y8*4C8koi_}I?Cd?+@vNGsGE#mtb_@m>hi+93(8pcH7_=(mgt_buQ4h%>jN z{a=O{9{{<|v;Z?FZffBy-&BFf_}bGdmd}}{kE*N|v1irl+*B zX*bbkb%jjF4n)9ZW)A5awIZ5tC)%||PD%R9OU|7|_MQbSGZoo#EPsCg- zhZ)n|%dMg;KTnrdGTj*i7cA!>J(V09E={L z=0NZDUE{_-mOo~3v{{TAwZ>{JyS?f((4ha37e^)>;2U?~`!u~>fQn+gJ8i3-m14&w z8KH}zH6JkD+dwaeZY9C{ve{zQnZ&%ta8!V7@s<-45F z!EeYLcwVBUS_bRm1yb5xy(6=>-LX+Q^yvMu_&rZbz)xhImi&&xgX53{c{tp;2y*aQ zO*{_hS>PxqAHIwKd*7vV&pqrC6fALL2M0l0(Sor=1<)EQfGhKRhN9QmBII4l*1k$0 z&}h;ehxPAYzQP@fAckxORO|kD)1-#;oUSU&jHVeINR_B^+EG>>km;5KOnoAz1ZuS> zqyqm6L?$hpxqy{*Xiz%Of3SJ3AiY;cU5_yS?&P$rxOaJEI1NtRfe&o`9-ztmxZ4pb zv=sv;4XOiU#?)ItaxDgTPHM3|wrj7HHmbcJqKpU9EQvj`P;Ub^2q3c~*QUw4e~q>| zY!Zp zn35Od-fpTv4xI~kmq_@>TU%|6xlVHaDu$tz&3{OjG`;ON;-x4!kUp48CyHM&JvyLF z5R)Tew*z$phGdBPwD}S3;J_v%Y6!8j2z#4y%s>$0}2Tf<|9o+<6mw-W{Q_0<)s# z?szjeue+d8x%8d3`1;&XY*)4HL6pq7lbMdb0J=tNxYyZxHt>@71Q5%_U~1>kNUY1_ z9E>z_$4RriHzF#N-g*=%bh=tYv@JI2-DUb+&-$19Y|fXx^6+|t9svj>mp(KAA0!*4 zOpBTZaDyxSzql`jw&|&jMMq3S$F@XG+>)^qkp;H_%+*JBG9S@*@$X{t!zjzB(gMY z|Aw|Wo+&g!+^d$Nr)NTCXU~r0=~7Gjb=&aO?%ISa_Z)^IZ2!(!Mmh1n|6`nA=&t@_ zf~@Ses7!rqRBrg5K#5wS1%m_f=%*BIId#SjL!2Vax0|D+;}|QsxD}uIfuOfLau|*= zOzdcUgbP*|I8Me4iEt;X=!b^zOUG_&4VksXstRdgj816VF2RmT>bDplOv{CFK+}J@ zO1yr8TC{Vj*FAq(XCyno!uPFeO2kma5XW)iLpR@;i5bbv!r6bw+~$xrl1CTlzir`pbYDmbeWNhl9nA_)c~oBObdi)5&=fa%YuTT!q>A+~ ze8_1xAKXf)xaIZXQNV*gjLvT(>}QKx2SpW}uabmmVf^@;D7UrXEs}XA71JXzQOSG2e1)4lN;;D%N#q!8iBQ_=4^{Y1S}$3J|m3x;{A zM}X|-hI31vma%|oou^in{Aanqt?l|d3E$S0x%ku4x0zhQ^fliDV{i?78g?2}G3+q~ z7EXY9U8M!+6$(b?AY0Yqt->!Nx=4Tzf9EXf6*VJ`wv0Ez2ETyBdRMXx!PZKDe({uW z`--aG9lSzjD_NO)yAZ09?w3I7?5Cv0HN2uD`0-D)!8DWwYU{FLgr&m}hxu7Bc3m89 z@!Wr25s4EuhKkPDYZylFlpn2iN4l&JbCRLMoTdP}Pfqsv&Knjv?_T(pShP?FR<)rk zHi)x-YMY-+ox@p(!XYKKCQ5+9MTiE$vGDjGFDrw(YmPw`hk_>CJ59DfG$Mp#gg~E! z82tcK{F>#;0U0K=4ZPdYu3-VyghPKJMfVyQxzY>j1!*|LNfBtKVa^HYt3D@TbMMME z+c_|~$UnOc*|^Gxxo~MvU+&`)LL&`o5;-R9Gn@;MAPctf%HOt=KA=bl9FA(x?+s}h zDyhmn6($H|_+mD1#dhr$w+XKgF*n;{ho{cuU%qvk<4G4+DaKuxs;7BBtAn%z_kDqX z7Shk;m2@~(UbGKI@%c6%kV6~qX37vqT~k8rf^Bcb>4$&C6cxDF=@P&XF_W)jij&W6 z>^-$LO*|%k_OV@3QN7+>9B#rylkgz%wHASKoN_L!5*r37ffS!I*LwJTb`@&<6z(0n z1eZ09n!Ax&9HfEU&6zmt-KQTq>-Bwz!x7h*>bevWG}&iaqJ{gA)>3k?AEMu8P*?S+ zyqo>Gkr+_Y&;Yti?dN`{1MP-QV={}gFfq3#7lTU^2aik88T)24-IuK@(5#u3 zNuW$qjOZu}lY8tS(d_=HX5X~I88M|(cT42|ldlYMUTYIHd_6HI%6fQz2WJy%rc>NF zaYp(?DLwAP{x3ru`CN-lY|aX0_;kT{W4mC38UiE7jt8Ek_7`_k)ksF!NJF@dk0AA8ZzY+Ciq zr`LY{SD4ozFemWF9MQS=v;q=8_`u$~={&9Rp<#*E^(y!)o+Jf?n26;{I6dHa_AZ+~ z%9HcRl;7a)Le3;QRstAi(^Oiq+vZgTK#_kLD!g0s7k-=sOru{W)k zZ1$;SO~o%aUIg!4(0_wGQzs+KO?~aZ2bd}5~Mr&MG=q5WH5x__j%vm_Vg|+w{75)!KO-J0b6@HIe zTOE*(Y{ekBU;(FPE}rn(B{XiyC#M`LYxjFC^+N);deie3_A7kZsi(()WZPH5wAIJwJ!vfQBfKz8srsrx4Iq#hI#_n?Vf8h<$}KV2X!B*AO8j=;%RS&Yv?A}@_fz3E3(>~G zf*z!S7`Jvxw)jmXW7s{W$ybGRoph04RBK2i(lqSdA&F_S>5&`yLDOs1b7-o;jz1$1$ucF<&%Wqg{JuqB8RWncM|hBU%xL7$rpi+Ih-j-4znx&bxzY3)#@s!ri5{2+T0wz>P4_?=9)rUmH8vU&kRHlPWfit zx>%7?>8Cb5>$BB&`8BpK>Tgb{$1AA1np?p`xwviUn&cNR=2Al@H>m#5<=JPP*JFR~ zy&wOf{zJ#VKg^+DwT#e$?{ApO4=xsGRUZDU4u7n$y=Ixj{w-A(dwWb$TWihc1oUNUCO8O!Ay`$v(4^O)eQT7i5w_a zuZnSO_U3I`3Ru`8lFiDsU*$_it-e%3_#?Lb5o>Pxr=vOohbt1(uBkydAe~X)ysZ`O zPh~{_eR2=r2UR(Bf1c?E*?TrjYb#g9=|B8BUiQf+HEg9-)-Zf#*b>ea)4($`-PQ`| z`Q9xXz%?06nG@L%W+#T$l9B>Sy5K8!xKX>?t!E{T)aq&2@n-uH zySzg44}G1YTq)+u{gI-IwnCRO;juVbaH$7H!^6$X1LLaO;|4+t#{V z6wQMZ<{^JzcPzq<6wi+4f23H|eho-~Pb&1MK0uxl-u`OV^am$ypr-7EJnzOMFUeG^ zL#WTn=^xXJVurEtUh2NHDMj3|2v_99Kfzn~$QNtBh9%Tr_4c3ak?u6%en@g;=|ovP z8QW}V_Tyq-Cn|lR7@qc)7UD;NAx73`QBX98K7i zqB74#ZalJ=qYwR#;AOEIN%ouG5Ea?{0PJJ^Ltr!C{M}#H-g!DYc;q!($=_bPQ93>RH&*hj-4J-cV=!sgd$_BAL)?+7-T5r#)yRTHg z@;#>;I6e@)Ha&kJ1LGR#Y~B>JGBcQ}f}vc`hDyANLw~1ue#xV>)U=+Eig5X zZw83ejM)L*M4g8B(7~;c_*%Qv!pjq)ik){cLCB1`deQG9&rN53KOR`c!0#fMe?DgA zs(SlU(dw-{C<+eoqWG7>x*3g)cce|j26_)X_^G_fTg4vAu+$j{$vB&Bh*KTn96D8b zLJAoDRs9m-lv3u<=JFf|H!GElCx@{xH3d zCf_eUd&n^#`nEc#$7AeyE&2bb8+n+g&H#Cl=3qFui8g&WglmkEQ(%1Ka^rTj5T%m)4A z^a~Tgfhp81|7rC3O1J~1tpDG6-c53WdK=MyMO~K&VB3GnzXEMVRMh>RSvmAGRzAXD z$Wxx^ZhdX(&OqF|_#~ScwE0SAom<;j$L>yIpXU91dTl0oKLx*33M{+&O0W$!zhZFh zxp3T9Wi`fL^qE_xy3bwAL!gd^)spMD{rB&|UQt1Epa&1$BS@9hyDX(;!zJnLJ@J$+ zuelEX_=(auuRw13#i57*>f21cyFVZC0o>JevQ)%F4BVz7){{k@Lb8Y>a!+<)8qeEV zO0yjD=!@og^gMbwD!uh0b}YXba?YZZxana^(;VuI>LzN3P7dkz-oNpi=?kbjZq6^? z;xgmT2_K`6^-3CklqW=2&`Co{iBTEbWuRi(op&+?859kckN z{rPmNm}h8Z**n}OY;$(#+s83RsmhM0U&Fh8mgIox6mL*}_2-nTeybVLNXe0(rgqa{ z`ML1fOeyJ9i`xy-a2#1w!8DEPR)0W~HnSK*AO?)*o)yp?x%ozdn8X9IcJFu9Ow&;J zHbTez+tR@pa@hc*Gml=d`bg2(sHTbCWWRJvMSBe!6^bphF=#ydLFjC*n$j5J}wMJXP+4ZnTD@s6?SNK`|gq;Gjli6OGALZ zji!IPRf4{L3?N~ALPd4a&q$;O#@~Ih^s!5`D46yY%B2kAl1)j<{m1A@|0x z5zpV&BdP%{g}F|WA=~tJJiB3$M)m)6)BZeO3tDDdqDP&+o;F)CNrT$ax$ckf;ua%j zCuqRcoEsr+6jmX{&=R57b|w#LU(mqh(f>P|gVX+CI6?xNBrA3>)pt}UbPV+gJyZr* zDfe>%DmKU3xJ%-z{uP@m#(nahomW=|O5@=6tq+a`ipgn`Y}L$T`)@iWJt*!4kgV0j zf@7fWh67*lNOxk{yj)^4bL<(wac`i>)VN-uz%<@R#AYDSm8Co!eA70nP!o3V<#pft zCAnY@$k)1td@P$q<6ZcCWc-K1OW89>X6ybaHAA(AtxChPhvupEeKvBYVKs$Q$E46d zeNQzPll(?X)9&;*)z3I}X-)e2aaW8-!-v1SHF9U=5QK=TZF#cd`e)nl(l<{3ZNFGp#UbT;t#Ez9GbX2yF0SEweR@)< zH1@N53v+eQWOb^|)P7^T1sOSgr*i&dZ|QVs$>VWG+m4Ragvo>LbCig0 zk6+3h;Yb62?O6)X!r3+QC9nB6vR)R;Jk*p6E!B|#*fiU zK2@6oVn#9*48es?`}ckJ*A_<2S8@gL4LmIcptv|K;7(WpfzXBu(uh(7z&SthGz{Kk6N zKN&L9-n6-9`b95z>O@3O^U(792?jE_*Tkje9tE~_{+`-p_x-)^?_co!JmwbMsiI9>mt+d_DCL6UOJHOZ!A78e~y&@{`r5y9zBu*otPtQ-eVxS!9uFpRf4^ zPj!ex6I6n%>ylH1>Wg_8Sa$?gEp*c`ey+Q0y>~Z%T}cBMX*k^d>yRmbpE~i@P<%bu zmtEJXKoa#d{GH_@wbp0Ue zHVH2pkmDBasQdM8zI$ z*Ui26HHYc##M30_B2rCegZ#AN&2!}JC9=w5=d~JzO=Xg1M+?s9R-YMBm)zXnEdRUU zR0w@x2Wh}^yRmsScrUBdb0U4ptBrJa_x;s7(cZM$PJHLl`Gan&`wDZp6yb)8u4935Zpm)3emw`=U9tLb1BZ?YcjVMkA?Dq z^kC1<(p?runD${kkoph7C(-VYo`p%{oAnPmka%KZuRjfc2k0XhQ=;{ex*##OF6<{; zlnZm1eQ_a$_YP2{Cie-jf4Bbd7uC2uuE2zq`#Ik>W*xAdYIMW7Afptz)&D#=O5CPl zGAbeahx<&(4);b`b5XxmLY@nA>_5b>-`oRl+}P{a_Dr`|d(pLzTwklv2rN*L z4{vsoU6xf08k&(3($6z_ z61}}MPnN%VhY2RBD8iK(414e&U_*$}dt5F3WpOh%i?-ZlooF@zGDP+TjnXRT={vj| z(dev^g@@7De?fqvLK|4~TDy@d(>h4=hHL{+V+VR>En-*DM0dn^EK?3u>>XZVgo|$? zb=QG`?YLDvQVWAv3?WG^@C%c>Y5)|adz1`EG6lhmY~vHINP_NvGPTFqVEwl+HxmJP zvtHEN1h(kQJulsJL*vPC+&t1X{H0KR%ZxEIP=c$mgOXzvZ^gQJ@(cDdeT^_b zR*XE++P~@1lsuexe?Hvld#l?_FLr%8uai7OwqAmZQSFFL8*Psj<_reDsCf1kIhU%2 zSpqj|dHUadJB~tX#$&CC>77}K(=MeN;SGlo(n`CakWSdM&6k9l-ode5(|jkI<495Y zR{F2nhpQyU+C9o}vsruG{C9YwE-5h**5#MK^G#_>XDod#1Btdke#-&TY1Tvzq5|a~ z?H%e`Xwy+tueZsWpvCIRHg=Sp?GHi%!9p&&W>0P{ze1sBN7!dN6I|D7xkac^!#TY_ zqUC>|1F<&;z{X#$NnKgHJ43EuD(!?Gepc9wDYvT+X0^U3`nT=+=bpOxan_xSwN3>j ziSrCd09aegu&6x|w)0z5`T0(XR-oF?D-J&o40l>|y&om5Y7pawY}CT;5%zL-{SrzQ zc}GuZ~AX$*yy~@ zS1%7M{sWhuTZOu7Yf{>nO&0E$pP>Ny2u>h1WaG<$vm zf(cTmt|mazC}IXiVlGuc?c;yn#1ry?LumjIep$r>Y03R=D4~yg?LXm+16)mFwOzLe z#>9s0V9kMGcIJ2I@iU=gUx38|+H`SsADIjdZupz+PQFskOK1W#;Y>`6TF~#zT3qH+ z7A|pesbJGZpW}qimI~ANk05hiqX~>$^XH7?E;N#Y$69hWND4|`_%lH+Uj;_Q-^M4Y zZ%uh?G!ZYC%4`)9-Hu!8kLw{BwDczrSPL8Q+8rv#VF=#cq*WA_WY%Ko`_ER6go@o5 zd9+#qv|U-mdIx;@SHME{$xQs(p=d#KI<-9CoCWhC5ovoIO?v*Q__b%dwdEs)n$djR zCNkYL5^#eMY?E#|>bp~K>8D?6TEi?OjIwKB$pAtdDd2VfUMBtqxS%eO0RWtP8cxNU z|0jF(3d+B?+J5#Y5WcqH?SdBVV8~?CYP$g;eK`L?3$6S!2oNL4dCxB`)js|V@S-*e z0sXHXe3sKoP4A|xKX*CE?gs8MY|`p;ez@gg*wF!acl#!3jtdA|fMPk@%@ZVL=Jf?A zVaD}c>uVU5^?gb?%4`sPg`=aW8SKs_z4)s4csQ}ot=e|FyNb;{_>kf%6Z*FxkOKi; z_fC_$Ua{@~B7*r>LyexaFZBQJ+W{mL9wyr7GNxbuD8+SdQ<1sF8C>P;Vk@3Kkqn)Y z_tbr_5u1Sph?JmHp`g@!>%g$4xKS{t*i(Y%Pu0-nSz=o)8Myau)a#s9&)tPn@;;M( zIa0oeT3r2*g#8dOiY)D}U6Z7$w#`&gRZu17DUjLul zU&?6D^qFB@f19d~UxhVXbAUSnFTq#0Q36S~{|ln~2nx3u^?htpOl-j&y}i9wGbqj7 z^DDFmcwfxp>S|5(K8g@)&}CjxT(C z+5g~vK*v^uvMY_;MYUNW1jprEGSgrHOXg3fiy2>s8P3iGq~hQBHY3sA{dW8iaLrC4>)yt zLT4K47UM#efe#ikrC`oB^Q~tzBE>}|IbKco5C7=A+|;h%wY~5eq;-SqWf(T2STS5m zgk`0qyLwSE@amL=?a*$+7ufLx);j&~qx_-t-9{O`mdXy>IX$>ZP!K0?#A``txQZrk z0y&oj0bt#DTbt7wzSHsmq78M&PR}lNM-Ih86SS>5j?g2PFmu}d_D}!cOCAq;VDdo| z&i()V-v7J_45I&T1fc(+p;>tER)bH24=Um))jYo94jliEisKP+yfXh+hy59Q$1N(U zunKKH0VblGSD(6FdmI&vSih#Ds}gyA^p){cnD%9r5Q^GnmLW3Q+WWzmWFN|Il2|>b)9%(1TD!Q^&9nE(!a- zKb`;YI(s~XfjtjM&8;e;IgkG&134r69_UChDjfanM8D9z(wgu}MRYf@wcnU6mj#oo z{O>IupLreNTp_y4!ob(Xa-oOI_cQSd$1`nZ9MrP~Oa7jPS95?1J_cw8`M}H$C)-HY zv=X1Yv^4YQKkWfOqz1gw);Y2Yfx{N$hSI$i9n9+%2Vo8e`&Y-vBLS*zJKy!B$Gw+2 zBL8iks)ETEdXKX4(lc#{(`ojV2%H=$JnRG2t6R?vZa+M@ntkmRfBUqM3UDjF{A3$A z2b!~RySi(2AC-wutb@X3I@f23SU`^9ubdI~Ie2W&MY7exv!_-K7iJ5Vs9ZQ7{E%MN zf0>r3MBW4vLp8xiPC5(0qY}359%!JDrt;^9_!QiKcBsO)HQ#ga!ID(Pr@#s`+$#Gu z6b)*PmIfZb0<@-!NfRL-xQ2{7*fqbN09NLTgFR;;KNbZ&Y|F-f+Z6J*a{l4$WY;W% zpAvV+o+p5yjj+X#QY6+P3%TeedavSAa5>$$dO>c2=d3k&Ien{vFZ#>e{?5(xG}xlo zQb(S$;QQbH=8?J{HB%B!!MuLE3!KV=rM8;??w^I*<-Up}@!;@@khmpRX$kv~H!oh~ z!}2D4F&VtR?KJk!a#|ImrNLab?52{YYLaF4lpzU4!rxqy*jJ3fDEq;;)6fm&duv85 z5Rb&^7H3=JH&`+g1pD!8|IQPE9p`gZmta{tah`RpYDaOAq=1=bC-f5M4&Qw~=k_qA@ z>853`@CR3zl;BM6X&~jT&=t1S;XkmoAbQACI69QEk>)DV=Ls1A*G4GCNtD4LZ<^M&Jom1+j}tXQS#f1 zFgSLg{?ZPT(V3j1E>bRkpPS+WU!Qk-}%%J!_?cj?H48A?v; zHA0BW=8uwmau;aTuYOvhlHb+Up54yy-@MhmwFw=PaIitre45O7sIc_-0r@`TNw=Tc zNi`T(w~BQO^T<6(R+0icZ>!AP{q-pN6a$8_3ID@{#Mc*aPu|LpH`k53 zO_p#s%S;EpGR$g%pzm>rZ=ygp*o(W)(})S+VGd^YlD9^lz|Gs5Q6$X*dv>1i+Lwsa zmZi^o{BDd)mX4e*ymqrfrb;RCONBQj$=r$@gD7 zB!7Fv9tX1gRiwTm;pE}LID-M^0PzyekraG$&-Nh$mIv1srlQ5ur+zvO{;{JhxSU!Z zoard>+Fv#B<0$He3BK>49xx&+HnA;hhogPX5MU6#t?NF!3Oyw|E|`Z}5HUk8EVcQM z2s)R770%t$FtzZ09NqQOG`cF42R(k5UD;BRIuRfIZ>WAQc$)faKtPZr7VIs`ybPkk zM1qaq_pq$!-`XAe+|CgPpvdv(#7tMFTkE%0F6lspM^YakQP%iM(&@stxE7#+M7pWc znRre=)*XFUmh?Jr*pZm8YyJL;hMZb|)DmsNDF;f;CyI$UoFdKgOqb82mA zNcx5)i8T9Xiv4oKu8mx|V1GPh&hWkr<1>uEpzxrM+8TsD@XiCqd#GHRy_vCMc_`NkB@Sp4Ezf5vIvHG8SOm{D* z$ow9?B74snZhOrf==*aJUI_2K>Ah`+|NArlRlnntO6LDk?B?9wlmDe2)7^#t$FKkU zJsxpEmlRarFNh5P$t2H$@D;+jIsjZY{L;y%uo}JKT5eOwMvH*Ds$3 z^u3#(hNc229`am_1%DT|Y(08U#HfMn*6Ojp{u9tsZ{8mN(y=?I3U%-D~h}=f$ zywKs!AOdJ!3*Zoh?Yc43;)Qi*V&iLWY9a%BHzgCh&;riuS6br90rr=tg6|+`_<=I7h@Lx+K3i+X_^%z~t`hcn<#jHH+LkM^JD} zh0M1szuYSwVgpR!0Xn;P$FpC?S#-v<{hgh&bqoUrxedGuUgOv9R~E_#S{tghc?XLFYn(M&=D!~g!tj;E7yPJz7G*U!+@%;#J zsQdgn7L*aT<9Yx`(mcnqS<^GyhFiQAy#^BV>o+}x%G06)4n7T;q&GO{)Y@cu>SQ45 zO{>u-ouQq1O8ywH#qwL#xV-F+ser8o`XI)d63;H6=#e82`9-$x2}|L-^K4MUcK9n} zhLpRzyE<>1j0?66LQ5zJ5B@;trVeq^0^yB(>ef9WdrP1c&l!?kH)u#R9{!}-3`nNB zd#MHN{hkE~q+1J6Y%@5q)~2~XIU>L@cN6oP!9g0N?}Ac$n8hn|lsmMIU(dZMM=nT*nI8JsK9tNCZsDfA~My zX{!v_{*k2>VDG)y4atZM$;$GlbVrg5r6vk~aoL--qIxP^D7>;Pn6G5#NxQ*wLmj~s zILc`W@DKfP_;a9@s9>P#jWeru1^`m1Ct?pMu3W2Ld@*c{iH&EAp}>&3d1e?o(1Vg@+Dr`2Qsx1NUhGXtuz##~|VYHaDJYax{ z)#^EWn-C;nJV<#dHJw?%AT2w zh;S7Rs4MN0QVY`UxXIRtA8kEORR(G0or> zC1|ZbaTZ$@ZjL?tz0tXZr*0ES5dkGB;#OS%0hi&|a8mIA-L4``FB>!lgf87AdFh2? zVni#qtkZ)SIO?OuNR1g$Q(dhB&UYv14ew*N{-m9)+d+d5!-cx7D;v}V)u_L9z|(DM ztO8O(Ymv5J?;Ve;yIB!XJ6)#$;krd9q7o(HQU-tk`v}&XcJiCm8DZbVfV{rzD*OGg zfDd*8jBm>$^D;>@n*1 zrwYEgFxh^VS8ifM))R18Y;6G2PM9W3X0Ht{|8o!bv^C$4?!sZw`=p8PRiRtrjku&d z*qCs_+MEjYEal;$)@$P>0uQhzEwBkFdi@M4++HdtT@kpFZgU%NfQ;#pl#&jxmN2!)Tpc#Ab!1R_)&A(8JGY zy0>>1rI39Iu9Bmve)GO;AtYo&A3Wa?sj~NxkVGqPps!*D=5-`{oDEbdMMt*I9rw$( zRAa82J$5XYPzk+lhqW0N!5(5KiU(?}_f&U7x4g#{k7X`iYp+p4dZ9_sznmlCD3t2n zTl)~8289=Ie#p1xLD>GHOnAMOKhH6ic+=@(T4A4Ni1IE#jZ-O7eK0W>%;@$vj@KLZ zHH(`+NbC1tyI)nLP zj*RP~VnTMmY1s#1Yvi@V3A4p@d5ZT1j<=}4hS^5RPa6Q=Fz{qU3U9bLCZN$>29Mp4 z;}r+s)mrSkr_3xF*cZKy3-3~sR70n)cRrI+X`nB=!*d|mZY(gTyvY|O8e`Wv)lQX8 z(uqqUA7oCS=bH$#T483AXA7MIPo7 z(M(tn-VXUb55hJ0Yw6p1O^p$xKz2?p&d|rEcg`J7Pf1$RqsFvVMqZ32jh~BoEp4uw zD?27=3kaWfcgu>NO9gcwXWIld;~TxZYtP#(t35yfP}Tf-2psJ_zR+)w>1k3vcFXYX z!??Z2nbNVT6!i&+R}u1el>geCc$+-gbG_k#!f4OoD$|i}6!p_16HAwpI76Jv(IJCb zr@mm%p@AArOQEl0Z_O1T=QXvHFJelDrfR{hs{HX1k-89rlrjj22tIuHP0JbXYTvm^ z40St10~OSZv&^s8Hn>l@S_00K2c+zWGxbKk;V$ay91!@;D!C5GWo0kppZiy;(FIvK zirHt&+#ZUr8%~G}g3KS2QVaO~2}@{~F*qp&$Im72wcu$VO0m6O)$N;MC+~fD&$|9u zjB6j1L>IHe0Y3ist=~VTI8v}9H#6~u=d#r*>4u`+j15ex?VgJ-@p{q$W&uZK*wb z$70UtMx5GGq3!#dU#~BkoUK`WW{{pYl2O-o!mQQKL_x{9ft?5Ru{CkkC<`wtYE7iG ztL@>ga@-aDP;4?Xa>uYGAvf-PoyFwv%)){66%0ttXM0)&^$pufn(K{>UZ`Ond4gJE z5L+PK8U%4=KPd_v&*vAE5tEUbVmy0!ql{2pXWjr5WnY!@klTNha%w4*aKEQ6o~Lz( z`GsM}6Zn1g(QG-%ibMhE{JR*0uFXUWu4#*Ijyw2zer=g2mBu;IE7A-c$v+hkuN#$SycrgaqG z%ugG$ZuP`(!4C8Kv=X%#b0toqHN&y*!S?V9eLi7-JU^DxhMoJ1Qlc+j;`d(DR&!Wm zg1;Q?kb%x4I}_#?e?c7y8{j|EQ)0@iPN;Jf6^FtJsmc#`t_YYQT$*4ce$ z*RSuS!iYOLdz&f%jk|2nEG?eRmTHN6S?syJ(=P8bd<(o#G<81*Z^sr%s}~n(?I^!# zR@(?1gfM{X=2K0-3o{^dOGW5*Q|U>3Ex0KF_Lg`S5y|+OrHYp;GPoFA)KD0lnU)3t z&d~vmHqI{L&u*omeDRXhJwUO3$Csl%7um zB}udh89;|QjHK7qzm84EV48VB4Mq$h6MMEsT|h&0#Bi6vOiM}*#Q|1UKr0{X#%tr& zI_n;(r~Q>S{H&PAFa`es#69$@$FvgP$v}2MXkc(1`9&w{Lv}L((i?FD!OAO7J7lr$ zG{vf{@vKOrcp1_AodO$Hoou#{FAFCPsxPBf_s3$nHW#Q)${_y;A~y!Ndv@J5S}4A` zv6_QOZJ&IkynlPOefSkdpLm~fByCjJz_n9QBsw<2HNR*#fQTV*{BV->Tuk3^raukt zKu5x8Y;(ysVtSGGPjVEzDcuhaTS9S%3@y)RcoHv}o&1XN=Z(bgW&MrSpHVI~LBE$B zhjZ?v?QYKF*Mpi9A?iKCO3&?9cGa=pM$~cD!>%}L!Dh5T_<4}G{vw%=&vD`R5dTb# zELNLBEATHVDyEfpua=2pA@1MBbS#<{(r?+j^G1ZgfNBz%^z>+zrH+Eowcrs@(t3^T z)Tu>#fN{b0n)~DubT6dF09m6#;quUJLykcQauF za5XZh3qO#MS8ot0e~;s5Q$SEnz>JJ?Ue8wu?)!}qBuvMy@e7I2{I^hhmAGk^DO5S| zuq#8nRvEFYj{-OQQp1q~OFcyWy~EGnKaW5T4>VeWDM}>e>KaZbIHA@3S~T^FM^Euc zfE7kjtu8)Co<_EgA}0xIMdF&4{`u!36U`pX#Lk~!nNIE@ zt$QNDJIjM9g$eu8lwor)N-Dj@JIl5wR9d*yhS_*>b-$T_j@-`rzNbHesyN71^qm#H z>8=u@Ax>Io9}xaHP$aV2zVUh^dL!ac?bei)-TiPJ?|~l00Ol{w)d2D-%^>UbLh}@J z0&s_KcKyG9dW6fs#JxHdi%j&pQvrUz(O1}~Z-da--78062_}aAtGRt}16NtzU3$A}&90qTws|crf zm5O!mnAG5Fs2Sf%Z1fWT)MpoA0oL8#^{<}+5+=Y|CA~;`9cIoTgf$Lq=MKEK@mdfA z$_GIbAWPXEDP>>YtJ1YB2)jrSXK1+>9RfHN0Sm5vr`_GR<3QOOOwo-;@=m{lz_|80 zImpcC(jx&`zg%(4mSLuxVwCI324%Jotfmm@+_tJEwEA-*8cf1hii|+ zVV;JL8FeJN#QzJW6>tv4OTqN>l$HsFdn{p%XSHgZYaj1wt1WfuDHy0BaH_6{$#2f{ zvy>VFH;BlYfBTOYrlHZk95}mMZe!wsrv14bNKpAS_IvGw*kbfN`64#EIcH01mgTa= zX*+(3HL$X-rJQfd_u+80K=u>)he7D;C$g5_dXO&VhLdM)>D|r5X)NPe?&UpM0O^NA z{1*Mj{A-<-qQN6KDIGwb0!L@`r$pq#;pe*yNfcaKlF#S(wRB>KS}GKAgZc4nRNN(! z(XTPAUvvMMvSsydFO2W@$2!`4lkYwwVhdn7K7T31a`v+HEXIF>V}T?w+W-laR1h4{ z{{_ATJ|_bm)9ewljd=ci<8ijZxi#*~#g@KXnhk&<%r0=EfC(3u2_G*5zAt z2)#@kipU#Iom|}Gy(3oP2s?l`d!qYBmH|FNr1bOMmrShu=E(simQzyJHOA$fyPZ)i zoB80;_x3ZnjwpFMH-SKFcfPU`Oc?{04jm->SItW))?h#}w!l=TR&`Ly{$X%!ti!;% z;3Zbvx^>^G8Q^6BRQKaJ5k`fYR@=750O97e;MB(xOT}7=?Z2+}fiC};z)VqFOPcDg zO7md@*EY^$>5OM%remSxi`2k0D?20dslr|Yl{JSJ=LwjKdt>(N+I*9&ckEIyez{E!h4Nl{ z0@R;)pJpHSgvBmw*hOtWQJxkPUjK?Kzn;@{>3!;KJkj&Hag4wouobJ2oZ>1#?pI_) z-Te)_quVTZybM>5r8`HhTg?f@4dpvi06E6T(GvVOn7#niKl@M4b>(bu_r8bRb4Ancm<=u%AwjoUP$`n;8 zU53uc=(u{z-fjubU-LDNC5$Wby39+Z;JUwQ0z3uic0wl+6NnV9+V{~+={GwC)1t^> zPth+p+ip9@@U%9|tLH!9*iXNKkeFBrmVH*Yy_$7z>2MI+_ECubBlQ~=kbFBEV&i#K zmWmDUpOHavLT@&bX*`s!JkN4jRWS$yL#riZ+St3 zMY$cv(UO2&m@?uTA$gD3GAkowopwTn_&GtG7?aLQ95RdFT=`N5@03q)Y9P{FS^!Bc~vqdd4=8HVB<2TYQinrN2JGI#Jv}3>S&coq@)tWdF z3iI@;M{>?+VS25y)l5O|8niY5;X%Qu?%#{DxaH^B3?nM}u_V~`p?{g3bWJnpp|qH4 z+K{>#9JBFa=-P;7*rBsOL10I{NLUA)T<}H5rCg|=jbC~)5byV zUnRBt)D}pNDelXb<%Q{(eC^H58xT0yIEu=&VLqy3;S_VzjFCRpvI|g37hai$(}8md zG&@W$V9dUU@y&qqGcR_c{=x!|e|yYf74sj2zaq(Y zD_g=(Sc>UIV<==aB&R}wH6>THKmGMBe&KuVLvCh!tC;4 zMvFX5qzhFU`vbf?hyvzQ!`rt3=8&?__s2;i8rs7x4_)#l7K6DgBpzSfIV>er<~dgW z39kNZDlc8CM_#4O&sJD!CDy1TybMHLFeh=3tiHF|V@E~T zWUe_FJ-vP^kjPlOFbL+X(2vZOU4AP9Z++taDYS>rvv_b3L$S8(`?|m-X$wV-l+5mV zW%F7}rUVdL^7bw(JA7*xRNNqsgYuD%iWn2UiY1NRH}sbPybfy=)KTY zJGU3U@F@#WDiUs3m3g-<1%2^f+rSGPrVI7Laui)1;DmQg!AL;nKN#-&3FJNb#j2I` zEfP&g9N?a(?Dx++;Q5%A9U8n=iOFmv&$fRngg0Yw-5U) z>HEzw@-3!iV0^ka>vu_(VesM07)5gWgUL;H0HA73<6-pI;IO~oQz!S(T)1uzjBYZ; z4@&Vc`5T5$UH@yb{G7AZrQ1$TIgE-NT$m6|W^U5AeE^i0XlQ6$KJ1ltrTfqdMzj@1 zkJh8-sI5w5Voo<0qOp->tdhYkv-$(!d&ng4X{I0}?py9OQ6S`#iMrbp%Sc_yh63e{ zI)pqq;!V3dd9x0QUu~OY8vbGSDjI{nADv(opUt|NB8Gb~>I(!{vrhtw`)jY!GTMKC zHlKk#srQ*OOA3IkPjd1%i%;2BYB{oYxff^?4n4lwyT3KBwc?fPm|=6fYrR@j|1h?mc)c4be@^ibky3Z#$@MVbd!fAjG;>-c! zY{@QPCmtb;yR}B+i;3N?as9QhuhZqtt-9>^C}kCZl1P@Xwg1-ZNIhS;UC4Y{Bd_M! zk2R@kqw1HqazMS_=FV*l=)1_q48N7&7zvFNQ4>4TT>bGmo|4@=Wp&noC+HDPzw1nr zCAWUZyZ4530pwN5;YvYuWO9-K`*qJn&wTa`M}rnmw;uP2D`jx6sZZU!igU>U8J-?1 z0F`(6R!K-X33dr|be}^UZyAamNzeu9O#&YFT`Q}3y6yFJ4=f^9>#ml-eQwu})8F)R zTAKm2t#rA{*_N5>YASVv#c(JaoQ;G-S1Hdo=hnUQ2YJUw}(G1!ZkZ?w5R}|68DVHU|{xBM8tUGecVJ~ z4j}265B>f!Rts%CykphUMB`|wUsk(VVR|3e;Fu$vI_IN<0hznJ%U!u(7npL8V$ieu z95ED@Jy_oHZVDeXMLQ#nEfyUG;aXH?03|SPq*66s+CM6z_SxmNoGLk_O|`z9=k&EZ z16;`PBBdHwlz+MA7*+L`gxW@qUY63-*poCL%JNKLEMiac6vg?$_|^jhkNJ8B6DMPY zkho@X_YsxCOWfzOOWaxB4%Z?kNLK^YPaC|xTc$Z z2NTtXbQciL4mH}V9=RhoOVAH zI@X3QUoO_4_%h)>;Kv343h|j?#LgW`>a=nIu{rPMg{vjxGfc9a=P$5%4!+4)7b$-B zJ$2seTGSxO$SKf`Q5&$5ZLATA)EF>xYVQ_o+tKI2Wgh*|9T2R~8PI_%6lyPDzf~&;HvHU?%9#G|ND`~D$dDlYw*%*LBybDn!KF!8=EM@1O zgJUOGKHD2?A7dy&rHp&#Mq}S#Ci6zYK6&2T7(GC_>z02|;O3`MU_WGwH9`C04)4Pi z3!JyE?A9l7z_vdED&)=aB*)L%yoW%9pHMzs%j&E5T->Mt*Qj`7W_MHBEyuV83n*25 zg-$kYdRC*C_&$k^g3Oz=pH5~ElBP?T>DJtrTfA`VDXJL1wH5qA6^vc^k|(dN-956+ zq*Lq?J&KmhsNLM{kCjk{QsdaA-Lg*Db}EuD;&tWSmxjttuGoiTd2^3S<1RPaGq6Z6 zz#?=S6Zm1>qRu(Q#qecG|-ND-K0 zSb~7s`uXJUD6gM5!FKmY6zg3E$5LG-?c8&F1W5{*0j$}l0VlP<2aJ*cZflKtg4_h!N?+pUMk$xO*~<~4;^ zzI}b$XQeJwK>88@H786DxRDQr%I%)@X)vZ0h12h-^F5Z$!aQg73s2)+yt#5Ie=kvc zvPDp9CG!E#byc%Lr}N_6GJY0cKJm_6t?k`!E8g{V_?gR=A=Z&Ed*U(25x>i$oS#dC zr7Flw35#C0h{uuU9#E74?sc}ydOfAUyIFbLQhl}EcxLzkIYz@9c4_BP#*eI~R5*Og zD9B;ocsF6>^{vYDW5a1+P=p0moNYMFVa2%8&-d&&+Ve~9sYDnMLIq~Prdvaf`?lOl zp~X9zwO%ipEp_f!tOtDLF$7m={plTRBU6Q1;fkrOgFYk^4j3e`k(r-aVKgS{#coRD zrg4V6+>mk29K07C_B$&+|Mrar&ksOV8)B?vwN@N{$fOKkDM;HT?mg0{+Hg=YO@IW} zrAFTl591UsED8`G2o3GFSZ!}f@hKa6$!!x1k?xX1Qe4($X66S+=NVph?_u19R>kN6_XxgPc_SI z;MWvHKC1CI#Mr3jd5BJV%q1g^>?(ZtJv4f_Hg~@4-*~<`DUC7>3r*etvYxM)O^{oU zmirBS1n=bD%~lI}1hjDa{VJpCLIHDUKVw5uQHI^=F>-y_8okK7M_pY_d7eN#c=g$i z)C}y-bzfWU65d`xE0=C04|la3tzvn)FT}y`^F|V9Bb1izgBm5uTWLr!W1MwL4G&l+Dh}>p~l>35qyv#wtHt!D25fTTtDESqx_V z5>w@cT{KbCOb|Hext8G{%DpLVS3Jgy93OmO{^z=a*0n2cG$11oUt zdvU=#ajEv*Pxatj&leR+Co|@&Ng`D?9M_8M@A@ZR;H*WtqO4Q#N}SQra;UVb9zf35 z+rHaWTiLli`p*1}ud|Dx=83Gi;sJwro=}lFP{!3-X#$~xosW>hSmatn$gag^ejnit z%DNF2jkta|r`P?b~eNwl&RMnCqVsm5ykvjTDx zCR^Z$4+d(V%b{VblXA2g^9H)kSo@ybm;3aPy!~0X$8T-ho)>>YX+^fby*F{!fR?a$ zZA768GWmqW(F0p5wj@i6UU5nlovFHJ26(0eE)z_neIeD-B_h&U3p_t3{-PjMLdXjt zkPI1-MWNMw0HOrfKl?P8E25w=e#^?w8~NBTYVzWSvNT8EisA*!VqDqF_kgDngjb>L zhr8fT?pz0?v;6gRV3k=lcg)he^H+TB@i%22NB(B#h~e~J-H(sOLrfYw{?NoTs5H1{PSl;WP@ahv98d|x8%_x;CECtkoA9x`8HHzIhe7~Q~z{W1&?DGhFv5^Tzn{@wVJKMYvv3lMB|63F``1pYH;){;q7qTu#LJ%r@H>*Z#R2oo-8=p zc27#US~Q=J+-$AoSw!Dqr|9OC&H8QM>3o-Fc@2N;(dX=g{{mD2S+Gr1#Ur&WFE+7f zX{0S3O_%BA6IUT^0y9^+e`t>(1|Msfi{ z<|I|0pKwuMTZ%#1(a-aaUAFABZ%PWar|et98Xu-Y!pJsgbv)vsU1^X(Y%sfclR)rl z&EDQ~DnEpteIY9SEuyDR2*~LAGU%U|s=wtr1NML17wBmk zCD(ga*Il-a0+2~}8NrdD3W0pSG`?HuP=sotaay&mNMpjr+)OCF7|~rEqcSltdju9`hLy=LPghPQ9X@u0>qyU0<`)C`#btbk8*N^3 zbQKYF+J2-iscp6XcpTrZH7&DQ(GS_b02fsw= zSZsg;q9dcE2~BLz(fkaMPiv1_K<@F*pKc2PX;3?Q6!!2EL4|PY(fL|!XziO>w?1=1 zT(r@;b|L>w+g8S44)dD$%pn;U$x&!!r~l2MeG3k$XK1fB<&i*gI?-!*WZYZ~T54*3 zeWJH7P*8xAQ$X{oXtu&-T~PqG>a!$-4p{ZsGFHx(&hC|c$rNb!0;EvX;B0>9%ekb( z$Y{qyI}Nw!q~CxQp7Tk=;b+|K?s$oX#;vcBhBiQ-KGO6@YC;wt^2>w;U#|k8zG^-T zcY4@5Y8!1`IZ|wLGeejRx}0kWHPei=V9ferOGpEvoS{ChUlF6e`AIz@6EYVnylPp7 zU2Aqasf*e}2)Ab%aa#5x!2DG;tLX~gn4)FjqZ2MCIz0!nr_O(E`zrm;VeBp@nRZFR z#-u{3dLyp!;tTD(I~UR+H}`jl42-OMFYQ@XA#bgE?9R_}2pc4xRTY2ZpbxIaK6~+q zFuoOk{#(I`C#4dXWdf4RnP=#U@W)2LqtFaf>lhj2UX=TXlU+`?ub;oFZB(n?8{g~A zLP*oiyX{;utJ%d_yj2lDaiyob_$6&EH5t=7i&;d5&wT!3z9(@2ur*#CDGpx z0!Nxtk}B>4t6xNteBa2Us;_0$ z`As|UHb488%!IMCZiu*}ixFifwqpahJ6J+DY1=Ll?a9B6R0o z$CAoMQh)KXsfv7*)2&6kskZoW>A-d2V7C1%w^XnFtDQZ*UY4S`ds(s4x|HPflnCXC z_-|Xt=pZUYqZ@3^o%!ua6QhRn;RZ>&oImd=r5g-aAUZJkn{#D zIiHuuu!}8gSF3sVHVk6$w!vrP&hlIY@}b!Y`La_-2%3bWwG7qK{o{{ZzjdX+w2b;& zz{C=QyCsj%kJG%q#`IqeGc}O_ukXujt-&w3^<}wX&_z})ua@v?71B%K9I=JOZ%Rz~ zCe7ERB2}Lp*sXvV6gyly_cqB*ucXW6+;VXXwv8@%TFIgjjz8Zeo#GM8-YmqwU>x2i z6}PRnw}uQDSOHwdYpcl7gLB+>aOetBKK*EO@pVY5*3lvTXn7w-+@&FuDfi;9y>7Xi zckkuo#fn6UQU(q^Ns1O`q=KWyF;%_8eQ-P(62!(sK6bf%W5~>IHAus zFTT5d#y|>sw)dwE$A@56k883RjZ)3X!iTp(XzoU_x}5V|aPUB>o!Z3T>O>I2W`(IU zNTZX%5W#cQ`pfH(~aa)Ip)TZRZ z?9kckJH?wdds!O^nM3QPF#J*rD%*NP*lnPehP@d&8WY1&{@@k9u#TyZtPz_&Z|fHf z69XAtIe%-0|0d#kEq`pp$mmDk*V~9qNFqO-e;sXJEy_W(TvcR6;kL#n0v_&1lp)2e!0SncH+; zRN7O$1P7ltZqiWO=xp1#q~yJDYP9Mf@;W~4-~}dc;PNAuch)788_zl5H@mQT>IJ2@ zW&NU{KAmkt6BC!cT1f!FR4nnGBdx)VGdJ!dd13s#ypq}yu|_3LBWOvnuw1;hwqCX^ zcF!o4O0a0G*|Tmxg9_&}S48dQ+kb1vkO1>H4La1ivJMu3gaj0PT%G~~w8O2zh1 z;-A?BA;i>@@O?Dcs6Q`V25nwO*N2u0zTZ)pwcO|}t}XK6boYa$4KM-A$2RXbXq;QJ zMW^S5U@uIrQy`~;*Te+-PeyiL?*zV1wR6l-_>{6yfHF97%d#7j{$ZN3JMWu!KQcFg zneO?d;tDB1sPNU_n+O#~>J5zT3%`R;q9m!;Uy5cezjwg3a|oXPo#c>VPu@v3e*TUx zpN3>S@09Q@pSkOgJ0l?UV;;##XZvj=E=7s{I+tv=;xh|rzxa^X} zCns0HXCOfc-{o?Y)e`~xas-{2^_6>Kf48{*Fxf2YNd*jf=d~25XO*t!2XFi6NgOrf z4n*n$S!T`vGY`BMgXi3(?9ey-a4fn^;BlZVU*|5lTkXz zx)axM2Gwl+oJbwp&83Vd?9T4i_^WIkbDA6(6E3Sb+`Rm$@l2k%6QPDY7(3jhz|*c+ zmgr2xul<_l1yKp;FN!Tc5PCfg;v<>l2sS6X(8;eq{xA04`z`79j~{QG@~EYqa%8TimZqL^r8vqm zb8ES0X6}{ZMxin-Gqo~zV(yio;zlI3964~KAeuRF0VWCwBA-|1eSNRb|M0y&zjWor zzF)8Vey+y|`&Y~M@~N3C!hP}NoX0l1Oz)w5U6}5S&#c;5r368;+}Vn+Q0T7~op^qn z>Iled=y?`*(0`f{7`s2kLmVLU#ma87`=C~zxk;}N`Oyg$eT=>qr{`o{)3>6%)CrD-u9RoJaUr5S2kD{-pY3W1 zQ0JroV#MF^yFmMJ0rT?y^<9a!_538n5lEoGz1V+_kXWs(t(>G02~+IB+nj5-cSQyO zf_?aBPI$Q4T?1L~TZ!$H<%aTDp*BdHq85HcNX***Nq&Dc;!(rXCtp6zH!AeoOf#0M zfa|$NI|OX&-Q-Oz>B|F?yGwkRtOK1+C2ArefcevY*z@_8p`Yyx$hM(Je8*tX3o>;e zI6f*u`N}Os&$TC0b`TO!gs@A7RSj#aWNKfCxqHr!niPCPWRXV>p03TaV7#-ua`?tguV{?9x;* zoIoa6Zqt>NDGOsm`vXJH_~IGhMBrd?9k-c0?05w&TVB9`p(n?N0x8b*w)HqHdHlyA z>d-7LL_R5xT=6n>mz+ditcx;!&vkhf78xi_lg1A=ewAcslRouv<{n^kMgCq1s`Fm0 zJQgwaHLi}5xpdcQ&6j|+iabivj11Jo`$Ilk5bO7(c&6@FCi|n_D^B(M(BiS(25e-Kl4+Y$a^vT0<(RQ|7mvwE$)NxY?dp_{dju zppx!a$H?{qH&f-JUUv-voLAd+mOVp%I!i0zVI*R7q64ggI=Q~NH}&h(K4Orn&GK;Q zeozp$%U56|NOhVEosZJlxIXHYz@J$EKm+w@>3h1oXYcSkMyu|g9WFyP_Jbq{`klKm zw5rcZT+F-D(jLU2Fe#uPO_t8Bp4jBkL)sLT(1UaMX6Gd_n`^b4T4(g_)G|9mhz+Y~qkmsJMB?Lm;TO#O?|1*p8$9xfK)2MD5O%M| z`rrBsVf`5atIu_I+$Ms0sb1CSzl;-ky1)u262tw!0${TZ1fh4+$xCh><-&P0e?i&R zgO`K(=x%A1+MdHd5#C z9;p>lDA97t-z)SkN~&rA>?<4ko>_(naa+IPZ_sE-Ad9~7OTuDvoi#kd?CjYNE${;pCCeTnr$* ze~f!=bEDmV$-yz&#Ves{b-0Vl5Q%@SiF5!3JYa07qm~sKJ${5i3{qa?3uXbi2DRw{ z-ePrl+I~YTUm3P~yN=$LKb_b2BE?87<&1jEFii`8CqLyxd9GRqLjODvz}42({MD{N z1S8{i)6|a9NU+GpE->d!)$3tkPKFBEiB4;d^~%Uh5>+y|LX<*pI;mWN&39gMObMS@ zOZMFOT~VsPj$pK)khF|E$_P)kf_Kyh>s8bw^*j)r_Vq{_rGqbuv$O?FSRVgimg$8fVAU z^Xa()Qu-g#l63edczXHsbb*-UTobmx7w^xR>4>W+y-TPS0aQr;mLC&8h7*RG+g+5?0}gXA@@-;8y@ z+|>+UlVV`(63EfjD^9ye z`CHDDpGa9nBHHsBZa;xVt|tr8g_N0$mPnP*cE?xK1$J>S)qrG;?I~C9?2fD zpHN&G4|D*8^zfTbx0;Fn6o;&f&2lT0p;Ouqd$Vfj+18I{<8u~dO`9$?2_FKi0XvS? zhXANrw7WCjzB#glbi}#LObZ4L=wpixXHrc&Cp5eTiUhZeXPuxFh&lf+6T}br*%vV;C3`m zE9+R*wPTqQD|>pTE|0@_%?>TvMoPt0`#nK2u|vgU_6&ztKLZ!SKT>sQ8t$1<6|)~D zUXp{<9lg61bWfvt3hI_MyGAn*)Jfu1`#P;Pef$G&#JZ0R`IPis%Zs9> zi1#M80a?(|o9J`aP`eKJe)U;bkg3W%_R0KcXi^!W$^74-Hv1VfHMy8kziU;-PzA=T zrp*}c*AdTKiRxepIPMCoZCdrL8k)=hAQHhG$b3fkSLT!1wl9c3htLYa&2l*5YS=C? z2|zca_rRsrRP_tdI!CO`ZJqFp&KLhc3QBKQt?mqwgmR>&cFwb$O@tXX~&})JT zB|41!HARexQedw)1EnaJZ(pcBSzNu(1)tuXywlP{@Ng<|Foj5`SM7Ndm)rV_rWid5rV_^O;>oPM?WSRf z(?Md^wNuI)X-f93GlIr1kBO(R{@gI=NR$K*RNlG-76z;@1JcQC-DM7@;Zq+b1BJ5l z*?Zx-v3?H;&cjOzRc$&U)4t7S{QwApo{e~(kbLPiKfhp~$LP12n@+Lwfu!38-^v9W zg5A3fY${GZhg@|yEgmCL`JxDSN@0Iu%^(f8)Szgn?bPza~?* zz8NdSztqad2})kuInz*^mIw^QcH+8#n}n}Uyg0oVehtKWQD<~p(Y0k(S3I`+^X@;V z`EYSGv|;!7qx;2bxFrN zi!hn@AEf30gcV}ZiBT9gW0e%t9BaA~>=p5Hrby=3k0W%Ov?D;arE6^G!;nZFBtLhN z&lw2un)ax-33<WI9Mol=2rCE!_`0qpbxf92&-8Y*h;oPh^U4B?j@VNO>D1 z&N@`T#-7n%Zw5jI1q*{2S zGzGq%QLQ<}{P_H)WY@3qrd6!#J=I)U?pvT3>zr@PSE_ED%wuw1GvV1PQNE?ILOUZ> zR{XJQ>nH7#F96p_$Nl~2tmqn#&(%-yF@YUva=G!^sZoF9cEWa~$5SJBCz>$KFImTk z$@wt2FWav5xB?%4qxqRk0dXCffDt!d$k#NN{%VJaO?i(MunBO^>m6vaxWlP7{>Ocq zM+|V0wqh5B*WanwYQ#LDJKu8n@grpcXg3L=nt{!C`NRuyOO1zP-SHx4AEWe=l;VTU zMllRCVnem^d*S!!XXY+lbQD=JOGZJngcJ@zalaIVGLb*pg9%}p7q@r1`hMIrMEeWh z!v_wJx@xqsXNIfpe5^gk zH*Ec0D>AQB2rg}+?dwoaSyuY_>R9i#R;m#@*Hc}q>D2qgf`vodSympmtWTcLFN>Y^ zb1^u%qA}XC*1>lSXv;NSqSV98owp%(%swgDwRP^p`REM~WkMppC+Di0lBpcd5vdOF zmeq6DR4<@9T%Kt$09j!d8hCjhL`+@Zm_9y~q-Ou{uA&WO%gaP6Ean;0aw`p@qz!BH7{103? zipgl5W!&417vyLOP2{+#jZeq`_5B%5xwRL$GA+)#w@ouB#7rpujb4$o-=sRN-L^ZU z(h0i{Z8$;^fUCpaVPf;lvore^fs~TF4eh}cz0BGD=Gq_rM29`-HJCEXyX^7?o)0Wo zKWit~r0@O`B>?I~HD>1swoC1_TU=RCpJRP#x2vM)~`12TXKMK$Zh;lSa~ zm51UR9B08ueOZK%H((vtIqT@)jS8JMVzbvY!`9RqX*&k{J9}j!(;t;NFW(GVK0)x^ zT!SsEyn1t&GV2B1kmQjI@!Tp@<9v04;^ zZzDW{%p#b-t#%hJ%f4mAiVR!}Y7EGs{Ai0Lt9fo$dMum0(HjDSN10jvwY;(CEp@Zx zyzl3$Fn>diLjnxRFW`-?h0evACuKxDh!c(LUNex}{J+iebWSl)(SyF^3W&K7klVc$ zxL}a1sz0abLqh}3T%Kfv5Lk(_J3*_E(_FhyPQqQ*!teGFN~##@vPp(Q-B%8T3G1_V zq)&#!TLS%NUKobgW>AXiy~Rb7T)1r!cnRYiPaA>XXAL@b8PkIU7<7~SR(2sC*23q? zMk>UkM_6@h^5y1;{ibzYz#PF{81Cox;QlCZW3K6{>{YZxO+Y9FXJP;-UV(r!j+8n> zrqNSHM*J*1CNHpiqp>j?JCN=DVK1AN=%0MG6VE zl~fN_Dy;7J1Jb7P`3nXVu;y;&>MVOTCdEZG8sP-7064_padvDRCD`b@6sR;9OIv-_ zDDbB!R02+oPtK)(2=rZxcLz0dYEPBs0OZ9Sz-}h%I5trA6fE24@MBvC?T(E4cGu49 z$oZC|+5`nh-2i5b-WEU<%I=zdc>C4@&Dwpy9DQ3dLUc7)82 zTF*jcwIr1Mz2Hsj)_Br7^SICO@+$blV?7wgSg0 z{9s&;&OTX8W*QQ^`s=EFV|OS;qZb+dZ|LG3*vUI-;_hvY1<<0#@Po-IW|8C0*sz{) zafV;xkPfV*!)eUJsIDan6b{-*NIp%;^v~D!a%<%+yvMf|%{cQ`#Mep%AZKn9l}&9L z{7D^x6M!tUdj`@jKz2G5`bh@olvS;)uXLwA>5mZgDTkaD%d_fU)I%t_6R#i3YT*~` z>LatIUlC7sm-G|+8of_(q6e!Tn>x(?UV}CiK>^UOCz~PZIoEzz?V{BWnp9?Zam!Of zYw1we=zSL^6q$dfp*Q%RkWkQkOTa?VtO^kw@h|vw`&*AEI_nd%gQcZmgCy}<;?kL8 zxb;at|2Ul^P%&qf#5I|7^b1km~8d_@R(IQAE6A z&W#9Ao65D;0LS;-)*-{lb{O}dxbHfr98UwLa3R|{AfOVL4)iT#v8C6hd4K#F*U(>t zmDW1{O?MGf_8o6-j?%8eRU9d#(t%3irDClAoV*R_#?F}=8x6M*e6I-A%%sc)w1e!v z*0*+nh70DmUbq+p+B#HCN@DUs8HnV8%UXgvhwpQI&wbPVJvERG#yX^RO2#K57SZ9A zmkYOV^*qT9KM#`&DvPCff+D z!5JMbMr{$f%A}dLQ)u_#tuyfTFluF2IpJAl`-;{AfRoI2Ml-U$&60gk%K;pGI$=U` zc8A1SyrhK>*q{@%MI%=mDoB%jC1Fyntn1PF&6~AnyTF#GNrP;{I^%S9#30edwfLv{ zTsAPICNP{*ZABaK(G-IOGAoMm_vC_-<<=>b!nG>~uw0<7$qtf((Rfw>6&2+%uYub9 z3lzs#Fz~ic1r2>Avn!O;bQ@w)4hUvbWun!%Iq13B& zW`rSa#{8$>pWgYk3@nJdPZ~q4j9kxsdQKzSxm?huXC1YsVO?_G`tWG2d^G&1pqP)V z5_R|93PD~bi4L4&PdCnL;Vw}Ud`RtOx^Cr9%|T|uldD+8f32GFMYt2>wxypf-|OLHR%h$dPhv z3}{NdwjJR|A-OoRBlxWkdV44l4#r-3VpH}Ys}pRG3QUX5o1QCiW6hPAbq$|?wgHc# znxBrwW~K-aQ{s(CFJ0#N3fH5!;V^TB{TpCL4E%c|H+-owTgUHKE0g|yso?2-+Our1 z$S&4vljEYkX(zNIE!c+NkvguZwK`lvyWlD^+9>9No?Fd)Ob?Yg$XZWc&OOJ^B;bynME>Yq`&+!5tu#fyZsbRQHz3){DDr1=BA}Bi4vw3(pp(L)S#+7 zVr$@v*IeVN0lvRU7wv%YnEI`$W3bB`j)>sO4IE2fwK=08^XBN}IX_Oy{see*QWQCo(w{M;VXbHt zQl=!3xkOCP>7bH2rPSZ$NYA%i{$P=9fjPPZAAwvJ`Y6PVlZ-c(w?PO%1eY#q&DFT2 z!8}V9HMz>NB~4*i9s!B1E)nL;YV8`QqNm!qNUoMuo}VOM9Tc%W#XANYE;oq-?}P!C zS|{kZ8xXERDsgj~6FUKK#is7B6I7f-t-ky^C0k^M@DhQq2O}L&vvbp>b+o=d^afMt zV(7}$AXYGbSA(=$dcfsUlJI2f&E=i?9-5A^@1*|;)aZnuDXfF-zoHRT3P7HUj>XX< zG#gdAGL;xC{#uSipI;=^MI8{8x|Tjl5ZF02KG|Y)P4U20`__;4^^p~nvKv4V@+m0ExJ8!FZMEpF|?Dt#RwJOuB+(GkoPjk>V+_9I-()rgOC zEjy3_A~--VOD(oB!2LE%yx}E{MJa&bRtjHB^t9pfz4PUFff6Lx*Q-h7Ni(#*68@qa z8O4tyL2Q#mCsMa`1rKpTnc1K8Q##Zx+)M3}j4Cpo@KqgCXd5k@b%{Ot8$a?B?%fdy z6d`G{=Gj42HvG-hKz_scD(RgAJlLfNwD%FDXd+$^*whvlf8*%a=NFD! zgsO#c>cL;)MC1Jc`^~!f{6oXNyIFQG$bmJrfnA>5wylTdGlgNcDF3Ba6CR~AZxrnt z34;DR>^tJ0%(8WQRQw6EvyDTsDQX>QbMq~bww$ll!_yHz?kNLg1=?<|d!r1s221WX zO|*=F`_*=p*J59YC~r$W3cAn)%&haijtpUkX8p*Gq=IV6{&z@8%3C~Qy&tQu1cG!! zTr|TdqV!FtbDMzMJXrOgIxt>ptk4u6io6xSrYL-by9gZk*v z;Z2lhBC%Ip8+4#D+x{ru?x z;T|^{+_lhXrcjKZdIA)2xB4jKV< zO%N&l@T7iI0m|qSdcH+wI<&EX{|um^_Hk+P9Y9}3s#;f0{;NMKGe4uBW=TRb8-Ozi zQv~ztbU0TmbBw($`JwF7a6Gv>ps)WoFOC9UjkR%}MfGp2tj7w#EV~DyDL%^dblJi8 zA>T|N>4`(Sck_2^-pZVYdyxwt-bpKX4@?sLbY)HUzO~uz71glue^m?#Ij?I|b&Edy zGZ+|&xL%cGHL)-vR#92^HWyb1+*8E9rBM&L->H`K4m;9oQ*+bSW{K*ov6W+VQ^)rM z!~ZM1qf}qNdD(~zg zsjMA7tLMnUDi>)TD-mciC&qF~@JL<@uY+~C-GoScrJd^4v-b^u0@Smf?aw98$9XO(lsblV< zBxsBhIOG?p=pd2zz7A&t1mh<&`aP)fwKtW@-sELq^4rLjj+yzh1$W2E-hXT@uhHGZw6Azdor0daCMFK-hOyj4E5*>TTMkbzxA9<2|*bRQ_%DouE};) zkc+ewuf~7qriu08HfOZEvr)?l3R`1Y$|CbE>qI}V2G&dm?y&Q=OWrj{PZ_%Nk@<6rFo2~@}Ve3SX?;pe%bZd{@ov7?w{EOx1p^MLTPQ~F8 z;TNoP6Ps%P0ye*tRww=R$U~!&{HCZlBp?hU8o8^k{=-7d$64s~^5bu`uIs}%-$%w? zuZaJm?0JCv9oBkq5opY9Xe7nQ@88W-Q&tHKsAvlf;Iu~T4 zVy6?aUlJlk&Tm6si&(6M71ID;++MT&RjvNddJj$8di++lE;s1% ztq9du9sAiL(O8>fr@4W_DRFH!^I?Q%SU;u|NTXbG9gnhmQp9{ZbhYH_`A4SnZ$(l! z%%U zb{3bBs34`Kkbh+OymW6Z+>-oO{9RcSt`|FRrzG^|ZtMe0M(B%pBVE__nB8Zv1TXS_ z70k6K;jL?o^`*7v8Gd-gw?*dPv5b%PURTIrBi9}oTW>NV-3=7>iGc8-M&xDH!0#(_ zw4jfuB%g65&H30y?KkIw%kLAbvyvlJheq}8DLnGh0!AKBdEz%E1pB^h!AaR|A%jQ7 zZSG=*FpzNdR{25b*0>n_f1*KW9g53HWq{A^tJVXCZd0`jvV(wugc*@S7;tI{iI@FT zY96rgJ^^1S_%yn$Kn!c@Xdd#`jZmzYw*&FkciN-h+I)7PWsU@r+6~6HUjBLd)2Rp6 z@qmq8PQ)yG_yeovxkA%tz8e5uDX&5<9pPM};22iprB$WEF1K9xN!aESYmjW~g#v4;eY4p0H=SpuP;|;Z9jGPTf_pmxzh8$hdfy(A*{=7u!EhvJTbII5y zOR2qbE#J|pVoQ+mCMvJxGZ$#C$U7pO$^Wj$kS`dd+|LpF^8$S9^>uYhImi>Rv{t}u(2P`oMo2>n^9??ErN47B#Bsaua zXD$@w%}b}UJqFE_9dmBK6d)=S|I==~Z1OkmmqknHt~v^6zX1{IG*E2nj$JE28m?R$ zJ|@jUWn-VTXh5ik%uoLL2(7bxUj?O18qk9PO2)H&uQg|Pn{HHUM!H)XG55@~qw^bp z)~wR9{PWzWdWpdGR7hvKm;D)N9RO&Tv|0C=qksM%30$B5v`GH^ap1sDsrmm;@)-E* zEcKuN_u&6qg#ULZ{Mj1+zuOMFHzU(G2B9wmW*c6xxD5wH?587muVKJ2#{riOR?gYiMHB z*s<3z2P3Ed7kB>m>Dgzslx}J+?EU;^EECmZh6Xu#eIWNtFT$cT*VO~3NE?J_(91hD zEr5&|GW`F3;obxDQ3qS2=ox_Q<}1=a(4UJHF}y>P6g_+j!x_z-s@VrKY2 zeBW%va>Bo1sUIrFFlrDb^RrL2fJH3N)^RTlu&slP>@)?LFZUgO8>HsHH?eo!qjHYA z!sRE7xc8UQ(iz*V- zE;%?BX%v1-$^@8zCn-BBFd#hxe$xH3DAi!JcB+|cD8*J&jHOj-U5U8p?akxB& zd8vT-P>xP4JJ(#7{ar|b0gu}2^BSDv?lwowm*7nj-+K(Es;*}pNfODOyC5Q3POpoi z+n}O%9K?M&!JKFlsWBi=8Xz>RQz(G!bw40~oosV3@-H7tGrCCiAfTD*<`Pw1USY#A z0Pp=yEXSK_$J}^AY8vRZbdNlAaIx=wN&f(vmRV0R3Rj}F*>(c-!B?_^H`{+Nr0SY1 z$<1X0Mz{Sz5h{0+c3%8i>c>v&Ck09{i9z$=gE^m}8;M?nT&63mHWQcwZS@?Kx&~oV zl>ryQ@0PmvGHGqshyd2?&*w~2NmpEQ?_co!vq;RgS_h!@pVe)ele;8uBhp){{$Ebw z@>J2e-Cr$e+7owHsoC(|IzxvU8J7+W_54o$5%~ma`-J6HFV}5ZgY!yUI*j`-O<2jJ z)GYTOk;g&PbrV)hO^r*&>8ik(>1x-oeA)Op*2)4OBiymc$4fxsaoy2KAyR8t|K6s% z0=9Y=4}+^nYKC1?51KWJAaQsEUN|=y;AXX+9oTxz_!zZZD+}+d;hv&w(R$zvLd9Qj ztpmG{=*tSrEut<0ei8djCGNifO6==4G<;*qXKjMpm(rx8$gG&I1$#{~AGEQ@&aZJ< z`Pl6H(op}QJP=NrC-SRzksdH(T?ASV#@VUgi5dzAdgZNePV5EQP=>gi9oOZ!_tH5Y z{TT*%N{0@773`!R+P>LE*Jcl3Ag~;s^n6zal-bTXnuFymTqBB&P@f|)9S@jNn86?C zx=c{Z)dP~^Miu{1Q>kt;+RJHR|1*M8j@y>kqmqGtW%v-DHDzZ1yZ-}qCdqfHY7PoKpF zIHnCZysWrSfzTJnlzs=?FZphbxh5h0`M_EDKXB0#g9n{jw?B2!A%gzbe{RlSSWZH= z|B^Lr=N4p4H^_g}k;QWNUup#ZEEZGoE(i9LEhP+SA6`<1?d#Fm(y%FKx~4ei+j-

^99)D7fH*g$hBkDhee-0vMJkC&b4ir&v_~{&!s}Z3S!xQ(tv#fZdAx@bR zGEDI;i<1f1Mg{i7eV<*AZbjySB8XcFw=`jXz~F$vZoPvr8g~bQmp>a4+DRGy&b1-w z(-?@L*Y3e1SFdXcQV^`N-3|I{V8p<2!w;H8B8U&{Ez{3hgHYiVP4MsDcjaDNtC4>< z2>LpPhQ<@z(yx+!HVm?MAC#F9znLtR;&57g|7h^9)@38TRu2869ZwEu{9bzT8A3H-XX&58~(t=SpEm3 zpbq2C%S9ox&2lON_dWhqxH^taYTLS-0jeGsG%_`9;GE8rZq?S9W*9tQr~_o|vkvnJ zWRJLaXE(jZkW&}#CQEq{;qESw;>NppW3ySeIB+ayC=YE(9A4;2%?(?h%#G(4C9<`D z?BEOIfiXA+Fvo#7z7#0QS;r%Za|Wp%-(Fj*wc#o$@#@wE@@s5?Vmt6bHx#df5I48}IBJNnW66|#Vo z$qpeqcb4N^*HnVSTi5ZKGx}7@;dSGnRXoEiC4WK=Rdotn|rx#n0+lU## ztBS@feHSJ&fjjjBzHIs-TN#Lt${P_r@O$)IAPjp_qN%b>CK~&4xgM;EMXpy#&a`<{ zQHcH~>w@}2Za({R8c|*jxvd~lZ6zZA!-o$Wy3%W+M=LORI>vvR50s)6vh6*tlS_3> z_aMb&clQ*uG#+ML+h?tNf){)L0ZG%S@`Ok$Cb>O7tC^&ubKunx;NUsAHtrhza6an$ zVF8@*Q&C7+Gv%rG%iC;$yfJN&*oS6&QFVqZlU`>nO zq*7a2dueBxD}gv(xz;qt`SL-Lo>=jqsX*P^QCuYJV6W~-Cz#nDG}-1c_4F(XVD8wb zYXksbv@2}OD2PlgAv*%Yg>Ltim71aj)XoZ;NttK;+t(=Q@To}cRsmkr73%&P5-x__n2C7U%67*moghlbfaKFV?jBd+Qz-*gH)*SJ}M~X zdwA;}AHlIP(C<5hbAt1RP?g#r>nUThH@&Cj^P0q4*|<=4*R+dOwbmcH!+NxY0sQwe z2_hW-?7AhBqVBAFGsFXH7H)7=R8|H{Nnr zOn=PXpd(FTNL?^%TMXjdU>raUQvt$4W3|LG)tHL}Fw39LsVy2!qh$TH!gkggYS`na zTaEG5Qm0*9fG$}fsEv_=FRl2it^30nuLMqDzb#>FxV3tSmqcks*rPH^KQkj$rDCy> zSAt>~@j%Yy*~;r?3Um-&vOL$CDmY`fa?EE33Pjn98pE==`NsQdkw(UkEMtDi^U2T7 zG)zQ4k%?kmeVHm@iqB*%WmZ7h1>lK>;k%Tl2qdy0U~Xg}>stR%$A`^*SIXo|m;B|P zGYyoT!n_JZ?E;YEY|Ol8Ip)ZY!gM(s@?$^B?k?Z^x^Tea_mM6-V5<4;jimZHQdoV| ztsed6aKiY=3(71MH1zws^<5t9B0Sfa2g1VE9u1L!& zL^des+3IvvY97imzdHq|Q}K|+5dVRb(5O*IfeTyiL;rySlC zw-mijZ*KrEew(#{mDfwkHhNEYAH`FdmeQ61sNAm6DW#Bo7y7z%RH`F>WFaEy-ME26 z6BJW%zq*wozV^#^$k^S&+TOA5vHjGIt-W~uQtp?I0<~H6v90a~fc;w!JIrZs8{ZIz;{VEo4vG z=E`&Cd_|H@OoN^hO4&yq3t5=T+x`gKQ{a_28ehgCOD_03l8=(j=H7`da zmX+*QGEYLf(CtGT{G-0jvhe6xu7;^hOQI;Ccdf&T87JbYkg)RlLxMN=`4h;MYQKJ` z;RqMRRc?Kjv;hDw8%?R%@#HG}XH{40&;I-SxSpc+rm&3Y9cf>JFpA0u)rv>EhAor+ z5uz3%v5dRd#t^qh7kSnaxlcv+)4`m0Lfp&#=TZTf!bA|sk#o;u=_W0JofrPI}fo>5(t z;w5Y}5i~Wm$F40aYW&AsoBrHqtTE0|J7$VnT)9Vy5KC#=K3$*dJGQLgwouWZNy7;{ zu{jZAt)?KzqU|DXy>=~lMQi8wORD+%$^klEsr?bMDAC8Xv{K6tc=R{C!4J@pDcnB))n zvF)$4;peBw!I@?GQ6saN#LW`YCZG|z1zq+4|B-?FRI*HT2{0QlB=v zk~O9$eYu*^_&#D|lT4%RHRGjPdzJ|I0#1TcjVaB`j2#rv!vCU0aGwO!a-Uug+CQXw z6aV~RfJrcZgjT$(!Q}GcM3wA4b*^x;xeY_ktK-;k%GepC)C>g;Mu0Xas2n% zUXod)I4_9rk3z}AJL}xXGN!C=@F0ZX_B}bDB2=x)m94L7xRT>G`yRLHK#of|;Nsz; z;#QdQD)*iuIkraN^o_6ZGM9Oj5(k;dDSv)^(L7JnLxy{O0^tHqU9EmZBUtyfI;X#Q zHv^DpL$pC0!@%Q=GyCIHCUC89+pEMmAtg&qg`39v~&fsA*p-fwucJQxhS zZiUVcZF?^HZxn5v1}o22)Y66RxA|;fyc&LA8n1;B%})5)>Nz8mLr(Si&RG#~>DZ9@ zmR$c4eX>`q;}VFDgXzxeCiFjeuT`2QkfbJ zyprxVfmeMva_2WtJw_co5N`nts&|yz_m>^H(K&C11LHexClKSc0?*sI+D_}L-x{s6 zcBCV@T1#9fpnk~-BtfS077&sl^J)CJ3D1g)b!@l{G)FDShwVdZWo{GFenQ3ZwczJ1 z@Z^xP1ANkyW3~?$Wh}y|H6m-?5l!+7hyxlSJk3K_1IHRM&X>#Az2DV5uLR=8#!#?m zx9-kBlG=~a8)ld8PRxy7Y*>#|4I1fAX_}0tBop2N%-%n8|M9mFu##^e%~UJb2bTnz z?%4-8k9k(_NzwpvE+w0FCkPTy%zEbVs#qrzjXZi>oH%D(zjBYGz{CJ2V;#uB;LT}| z2-}wZ94NjDkNBl`XzMa)zU7cuye0y3e@^yBPic!J{%(0rf}Gfz7eKq~zT?}Z!`YK3 zx=bJ(=UguqG3x`txLJ16hlzx?U(2BV*vV>UnwUDYko0+eIbl4*NBPcU5bvkW??7Jv z);?XviCv&^dgtAvf8bS^5vNfwPP8^DUH1yJLbs_niBKK@v<+|-go5xuG|dOk!p&xx zfOQ;%R#?|7y-!n`b031LisXboqQ55*`g9&GdOLDzOs0nw*;vQ|+QCO$O(nyBytzUb zU)&B4AD)xWr>ZMq_FB19DqcwP&y`yDM(h&Q*4kuj?*nz6C^x}rM8(A8DAdrzW4H;I z^}@Vgb5@m}cT=VOQgJKqhe<=}tD2XL{YJ=%HRSzWQP&+Z2OUo7NRqrcZ~`v+Hc}90 zec##ZrES%|`&8gT(=Wi5N<0>uIDp|CMo_rcR&)M3ANu3_>SHs$FwH?uyvL9)tyrPH2Qk+*8+?I`U|kF9-$VGQf6C) z3D4dFj8GgM2QG*3iP}VzW_)y6@;r_}8qMgIlfujTT~2Wflt&NjI`- z@jW4ziU40%=ELD4^TLnU_(F86LOv;b4K)XPqMnRABAWTAKOcGq_Bb+;8GNc+GuJ7}dnSd> zI(~+z%oyjfv_59~up9$i0aeRFLm_5Xeb@b-v2y8B@Pa1}?iYdbdw%Z1H;) zBXM;a!Rwan+8AWckag$MklcDu>~r_)l05YMp8Q{jLlk-~VfqHvs}RCFDj~ge&|&I* z=^DQRAScW)c7pj$Z>2MPf@dk7LU6yUWxOj7bxi}_cdD8?Fe&Q8TrtYOuJ0LiL{BO2SYzCKp2JfRIb6u+`$*nE;lbZiZ+Z@o=%$|*9 zu@ANqGJ%#EA>1O>u%MZ!TA}~aHz>3l!Ezy{rBTd=E#@dm+be$>d>y?6%s zZmJ}CXTpWw8W*`_3ejop_FYT+JXbSvlmN(0)bgLbfV(PqU%EQ>r5W^J&JsR-pkUkz z6jThI1cpMC?vt^u={@z{4gvFvuBE@9p;>R_yl6o7I4f8e7T%_o-CI85z6bpT72RnG z)ooo_8Fy${f2o!g6rTJ%NAQkH3tp!B`R8LAICWIp_T5 z{SFZENg`A~E_CAx+Q`f+br!I%)3HgnvF5p5AWmLBxT^%fV+0PFt_)>q^ds7VC^1rL zVF)^17*F_E4s^8K^6-~Pq$u-q`V5GZ?BG(hNQDSEP9f2AId%-pItA!S>?!-3g4;8Rgux+ z;mZf^AVO3HvnkxlcLGCDFcm5v|MemBr;T_nA#G3%CJym!2qw}2=8M_^D>+BeydYL*VP?{cND>efA ztCPO0d06FIpI<}}LFW_8m5)+?1BCW^bEwxYybGM%Fm4`_bfiH{ul`kuTT$u0gk@KL z&J@%4ape90SZP}0(?hI2zpu{z;sdVcg}ZOm@CZ&|iiE~Vki2!Z+tbOcXM}G48tBMv~c=UY22OtOjRGLiGk3{Tddk#9rC-++wkV#CV z-vO_~Gy8${-E%iog_R7R0BLt?3BO;1*|F47JA| ziOCoOr5?kHt9Wl{FT1ZUBUKnyOVa91y57SU5cX)1tu&Wr^L;(&F8rmyfv8)NE>BMh z|B`|yNyX`Gt4tT7O6t?i2sv>kBl=y~;Kdu0u z`uHY_(Q@8%s^;DP4uG-6gFmMvI3|{*Jp29v^Lx~<@qtwn^tSj^o(3dEOoO5tCNciV%=8QU&bh>Q1Rhf7lytV?|p!GURUz-w)%(XA8ok5QaGq zL7zX(nr@2F2|2`P$}^Qi^NPNiGhg&Bt@7fY zb;<(ih4-PG1tVW>-QF0YF<)oafes3Zv?TI7hdL#*>~e%GVLjEe=F=$k&&P+c*uBd8 zt;#sn>CZJrZtwk2R(#fJhJ^V22Sufx)VAI5!oy#>u1tu(%m8{UNRyF9XC6m{Q&M!8 z&q$r=)S)x#KA*o#S@&DQ43hFLhz^%m->)fNuZ#sB1XDZdx`SD@3!GZ3+K5?mQ9JpM z_+J_bjPK~@lGr~IG)ad;2W*F53(IqYOQ2>88Y72FY};y!Ac6J0_nPR-jx@rYEx$x6 z&-WLHdUH1t$i9Lq)R1#2A_o$~19f%=`1eyxcPyP?08YuTtZW(nT&R~Pb<=O66cg~k zYNF?eO<*wi$(xd-sxJjk%WbNo_s%_gdFNj(+(u!Gq(c2zp#~oR=T;5Q=I@bA{$-^- zu@f=QdlI^Obsx#odGCMXOslx1sEG^2`kt|rdlUMc zn+@?m>UUs9p*e7-INo`nTBnF*fk4(r4{EX{R(}%QW6hhyqI7X3)|6rWjy5&Cegc^U z*{(|xDT);+zy1_dM@~|bCqSzR5CF;g%Uk)_fv+cl!ugz*xw&*VU#7~HBEw|Xaq(TU zwa?i?=R=61_W-v&@eBnqey48egiS?L;+Rzx3(a2WahEi-*! zdC<`({^_9;3R3d?1zcG8dVi$vi$XZvsEqk&OUI}-x* z@#9T_)c(js&gIKnTkGNH;4Fi{kh5DqJHgf!Pfnq8&W6OMeKN~K6V$E%aMPYpPHVO* zq%n$<+|(85h__zZX~x4wEdS{^l5#*wN^`a&kAHQD-IiJrpbEZE*RUfO9x<)NQ~tG(DMg9((%StFi4yQQ-f47FDzm=M1f`bRLbe9 zn%rt-1mNvq)5^yQ+zoD#@ zka)})=y!SHn)yc$7^I<9eJ3%lh0T5u+_)P6VbyEAWh*Y{hXQBbAlw!`gm6HISMN@- z9|BM~gyHG*3&%zUERM{-n{Q!b8o*W{!yWXB$k|J`U&4=6eXi(X>I= znqI~B;I#S<=S6^qWdF%EW;YpI#9p$Utxr>YP-;Ry`pB?x4EYVTD+%{|KTXlX%RM^& z@A8oI+j)JmT`gVWatt7`v(s+fTSO(PBv|?+Am^C?He@8s7*+- zT7Nb%yy8H(ogLp<%46HWE^vbf)i39)8J52H21}pGGmQZ>Go4IGR1|!1^#?0K8*ZckbJU);6zW;#xar>nQ#!S!W^SaLKJkR4ekK@ThAy&(l z;zFFO5URD%E?M=gWZu=`mSDc6;BGFlLpyck_o~Uf#qBpglFQms@hTNQ*3K}}!Jjr@ zo`4tqVoG{l^vOv$Dk9zQWdKEaWYhDbDyIu0CV^aUylRCJLroa_hW zgbzqbW|u%|Rza`_j`LXymQyh@IZH$ZQszkwtNU0)L}k$`^)msxBexDoNdh&`osRqF z+fB`{?V06&D?`<-FLtyDJ%1>nE_eFxDY$!f?W4)F%*eu?uBO*@9WlMVnY7}DtacPK;mNE?_FQo9dQV|*%$waZ&9ZhXy3c%E47RZ#u$%-JdMf&@0@>6z-e8ky@pvdicqAI5vG+nVAl&EpTzWyCWC&>hr004ksa5;NwqtS z#ZgLDS4o%3(>WjJddo}lmWIOX2mszQ)quls`#v|;mcUwNC4RWglPfq0JSl*KGG55k zD^_BRf69#_e66a)kAkc}&kS|`Mcsj1kL$5#>dXxZ7xO~55y zw3-wq5zMltJNZMmos0Si+kOjfMWEdHFi8zt8@X!A{%!P8$NYG@JT&5AXkpx#e7xE^ zb>hAH!qI@`Uqj-`?1!}p8rS+YhQH$%fPC)Kh(@ zE^MiRqqu_;H(oqo!Gn(IlzP5=uX4b+cl9P%2g&KGo=P4gE*45w!VER`od$U zpPyzN9Mpu&q7L3*|BzB|*prH8F8)b>+Xktin+KaVjOaf8kSB49$XjO&h{5jJ#zkJ* z9Ey2qg!dsa_2~v`+FHfRd}?5SF2Mw}uAXqq8ts232rqjYN`C3ZzzbILqyC^VnJSRk zO+G$Q9p74?2#)AzLF%X-S2&V$_^z5R;hZ+;JREqs4jF_Qn`h3-SZ}g~xbO0=5;ZTRNIqfIUODUhQ|gY8Z{1gs;yF{o+e(z`B7Hn^$unJo zH)F8D#Nm!m=I4@#w--;z+07p1IGa~XHtI>`t=O_oHniWL>H;vuIyBOCHQ>whuj~_s zjyIJPdO&gaMtCcHXkUsn=d<{m6461=4d>ICwKTiN{inVCgyBVtqem^cZ&6_^%edeu z&-+h<5R^+9V_C>uQ)xJ_64*?_!NF$(e+rP9reBWQKF$B9({fL^oVl*q=973y!Lc&$ zy%Uq~tiS*zrcVS4k#|!b&lOfyJ_@`Da%zrG&XE;_rj)F{omI!>k_&rnw(bNDRp9zF zuKTueK0oWRh{-gCA2U9q=%F6Wj&dC;&E%+s{~b~$2CaBx+Y#KQa~C4~pLTS=&qMo? z0ys<{@4TLP(0>WRuIRWUBx`#RRS7IU30l|IH&aGvzWhiiCgXKy;IQO%Ak@equ?xD&PA~k z-&`0?eMLYkB|3Au#I6C*2THzwe2LKbIsM)A0Yz!)ng%VbF^(nCfYHw&`>m#JFkpQThf*us?yp6+G>xVlm|F zCt2&Ty`ZxsrH*5~cot`V--sm|UJ>5aSJ*A>eOm(r6ST2o=jgqh@*mfL!V{~!@0I{O z+LjfYMqE0CMD@K;<5&1xen5arfG4`o!Loc26Duf{b=~(`yKfm^qQ@0;dQ$(xVUb#w zw}R>r8k(UurW2-PA0g2X*Km}~&NTC0r2L3zu6`fbfKAiD8T9XTkKPu2(K?ahdaA&r zhYB;2TBIxdEhSiezylDX?6_ESB@B3aJM~ZGAngvOxS5;RK*)WxTyQQI0g=TsmR4PM z$DT9he`_s1?my9k|xAk|VcV^++&+9ox1zVGz`N zse#%*YBA+YBQ3y6Fv&;44daS|s?|`H+bPx3Tb^lS+KhnSfZlY>=|!^O-7-1BO|{Z7 z!z)jPdYL@f1j`K)XnX%YpJVf|AWDKtrS2O*Cv@9Hj}-s7P#=l@}%VX^Y|N=R|6Hl=lUsm zMeU3u-ERa`C_Iy&y$EPL+k|~m#>IUGE6irQfV2?$f~UCY)6u|+WmEF$ATK|kTx$kn znR&h!a1VEih#lysuJ%TO%8NKh+n|W;jRQ26fMR=;D+4|pYIn3O?XK)A@QU{PNAC_C zU!A*J5^tiD_;a5rmsrMPMlgc*qux<|H5|ccybvB0iR8oT@NI6bEqM!ZmxlksasVx+ zmu-{t{@9S>boSC*#Ol0I+p>*w$sH7FaIig)ZMWoJ#~jeDORHZP zQ-OTf57O)jJ!Cf@2t|DjR^WPd9&jse1&QU_4IjN=aP7Qa_)|$svtiqrzjdg4(zE_z zje(zeu=>6VO}XEbQd=mlRk>LMC+E|`j%Pv&?wtv@E92CqRoow1aOxWmo-OL^^$7l& z+d-^li09V=XD|=Xo*#38>y7^OzuvBDrE3|k9~y0=v7RSQ!!g@{B$1HPmpEW$)9h)t z_LUl;J7cjPloguuit~v*!)g_7QS>DKV8gk1z5EJ|t<|yxTS5e!Zu5l$g6SNlQt^?W z*>Ytyt_hc4J|$}kOJhvh0Z$hdC zbAkEnM~XaPB<-&Y7J85>3v@6B$}AnU-a!YK!yC)GiYj*8j!|&7GB)#T$&|SNxwm64AEC?u z)5+=A^d;jSC%1#wUAm1z2VX{XS|B%C3qhLj7OcMkxWvLTcIh;<58Jyi8K9eDm;@l!)Ri2P)%r9)HsdKEWAx|St9IH2$~UT8)tECnGGcMxmNZG^gH zzV#4tJH%W1l&PR7y2!j#Lpa(*9A^79<;0l`XC3Z!vB-#0guQWobS>XuZQD+CUI=E@7hs+j@HsQ2m4A-vl7x?4tUP4wHyt&U>0R4 zKJA4IJ{NBI?0zl&=8PC`g6JC8Z_;z828#}YpQRmS5JLMKY<{r2>4?B=s&|Cp;_^yqPZ6?xyiCTSq%gmw#^NFMWS1%C*W5&RAn@#5JE# zhWM55YJsEXj+=x=_pW#M4@y^Q1i86V@|l2*?jtk|Y_muvX>m(V)xdjg#MijeFz66h;&F2^srAV&WEfhcXQ6x(;Lsj| zR{zR=aNo{Z2bvJQuak-2CJ*ZHq>(Gzjv+_HweuY#P+zB^g|QVr#g)R_*%rd?cowL08KC;1-FURf}6iW)Q({@RC<#3oOaluy+#i&d~;~>>s`dd z`aSfB=I=n3<9le*Clw)XoHb5!d*-ut;qz{0VeX{n2Y5pac3N>|vE`^i#{KaypfnO91u zKON}}t*}G&g_&2^pw|`%xMl0TT&62{ZC+Ra1Zj6tyy8XjY-XDlXM5rE%^aN8~ANx?UM=^ z%PiOKzW+5mp#je4{1xjn$TpAYF<{<#waYL=i78MxmN};erCwEo&aD1nEh3UNoUp__ z7uLJ4g8#h^WPMDW2!ZwNqmQlkA$In|f4^RVGMQ@>f~{w6_lb(QaoAk}keLPQ?5`Q= z+3k)VqIf$h1M{?Rb_JaB*aan7+pS((_EIT-U!?=vHf+;fZT(~3kRd%+5;IVo1D}@5fo5^R*L#Swm{q9}={Bu{}-QimycX#jA!UV&T ztdyT=@4k1D8KNmhif#OynVC77nYr1D`Pa#ni!0zJ*v|j+wLkRVt@+>YpI6=a|H3Z= zlEbDF?DT$Q8(loc8+|M1|MAcLf)DTf(pS`N+h~o`2jf9dHJ$0t9);kZq92T65F{E*~b5!}vs`wl6t-vZ7 zYRE1Z8WQ;?={!>L)7G1*r}Ap&FP`ga|7U@H;k&QVD0sUppVU{crV@<}``-Pd;broJ zxYvfYiy5d>w|8EBBk#vtU0=kUL= zDL!Up`_iu!$R;;;N(3e4d{Msj6O+}WDaXKpTuAu-qXrD#RKM7H>FJ+#hCPAWIYP&d zYgyu16E{on%Z>Rq|M}31%FfQ^{0_oLY9_uPfoq-b=;%lZVa4b;ysqbapSrl%L<^kt z_;xh)6-{XI*NhA5cA7O+hZMi#t7Wz($5DyeSN{XxmD^i8iXg6olRFXrMBKY3!-$y%}ln( z22w0y1cS3A_?`FfykQPFsJ@t9kJSisQE6&QNlMZt@Pm)Bj0kG|V-&T7@{u=Uk3r-o zgEh{mdq3?se?BES;|W>Lw$9!6@A6q|?Q$G0x-KTlvN%Lurgz;1b1I;trUtT+uN9*P zt?|Sm!jUcme)ziJ^k+_*tS0)JidD16#{kLz2~c+zh3$Qg?#%d}J_fE18wIqK@ijAL zc0kHPLh`bl9L4}^X@~nzAWfhF4`6TQ-@md(8P+ByPe4*qCrj8G%l@i0Ry|b&FIkF< z)2jlLPk12IB%`?A$+xc3)7x90sm2)lRZzAFB-7(LTfxD|@GdVKq(^V~5l}Se;Su!zm_e9e~Ecsr86x(3CLU}Sc9NdKI zbk5ti58|$iyI!rfB!rO{bjA-alQ7hfoHCvek3~glTc%1sW#JukVDK*|s)( zCM4i;CW>C&lbT0a3mY0}VMah0+gO|W1{zszn5k6y`?iTm&G`!jBQJOo9XLsF_~WA& z*d824B5tw3l8AID!Xk5X|Lh}ml{%Vj6onLk*liZ z<>cb&Q#LJfeNG-|lY*aPVvaqBzTp#Ul^2~XjJpG-g$Rt4re%2T{L?pt(3QD;=A)bE zk(*|3Li-})T)jm9yE}r8|J?FyWl?4tY*}v>tcMY#xsW7WjP5vvKt|nQ{ZykW)&i2- z5K~FkzNT#qM(7=b&be7xfu~_=WaJWeywd0ECR04Z4M>T)BsYsJVMW#GjUfd&?RRX} zK90Dh+SCT-h4<%2mszU%&&`3Q`}<;+CuVMr&iYiHB*I!hdNiqmkV*=vU&tC~Qa8 zFaR4fPW zKknE7DdWH^)H7W4(PYe`j6+k)wMQuQLWL#8RVgHkRdZ=fEZKs;)7yzqPlOmWk>@bc z#KaW|tl|7-B>91D=y$JYLK@0UJI_uNd8+kde2bJ26dVzc6>bN?Gcz4S>5C0nMZI;c ztk#Az?Yg6X3Z!LJRXY`LBLrTOzr>xtu5M{u6gh1@Y<~XSxuz@CIqkGdT80gl4xBG+ z+88sCNuPq=G9Ia1u`^XF6|1U_iITnCLWy~T)7&c67P1t7DB4Dg(GFsJ=;@=q5UejE1Yuk=V7@1)9e@@sLpvIv@_DFVA<#Qqg7@t+8J8w63W6YCo8^oA#mi7yn-Wa*~s>XD4X|>}^akV%MX+9+! z(rw_b>X*egWlj`B_OQ#Cgq@Gt}Nz}s}(dWoyEys6A)>>-bUm+ZU}%!WC)z&;)>7K1k%f=p`o%4@#Q z$6`u^=F`1lPd&%N9YG7FBEb7s8aPGdQhoq7?*a%zgQa)qmA}bmbK@)Jv1H8vH`n5M z;WF(YgP;TejQD)lA1EogvaS(K$t+yn0zw{NXq^IBZ9#RFHkxj7X3|uYN3X`=JlT}`S0{SKG?TEX>w*N?HWs{4f`bVqlN5PGQ5m@6tVS}MgLR5_ft zPf_lJJvqK1t?>+U?NbatpI3YZA9_Z5I7s=n`(;une^UgjP{ykHmkbzWeA?~#>lIHC z_IBfplE?TTsn#ghhe3*U{k_8*uT4Gc8VEF@%gCuN!=}yQNV47Izk5U)wsKhxRGG;B z(C_S^IK7^Rx8r9m|M^ApTz`L?>HjmL;cbff-OLbFRadvE8D>C9g(_;aR`?? z{3u|4xJdnC5Z+uDwqdL%lubf0o@(?1PLz<|%!;i}o}~-?ZyLpg*RIG8wT*S*GD8_O_bX-bPvv@Qd#vxPal((aZ<=f|J z1{RM-dyFl|9fjZ6(LPBQ*){-4L43FIQIBMXbtps3hpe@pVj2E{eY{j`g~(#cP7J#z zW?kvHV6ZjHjbEs?H!)!*h?H9XVzNBxQC}{;ahq71dgTk4FbMq9zkgpx&Pd0S$GSj+ z1}P~0V?*hG4Tqb{ziGziJb0%qaF-6OHlXB z07L=WFl)`(B%fUVCRE^QNIVo=LP;%)?gD2@3CJ3(>w&B7c$=E;}LmGZa#gZ4e z@^0YKbh%oOUxt{G)sODu3aN>1jX6`{^CopN_ATR`ArxWu)QNNz2z&_-yEy%~L_Y}8 ztju3hiS26i)f(-7Kjk^u_UyPwXs;*mBxoOCn2^u*dbfT>S*Qdrl=BO3J7xlR00)B% z^{E8Xe(Fg4N##+d=H*&|*7gq^xRGbfsx6e@H{VI`LWtpEn6hwvT8fv zn*U@Ed8dnLj*2_o(zwoPU^s<7n+Rn3ZgQiNNItZ6yOtBrK*n%Mpa)hnJ#8j3YnMT} zV5lr^*}^xTCL6QX5SJ=zLo>vWQM{FW1@#tQ=WwKsamvj@ZJI*tCT=kkphxLFm9=&~ zPB6GAp?F>;WU&IO39PZ%i#a7C2l-_N^Ii2&zVf{qDQO|?p6MyLc$2vw2n7lrR%L^* zNo1!aC!0*tRJ_?|p?eBahC^62oPmm;`O9)+#8;eYsB}tIowaRqhUMqs0>+*z|1ZdXZj3t6~hfIW_Ke zlM`Sb_^zKQ#TubIr8&V}+#LXG^b3%3orl3Kk3FT&D|Zhi?G>0TS3RXs_{VUv=owk= z;UBTzNO6^r@wABXA2y8G}u{X9&?y`NWcKvJKAhE*W#n`IMhjovVu;Qeb2(onwp8Cr{FiN~$d#tRtGtv~H{#TSTo;2!0FLm|F*cl&2?#(-#O%ly_I(txD{ zj1*2~O)+jJr7Vy9lfCClT;xj2`#Gp7txbclvOOu!+SAJ1<_FnE77LoFuXso$#|Yi? zbBh0Xb{Vc-D%^jv&Wq&)t!{Ot1v2NRuy#8AA=K)$m@_RAtQb_|aE%l%1{FwkcL*E0 zboi}13$v}-Q-7_mr-wG&)Dgls`7{f;og21M!x&!;@t9jNJ|2GlLKVA23+tnNz4^$X z`=+wo!rz<#P7i9-bjJLd_3UC3-?Edr(023nHJi}|NR7v)8&p0gz(qup?iJC>E=)&V z8sbnc)aaus3APm5{eOb}8#W>XGAxd(f~R7@#b~XD8`_uXXntq^KDdXxi)enr!3YHRH(@4?Ph$k-sY z27EepvaJpQEXIvA8gb(4FgKzU_7EL{=94PJ#BR2?Uar&|fS4ev1L}(T%~X={&GoD7;waU`OGcKgIMwqT?JCfvVVuTr5s49qYJs*a#15iR9-+j# zL{0Y33ez*|k}_k~%tLmziJqQ}6dAQ~zpVPLAM_o*ql7-2&qu9+bM!gj_QJ>d~S2v6^^MF_Od z;>1X`siDxpEqp=v-(X6mZMc&U;n$)H-=6i`Y|Z}Ajk;%Q&@jbcIrPXBjgZBBzBE?z z1yQvn$!J_iVhF!z5y26_U1J24yE2um<}r1@yV2nQi>`ieC- zzI<(8e8?4aBtin5DY3G`=AT#U+Gx_)prD9rbYwkq-Y2_gGw;oA`eQwb^6cV>_T&6Q zg(;Zk31gpACxgFhz(0f#SQs$?juaD`KN>I5_;`6+_*M_i2;rz>QKZzyFp~&gW`u+-e16Gaxv!>_uD)BkVUXd>mb; zv4!$730xuyHE#YkvJzL=9As4hgCnmK!S!1*moE7-pKkqBiR25ecS0mKVV-wt@PsN? zH>@sUS|G8)6KimY)YB2s2in!{30|4d)-=CwXa8`@8;gO$zkA~OrWy9AcbRC@wllYn zBVbNnT)`(iWV&N}>GXbr>|IV;tq#z|AYgFm?-IW?P!@^a(v7364X zC^tb?VX0I+X9?XA(yKX>@;9pzz8%8bB@ye0I54TQ^5kzjgu6&M3T4BHSPuJDi@TwB;K)O}=2N0`_bNJ7 zVh@S(orIUcZ$VNh>IMCfKhv&(ps6zCUVPiWWm5_OJ`M55Xuf=iTnd9uYQ5Bo1=u&Kc&Ru~vOBxYosVrUN0EHNi#dfM5C z9H5Z)1*V^$UxiLj&6d1k_NzogpEFIwIMuq#wskEn@4v(%A5H%HLIa6!O>={6;Og|r z20O+N2DQ$?iCZ<%<<*+V&0Y2MnQc<)-dlbwA}HsY8HMCBFa54@?;APuLgrL5ACRxd z%(J8};~@2KTX}%}ve(o56_$Cz8!#yU5LoiIcEDqBcKgyfi^|c7!e4{Vx%tzrOm^6K zOZm-r!c|X=ym`5>9Pho8!BeNz1!UwaM>-L2$^(3sg=AoJCLtBOtHyqD*OQ_Gehg|! zht`pFQ?V-fd}r!MDiA`^vi|F~gz3v0;jSN96@20RU#Ce==VLB?ntl6=G*-vzG|0Ga zx1;z(V9YDIF?5);|X3VYL%VbER}d2xw8HAXO@JvLSPWW z{ttV9J7=zktmUIQH?1TS^=*(m_F6`U=C*%AeBB>18naz* zfr|yLI4JtLG|pkZ5k^LP<0A7zwUjmTvt_LhYcEnQ?^D7lWzHE2H7F zZ{EBaZQ>F(Lw%3klli$+x;U1!zm3*;Ge|5ghUF>$*C;azf(gT{WC(s#FHT)Dn)%tX z8E1E+JpFwAetx3EPB|uYw>jHHV0mHI(T(jY!hqUslnI5bcTFQG^}fqMCKzq+duT@T z0Cpx;ylY<@bAnG3dIY*0mR;jMCWql>2hQ~yQ#)&$+)rGA5_7+h^ZisJ{v@a8-KUH+ zxo5fC9Z;7$Ml=}p8O>BC3fJ^}kmrR5W!Cd!*XQ2pkbgumcj2WEve@A4sj(-eFH*7+ z_rWWd3|HwrBSS(xk^wrkGIi z4W^Y)H)T_g-qqwDH8J%i1Hi!wh^_tDA!!4&0_%*^P;Is9_wScvUk&&PFFZW+EonRp zKnrpSypzGx=Pi$2TL`-v(UvSFVeazEeW(m#Rm zGbVH=m&lI?`fnp+-B*s5%K|UZ=%D~jAI5XVm*i4IlgxxD=XG_&sqC$P{`s)DIJzGd zbg6XM^pQFkOP%M{D=i@nwEpssur3TuWJjv}e9=yW5%o6?|)K;-YA~5xu(MAQxm7| zwbOnVVfW33TQ|B`^&?#$mIOz}d52%v7Qui90gv^dlzQTuHzi09Jn}bT&2_Xx&*G)W zDK7Q6HB^45lB#!$T6sjDNFpzm&>vhNuw?QiFxY!hpFX#eJkfPN7 zLs%YxlSgXA%mSQ*SMAV$5_yI&t_r(EVQ8-hi$m|QIJS!=TXoBBEw)rZ5Du=Re@PCU zl14FZhjXwGam(Qcup;;D+GrhmDLlH|MkiH5qJX>>afqkv{o{Z{P*YMo$oB-ZmA0!xas%q3`g(^7wLl-9Wv>u+u&mo$h4?pa?} zZZ2?R^20RCo0IP)6>(ed*@Jx%N~D_LK6mYj_gZjlj-6Nu>l&q`e02EJcSmZ$PAn=VrV#>_#Wx4zjb+z{v0W{8mV|y< z2)i@s((>PhbA@DllRI?L#3V2}T>n1{C2OUnA8;;~mbs59co14^$7U3|$ghm2dZKbF zQ7;$YKNR`zijf7AWwlOO$?+2n{-dQ|jJkO**woKYEL=I3OyRimVv|0Y?1mKM3W~)p zHl~&LO>R~+1BU$>ttZnPtM&sg2=6A=OAwPy3n>c1k1z824yojIuBhMM6D4sckw^DX zvFCu2duu?}Zs+v~SW_4=6caX9lSxsDV7HwYvff7nxqo-@<6qjwMeme}ZYIF5V(!vb zhScFo*M;{?qVvt)B3oJ9n1su~ZpeOd>_T{;;?q$5R%sG2fsL^QR(zsKA&kwwaPMth zQK?d%WdbAAGBM-xXDiD4vs@GRKul=+KM9g&$Jf0=QCc9zsX!@UG)79Q2EaGN^SMBV z(p4}9_9o77|4VY5_Cd#ui{SCbSy#D%5?<}<(4 z7lq*q#?Rg~@V_Jn!RMf^U!vqA;pFHJLrsFVPtY=!RbgbMGwHL^v{1mtd$18@=EHyM zz_yxlMi37*Ek?h*?5;LnWn)20Cll9Y4=;Zl+ZYleIHtKtoLx4^WSrdLD#cf7wn2Jj z%H9``A2Sz6R(+JlnWJp)-!Bc<+UTm;_Z`jC%OYqYl?Y{BiXYVcrSzbu|ZE4_}WBBP0S&zJazoQg9c@v zexEo75~Zo3H@O+PCw32QsH_O)nQ*5_7L9ha@;$Wi>C;0It%x^xk~jMIpr|-(&ZAdN z3-;LS-xO@Jn8-ve(!GJ(m9Y)^J*JR}0)Q zh`6%w4?NXQKrob)rB9!+%p^ElWS!CfYGhHl6~iRz=yV;<-s9o}cIt}A%^>3S{a9^$ z;A}`VSE;Oy_8wfRlM}4%xpKX%%&LG--*`C>jmpaBhSP8?lulqYJ%GeO*8LrXf z%3!=;R*I0{Xk&Wtuydbl!(uiG+Z>Fo{uhBhO?fDZ?B+B=~#;$HTqT*amd;K?!;2~?z27G;x2k15RxXh%XFN9dg< zxRq_Jd$I{gVUeJ7*Q|R?Jed5#@<-9l6t=$zKxTF#ZnJpF+x~x%FoI-B?F&P}u{p{#3 zS_Nmj2EMgND8qLbE3@Sv5#+fxjbU0tQsDZ&otS`q0dhti+RzrdEM$GNkC&GZFb&d{ zh>$NRp3SpYhD>!km%1L>2-_SUHdNSW(D zEYE|W8~frXxne9e{;A5mbI*`rvy^2cr4 z@KMlIr_7p_elO@Tj#O!C6;T*Rz2nF~7uIn45Na#55=uYiBeeGQn0l6Kz%ai~L5-7L z$I;O?ou2V%Mpv&*Y)gNseh_HiDm93@O)`Hc&qt;UbI*L#(X~fbV&-R4j2jVr9i_?_ z3YH7?+T{UJU~ox(x4*hKc|z}=6=I#hp%b69r^2Ba)5H&-EuZkwZEFoMVhxulc~X1( zzD{nwFJ9|X6}ps61*`#^F7nYs%_?o;&#;>!vO&X;3l$iT21uOyx^=)wq{g+YJ*^uh z9wUKE((9@iirwkAR)X_l?rNJ=nTeImL2zt=`lEFVm>yGe#4;ls9oBCP910)260~ZF z*%$N6t|LH@tLne4gob^weReWqTDgwkN%;?Xr}q&E0PE2n0rq9c^ z4EqUxaOkS>)82)qPEQ}RK?lC@B((lEF|FiT7VN87dwFhM4d<*Hj!ah4r16Xc3prMqUfFnsd@mE@bM=aH$}Cy${(Xe+W+;Pd zlZpr*=YQ$7tk95xaR}H`oI?j8BN?;Ej)l-IGlwHwMTH`;}Fdq+=(Vo7zg zc?>)(fHWNvT*SS;iU6gEH0IoujBUI&bo7n(0Y$_eOy%!}+bEU6^$ZfjwrqV+5G+|O zG^ALV+l+Bv;6A5HgUB4#+yaeErC`=G6q&H!4OQsf^aMt!@30rCYY)soFSo5oVW1Yn zdG^{EcVcGjO^h0NlRFX&VSOLa-46oYeG1WpA(7zQ8)b(?@Km$^2(9UWC~%Mt>qQ4Z~lOmAwi_ ziwBO{5#B63ig2j=$sL&HXSjZaH`BQnP8I%VZgU5kiT8gfQd&7y?Out;rZK|l8Qja; z<@Ue6?h12!Nw&5Zy;T;e`DavNreatR%<`h7Dh~6$Kq54DU`G<9GgflCmr`5fKK(|BjR!Iz* zA#%9$n=K^ET=CcUzVFp9U)~nc@-1beT5ZU;-fjU~i-do>@jR28d6{p~o|#4BPzVq1 zSO!MbB{tqV_$z4FY5a9Bm2C799@MrG>ZHxe)qMDHr_!?h5qdpv^ojv6Vmx$^APX2% z0tX5C!2>W>0J6fizK^gnccI$DppfBN2bIWvQ}~E?bx6Xl=i*Co^`|cxEu`@dOr;XA zp`Y81)57p;@hn}wKg2Fpj5_}8}C zVXJ>w)R5kH)n*YE;)OhLw7^6K_h>$lJ`ItP;+uqK6v#e2NT77ajV&Q$wfbu@ChVb{U^$~=L72Eh-fKk)` zt0qp0W$`&(AmUq1O;ayP6p0@e=hWUDAo3|!Y<^-bduylOe)y!o$JT4+*ytB-=ZcQT zJxY(hp7cg>JlDzJ>S3{BLLc!VtwSaa9bxZ=Y&}i2;_sva9+3W~bufS5* z`zeo^g#ZNnewPWpa%!;7${pVt4xLcUQczC?dGP8K4XLIJFlI;hVUMbH5#$JH*4pOr zpy`rX$Xe}K>17OPg<(7a+UtkO+LCpFZLj^VME+ExFLdXU29$|g-TWezECWz_xQW2a ziZ&PdrEWq%1N`Mg}r#s&Yw-osNAjq+tBrsW;5fvxq=SCcCaU&$>62i7o1RM zqO`>7vGymcdI7mKiIP+NXg;0_Z9F6CM15O7j#~dsv6AuWN??Aso2YYT#EWYBwG4mM zjc}i#i#9h~ahai@&LkZ1&>XRu<4AI7;`wzrRY~|7V5jaYOX2Xq` zd>4~!S?R@pq%vo82%@GzL8N=z(&D6_GRU>YSYIG`3b$g4)J&|5J9NFQ7a|y~jdGv& zGGLsbqB%&3;dKuVRV=-nyqFZ&i$9b#YblPB`W4UqOO3=YrS=YZ!DfyTA?UgK#6pio z4~`V|B!^a5%-cTYE*|dy8DQySNx{o8c=E}n4)a??vsgXMg8T(Jd<3b|c z9{R5z3}0FokQ#K~(DrBm(5dpzzyR_keh%WWsp&j-7HP1@3T!YOX$WtRCON>Sw1vLy z^DYuIm5c$WtNF<>rt#nlD{YuEGrt~Bis(jrwa!QOD`#nwr)Oa)nys zrK8eim((rM@WR`tuDdjf1k|jCtT$PHVzb^IL45t29rm@7S&Jkm^eAY-Cem7qb{G*+2NxV~T{; z{qxg>jI`lU2D?lJ%-4@j#2n(&ffQ56>v5olzJ-Qn-2`|@^^L@h9S#zx*_oMYR*L0C zUt;*hmym`s1uwjB-3r%L#3g&xthF`=P@Ms{2e}L|QdvCYw)-9U6mZmh^ zva%d1b3$#nXr%A|lgs ztlRf~*Lv3T&$FICo*&+t3oRdgx6{HM^JV_&ddHn$dW3O{5@?TfPRhVX^VoKzUycR#V?XZQvnRi?rm_6# z+ux#P^|1yoI4>!?T$Z8bg{$t%GUxx ziO@bu0{~z=1LXOM058k zGETV1Vjg?@Zr8iu9-s5{&{37Q)aE7%?f|>hZK}JHy%U0bc)i!4{7P4y z)3s~YvTpg(|MFX1;LgjRKH}$>i*PndcXW-z2ZTX^6dt;J^u|6Ws3!%_8PVno)E@SP z({Ao@JYImS`oeJ_Psa!!dJ&$xacMNbimm2Vi`_*F&JSd*?>+QrB@mywMeonvdMYa7 zL4cIA@br(>lg0=N(=fD6hHVb$9F##gsT_ zJ(;^Xc20jEd|~_YLdQFCId5LS#+x=9-SZBbY$)RvI@;UWg#qqa?Y*RtNoU~-cLK)x zSmooV9{SRAJhv>*gWIkx6a#hl9Ad-$ujYEcVT@nwGwpi*>-9**`0H0IFhGf)T+hJK zfx4!%f3Amhui!C)o@g#DuY|-XZe?JT`I}W5DGi>xci}^9azpyc?As4rAG%1bt;yD$ z7_Mmf$KqbO%traE_ zvUhw(d2Tey`0lw0g~9#$8WWHCH3%R~y^flJEBFRD>T944ct0vOW^nHs}kYE0OriM|+m{KyL%_lR5RG~|aK?$cUf#zh}TQ<}cK z7O*m_tEdNrc7@!1+TmFYViO6*9O&8RQkEJedD@Z1b& ziWX>>kQ~Buehp^v#u5)`=-D2-qzi0)p~7nM2hZQ|bU$~cVfR}AYk)4o5iOF%y{RGH z^T7Wqo`FADI{xsYTLHvx|9(}TbXI>?Wx!qA-Fk;f>(&S0_V0HG36ez{a+uXIl<@Ef z0`)`%z+x_L8wI2CrD8A;A3eNr5iZ*T`2kmbN=e|?3;}E0wnVCU+Uk8KEk4EJd?mag zahy6l46rkJdgb%EtE2Vy>Mv}(?uJ>k0utMO@}3}^5)9cD_4)TcQk{n>wgsD zt1*%BZb&(K;zQ;9U)9d!9QJtCWDDlm z6My4i#s~9@RnBK(sdjeLeF>_60DR}rX_CraKt(i|7io^zQG1W~^n;^{%JL<~9UtlN zSj3R9J9JthKc|i%>-hda!d}u^Bd9@rH|HNLbpRmKhY|wifE$2e6h5ZLTt9iJ)R&O7ut$PWfavr0`*tfD>IODVe{E-&LIdi=PUaZYt(F%TI z8F@_fC~L7g;^Fz-v(upU=RyX$zq+M`A7~1uHA2UlM6-d_X-_Vf{*K#=eR(J0JMwk? z;BB_U&0B$@!!GDh^&BR0aO{Z71JBuK$0^-rUAgqcF-F|UA0+FRcM})`ebTpc!L-t6 z^N|3C*PLE?@Z_nMvMJh8UHJTi|GMJCq;sI#<9T^{d_>MU*U7Q-F|Q=GRvQZ!IU1G0 z=~dlr_)n>EbDN)hxk5cG+xi&@zyd(WLja0fueQmD@XLl+tFw~ay#_i5{^cZZ;QvAP z7SB~t`F}=D<_EqsP*Ksv+5Uax{Qv*#rO*uT7Ef6*o%X+xt3UbEymPzv?%4_f0b+hV z)7qRG@!|iSa{Y1clY2XMZq546x|R(<(_W<6{~yn^8?7(i{3T-SztL0(cKISA{`0sD zXsLhxt)jAK?tk9=pGH_55C7LA;ge$&umy4QbXBm+l`H$Ktg^MvIoR3R?L9PCSO@6j zkh#}@x$7)IJQg$G_oZPkIOe@`B{1f=R@Ysy^%svEc_kN+!BYlXP8>OMGiZR=1ozUj z@hDXVY2S9QC@bqK@kqe|`rO|8&m)4GdmAdQPS}|vhRb}e(ysY_FFXs}JAZ>k{CwM^ zN9O{0#Dt0fv1N6XF6%jSY`zTQ7PgC#HieGx>AM+Q?G~?mK5%+;H*9Vi2go_9RXn5` zuYlR?IG_S3AImN)6?AL3R|d3y$SUWx)y2(jzNG8?uAD}LL~9J5JwuEYgadJ# z^l9mbI8mIgUJ0?hOD+Wkl3Oi$hBzfXvd%+u^=|f4qexQFB70TtPiI)8Gb&_ zPZI*^6}5BI=9hlIYg^K}wa_M3u>ok%I?uS#ASnr1b8T@`+S0H6&>04tRJDjc(wOHr ztZB_4JyX%jEDL5qXq=>z)Wwd)KDMfpW9+RwNuFu&9;AkhYH9p1t zk?(^o7W$IG<)LW8!h-aBkQpl2Z?68&tZ~Eo^`LC5<)C@we8I{XN;c^d6bhxx-f0$} z1miJdA*&gF$O&S*!VC9dhKkA9wsvR(T;9sxnNVz1u@|WiP|@{5mzx88NZ;+dj$BR% zzR!&pzj=Ap;)HrD%(eo9{ICdvv8%Xoz*0{v|Ah~yS;l_708P5 z<2YE;_!)Y8Wvh@t&Tbi$SzmiqZ1Li{oShx^SpvlOH!N?>26(H+UuSb(su13(njNcj zIS#b7WJof=Lrf_==&Duk-?yy>n_dHU%h*SWi_TuwyS>rcU-;5geXMFE5J_1Ixbf@S3R30(8ycF%ephmW~GhsO0wD}p*ZI=F+c zwpaLYR#RV{|19}PW5o|)0_KL$Fm4Fp(16#iqeAvpi2ARy{abkpm&-6Q>_ZL5b{yI7YM6!-@NS%TI9p_=@IR8vznXDD|{)xxB*yq79^@3 zmRcx0PA2R0*4w+$_pe?1uzPOGewKJ%*SRFB>FZa|BEE6rV_VzZ6)>@7hK29VR88e$ z&SjD>1<};lSRz#5BX>9UyrYj->;|Z`tiM58X4e2!=m$gZoqg{v3IlPtV7LH0s*p?n zbY09$r!tV7!z@gY^)q8+FP zEB!|8cBfBD-pTVud9;Rb>Cshx9o(zDZEH2~u20AXluBujh2uQiaJxdB#Xuks$2=51 z`&-70@DCiiqoYGx;=ZaqZ0ywSmJHmtopD(6PB-#2UFxBW&B?g}%p^|+T04DPFAmWs z+2d`7y`vU30}ue##i4-n-QpICHN2|JW?$(>0m6gJ;!Q7Po%#&ofOj$h-X(i{Xlrdm zOY%(5fW1d=NmaY(`P=E(@dLtaD&u3iQjW6EGOKNQ=$SoDulC(;jrs^sul=c-#vJxn zz=yXN5SdmogM3EM@BfUm+>8X+My-CvVdgM;qcHaK}kwCDKDg$bR5Hzgvw zm*#wA%Yu0|)aHE=JI4G6!x9CYo{$d!)j

v$16;;kL+2j$|U3ZdXV0%((hX`L137 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 literal 0 HcmV?d00001 diff --git a/docs/user/images/app-navigation-search.png b/docs/user/images/app-navigation-search.png new file mode 100644 index 0000000000000000000000000000000000000000..3b89eed44b28f8b8801f73bd26fe7322a8cb8467 GIT binary patch literal 49936 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

E$EQcw(2rXmh6+0s=A~+ab_~g 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 literal 0 HcmV?d00001 diff --git a/docs/user/images/features-control.png b/docs/user/images/features-control.png new file mode 100755 index 0000000000000000000000000000000000000000..abe75d5ab6fc141996b05ece259c145d27c8f6e9 GIT binary patch literal 142443 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

6SzVk?-}Q?w1xWO0 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
K}td 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 literal 0 HcmV?d00001 diff --git a/docs/user/images/home-page.png b/docs/user/images/home-page.png new file mode 100755 index 0000000000000000000000000000000000000000..9ca4b7f43f427e5539294362e830ee12702315bf GIT binary patch literal 516322 zcmZsCby!s4x3vlaf`W7jNC|>~pma%hcQ;7KNDMJZcT2+{-OT_)3P>Z}DLHfuFf@Fl z{=R#k``zy!=3(NT_q=DHz1LoA?J#9UsTY`Jn2#PkdLbh%uKMT^`pBb4XlEE`$amak zR6`#<`ua#lTtwYtem@gkTV0Ac*tdC{|6SGDGAgdPIw~Py)y1+`Wp(=JoEvAr$}5 zVUYjEK9F&_@dcuV(7t7T_5g4@LH(a+`tu98QPYd|JSuYlze2{+70gXbl=XrRhA@;+!| z@WoWKzo8kAWp@T9v7smwznl;UM>zS;)R4(rWYhM3$lAF2Kmt7cy%B6Gxy7hnbc`}P z%X{+T?~$_q6T#p91;)q=nr6WapW^}@&iy`iSfUEX_j z3X6-6_ov`@Ge*p;CB7 z0z;P3qer&cchA68?q`AJwG6K)yp;txNX*EkOhJ_-w_GiFZmnc7f@USAuu_dG6K(pT z1Ka>DGV#}}?=bi>x$QpGC%fJ201q!l3hk5o)l!vRzqGmIO)0`KGZWBDw3Ip-J_vOM zVZoeXJ=Tjd1~L5oS%m;MRtKYHzXj_Cr|ru2_ud=K7%`#2ka9}Cas|-N-`mUhp9J$S zpY+u-jZmB!dUWTwRBT+C*P?1~3uZz40AA%v*sO-f7pD@fq46o>Tx7m&`cD~_E20yQ z-uymmM9m=0`dW*B7%w1n=M210K{Q3tmi%7Qw1`t@LInx6RjJwE2gJcBk=gDNoo2*h zd~&=8*D{Ik^75(J(e4XcT3gpndyBous$&k1y@L}T&>G3%qI`E|d(ZlCflcFG0{1xR zEL#DgC(TZ-H zRn?yc3roE2_53tRY}9bO^}#G`Hk%udjf|+yq@Q=K-B7X!s1OZVI*W`;*3bunzuaFiUU?uGGy_ey<$%mjW}l2qDaNs36|or3rM z)JzK39sYAtzE?uX>#iDKZ3dn?AIw`U@6PpmkM_x}kfE4;8AY>#+ysUSZP0x0qn<>e}+ z-oPfFzvgbKgI*(P zXMdjgO`u^PK!dqoW|&3)uq(<1{5N62Gyc46Kyw;P$N#V0z_8DraM>kO1x4 z;`K9jv3XfRha*o?UnBmQ%Jx(gHb-9`!GxWKC3fTA#*ta?pC!Cel1MGW94Xr#VL|X| zp`s@G8yWN526SqKgA{H46y9$zr3grQts7`H`gi;)qh9^1fkfuY(CBE%{wj{+W6ZCR zr~uuT-jly6_dlr%xCI-bv(Y~asJ1*&q8p`O^RDgrxK0*29hB$z@LW7&Z?R>Jh5C8> zi^?*a*xis|eO;8rJJ!v#jzfUF65@f8N+#aT)+-RFe- zP8F*m(%7C9{f{fD=X{?Jqqh4E;%luApPx3i+GC6-5lYOvl)mZ%?42>56HYe*&TKRI z-O;8>8Gdw}%q5I_wom}anbRHl-mJ$;yUmndv*H@p zuLE~v!|PAb##-rt2cdbz#ZOI{>A(9vP+$J;iaEZ!5H{}cmk7T)B{uFE(jj4Y`M5i2 z`~9Hz1vVdX8~Wb@^)G<~WgDzaHXqTNP1seb>pbRFbnhMLQS==l>0eQ*DNls>T<<|n zp1Z(%r6UHkjjEoy-B#MybB;FH#>i!!_P+@$D*6e$moS8(s!8G0m8;^lhx1S+f2VUd zj5xiazb-}OH zm+fPGwREfdLAGLLEMiuF(I8A_yMtF$%?f@`o>n=o2rahx=;7k&YBpDmpZ0!3#(uVd zuj$63$kA?qAKQzb4Bi!!xf()`qdRL4>(Q5AcIHtI_`is*fKb>?*391-)V~tGt$JD6 ze*fkRpH$-0kRz18DDYqM20!F<#kQ>AP5JFXD){9-HUHxQ2_>Mr( z>s%s^MnXC7v-9k2Z)VJpDcxt0?@(&(OFUgPNy&m3MB(t^=gG#W zLiZkgEj|GzNVL_uj2=ZnbF31kD;s}wZN$GnWGv@bpQ2I5$~P<7(KSchWM%&RU53C* zs>2W}^zM?Tjp78jRt7nbFuULrADIVj4V0zvw~kLe+a$qqF9tYNRAwOn?usGpmD$2H#Yb5L09JF5;8qvSPu?mse*0G-uGs%PgL8^P-^SwsDNQ(PcYmMP+V?* zKD)iUSeb40!s+ai6Lx!|zj1Ycck>}8@o!$q78n`h4R!0-I#`crSoTDZAY#!NZ*lRa z(O>d0E(EAqFS6KW81MalqQ&j_EjqHNXsps?HK#j;U94I88MBz*KUDa)ki_<481rYA z;;nX2=OcGk$@#p*T&XR1auSQjd5sR6iZ`5=tWYttCtof(E-5KEC?YAxJBUYHhrhV2 zD|*EI?WT7$PlM}^uYPQ+^AKAez*eM-sntkkSGiupOCwGda&mqR!_kh8$I;Ql7ei4y z2CbvDR#b%Ei!Bo5gfHKzY!I;?Iys++<^dkr%q2T3-=Fj-=`RBu^{(8pPIScuN_f&+vZteTQ0myBNNTedA*wVdk${*wP^y1v~b7gYdpAGw9HN1wb$ttyqC_K%crSzzPqKGybKOZ8qN@o@3Yk7XMjW% z2OTb7NYp%P^!PD8Me&Agi_C3)C3OTgY z`>x_Dec#IlOS~4yc_(Wdm=r$A8tQM9aWOh2$6tWqst=@2M+xXsW)i?OS+$^)E#d-$ zm+x&N4HjE)2~=edRu;H_QzcGau#LX|y}Mfa29rsvxPn57vHcG|XB^>ugM8@&unhlV zp%^QYk=vhz;wQ5jbovKHjD9x}MVBbU-%*jE1t$wxGL97gq-j*>!UkOxN>?i;J)IM3HY#xW^8F zNvN_y!>bd2uWjXS73u4@zPG!?sI!~mlB&5*)fM>eBT3_bR|Wb^PZu-tzz2)L+m2_w z4-sO=I6nPVyM5}N0E@3&nVm&FSa9#`2tap+!8!GfS_qR1DkXROczLvrQ0VxhJAxMVe-M|>;8>TLT%S&cSR)_P)FxUN5vf;3G(s_=r31&02;V-5dd%nZ{vh@brgK-IR@=i-g(VjK} zU)M6yIq`D$K^u)0_io&X`^A;}^g5#kpKMgWTtKi5?4JctE*kb zMMdMoW0Mx3fVUnGcR0T{^0c0`C{xpF>&|7{LAB|oilSy^Pn<@1OFg&$G);57xke9u zJV1Efyy19K49V4iFKvqIQE^lhw(B0J`1+e)CKjbySkIrInn*+tX;{RfW0MA%zHtkE zhyTLp;U!@?>&Re6FAf&b8ic))Is0B9+7Hr--lX-ScdViGVSQ(wN_wT@;Ni#KlrlpN z-TtdnSujj&-VHs|aP!!x(TN(Hia#vV%E7GPuf6(Uks_lVgswGW)O+4q=;X6bxwr@< zX!J(2wxx)zU!+NfFFtSJ<`$X%72yN`gk-j=ft5ZNU!z7>FqIgBUhlukm}-BD`wJQe z3nn0icb&a+fF$V=GQ zxiMqQ$}PK7V+Qhw$RIOTOA%auxOT&IEee?`-B)~KY^H3BBH{|?+?{Fpkw~>SMc_)X z(c^NfpB-v_pkAC7y6kgp);Eev^4_V+E&CGh+NQ4^9f!0+ko(ay8A#Tix&;$_VF~^2 zx;0QFAlRDNuL5c#M+aCyWKLc__qGltE8X8-aD2VSq@l6UXH{TUSh0J~X*;D6!E_%s zV)wb2Sg?O>VEcB!IQ*dLabJ=%J+b>iBV2XAtZ79CCCk$!aDuE(y8od-}8_OM}qSaVR?D>E%=)x-{>~DgNM+mirz(X+@!c z0)_M(d3D}*hApUBLn*~uuYMjpLqt%a}|j=v7IAv~Gd zcYN<~D?ZD+ou#X&JI7}Y)BTkgm`tz$Raj!Z3w=i(a46`>{Op}+gQ>m%&EPjN@l^O9 zs>pu9UDH%U+)4WLh8VHtR-L5hwCAc~3~=c#0{0bF4USm)+&Q%Y*o!%prPnM~0t~CG z_i7Tr`Ly7cIq4rj#IjHXST3?-RIB&b6)=f~QScQEo|19y38;gG540^WJavlR`h36u z$@Cw^ZVG<6IFqD&GAjXoD-Q*c7EDyc6W{?e=eTqzi7)*Jnn*RC-?FVFaJ5z2(= zvst3Ss9p723`PrfFFQCpJ~`LwZ9i#HxHMU%u^(bJz6wt)BGGz5ANfwD0c0i*<{*q# ztk|Av#;MSU`jck;@-I}1g_{I_nl72LH9a0RLDDUniU-it(+RH}`A?7Pqr)F&a))1J zq{oJ?NNV~;s4a`bVb=3*sjuVDg4dTRV!p{~9d{IvA7_9_xzyN1E|%%FZ7(#*hN<-Z zV-OxYsmWvR2d)`V@2BpFXsP-5LivTZNs@+{j|B8j^#>KdtYIm?M|V4w(8X<WY) zk`H;lY%5!)&pGeFEg)%Dn<9=^x4FymUHRrf(~GNy2{bme$Y5^^o=pjBzu?U;W5qsdixC7`81 zF%xZGh+^_Sf`G+uuC{$_{`P6R-E8RhSd3Y#+M-W)uN$w6uCb96ga)%~UcICzRb`PE z?{a3dn@NAo=Js?hF%rgKX1zjoKyXncLjCx8^7#sgUu(*TqoHoFJFJCYR8;i+Me`97 zWa?>ex=hvTFCxD~fP(%)qxh!;xL`)ueT1MazjH80?_!yR&n{1vm-YO|p@I1>CO^8? z<@-HmP<$I>3y*)K;{m6>%FttGP$|_ z)24H>{K^s#MsU0^a{u8iQJID;Mc>vFJa@qJ&V8aYAuRE+07W@+t4p-NqO(2lvfsSd zYW$Ta#OR>STRwj%@`;Lt+3&5@!k)=dHr#F11;eB3-<`^dEV4HleK+K~OtQ8kSe8jm z`fQ7TWRHJ@U%Xa|j&`nOlrE3Y^+zH$9W*va;Xf?(4dOrxu0Px4%8!Oa8hm^_K7S4@ za>IU!O7-wEFeu1+W;e6SZVvNL8_SzpYc*o}GcSf?g*6`0wX06c@S8`;Sw8gG# zl{LN<8jDsNUv%G@e<9Q+lEQxa;ot}`5C(0$?wr@e(ZcaPSXsVQ_bR`H+@lzZj;%>D z8g`OUn^+iIte0)c(tXV{ypyb|@c|8&?`H^%HmQ;*wx|heL5A5B6gv}(Wg7W7?+ouj z^Yz2FcRUC;*PkKi#rK;A8$ilFfDQ{TIeEn0&4o!nSg$I@?O9WC@^{}O!wk+>ockaB zg<2zDNImpqP98|>*#^{E(7U`IE}z_Ubd-L=>d@6)$~|V^Bb4H*9TF-+UTwL+o5(?3 z>?Wo?%Olvz!_6*M9Qrv!NkKwaSJx6VqKq3T4|*ql3%WUYm6Dbw^d9$`5y`_OwR&D? zxKdNPf4KeH--h;_Kj|LJ;w|x>O zd%nZotj#kkO3iK*CN@d;0ova`Jw5<9UG#GC)a|2iZd-Ihw4bRBO@g$a7mHAG)$iOj z`@dCNQ5!qCRf_(D+t_JG7U~W{kt6{T$j<~?0${m09c4XyqW})~LkY<#s#Z8;eg36l zzT1eO(3RCxpD1$KSDw{jnF&*{O&4!KvUia-*}m@dJBNkYXttWx+3ti)TEg z|4!q*G^_Rx=cDNg-4HqZktsSh^pLMnt9X>$@Pj$+*s$7S`AePBYrBaSR&zv zmR2RMHA@ZDVh^zkfwwHWAI^&{ypiFDK~zMqLBPYZ3y}$;at#HSxdN+7(DrOGzrUy~ z^W#i>q(WdwyPfsB)hk6n%L2qfq|>K;Hq0ZrqjpMUpY49yTG$d-r%@M3U!tj`dOkqE z3CaTPR60(#aNgd^m2mmo`cGPoI1Tyd{Z2lKyU-(QlFia>u&$gB)!5mr4FyPXe=B`i z$br}AHXeOeX&*9t41WRI=lMhC|Kp;r7#Ix~F_^U~)H{-pZF&$S3MHI~Q*EYwErH8n zv1p^BI6pTnd5pK&!qRzy7h2M13jknwBhe4jtjlen%zF+Fe@HpD##(9dez#QgE0Jo` zZlgYEMUF(o>PElX?D3wXlk0`rV{9U#dZx^~?ug{$d-Mr)0}nY{uS9DdS!&0{#%I*| zOvdARe@IrGXi7nBoXSs}F&k9cs!PI3b9>=?0`H4mbuvKQtHqmF-7+$>AK#$`;I)j> zb>aq;c$ugiF%tHJoLz<&5W9t*qmuQQ)oHfMe*MVOU}#2trd`-kCanV4?|#DB&&!cSLd5?!wjegJ`01L zn4e{-O4&IXflW0T_8ph;K{SEgN;MX49HFso;m8SlSW1OKLmOU$3w}RyG1Ib-yTI;;e>mkh%JV_@_Ne`#r&z79+Poic%F(*rs#3_qeGzpi?fhxm zwM~7ALAAvoQJpoda1tM3`2&*UDq(|H7&NPsyC4~Wg86oVq{LRQixQtSTVXt%P3S6- zKF983po4v3*Y;Rt$q7dti*N}t06J%9R+N1R=y7MIxOU=~gB=(hC88;>d*LqeqGx z>x{U?CDaHUahyyDD`{&+q@0P&$JUE>(okM4TQZCsMb8BY$btvgAN!e<;EX4oDMV|T z*Jmqb^0}+w-UTBzp40&cxl${W&0;}=v*bN0Hpz0Rpk`#3amqmyJTuT{{CFrn|n~lS9IL-h~ZnrS1c0PG0DE@yr8|nw98Yd$n1Qp*Gl7)Ilt%$2~ z`%Du{%jomHnbNdP`}sOCcC&?E)bXa`(DN10LZHvhMc4GSX0yXg*U%#7itj@!TrhDx zL7k|X*!q^vqfRPjxzX)_tf1`z%xB0MvyLPwBbY6H?t?rX=B22*=6kRWnh@%^JE}N_ zuhJ*K2|N9Mv%2`2n%{K@$TRhIcicy8c zvKuSgsRKziLVg%IYW7!uO6E%Fr8hPlAtCtq%jfNpfWm@49%N}-xk#N*P@75Y(%S~F3+`o>{Qd-aQ`n$MMfneJ>UPFUxM>$qV z)%kCUI!||I%m(c!yz-eJjS74;W9dq>(d~+cKfV>KnOJ5Ij_tlkJv%$a>3|cKej^|z zy@MiFFV0W3##M42X725y*CgL;FSvh~b){~Y=MqL(+bZclYA%v0(T z+c&Z7Ol|z|85D~Vh{L6j<71}--U;-2&W1*>cQHu9$&VQyS|cjuKr`xlY4VtcX5h!{ z>{<3*LXpRG%pbzTP#t(!ftok@_F@PNcxV<2JIejkAqwTAv{u?RA-4fXsF&+Bx0Ot> zlm<$)Stf4kG?bF)(W?!}PBw`9Cr7_4j8gKCZ-9F=Q2M;oWm3%y0D9h(hJFFIG$R@( zf5cyFiI(%d81wyMOz&Q*QJEcRrwIhNDLC4vlQC*#$4|G1?*)q zTg*)Pj5vj(jC}@kF9Ty3YcE5V8mW8g$5_v;;51m`I=+Ud4^Y1XOC0QeIDSV7WR?C2 zZ8226ZrB?oWp~zqTyX1pt*jqK0~^2?%o=ZI^7D|7(aZmI<}6!8)AO$O#CZjVKW?dr zKF_R*Fgn;}kH=*3xncrfAao_REQYkM>KUlnP|A$P*Ftf1VrEUz{*7w06=fm&$!#r2 zyHUpEfokre2rLqbzcqzQ6_?PMnr;t0wsAGM$mEjQwES`$+wWKnCwgF`M!qHoN_)p! zdh1By4lp~vV{B)0ukXdWjz@XvV{Va=c}!3|Ur1+S(f+wnm$YN7h2&b(t!t=0J7tj$ zqgqIlQMKsNZnDeI9p@>RMMUb!vrAVGdV+Tu#Bc$eiTc6acuw0H)2{pUs{7w?v)p0( z<@Ufh+lqB#8#1me{-79~!_MW&4~@>7Jck)N0So*_od>SC(t4WeT|8po^i_l`*5VvL zSwlreXVU>p*=s0^am?jKUZF3l;dAVnS7v77Z-Ka#eAqX*ECy~yo3D%5rV{#28S(?G6VqOOTPWA_ExpF0 zm;Kh5&XFC6JDed{3Qlgq`jy7%`x@e9&q)*nA3{m%?bkw^nVm-`fu;-g|$$ zsWlZ&4yE9AA?WC!uBvrk^ELg`xcaJtBi;7@5x4Q@D#U-g5O;X|A5Dgxw}jF{TUd;Cax3dEm2!*^yy{Ac+Q$X z%)U8G2Z-%kq!hd{9gSUO;xj<3Zms5y`*YhbV^%wBWUE1@mL_?NCU=vzt>zlUJH0%w zN)h5)1480oli%v8Kv-a{HB@2qNp1b7c(^P#lf@W7ZdcHowRS)YPq&~Rkz_vwfP;92Og5m@x&}ng>yAdRC^xD-Uyzjsq2NRaB!d#eO1>iu zN)=Hw)D}k>n8`Cev@&4u|9cQ~Z zECM6B`?1NYN{LSZUAqx@&{-~PzJ9L=!JLJn^N9K;F(@bA@4Fq8MIwbcJl+zuK ztTK{`(g{QTR6YWCLGds*?BOE)d?vTU#h^QM?ALU4b!i`BuLx<)@F7k=rk9^(XXhWU z?{Z4Z@W!yUr$tFdk(GKLZ)rwX)l3)LM@IgdG<-gGZj!yq1c@_p7Nh_jnK zV?9E9p^sx&Qp;uG%xZblK)kIlVP9Ky*i(qvA~o)E0s1I+`+=zYo)An-qA$PB*_RVx z4$x%S^z^iL-Dg~`SB+@tF|5oY;BuZU&u0=02AzRfmFxTGAIo@wsE5KJob{lr+_T-Q zOY;#TJ2@G&`?e=}4n3Kz3C=f#uk%<~LhCfZjR2<_#3Fs!<>|WtoMnt$%?nuo3e5zT zyMD;C!I)AESRNU~#N=+nWqP3JDV1V6IQ8E9__xV>c23`oz)JCsj9)Kjgdb`|Bnam$ zFm77nVR$lX9A$Qv{LSu1!PV6#g7Dk5?ub`}3?izoXYR76wBr|C0cJNc|R*wY_!1O$2UXcYP+-5v`FSfiAdh|DDbjmO5HdQwKCkRw%%1#+oR4!NLBJu zA|6)r+2{#|dBz)?b&CRu%Atk8V2R&TT1H|w*qI!};|sI1Na^eT6Ou?{dSc|Q?HTTd zL%`+`F648wdO9N9`!g&7R|`spNz4k@XQ}6AIgGwDE8WrtM?7C)FToRd7yhH84rAH+ zcZkN_fN6g4qVE*@D?fxJQ{oLnxtKFhc6jq@nO);l8w?ZKfClB`7s#iEvq-Vgoa`}I z^}n%iR!ngtRr782!{T-{n<}QoFyzD`!m{iDt-{_<9Af>-3Om+VFmKmV7@6MDoPVgC7f;Odwc1uUZhMq%{7cPZoz}%Wkk{KSN!uKD7Q~U zKP<$NJ$v)!%|#y|;D@lU5aHbo(n%0jE=x{rw3soe+=l~XT_9rAMrBaU=qu}|RI3h0 zh#mAj{P;t+K2!-1oTwvRJr_H}ptUA((M3NM z(pftx&`>ii>~x#GYN75`<1~qQwh4X55zA;V!_9X0D_lUcO;q3V*LlbwKndC2CdHZnDGx~0kDoEfy`(sm@bR@2u70dDD6S={(;urqDHs!@`&H5yU+=zXGyk z!ABs@`?&}K;*NzD3AtO>GZLXiCyD~now*lli=bV44lRw=Q-td-JVNk&SXmuhC~1Y1h@|xv5zWwDuhPE zWFYD#Jq&`BI5;@3guHpw9uYICV-O{ARn^t}BD$h3GHm6Grr<9yWaFe+TTd{yPyc{4 zF*+w{x|Jx9dogS;`TQnV#>_U!M{;5zfj`CT?B?KcaPRxur@OpYaeUsF7(Hz#T)Q)s zG2?L-Q{k`0vg{8sD0$F8g*VYE;!vK>RS z)Z|OFTzuU^pYKU^u!1w{{>anzqvM=pKY*Cq(RGm%Q#?WVinCWvb z^MxR!T_Q(r_X_!o<5bLN5z55%Hhuxq;4`skxj?Tj$zSS%9?05q$r}fQFUu`??Bqsj zTwx}1#Q6QUx=_nF((HQVhegb0I=pzK9$aTiKlC0MUwL_XziV}jesoR1`n|s=pa)ye z^jZyPbPT0_P?68KZ|G#sl`Kw!nf12acOtDu=+9z8Ro^bq(>Isede(nRWsc>|PDaD& zM)L3{3k3>AcvO`FIjHK=QK$9`_3M*6=4X4{j|vJbOHfxPFP5be-YATfx*OTu+9%-c zw0fS)u~A$I^qt}z&fdCMWO!i$+d#^|*(Aoi@%o0yaWm<795e<`=3u&O<08bx>jNP{ zd#_1T_U#U|17%8zR(T&jO2S9^f zEDCy$Ws5P$r*pk9@;zlCdZ(Ln{Q%oyu{j^;PhfDqyFL#Y05IEnO_r!p^>SQO3#+Sk zt=(RoBDrevt>Mgwoe5!)83)0a1p1itvMHT?eRK=`ZMSi-3M7axrJ6cxI%xPN0P{8c z3%d2drHO;{SD|_AMm1lcOsTXqI>T`;G{XuCEXwUF0B?R6|3OZbdwtwAuf(`xbJICp0?ikLvDu8mn?ON(?=z)i z%4N0@3(f}d)4{dJ8vLzVMs#%3nmhBGuU2M+NmMKbOMT1}VY1=2cZm|usOf2ysSEH* z7^cOOD+E8}j@f=TqY-@&0U9Ezk>Ly`F*Fh?sPSYw6ei;BO|nG}2q67A|9ppi?O{=w z39|U0vsw1rlin<*uOhNrP%jUYiGr1Cs}aM=@`XnQNim8`tTQ93ZVpo=Q%qe>+S+fm z7kC(fLS6{FO&=teS60K=jcZuKbBJF`Hrd_t^1~ z?i_DgESxeDl8_JU0PRz67aHt#QXHI88mIbq!EF42tNWL`JlxbG&hSi|*^ezW(XASs zQU{pDWc}-SRaK4>1RwkFNVcr!5nr75Li;$_kxnyWY@@~~rWJjOxVOp5@QK08lgjou z?>jWZ|aS%vNdMZ{u|4y~O~* zH%61QpQT*pK~m~aoy62`};u;4j@5byRu1gMd`|+N!DCm9NQ2 z^Ix0SJo}=!Ke0hFt8n;+B$|IK!MgXuI(G+!I<&vKvK-9G(1UY1yL;p5A)6GDwUK*QC3y-V(W zcEZ4@QzBn6VJMT;**W;#tnjd;0y@dsFjDX>CKOaW#VT6wp7of~#HP@mIG-kWv-)S= z{4Qtk_DzRM&oD7oRfY#Bf?D=r?6uV`j?cV+Jd{?vLvbjDvyS3r< z30ZUgwR3bvIl&4;)h1nNiKLB&cfln+K))vv>c-nU6NS}wa~!k$o}JRo6~l*Qnp-58 zMDGG;4817+7>q)wKzVlt$`;s>Oyp4p&F-b26lGNIY4lMB*$J4`NtiV&)Inu#nZY*L z9ZQ2sS04=|U;0D1q6NJWqBgrvGx%J|lDIrm^k!|>jYG!c*e~}g+mCy*(x*DTZX$t{ z&rIJ#Tsja3{789KF)&0*xQg*XCod%cy#Nx?v*V5*%g1gNf3y3IY+JkIX%HsSC(~Gp z`u10a^Dlw-&ZRFK7ruRsNW^VBz{{qY%F+hg+9T=uQ@2w_g1TQmNw&{Aj}aJOBZBmn z`v4u2Lr++A>r~%9kIrBIS?Ig?qaeX|eJIM-!*zzrNf(|Nzlz}fDVy664`AalI80Rftgf}S0eB@UCK4lXabNQnEfH!6Zf@40njmdjGnm>3di!|<32nc&2 zGGq--@X)wMvs-L0D9>pT2MvJtuDdrRm*d8DpXbo1 zvsI)^bdIv&0s!iODD0q!)NIYn9_=b)KV6wR#}yFaiO=blLL#eR>35?#kFYcnw?4eg zEMfTKQ>5bePm>%t-GAEXR`HUOG|GCSp!6Cx9$Yp}D8e-7N9iiEzC-bKmDaE$Mz}(S zv-7-S8R$~|=7tmFbFl%XPJw<;jyxM-e>u_&)!oRHYQ3=FzMZpc)W0SkmY3X1chDrB zBbRkn0cN6UlUD~4Qn2Olwduy&u)V1d$E$;T#?8}&T2Rqgnwc^)SEu6xSz&;D*<_?| z1|)vp+<0Xktt0(8u!LCX^Rd&U`_!X_ zpe{YJ7}{+0Oc6*nvl}a636}oYf>@Rd{#tfZtXnQA>4`*nB;@~Qfc{zLADDW2ERi0u zTv@_5)zg9!Oq?uVtb~W!n=1W z-$0j|vQTGV30eQ@-lL!_l8&TE)aRm&B0u|BY0T)OHbh2; z0jz=x8O2a~L*MlmoM_BiccH4Q`BOPh)YR0TBWLB3*^DHQgnUdai`6QVLGR2{WE*FbrB5bfc{ zNQ8utCDalZo9X)26q#@!pbU>fJ{zf!A3f|B05&GLem`KQB>{%!XUN4zq*kb?QL4eZ z(&WdLK9hT#!)6R|(v0qg`beHZ9s0GjzwBZt8Z`RRw`csUdm1u!A7D^)y-UR?XpCal zR2BeeI4`>2BC=b|^v~^$Hbd@aNzW(6Yc?w7hwtwbT{@j?CkiO|-1g6Y);TO`=Er`V zgo9F$4U@BI(Col06j11Si}duQ@ViqYCCkpfS@rVK{-2X%^zwFfFd8c6krxT+s=cE1 zkh*#abM>cnpU249+HX0Qvk=ejC$9mU1B6q4Zm4sX%omUcZIAk9Q#x|Kd7Pz$y>U4DHI^V1>u7UblcE+rJT_eriso6Pk z^z`lyF;c@XO4SwQiR;9qiax`GqtbGaP20Q>*>{=Rq*S;js^e#mZ+tO$9poDIPns_F zb2lIucgR+g$;-C)akWB4+m)C(`~8INMzk{;+hJ zFmQTCrr-8?s>Gn+ljCAt5SJoEMQcwCi9x>29xmrL#P3J{nJz(06*<@1#hpYl;@^$d z9W1wrd@hX&F?k%&(x7T(7})aeV=R*JhtagrE40>8kMw)cTW3Wf4L~_i#1V)o6Xf}` zF*5s=R(}=7m;TWGbW-R^~PH@jW6d%zPElzE5C(jHO#Z@V>nUm&p-O0{6u9W8Hye{ z_qX>P-R`F#<#Dh5J;%LuI!s1gAF$i@55`oJ*bk%F`~XmPn0Kka|U( z#a?|?jjO7?9N7g@TV6v_DmHChJl21`L850vEJ$mBdtXt>9t4rR+Zj%8$iPX`*sHS+%%~W zsN?bqW>mFq$ANkO9)B7Ote8RmeyslC_L5q^-4z#o_%1rP_{Mo-QRbDPD{1?oeD{r9 zjLB9SK99>TUj5Nxxlo>>=S*Q*5&ZaAnG^Fl_tmHz#gn)}CI39@zH5||_dvtWKK=Zk zjh+W-p8C7-gPkbF2H+*}T6uzyB&Hj2`0ag|BJQjv7j}tn{1XlH>juDd6m0Vh0s-JG=u;)+Z4<4&i`PMI3e^or zNo>!rfC@0+4kAcXz4L_e{9ci3NrY(u%lp*B9Jp@vP)jikc-ghidzM-f35b9!H?cFM zIlm<4v1oGA8|+Yw4)pa@`+Bt$RAc&uG7MU3585ZylRbA(>V$A8IYgJ_(ra+6Yh|ZU z2}Rh5NnIRg3pKdl`Za_y?Rhb7(MXld@AFJs?QYh-+#!`DWS5g;6WBYvG$~{=SyCFU zLK!n9gcH1;+(v5CHAphBH%0Rm?4v&Kwo}+Xt%x)`Ozv&)LX){RSMKW8@jFub?8x2JP<44RV7-=24%<3< z@?XS@0KT+!9emTQ%67(q(9a>1Ci&AKGBsg_g@vVjfbs7yH4}SX9H^Z)(No+kx85Mb zm*d>~f|BiRm;)pJySIx<83M&bdFgudZ7H|Gl{x0BkjLcJmgE-h?`VM4w={QpPBBnO2o45rFooqF87r z_A+7W8CX^fbZhI;o2yR9InzVhktMhxc#!;nN*NQ$I0s)zUF445l0xxds)YEIyM4i0GONnlp!NsFE?AbYU&^d(RZUIMgqo}lF)^{- zatr5aNS7omRYh;?^z8!Dn8MwY?%0}mb!mDK@!8b$dQ@hsFcCJVV40Sxm#tPfJRhSU zZezQ2owd4okY0&oAiOn^)XaL#eDH(wKc|V#h{lHZc>feRvQ_HLlToXSXaH(M;ZkL~ z5VE7_^w4;dIGlGs@U!2Z=dP8|6%m*ldGnH6k`7l;3m8r+=R*`iPMd8&Nqsq<4Hb@U} zj!15S%}Qd4x>P{0!b>e^316yK-0ykNX0^?;`1qy0>~+$Y1mQ>}V5|(tT<0($B4IpO zEuI|H3Zqd_9UMIA11Mhfdsz3*2#cS$Y94s*SkF?OP=?1wSEhZZ8O)D~p`j7u7!-Yq zJBde1MXJGiDwu>Nzf&41S2}&5&g7$0Y>;hAh>Di1Y2Wt*T@3n0OyMdpPj?>bE_d!Z zr)bC!@stj7bBAnq8#wC6S1f<&F^jy$$Ev~j^s`C8kf~(|CCfVOQ)O;c>NlNuX+YRv zWpPxDx!i9WmH%?`0F$a90CD8>&xa4FSxO4sR!E~}OpK#tiL>y-Ygc<_tP=~xPT|+6 zmX=$)0-uP+W5~`+!rPM4^XRg8oH-T+P~dd%3!NW}11i7Rr9a-+STNU}U?`t2(bpK( zIO2F%GpY_}X{l^2G#sg?meE@@zZ0EpvB1(S(+`?6@+}{4)`~1Id!pw`dWym!In9N5 z&&{m`naFC8cjdwclcW%Ytsl|-zWw>L5O3RErtkMoeexX@sFW`)2`ix0Z247IJnRZN z(M|P)t^H-0!3{pqEB;t?xHnPGH0#epxnga1^XHNktY1}ZBi1%KpPmfN`+J;P0fts4 zo@zN7er;|>A9_dOGUe;#MbVbdO~viFtZHbV?`PR21 zxW3O7%PZwvOTkwo(n{^1yOCoAVhnKremi1N({~zxjzdjD!=bcsb$z}!)rl&ixkbRD znekx#FP7)(HXo}UvY0Hczl`mw=HBc7mwNtnFpis@%3$U7=Hy^e2M*bNwBTFU4 z1uvK-oU#|EAHb+`SSbw_eG;pDztJVaHaO!&4Qf8f>cg})Y;k|<@7mtzaWhgR~0^N z<$UKKDXcxR>$YOv(ph!7`gKd!wyVMuZ|D zq-EAyzz0n>>3IM|KWk54UBThR&Oazo@s1^R)s7-#bz?u6(g%(9YKZg?(S785OfBH! zpNIic>FMj7S9$tY8aZg+?x3!7)T5LU$)Z!zmrL5w{N zq#rqDw{Mmo4$I6!4s^`A9)861d9Xg*xboL2YA|^^ywVsQ!xdsOIdAH5&T#sCsIFbk z_+%=K^sI!qW4@?4`-b^`&+O2>Ukf~t3bF8b#Agc_$%s6gSM$Gyzv z6;fu6P5M7{eRo{a+y8&vZrM^>R%&Y7)YQ`4sFj(eIZDwqM`rFVE<~iMEi)@~LvBTd z%q@=0967+f0LfHvAu0+g0>5{6{BGU*`JR8^ao%UV&Uv5nIm7`;Q4d@Lsg%$OzZ6}VSrs3l zJ(chyczV1so@PyayY;q=;Bi{wbolsgdaLK7%f?Chn`7VPR7bVsCV*~r!1$}JJznwg zSa?#cF5|$OhIV8Q6dS;-{N}sypQ93hbNK#ROvU@@M4Qz&K;HMCr_-oTr11T#>(rC~A_fDH(YZi@nXi z|4=49%{%$rN-q#m3mjA$LM18-9W~Wn(%n#j`R$-0#|ZOTX{F#cml{n@RiQL0OkdE8N)D(j9<)z2mf3SCu!eebU#u@u_pO$M;DQgd%h;9$Q6mE(HZ8lC)eTcYywNLTsgYDa8veNBA zC2AV*2O8SLS1oy0bWcp@S<=@AEBrGwbuJ7S4NNgaqrRVb@xhy@T8d3p^95pooE-09 z9_D2oJMEnUz6FFxn(WarLAJ$QHoKz!@{q>s#40!tU!03H(}SOy5V&>p*|`0ymZOS{ z4IM)mzdet3@snVx!R{Sn!RoL`0RSTRHyr3Z-!4*u!1?u_vO)Z@_X+8kK=u_j4 z!yGCXCt}~eee_JkX>{hm?|_T*#O!R45Bl2syPMXrabCrJu%qWX4;ZaA%ic$FmgpMC z^ogihcCw_%u1Urw4(P=!0SHOHPMH!2q`$%oUf%Eh5~9z=hY#Fhe1AQon7`Kbp{(K2w!OVE_etH|-4@#;rS_k0it5p6 zGLS+92>0kDz4Wb3&7ZCgq4zV9I-qOFwgc!#MJAku)w4{Er@{QC9>SUVtonnXyXqt1 z9J}^JEz@o8u``iRyc>+CtBsPLxX2HuUpadWEMVaQezL+}XjVy3nK25_T&8n{b$*C1 z3xFya9k#=jkaf2|Fn-_Ok4Y^}mh5{VKod|t7rRU$INY3e6>f^$DH==|%+5?{Hl67o+gn&8W3JP=w*FKssxN=QZT->4O#%(ZoX$mE zl^^eI5hP6ud_9zH*mtQnk2oKzQuQZT&L06xiFF|5#HW?RG7lH=%NLG z?jE`sF?}jHWmOVesN6V-+L@4hYKc&;Qa>aoa0YM}1rSCihT=cBeA%h&(r#9|fc~ow%c9dj@_4~GH)%vOL4_EUPU;KbWu=+Le!p4AJJ2UEG?1|-=iSOOh>LnYQx!ln08<3||Et-`>30-){o=tDZ8+K{l_ziQg(_ z#RtdDQpDvG9ycZTe;e;JQV(($s2%Ft$KczGnycwMtxcO=_IBVJZs^8x)#OX^ussg zMiND0huRO9;Rx=s+B%Pe0{Dn|mbMy5aW~~?uRhO6lexq)9RsfDvgmLoRbG}n`N8qn zt5*q+uF%y51jQo#$E3&F6kVMFXam`FU_WjC;c7o2XX(aS)$7ZB#k+d9GR=63UF6hU z6X={Pe3Vssto2g!cOdTg(`Z9f@ga_k%KitLN%3K`Q;XH7YE$1x z-dE&o-6>OeqlrSnM5mf$Rm4}#0J3ZoFhu{OZG(oW6M|2AV{aDS5vdAd2p$NL;vN3S ziT$ro=_8NUlOgxS!i0Tac{kE-de`wFk^Zqy9$p`xs;?AtWF-A!DuAl_E} zrLvXK=$9<-E0tk8UCm8Lb5Q+hSDF38JX>b?1Irs{28T61D;_@B)zy`2k@q^dsM8kb z=9WDt2S&db9K88fX7A%~-*$y^B6Bs#)|7Lp{g=)B4u1Y@m}ZgZR~m4{Q`h;mb$T&( zeba99qNCIOzR!k(l7T>N6^q{CmrtHNDv!~folU*%?1WXYI9(Xz*9)s}Q@cs;>1(jc zdd6l6KdwV;1rP! zqXd;;acj8-P!w$y+V&V{wE^+spTh-E>HP&Vweipr(r;m@>61;Cl6ghWwQDW7b96D< z$>WEuJwwQyhLbk(`%LCWI;WVl&_!);+(u8B_V;`bfzk-=8yJViWE}5&{bp4CWK`6^ z`SR{lm@c0lQ}w`!qpUe%3~D+rGY@W}z`Y?gH#Hrz8qP<_Yu8Wk3a9OI|-HJol7I=MU_WCUkn8w~)8;P~lMz-B9N?fS!t|Nl3^Um#7d5F3>Zgdc5L`5t{Tc2HAOvKi1y zTR*&nN|4m&Rh)uA$*bF*df=RfS#{9`4`P>%8U~sTN>P;N!;7S5&Y`r z?u{hZ=-2O9mojO`1CstA?pNrjU9Z`L8nDk@eS6Q2ZajDcw{!o}^G4)Zu>W-G4^W?& z>T;j6!dJ%`b*826NRzcxY*w5dq@40bpe7TCfYIYy8k_w5Sbv`7l z@*)(=JhOC>HRd}g6{Z)YK<^7t+L_)c-As8rY98U`QUte-STjm8vx5i;J0hJQkFisY2Q;L=aqca00T2pP^NA9KW z1ds+tH2~DXgUC%AHXN1`>h|pS$(;@oLkAW{1{8)D%Er}L#+jNIr7#y{2hi6F-^dNj zoU1O_#6*No{Qr7?HsN=|y>mWHrGRotrES0^R0wF=NFC(cV`+zDcQ1GNTF6JzfSy(( zLVdQ-b`myj*x;vg^!|9lnZLp6oNQt74gromBHPlmy7JEkzHYv9(-O&*#TGM1Rb}w{DD%Zvfm# zd%iPcnv{jB3Z!oX#u7f6mjjy|4K~enFMhWZ|AYKEVS;$BFaQ1@l)qBjv?NwKooBf0 zX^|xcw7e(>$z|CD9ZdPr=ClKJ#CL<-k5T`fb`nZ&*Z=m%ocxt_?|~-B_mjrYg$h8f zKW6gpbUSo}ud%widKM(`ch-gt9~M*o81Y{@@9zl#WSQ#z((-QwHXUozYPWYqdH+;% z-)tNW&Jn)*$E*Ci`g>slLEVd^Vl6h66!uRq8*KI4Pp=n@Lb*MWv;)N-H{OQO)|VZq*zPw!uhLV@5?=eIg`iOM{| ze~O-u2x{61f|%HUyeqmqJES%CfxfV?@Y_EH-XFH92@7g!h3x({iFAJc{CVHOgDvwZ zPku?;eOyB47!1OH^LG-S{!%jclWQbxAjSTYwg(iPe zC)K^Q{#ARWy_Nat)A>l+PhA0jE&QDRQ+q|7_U{H)ZvLlZ9ZRIckyJk(5Z&=A_@uOl z=X!11QP5cvT+fZb_SKPJyVQyUQuWKGZ4PIr(v z?-7e-TTRowpBqYM&Wj2E{tNjcgEyf(Vb?9RHz7V?Bg%rTa?lG|XbiY?$|qhQETtBg zta>>sg+V;#_pU5+jeR?So2DO${?1xgn3YA9oTmoLKHdALDF_?tqaui(OKI5X~f4B4HlH{@12RUwBVThnXVXy3!vD9a4Ywe^*kpDLKW6qUq@LfXi%8^wpHnVOELF=!(Vkwt zI!)n2RosK4M%D;XhS#ovXak9QE2atR;Bs=;WsJy-y4KnjB>k6N5jdhibR@DFrk0^A zs@}v}ZPVGoBJ<`LDFR?kI~qw-)&@@yq%1OhBt$TlCz6i_d7$0KJi&|o$>0TMIoNv| zRVn$N7BvdJcriU}X*NG82s7|pQ4s89OVX&r?=KQ!tNm* zB543-T93+d@=R8Il*}A9k|t^IG4eh5UyLQNzTJ^1iln`l^9Z&73Vz%gZ(iy65ZvdP zi4No4vC6bsr@}?*xcjxvE81qgw$+83_vPTuf^-S<>z!$O&!VIIA_c`u!dDGxo~mw) z5x8N9PK3+C!nWZuZ(MqMuyd*@d0p#Er9DJKK-E`uC{P*8>6~2w%n4D8t9PRywrJ2qAz3M ze6-}4W7K4hTEBPouuk`d21>YrR>g?&z$4 zI0ahx*<_WrZ|a)n7jNm>X? zvr-$uzUm2yFb3CDL>%UEhEHef=3P!b=>aEdPY)nI7{)1JkLnxc-(1*^ww?(ilh%A{ z&aO+4Bbx1GEi&IS7nEepYpc)$RVK^gWxY?N5dFJdla-#oc+q=rqvA}}$--26e)gU$ zx%|78k+QZW)zhjK7Ycpr9PIaIPfY0;|FU{>s=VX~aJXykwL~RowsYG%>nJ?_`C`v} zbrSoa7D%!CL#=ECuZ&rg51Ckfu2|z^^+ZZ1eC4!&uw;o`OL92~7CJ}Q)|+UxGK&*4 z?&S}~&!!D_jP^7%l;?qG`m4&iqCE!3RgDwQSWiufM)~nan;m*5tCB719CwiUpPX#U zydK|pZGO&sI8X&Qs>-RHF2z_4%yIYCdgHX;a&EHytLQH|eV;1Z%}v=K<@%M`S;9JN zhG4WBIB!vKsnLoer=W8|MkbFH#=zC3qWiqkwHQ7*0H)TWtp3j<4HVtJZCe)*BEJ|B zI;RY;0;Kl>PjSjyX)!gQ_WAXaQK;jA10oo6a}1+~8ooY#3u<2;0RiXRI{=7#7HLNt z33+!Wk~~P`Cq-lfJwkmfGvhI&L7{q-8wD8WW@jAl{rg>Dbnm574+kt#8)X$9-qaeb z3{uluPWGuZg3#n!f)hs@D3;Nh#zE{F@6ynP?t+-)dJdpK&7Lkznyil^%BdgkjR;Hp9d1@{CA#@J(5&ls@NMp5A9O?kv}Az!vH(qv0tG# zmA_U#hxqmt)Kv>NkWlwKLnzbL6(r=N4j~WZSs94vgpHv=Ua*=iYLiq;es_(xs8E z8zV?N3D`Wu!{Lxg^yFmCkOGs;=7Xb$jp_HoY53G39rhRkaecxgI*3q5bni$CVmSe* zE7#w+l;_=us}fsozUfV>xaugbYC~lh#mif9UfO{%;S$_4-w)Vl{kkvp{@j<;`Qf&B zY;S8^KJ$_2HA!uHN_TwK`Z*9*X)Wpn1nGcCmF3R+zr^a+NtD%CFEO_jvooEuiQJ_gzC~r26B$Oy8(k$;GA;0>RoVx_p1&*41?Tq< zkphh5RNMmJFjZ8R=WtRB|6Ykd_MEZHnZ)?TFe0;IzPf34L3U{F0CF#d$3UEN!)&w?No?LVIO4At>j|2-=Qr42%9!;sHJ6tg(PAWT-9 z&ezxgZH}qXO828 zLXjxk(I^^27`!|*rXy1&E@@q2U_9RND?FlTiZ!5X{{H^7 zbxp{=2M|F5U;@4qMeFdXFs6hnUzCv<5*Vd^prhctXv4P!8D(@$nelOGtK!5wiJ*JF zbh`8d?JDXl1oM4tZwhfYDth&10fczt#trD=FXxI~Wc#(VXU@C=#Hgz8k+dwMJc|Jq zAVHj|tmP!N)+m{skb+%#oDcqbGbjSG1H4L4PH_m?D2kKMk}W{yX#s@FMi5tGv3m6vtc`1rmEPa_=6=YR7Yz(d&m0t@Q zchmAxvuho#)F7-88R5BNRL;4I>vhp$p2AXy_DV{~@G zimy8p8_FEs#nE(na`L@~K0>KY;=UFDt_;$fFQNuibm5;}@dZNF2%OCJ*O# zmrGGSZrkqyPA&*=d9_@&F}5hB2FaRdzbF>&`-teT&5lL)R+tckCDU_r?W_mb4Nr{J zKx*ED71zCq+F4?yN$4A;m+@@Ote2iQu<$MiO{-XeUB$ze04ptr^*^^k$2_U@qO;w= z5*5@{;qL@+tHXpc{qS3Cg@`G-0tC{X;P1sLWR$=>0Z)Qf&QuGK=($*1EGr z`dUhQvafp~VA*v8{CHxPR_}Y+F)kK*9IA=s2O`PA~=jm5$LkX_aSzR*nrW7d? z2lz|OD|Rq%r{$?0)4aSPf*!BV!9asDW)n0=(vOY3KBS}O(d)*k-q#{9L8RMG>ASnEkCY79l3*Eav_(JbuQ-9UWT-4$h zuD9grgAGj3Td7DI0NK(72xX-{WX;D>Q8V+S4K#Q)0DzGH{{3M<=(3cNk#U9VoRzr> zgmWip`QFbqX8NPCp+VBp z9ESl!tujKQx!|SdFK)}zDoNTfOmz*s)GrP7AjKPyslWIAay_3^a*3oxL_|P!fz#?s zVlj2_g0p=>-6sct(2iodYbcVlcqbHjAQAj6N=V`(!bNhJ8{j&!@Ak-6zop5u_Hq9V z9{9(*|4#l^_U0Y?fh!4V=ZOwO;uw`s{mnMS-dDGQNs0pm-(PmpB^N&+=wr583Q1!h zMACHr_4uvBb|Im6b#*}99dXhP%)&JEnN2vj8T2d& z^}#a=ZOk@PabP2WLw_c1v8QQrXd*&0;Hl94e`_rkja)({e<(TDFHSVhgO1AUhbSyv z?{jzJEsR3J4k3LD!EZQtdDfxDc;H6y%Lv{&C^bNYh{%V)EcpabYOnX@M1!3fMQEOB z;d#_twxPK}J~LfoIPP88S0C|z(cG%|I5P4hU~io0n+GlURn_jel|SPemN(nmg}xg( z*+W)*?$JZG7=gWLy(XR^z3%F^P^+yO{Jop3`X4fYm(;Kb$R-$f{-|D3v1bm_PV~K) za#P-yFBI^d&%RKv%x9+J8N>r?lQ_Yd(jzjXuTy70%KvFcDau=T>(<+$5othc$VAaw z(o!1fd^EjbF0}(DjwLx`aPXAKMtY!h= zobDsl(47RueZXpH{!i_l1PHKC{>Ot07cLws)0RF9BEbKpMmkluZ~y-6d-pZ~`m<-F z01(K3I_>NcSR!d>B59Q||56i8!~=`*e?I`g-~LdCtzZ0~#N3Uf6#(N0 z1|s#JcMULQKvWa>pY`?us<2jO%9sCn$+w2$=l_p-cmGnB)tUXzE{hgHQU9|)fKDEd z_kX?!;Nbp0hv7fSI@I)k(D~OUN*sk+1g5J3J^TOXb-fM3Gjk_@A6-AnK7Q&n$Yw@u z?mMp~twW17Ji3ZWxHKpHk5}KKezGX5%;U8 zGF_QUVnL+&J}%#{UJgilZ7wV;&;8Jk5q@1~4UOnrH5jhfGt_#8>D?4{2#<1~GQ1j; znfls;N4GurPhe8w=Brno zv%gpfPF+!oq#fA~Xi=xfvQNi9T=n!wm9w+AfQS{?t9dZ3F()rvKEP*}W)Qnq$GwJ?>kI z`$77@Xdjd#Xn1M;-SmbH?>}jVEmd69YuIsOMTKPpgR7U60u*1 z_^`M>=>Pb!_qPx9f5^D^TkRq!A?}BH>#y`j&hg`Kf604Lvj}8<=&u4bD+iNEWEW4v_lr-l}-_ZvvqmXI65l3fFP9|q|Wu1@pTYz0?Z95(6f zG>dU$T9l>YYDLR0yH6QZVCQ4%x@448!DJobPBL~C-MGu4R8rNbTo6@gaES2oCK{Xu zd;5KsU=Ug?;P*9>W^cc@_k9FB@=rtUkD=L;>v}-hyLXi5t%?`-t;dOa@IEpq?BSi- z3u&m&Aq)b};kJ}EW~|wCa^>k>7QyK3A*BIFEMXx;2lozVTpfI9 zOgi-1j!(5V7>Y&TjLHQRv~XZi@a*=jFW)s+%ZGidS@jr(eQY8SGc$V|;+;2#^?NRA zq^N(L?J0!yM;@Cqww`!3#p&$9;MQxb>*RMO5W7{ zQ9QdUu%W!2c}z;?eM57>=*YqgwUY@k#@k70k>rzZpC1$&>IR-84!~A@-+10qGft_q z7Dy!|atO8n%vG|wLO^o+VXyx0euAq3NPFJhciadKi3iEv*88dru!!RG*a#g1-4_l{ zR$a`sV39do4qZ{%l5rze!G4+f`1`_0H0v=pLsB*^1<}O;L-Xeskdz z#b!*W9IB(s)1keC8sv(In4#&fb$iyV>)o049L!4K+u{edGuMHz!d4B5r=lqavqpVU zx&m&K@sLs#rK0n-FZp{LnA`RqL^_BxFqibAN+GU~T<|GbyN^Y5i#JfZ@3{(0^$kQ$ zC;QgL4?MbxXKj|FpAUN(*_Ev78=H*FX{xI9oIsbd#sqqVJ)2){HBO}#n(A^cD%Px{ zw`lXzN8pT2(ULRS-(CqC8f%62?|8Y2tglZC2*EHiVs!eL4;^{|vr1Pvh5LT0R=EWF zd{y`@Ny_rL#tCqn6(%-^77AZz9AIe*>@Glu% z6;xbpQFM#pvg?;JDqSLC*t&!Xj}h-@pMy_QhYma8lgQ{{@APM&skEcsO&-p<#T^Hr zHL;u@SS&8IKO#OM1rtdKo9POnxnKNb5}jCX>|iR?^D0(uIhLkLHwJ}XZ%`@T zMIXq!AiMM^XS+5P)ATS~W=Jwv%hmVe%6N=(fIE$9JEE7WvtX{2V^Xo*Dmlm9JM?0| z``PXk?pRcwI5>@STu`*=(r%bJ-7{@<*n;Mnv~#iVm{j|~OKlD_aNaYLCNYANi*F`b zRMbR0)15+QIW46bIlt+#Kc@3P7J}HoRg)4$+Z~Hr<_h!s;Oz3)d{6y2Sv$)UMse2{ z_GDS4Uz#dFsJRbcQxt~k*B@~V<`BvYsZnfHMsu!P*Bx2g%qNj`t7U+|+e&synX6&z z#06upZy|72l~G*BO2|||Ra&FG*edbViQ`FnDiXSt-ngade5?k_Gw=0@Hg&`NOJB|7 zm5F_8{lruTEgN36ps`l(SO#Bs?HuOQ$wY&5(rVz;wRXC^ef3+e4%G>8tG^C-YpS>} zS~NCFf292B`fzV__3J3~_LU`fIe$}bgpzdo2Iuh#5swiSMkLLpk&NKkW~9?e76rvo z+qI`K^>x@XIeA5DSjW9gX9r&qILQuK4ABL#t*gvSVa7MUssamXv0IR^*Ia07nXmA! z?V;1s*xOe|9~ViavRGwR^o=NgS$>N;9rwE&o7sc5Q{EuH)@G3nBJ zMU`146M2Da{Zh)RcLk-3r5wK{oZReXUhH}@@A|-kM;GLQ&2nn4midNPcoE{Z#+gp6T_glZuy7m^$Cz{v`zZmaItH@6{Iz z0?{8r>5RpCkVr6Vh>C}J3`*rM)p9gzXrV#9vO%jZv2$=p*)%KZaJ)1LOo0l)&mFn6 z@O8>+DfU4}a@_jBCG=>|UhsIou1Gb#_2ygmh>=MB<5T#0b1-+x-)2wK{Q9CG!5WJ6 zjiR}Tt9p?NOj2>^i;thlGHkA%FpM+ry3+`*3e-^X2(OW~L{F^a9!Z?G%Dt)I_$&@^ zhq5_!*lz)91xuD1$!LC2`jCF7(P;9aux{66mlL?$2TluX9ZahdMOtC653DAA#0TxS z%4$^^w@G{E(T^^4;IWfmvvS-#SAv6Pd#wSpR)B)lus!C+ga_I#rPVyeJ^0FIV(etrrS>Jk-PMlSJ#YOHh69! zLN!2Y4I7JXil&v#Ek>vOJxC$xwwhtNeLIv(;HEcTM&*b$@b*eOS6@Ax1Dw7+Hj;#@ zg369~V3%^kOeHRZfLwFmJL>dB8pjS@T!X-mWXh7OEoysyZ#9`xj;`J78g+)-9X>WTLQPF4O$F9nI)4<$_>Ztpsk>9bMdA&yuT-z2J7cu?0fq9HJ z=)m(pbdc1P>rQfbempLf#4%&hOz-&X-m zJ$oaC>*vsYf&Stu5DidRH523z=jWq0#6l*GmoF=t#`0ShrqDR!fuRxj0Uus6;5KkU0tv*bTbGXn zC)?USLkGl(1s{a>bQI1WX_GcdLIWCAkLi-BHOSJU<$<1-6z+Z)WeBJyrYS-3oMYEG~-SI(yhxr9YHK+F3K6%-J zXJGoS8nd*EKh(q8DFEmbn8Q%@9@Zo-C);hFqmOwCZ=>Xg=;GE$Ua-g8{r{JOab2{&|wSK#fdGb04`ymtDFj49hcPQYPo7`~GR5MbOgCId!0E8RsDRDHYCS!u|-o!(Rxd6PT>#b6+$dgpk+)P>1w0TS^ zda%RDSu1*A@s;-Q2jvU5()SR=`V-ocy$8ohrZeTU5RC5d2+xfcCPZu4xW+5dn`S*l zNs8+jC1p|d6TeW;y%DOSVk@Xln7SFMLE6LmW_)C3d0imgmsgl)f9$D}^! ziJ~=XLUY|*P5Ta>Y1EP8dvqQuRkIe3sQ467Q$uptN$6NkP-}j~7{R_O6pe9xRjhmI zOXeVVzMZbhxpnE<$ImJ7ww-r6COSr^^gcT&vo_OAAu#@Qr7J1~&nA5*8Pwl$qU(r- zFTx~s!g!9X*f)getu&4OcguqGN-n0t6`Ld_`4z#9&a+G^Qvk0*Xc-RaV8XF*pG2B| z_+r{2!F6nIS}Fvl_nDpd`IGxJ-}s(nIJ|1Rt_FTY(M5lCVXV0{K<|Y|-DC!Z=GAq7 z>BZ=3{n9Zp9go7ua49#+lQ&krVP922AuR1;Yzo6O{&Caf+L2m(#9Z2b!kXI*CX)%0 zGC4IeL>1K9Q-X&Us>V33V7P`#2BI2HsHa2}rjx$F3X)Zw`li7)DAsM%a&WCZ`DN7x zqK#P1-bX9_T-^_CAIhvET(cOBB9G(lMv*0%kYh|89qw93Y!+_R&M+)$npA>IL%!a?gngiAY$tx00#>}syw{dV>KajpgvYFzT-AO>UmN@WW{Hz~ z77cjj*7z`&g+YV;mA+!cxM{tpnmpk0uSpCV??pUPr`%;D4|3PruEd?X-fE9Ow8kwR zh#CUVxQ@h7^<9UafvcDf?7`8xnXitViiN$ijx)**70Tr@XJxGKKYuq_)Qs|zM77G& zY=$r|_zQJ)^AG;KP~H1Y7vykAtS}cY6M!nco_uW2EMuk#Ka_cWq<*bIdYqIUn0$q%?!HtpsW!+kh5bJ8jKPzY8Q&ahgd70*|~K%gTL;Z zXH)Y%e-Bx@CF^-Sk5MYR=G88XORCVXbEiol8Aj@-%2#hK&00mE`J{7|poy&Rnv+A! z^{NiaX<-_q6h?~(jhO5_TS(lKPas*&j5sP!bUOjZTZ~n4e9jD6*V1j7db4KjKm$Zs zeZ2=-5Ee0)ll>5$LKnmCv6G&(q^SyVo%7y3p8YDenBXm@?VB&QoQ&3lOXu~Pk6Bc| z$?-XO&R7k^D3p}KzmZ{jcKEsDTdxRmDN1A>8iHC*IUa8^26p~rvCQ*IyG$6){2ewp zv0j8qc0?oQ?fU}fP!8FTr*E%yF?lZ$z88}uZ1_}w(@5|x;7S>a_zqO%I$SLxRU$N%QK{tweUt>!(sxe5)-*ze;{7ED zlmUC|StVjcc!Pb7a#-AOwT6+%+O;B1(v0$(Kt2Xn5K#pjQ& z57n|SaYxv$;1^NL)y2$v?pvAp7*UOw@>{PvNI-L>L8I4u#O?!)J8*f_?bo_q=W9 zs!b@1(>qlTjvwQf;`Z%MEx>A98SVy2=j6p z@TKhBOlB#@u4bt&F_{jTTf&~jPPmwHyPxQLxR@Ax1G(CTne=jf+k3S#EZq_z98<*0 zM71jx5%mUzsRMZqu3jZrHZ6ijTPI^6eb`s=6Of%|~5Im?)Vutu39ORvZ%_ zF>J1{WpmZ18E`${aP`s!3uta0bC~jZh=vSxeHlDw+)-5xm7EKR@OhcAr+|egJZj}; z4{}Qjm?RHcINdjpuhOd~_57t#U%9znZ^J6o68_DC!^|>cJgs6!TZaumYcV@w ziXB3ZM>RkoVtW}`TFdJp8`pGQb!DbVh{vfYXoxxjF;+fdQwl{XnHIeub$CddA`71G zl~w`~Oo5etn01Jjr5Q902)?qcSF?(Bpi8mWar{NQXqWfvjqwh7*Af@eH;a-ydUGe% zIi4^bW=}b%eYDbA)V?lalhz9p-M|&lKx(?F4%Zf(Z&@BMV^>zK{b=3EG_bpNu!4E$ z&RBxB?%Jmdig}m5D&e^|ugm**71Fhog!BT|mKje+^3(Jver1gXKG-=gLTQqQ=gS*C zLP)lbT1>3yFdMXJw87Tw!N z^E;08?!6)8IWxx{_OTq$h!>wIRWb#NhGfo^HMV6vj6$>LyM1+ldwHKvZGC_{Bkl4- z_`ZFyp?JOSr2EVlBG;=pDh`#PX-t4=%z!qQU_mT*$^UxZw=#I%B8ISTk;Be}2oXB& zttew}gBfbp3iPJwqzLnrHhfo&p6k~v;_rQQ<*TW>_U~o!!4ob(sKlds;`gOa7}Ek% zoC7eM9x^_-6b8d!5cpdYDfDU9*Wa|&!?SHAC6nQyJ`rkS@#q1BHQGkI1NuliFashL z7>=lw3@$5l!CqyNKC^{V)ATREW}Vz47@F=~LZz^vcRpeDx3X}d0Q`lp_9E3wo7Zn! zvu1Y>K=4)HSyIUzqk8v86HLvI(84^H6Wp-uuOYM<>Vcg6FK;B#Q=}2aCTuz9LM`Bk ztDB}v^SaIz8ao#;lFh`t6Cz4ttq}IvM2Gq7&yUo4tbfGX=rS@6@NpK&!6#Enr&X95 z=T6+tZ`BV? zLn_a{BBI-Z@!=!^*%jzpE;5WhmJfI;5sa;#vE`J{!k`S9R8oN;4N-5Qj88i*1CGWDmF! zTXpHq0#Ah1IZ(RmNg@%-Z0k`OLgd4Rs8JiksPonRpWGMitI0x94;#jo<~ub!`VdKy z2&b3R?BvWNb(jwZ8#K7a5|mY+-dCvoLmYLi&`@gfXr=ZIy}Rj>;gEOmXt8;+5)S1o zh)*3L#^PsS#rMr;$E=F-s(f-IAof=i)|Qtdd**^Yfya?H@g$E|-Fk%L1cC|qILt^KLko zLfeLj$}l`grKa{V*#$T_C=ke^iE%}~S?PUNU%l&tVQy??MVzvxbxe(xslH}t?sdx5wV{xVi)GSo zPIuGac@5@|ILLq8l-O+=t>qc&BlL8(=BQy@nDN`TF`wy$dfN_L5ock9_{U=x-lPyz z5RO%bK}#hKlPLgCqrEW~o{ANw5+S$LX4R}6t_hVz?H&0jclG(_JLi@VZD2J0W*mLm zYaqkN8aO#lE9JZBir1M;AeTPqu@!VrcPY`7sJrTuGxn&Qy`5~AI3))6$jxOjmB zcY4!JKQdgmWP^R?;tXkCu$du65m~e4@hUNu)}>YIZt*@5BSaw|o27*GyywA#dT)Fo zwJxy`itw6_nX?uQ?Lu3ysq%NIJ@`3+xW87*nq8}DE-{ig28vi1VjW;6By4nIDE<~W zvtwi}_{Njp9Z+Svw?1 zK+T&C5H_j!(j_ZN%p5Vngo39$;z!H9C|=6eAXdF4w*7-muo2DrT z(zY+Joy>TOuiaIqJhrOrz957f9;nWnKpxQ<{-`xHqQ=TC$FXN(%av+f^=L3&9#H5~uSFbeUQzpb$($N;$8oeuQN# zeoKG5pJsmqF-?x zOFfJVn3_ct%t3zejSuMM#Da@>Rb5_U-BBmg8jTZQp%L&2k<4f0niLM+{9y_lo>z=b z)*DKuv0AK?dj|_Hg&-Yb>a?3{ugf#5CusmorqBVzn@WC4+xpC2ggo((r&VJz>Y@FP z2IUcp#rcC|R$3jLwBq}o#0|OP=9ci=rcHkuDuM4-qpkBXk`d!)gJwpI>~PELx7wYh zJcuTJp1nnS-vO7_4#bBozblk&8s9FGkM=0_Jm-n8K7`FwHC2aH1VW&YmjRF`K?MK3 zvxhaA`pbX>rs29;JGfF8-lc+4rtPhxAz}0*gB{OSqNIF6>eC{q6OxGZhk%Q}XSr*~ z@bXY?d?mJyGbcb}I$IRnlp(KT5i_6a^)z@#zLA{87RD5R=p6wUUz#m%ZpX-v$Vwo6 z^N>U)>zz)%Ka@28JKgwN`KBkO#62Zmkd$}6XTPfiS!V(|*mutYmtGmcLMjtSdV1kXh@_&F!DkW}=uDlUGBunO}d?N7?E_TUJRs9Fj$%d_35(4*ibJ zrVd3@wTDTV*9yH|J(5Yo>-ck!IOI?h07zL1bFDQDo>vE$S#24|N;F@@7wVYw?lE1P zzSvBsxE7KNZMZq;yEKv@ftJK9FBf$p5P9n@afXH(fdTUgMRrtBEtX$pp)VO?UIc_b zQWpifV_FnLV|dH`wq)%D8ZIoady9RndlOigAo{ z7mBvITgP{hq}tEG2T`6DERzs`k-AeZ`K`gr&V`mwgS7fEP3yz3hRp)ktzJ|1(=B3i zYL-TP+ujBar{OxBzSo52Ooxa`mschSe(Ssrt2^lOcrju17MORx2+u33M@Ff8IwmnI zf>t^yIxo%lKjzcb_N3^57)$Q=zMBi>&*dxAr%CaA+{*Z;bDY>6O^SmaQTIb?bsB^Y z=!({jVEXYoAYB)MaNRW@i&2HWeXg@>1u?EVNaCFvffP9%S@KtN)Ow}t%ts)&_@XIv z*pc)Ce4QmIkQlGxFs2GCMh7ojlp^gH?RGn|H$xnn7`mB+ng)vs&c5h`s;@$@n3OU^ zGH2dllXPBp&)8C>m06RxUJAjRv^%GtdTKY`(@>8F6`26BV_fKnX8LWu6AkKRqTxrK zCmlL6c7j%E@p;v@En;tpDrl~x%_W@j_y{bBelIWh9j@U`kr zcl9thGAED7&x}zcIw}}1QKiFSPIq6!qgQ%8?fN4U5BTnHq0f_$TEnnZ?er!IJy-V` zn?84j6U_`i zsN!T^cI?t2f0ngFopAv4i}5m&+5b%5UqkczJ*fOn+XGpV@RNBhT&I0i+6u&4-`eJ+ z5858H>+Q-Mk~Y^x_U!U}9(UcXX&0{pwV$XK+dHLlq4s&1Gu?L-uS#LtG$orF{y(z5 zJRa)r`@fY+c(*ER+LS^FVTMYj2o;jO$Zp0mmKobrib`m+Gn4G=5XL$)p-7go53`WO z*oI-4F$VK{`Mlqs_ou$U`Ewqx*L~f4&bjxTdzR-}O3khMr8fx0AXMuqj->pjW$2U) zjaMmq90+v49XqbpDtFEX=)82RMW3369`!j)Y;Ftvh6oGMR=WwtTRRMIi$~j4V%}?% zZi{?+YNODUdsUct#=_#OL7#aY{BU*Ee*CEu2J!yImvY3o5Sz(uH4iu+;eQ*BXYEfPeUZwz6mQHsowAr&so(E4GD#c!lZ~IlAgpI zXapT_OjXF#cCr&2n^O9ifY{u4m*c#fWwQP{np~j|K)OCMNH`6|PR2Xg)rUh;Ft(LC z_X27DmEYqF?#J%`p&AN=%AD-~P9nK@NfM>BGpg%T=OG%W_hEZt8Ljv-W-u_>U1y}& zeY~?ZwkD4`3YG6}(K}K5un=2J4Xw++&^Fwf*hfkO&uD&?D}G*cT*$mlXS&q8Q3vfGNci&2tdvFrbg9w!u)Xo1 zt3@>c8B`hh)mYoXqB)smf>b+wUCwx7W*B)=#*FWkko%Mc_EiPV-H%x4U?|hmX(ekj z4`L3tVFnLn$b;qby=!NHh=Sbkq5HFuZQISp11p@jXCJOglzl;}d-b3zn#gl)kG}_C zdAY-N+{dq(>u7?8#n3@9dQVc0?Ut<6gK2}Xq|i2$%YY3*lg?Do%PzRCABa(Z++mLJ&&rEv*kGrje-D{qc&~f>RcHc3A46<##BU_ zYYYdr6-HYYa8)Dw#)p$AXoJGTn;JyKPVBu6}lHw<+Tl zhPFyyI-+&;Kn*;Pc^L4(xTK`3ntekkhJjC<2Pz{c+c@y4))VZCOqw8TX4lAS$%hWA zsH(-)-1*j?r7WUA@C2I$<#fGb;W>QYH|t<{O+R!faN+v)7vd5ndR;dJl_p1~0J>`E zTAXaBxSGgzEZwme?>N-F)WO&fih|A8n zyD9bakr@_Rx@fBZ9CDmYs&12AoE90Y-+5hX(#CKi1;? zjZM%1iS8738slc&L)aAsLDcZYNy+NCk&i%AbTjjUJ}vrp80*p3rV|5=H3{;zm-!*QIp!uu>CAh0UEN#4QOa{4X30(dfVIlqBeH$h34i&yvGVM3lD0eMdUcOQrt#uWLE ziPP>DL(td*K{`6dHbaSa7w~w$V2@raHI`oClvut5aii9w26a@osYLn|5BS&Z3S@RB zL*@h2?8mD~77%g{C31NnE>xzfsGYsm#tuX(jZ$kctN5wZ9LV{t&&DJi{TZsJp8$qE zs^5LSZE1)(^RV6Suzv@|_cNF5h119lsZ2$)sH=7reX57IzH!0tEY~BzgiG2VywFCT zA1=;r0}4#vp*8g0eEAhl#>?G!Tn}V>owJcK66phi)!LjFn3tLyi>OOOR@Df5#Yp;L z-m~i1jL|z4f>5T$5y2Peb zdMkeUy!N)#Qk6TpL~szCIrAwkxWbLLc zoXLG<$8rP)>nHMLXMJ3dFucpz%*Y(Y-P z9^Qe}&>Sds2&@kF!yen3su=?L8DgS(oCpj`A^48kRY;%zieImrK75?`h~dkj#79RC z2Y~0VK63C$YM9&QTsB(rmGkVrB>fxbFcM1qy327j zH#v@SQVCVnBH0>N&a%LkgOS3jX9S<`tNb~%OELSowdA9p7jJL#spHcn zBR=FlTH9BPF^Q#zZ=2~=(zz`)EvNQmH+^#@(Xo;B)iB;!24!z^MZvnF-4wn~ z_Mo-P9MIfNHtlW0EebY;%;bt2JNzN07J6wqMa9X4PXm+IPR~uloSW@9I|? zb-{`Sl}0o!a(`q&x%BSo5LlZ|-SWf5Mey7+s_B> zmeod5VK$i*E#pqLqNd!-@Ak+$&@UnDDecef%Iix&&*aKudIimA6&|V z5jm-aMqPQb8Q=&`of@qIosA|cPh*F-Q)JT8>D9QM4G}q_cvU@(L6MBa-o6_%)+>oE+?u2rXfO>M=d4W!u3 z)Q#FH?e2omPS!9rH6tnhSY}9I$j4+Pc(|z5(7sI}cbHKZ3#y~V1~NgVIEs(n;% zPIn8ndd(GW2^2B}^CD`Rhx#4g9##L6rVgqkczKm1eR^1i3Uzs@sT)x}#CB@0Sl5EB zgW1^xyfV)rl-#~+b?sdwivD0^H)X?w2h6{DIn>?U!K8pNNN_q)h<8sSemIC?EAMk7 z$+MAlVzZ(7*vkU*PiB@t*0$a*q1bOrSC#xvEgk~8eybbbPrLJV zon4;HPM%j*+wXP$=Ju)Jf{V0t`OY>{I}Pme^=_Z5ror|pM8OAc5+e>uzv9&${ORLQ zk@3(|9@=I;7vDn}6ZG--{;UFBjpGGk5jE12VuRke7H^|_<){3nT^u54WN|{V*gf;rrPSd=*8H<#s=K^Q$sDz%x9XWe9E;+V4 zM=!qm2o)>V8qtCi6v+1swKuY>l5O3wDF8|CBoCck=t=xbtbfc&gfY_qYp4$?K3NXOdQ^esx1jK`B=j*|Mo0wySu1=`a~HnXEM!R%=8r?xc9J^h2%k%6t~FP^#{wl4urJWuuzq?4p|v~(WMJ8slyOO{ zUN+sY*7qQa2*W5v6YRW!+`h&K_0VlE#D>t>iB&fNZvHaN^S3CAa)WP8`T15Z5mjB zqfRDt<}|66Yomx~e|$h^;m7*50UQI$NWWoRvFm%1V=TB>SVDaqG7t-%pXw|%y#`5<-_|}}LU2$ya-5SwJ z3yHM|!IZh8PLH*OTq}yQ{J>}ZO8>(JPHzCPz1KP|6RPmMU5QS%{m$jmybzT1if6G9 zvU?Dz%WRR1D~E-XSpE9QZSE%IThxH5rg0f#{}FqV{nk%uWz+}}p$12P^J!&EKyIr& znAy15QjKznq8Cvb(*O`oWuu7N+B5qqgK_79fYIXqeYPem+4gOH@tzToN}i`0RFI-gTdLO2@_E77V6<{I|~;j%13d=8nwcU z`c@%Xs*VeQ4*{Oe0n<&z67jfC{N4%8HjndeWGK8_ak-s2h;W6>#Qf zic;3|yk0B1|2%_9(641_@KR31Oyf=H)hSZ5NU!96&p{Ya`x%M~ybTYXYDR2T=)m6PLXtP#De!Kqb)wuJgp)ak*sw zsqsO>is^}1UBPOnp8syG7bDz{%tJ-@By#kJ7D!cJt*Mdgfpr>sW0aM=>-+4*P{aKX z>V#(c_2&XTnGSmD5{tzYya}tp!B3y~LVloA|AcjP_sjun*7+-i!gx~37TXOWF#G`$o3Me zy|oZ8{jU7{QT5ha4U^ymu?SDnn1X(_Y+_F%j29dLr59Mg3eYPTO~I0-!g~?1kdlDj zb_n?Xrk8)R@Ge8A%nzrju2y4mr)WrZY5M67 zMvSf>+2%fb;Z&STGau7ZU^B-e6adDgrej;GzkJU(iv!6qX8aok{(+d@l9i0dhHa&~ zsR!?G+!PRvNa+KGhHax~G$iwwM!|j;AXee;%R}lrB-VZn53s1S6m)1pjb_mtL(}q- zL_jT<*pp*g0_kJF46BMlzf{*!_N)~?SldcS_zE0Y~L%n58H)3Xyo-QoBXzLXUKRQLQM` z;*6w_TBgY{cW$s&xI^gHa#AXVt;Ns|PRp;9Qoy)GoI0m6`+ho?hEmWLoyZp1M_r#< zSld$R6`Lvxiq*hu-(&r=BB&LA_CXCTApp)tttCwCWhMSJ3HFGRI1sT3tYN7DfPLHt z>`g3g;ebU7y}S{t8v<>_R$fr{&tOLf7uTFl4ekR&ug|#jA<#`R?z7SPALXad!SJZ~!xxak>4Pd&2@F zPZk<5(%v-+N_A1(--h)1M>zF1->`ci6l5 zjj}G^{h9urx%%zRsQcwSUsOV( zrm*K*IuFPX`Ymr)qgUJV)w+1*S!)+?+OE|U(Hx$w#63!SRze~c*Et}W^5IM7402c+ zqjy8UcZ$9#&P&C>@G_RJOeF&T?`D`$uN`=Cd_N2nnHMCedq9o1E)R=oKLBdOINY{gB3J;&DoSq-*xyf5He=H}Mf8GuyUNt?Bk5CSYKLz%XK3{6SRpr1|eY1u|$O#Khl zcULAB4jyaw=vjH?QHpDseq;BJ%3b;_VM^OC2mSu0oKZo_C0=`)|6X%gh&11I1O{TT z(-eW^Vci?(*xg-VFXKKbTe(^{-I#|~DBQ^qHITp6QwNjXw|}wM_%E@1A{!*y^JKfw z4x-XiCRI_}w_VE?+1M_B`(rt@mJ!gNsHtrt7-s%O9L%8#9Uehk{IPIWX^49QxQUke z4ZbXA)@7nX77jDWIE|&N!?YLgHOe3See;6@UIaSGWUeS_o}0<#r`7KDLpJY(cEGgt zOD*(d5NgcZN#n=PhmHjR{tt6CyThY+FCYk_RKtj)E)w2VsENW*6ynrOCpHale+pxz zXY;x2bO7Hi_!@b*5_eXJ%1YREo^4)$KIp+&@7;9HoxT~oHr#tZ=YHeJN|#dG_8xVc zShCMWQG%xV_NnFE=a%$$fb(JK-QA|#iy0%(0cxvOEUE6bD+i{%T0%(Mw>Fp#eI6tM z8b1lrO-%eKrkge$v5&3N%I}o>wp~H}M=4Iu3+_^ra)!(^ zQz0M61t6sL@YB!>p24Gy8MKqNTnGb1m5Zda;h0y?yUdkhRhqpMrDwHGJg~E{^pv%; zWPx)sqP8PX+N0W4R$_L+WMA0^EEZo`T835ECGWP_Q#K=X8e~H7`B7vaKeZmwjrc! zPS9k>?Z;vf*|VG=9rwk0Gfv{Z&CUP_lr71p?5KHmI)-Oi-o{aeqQua z=#@vD$imJ&u+%MW2yfGj+u~aO6}6X+E8&QM-VSa@W)pX}3r7JdHn?3XL=Bz$1w&He znbyOolNc9)+OA8xdf9gqWK1cR;~rU%tv&JA@~)*Q&n{(IHNIJ}2P7F#*eFH?BTv-1 zYWnVDy1;Z#SAk7cm@f)@T*WxfUV1iu#7Vz%Lg!NJ(ZKPQ@lCZ*3GA#-M)&|W$guK; z6;{22cJG~zcqGmlxk9=VScIMK`-wE9mMjFML9F>yjeAZh+4wLJXXLC{an9W6wlX8# zSG#U&_VhFMOGX=6V{ub7Hu?1KlloH&@yBQtb!^Hz6+1%{F~9-CTx*UjcoM851775A zA=Of`Yir|V4bT1{TS~wyYrDLN464!TRpIxw?7yV(2C(sSp1ke*P|XY#Fhowd6@O&zor_>Z7}cldu^wq}SoTDF1&IGO5dlYj zBpu&g6a}j%NPP@i`J_-&K)G@NGIIta7V*7c{yIWU;smByumFC{zGB%KU+k_!7RG6N zPQ74)z!z6NuRJey2WzRd0f~6{u|tSHw)x{6fyosSN5DR)&L04Phjil0n4n{cgVLBB z;K1G~5s*k%yhXG)Va08kPD+4){ADSs_1@zrWRrJ*H0 zlBZu>T5vyi+0IBm?}uZ!pHso@poOIuiiw1*HMoYPZxDQTd}1i+Ly$cYwmDO{yH0w= zkb~1|YIlKhLI+kZfY<8%H0&#AXuFDtY6OC@E`PVV!%5e!&t92m-fYbfu(>c&EgUsd zH^2!<%2i);c3XT~w2)GJV{3B}aPuze#f%=Mc6Q`U4@yV{-ddBpHq}#qN&LL*oiJ4m z*Iz$Rfmf0VP-Wi3$^1hapb%6rDppvTN48$lX%|OP)@mKvjcHW>R0^G)y?uCl$|q2Q zFp(jANd0`-NN^AwO598duk48xGA|xv_OkhluX{%=TlKHD&4*?-UorR4ymPh9k#Sy7 z8@QtnD|mwgwO1uyL*UY-YIdB(M9&U$FJY!oOO@<{>EiII_0cUr{Gvg)9vYd=R-q@7 zU_!mlP z!gk(zTr&iC6k6Gte8}ae?&V9S%E9l5WzNC&<+61c?bsPjAGfGC&>b_;r8~t|OBjQ8 zLhb5Tze?RI;^a&}CY^Nu{b%f5Bsd~?fmOFgtBh(1|gyk@AB^DCfrOHPPG2TVB5gF%ZCu7@<907a4my=n zZLDNQvToN!j|KtZAhBY`X3bEy_fzb|OpBEcUmWgThsrPLiuV{$2Yq)Mud3$3jeqr_ zfnAp>_Q}>Z%&L%Ir)J6Vm%v2Un%rV=vZspb&y)wTq?cwiOqPa|kMgfd7*9sKZ{4p= z^$91APt4ox^YDVplrWOyMyz1KYHaZ{DU)q`gqk%{*N#o1dOJm8o;3-EG=LE~fBoX4ynmkW391iRE za~@uj{Odj{@ZHcP;hujx5id@VCpLo7qU@8q?aP$B zDm6CwNn>isF@2+qraUO|hJ2svQys~2>UOwXWszRX=_$HI;mc_K30;Teo{o%jh54v@g5}2a}JRL#`m%`!bHvJXt8O- zGR1vBshg}<3i=b^*mh*JE3I!%>08Qq7SvzJ+nT*L1R>9jVoc1|VqGS3=11~yk^HYC z={UCr6bC{`ENBig?tk_96hnnnx_SLg5Gyu|{8!m<+O%@5Xnv!p(Bd~<4-0UhYiEn{ zt)_|v*>LCi#{}^(b!*Ie`Sq=GMcv=kwD)_F`zDLuK$m@3H4o^Zx#h;ofjB(DdTKtn zyFBfnv*lJf{VK$eNwA_6SdsA6+N>FbeO!GlHo3As%^ndHdLEm^UW(RKnhL$pNPBNa zh$S41XnshAKcKvUm<;otP1Cokbf~I6(Tjoebqq;9h&4J9dgNhxs5}tIE#7b?7I1 z1nZ*Xg1Tn-`a#)>bEjd^7?*y=+9ksSpTbClh3q{Ium+YXU^gn?`*i%)Ga>s-6(=S0 zK6kbAeaLGu4UAXsAZypmi>!>YTFty4RLn8=XuUuEtHY}iD|7Q^H{CvDvOfo~QEI-= zuoSE_Y!9J&BJirv*c6$t(d<(iRAXZqp}%hzt0_7?s~&6t@G9F3yvW`UcEvk4ug{EV z$W2Emj+&RGGi+x^pDaYBtmJ$5j40l$c=jzEjr0nAf%!@sYOIaS|R~JV-rPH zkPzeNw3Amb|adHhntKt$B7UlvbEF&S#8@j`<#Pel3;2Q8yy;>pv>D}r)WHl zodG-9dDYql*d9#uPnM8mY~j#&BGoBEfD`dNPqSVsN2sWs0!iF|C~FT)!##e`3tQAQ zbMsb|7kzpJy6=Ez%zO*_g#OnVi?{^+f@YJb{dk|s;I`Bc3~{%3<;Js(9ox~qBGV6L zJYs!3shcak-kBt~4oc5~opB7EN|>IUi-cA-SZDnf79B}}IYbS{oXAdVv#PdzAaPSd zoiUUq;nv)vqrU>`ZaJ-APC46(owkKOXPA;G*ucQq83Ra$WmxknvM$sZhEfoT?ei=n zG-%BFPJ1EoXLPz0C_40!M$4^V!Ga>S7%XE}9nz*l5VmDR%cy@L;5oYw6zkqJ!UMTG z&M78#O0~l5Y%z+nSVR&-4KP>-`=xj^?#NCUm=bzFygM7+)~lw_?Ap|QN~1h*rQ+)+ z9z9(ndx{VqpNt-3ILq-8tIFAq7@TF6UHH#7Wvm`1=a)Ak@~vrto0n&@S1}387=R2) z2OiPK2(krsQLr;=LCBvYu-!(nHKAp+vqU=%!)A;j{3mwg6EJhb42W)B^&EmZ-wrN6>Km!Wueo<(YPV^9LPSlql>(3d40Wh5 zR#4D=l)W6Xx*0y@QjB0A64sXq+99p?FrdYV3X1P}NXo(|JC`6EgYPevRw}o}1ZdB! ze?sq(0SBEucD-Cta@c`+I?mMPr`n6Sl5&$SNZFv0U>~z{N5b;tw8IU7UjJM$$n&U5@u=O|h z2O5ob74p)1iWAoGjEY%ZNW^UIoNU?Z+u7{LF6iJckjqiAfA9q1$2sYtB2~owoD-+d zfvydylJa>nt-bAsK%vnM{j=UMHP&GX#V;3W5hc+&X&Dir&aReM3R83kc3G8~T9iv> zTno*1b6gCIou~-GKeXg z-!&v;W%zwsiGEMl0tK*QffwP0D1=w?ECXamL)zq;L<$xxxs>_qOX!BGJ$%NN`B5XB zs!+LGSm+pt(79ofr$Tw|+6PYGF&q1CZZ_z9ptr_f!U(O?xVn0MD$UQ$6SMH+^(`D{ z2Nl{c?R-E~m@SIoXf>_0Gunrmklb0zQ;k*%ItqFII| z&xNo?axz^q>mFtq9Xn{|fmPC;>P^+PLpu&2I;@e#c2_?y^V>u;z~3ap_ZnTxy(Q&k zj`S}_O)xOo_U#rpz~~#2-e>-)6jO(i`X~ZGSHaF|sQy|ElENcmgG6nLn}^eir)Vc9 zzKXg`R<^u>{@TOdV^q`xUQoLKl_jjK%^!tsgq=}Vz7ZBS z>JMq3HqUhRiBoh8PPfmO@T$2%)ZwLCHqlogpD%_kOw#jBbJC$I!%E1>F?k5P^~8zR znl%_0}An4lEDC?-!JnY4hRI| z@H6U4+!_0pvt{J~Lknw`#H&yfli+fqG^&!z7bWU4HsDjV0#>Wy6+~5dyq%Tp>ibP- zSQV(^VlRY!r|V}H4PUZ3xc&K#1#I!c8B0mj2gfGhnq3CKLw6bHoG;;L@iN(#!LEXIv|DqX5 zH%ut%qy-L-##&u#Y1W=hntXXCf2!NCAh#haE?PeS z&e$$n(~JmI@cMR=YVcY_bdGRGPa~_#v1tlGKKJ!Jg6d(eX2kNQ##6j}aClt?MTvr9 z$Z1@bc+Z*B{R|>ScN`-4%=+X6>qvaiFSIL=;d9DR@Sms4Afy^d@x9VdibbwiO0G@y zr+f=cv7k(d8Gvh_ReO5mzJm(npOl+Tk5&(Ul+ny~Y&O2ou;bIFj-HBd5%gpFF~>nf zA?-Q1>%FSWm-Q;-9V&!kiT+8ooG&hCRYT05{^X>ECaFKfg7Y)5o;`J3p)rQ4jXjc; zw$59RTv?66sJSw4aZOUvzo|{XPlQht%2ur*Zq68!v}vDUk@SZ&$PWC2QRiog%>zq- zQqtq3QlFRjTfagS|K&nqCzk1S6gxINzEWw_juI$dXFYT9dQUM{q6u1sl(ZXd3O zcuBBK@GcYGN1ao(`5%txqaHTMzQTYYx}3&bwFz)=ro#}EvNciyp~!|yV&ZcYev&@Bf@{f^S;j|T3z8pn|jkn4E z@oxaU;}7u9SP`P$#P8s)%w-@a>_Ku%>hBXZnM3GA{WqaW|^rb(Z{e_}mx>TR66RPzGox%Y+MYsd)q5dzS!o&4;{ITp- zAo+Cf-tT{`MsyZ|Qm0oF2)cg^3TuP`2?_-M`9H`EINj@$)uaDdR{BgT{Y?0i^G@IQ z|M6@j8>Aa_vo%49G|`puC;25%f&v2HDU_5y21=SKVbsgYw*5g`CP1IWKOUN7E)tOdxbB}6aiW7ncZmR5GXFix09T1iBz^G@%AeQv4EFXe$u>b7|3Q2z ziVgtwFK}|`zahtd-k|-{r{{hL=lxt3j1t@qc!z&O0|se>L`8PK`IE$DjjUTi|K}-} zKPIQI?Yo!=94Ts{D}H~_mL362=FfTi!oz+6&tCqM<7YxUSb3&ve~@3A%mP6=SZ+O& ze^C0zKMVYms(x_kwm;;W=mfyW?*cIUb^k!t1ziA2CU*9fyY>9>E!2xYAeW!g-C!uy zKd9xHB}(~|(g4e^^kggl@sytO?___(O#NLXF2ciZ*2|{<8|U6&FVhC7CHU~;K43Nd zo7TMX|Bq@Q)OP=afT;A%KiSI+ng4lW10(26gdGRG6Y4jBU0CI_76SPBkx0Y4nk%O{>~irzVv-@ zNO27}H1zZeN4hiYl#lGT2{}xLEeFn9_qU&7&y3^FYz(~WG1NF~9L#tBI%i_mQ2)zR z&+n665Q>isop_z-cTfB0-g2mEY6GcB{FV%@RgyMNs@%vjse7vlKR9j~SqICMUCa>^xM_1Bhx{&Z*U<}J$!)#=aB z*$Vd>hm|*Nigkds(dJ)R)gp4@pFwYDB4kpAgwmL)C_0iaC>nXE{6rAuprBVb@7T5c zHV=qdi6iX5G}-Fw=1t7Mo@wC=hqi|8y?T59RfLt4c7Tnt53k~^XH>8k`~t)Pv%0YP z%EFDg2aW-=J>8uOQ>w0|bx{7b?I<34SKo@|GyB!;-}X@K3^Z0v`^eE<=2A`L-{Tl- zer_-s+r@4S7irzDW!LL{=n*pMaz&k*KlC{A^R4|?Z%bSI1dGGjO{p?4!G=)*fc z>hAY9j#i(JKVpL9fHd4&!JHHF7kpFJH6Fao#}D1Us$F>}uNVVto7=krJ&S=kk7G<; zemwR4SnJR;Jc0*R5Ps3UzV*g9&cOgb8H(brO+OBA*<~$}A)|k`Z_H@@)*CV8?b3ut z)YB_JiSncg6Wc!MtNg%2DQ7GyIi^_$b>!`MB%%A(Nb~t?jhj|m|3aAp^F-M-#~OBN zb55W4txefS!y}D8au6aBkQ_Q9Mh9NfNP`UT!5lWUn0$ueO@1_1*l}yqrl)k^#r$UrFrgRcA~ zWpW)Pkjx_iNf^ZXRQgl8%i-A$^-LhI#vj?4!ZqPDPg* z7#Otpa@M)an~ribQwWU6W7ZDrZ^(LgaOa@DSkJWNQ((AFBbxu=_QSz*aNAzvBIP-I zM)1`$+;c3Tj-0Exq@^L;a^ZDiVYGFi+f3obxS5+6;CT4wCEr12NMFsN6DLogIifPg zv;I(hV@e$)A-gTAbB!TTaJ>k<^T{PRaE$&GLi{fAn6P&pCwJ<~nI1nCYHUn`ZQb0Y|-ZpelA?E1#_@JO4{4Rh)*)V}{P(aaq# zU9?s39W}VAjfFxJ-t@$rQa&B}tGue0SqC1`+(861MFR|wrk4wgOlk*!7Czt0!91ns z09MMOL%Em21@@rOr$M@|;@x=Q6#MIdK5$}h@Gb|Kac(a&1X2zp?N)PMVVeWjcMwH$ zqEoNIMF2l3Y(qb6tm`PqM2JZGn#jRKQh6jjfT<`Jut9UUOj*X(9onn^u{Zw~9Dg#< zpQYsO+XX#1O3PRLpXvQg&fb<0?)obx=ox}Y+z*&8?x#k$hl5Z+Ur1#&kiE%<9{HP5 zPGTrIdIpnP*dx#Y+n~eN4c2eS>IHMv6M!ejZkPxEh#+DIu}3%L$ls*i z^fyJm6E+@Cz6b~IHGrbo(5XlMUU%J7`cAn9*71Kn@XxL94f|uumP1%@;0U)37|hcCguUBa^OUya{9gs_ceXAq zo=ZC-^}m6h(tqEFPwCt{-{16ZER`R_T=gS6@9NgBIDTbAo;Hqz{Tp()gUEVEUfU`D zkJuaq=}(m~Ht(ja^CeI&vq|gjJ-z8P?R_7%ZrVXSy&+f|b^I9?%257Q^r~ZZ?<2B$Gmz#nM8!?G_cxW9|PU%Z6^N9kfR&7pb;+O zR^5=y?_bHFbAi^LJBdVV_|Qp(i*pwrZAkrw%mY*3;5wf*!bSi7h8oDFgT<0WxlBE_ z%Z8nao9vIcFYqU!0rYiN{crCyKBF6M*>c{s^GE}Wbr_Z4-eWeVg*A(_lQJJXW%&Pn zQFR0nF8k-}z%}VMA#802k!S&jhO-u=tn=N8G5Pb*K>7kO%6J8K+xh>qRK8qU*jI_5 z->4%ZhB8S~xyk_Asep9tfdUv21+yXxIa?l7v@ZmcQ>>a#=&?;;yP90w8f(_=RM?^BZ%S#mb$R{LZ7o`1;iaceBPoSQO#B`tFUN(^#fE^ zFg%;RK(jxQ`M9~{0ix7c!S9V5M*}x(XcD$oN*0yMwRgVR^?q!#)xpE*eJSehw!P!K zJu=i!do~8Oiwbo;f5uvzT)rI?_Bl3axrgJ&wYF_vNG`ZXkKZ==uWaqx7S03(`wIZq z=l@9#Gh~^$N8TE!n4phvH*~`CCj)=Cw0u~BS(;V8S@S{pzV)&4BWsK4+!I9-1)jWd zz$>D_Ta)+lyKA|TOjtFLs-1uvx~;In|A28e?)z9#P~kWy&CrG4dePFN6|$HcBcmHe zj^e*@Yz7R@k6UltAO|uZsef&9?OnJSdciqSdV+)B6$s69NZZl%@2(TvK~&|Lf1O^x z@omSyyHllswddFlpeqt85bCpXOBQ^~YZ#<Dq@?#nzAs}ko5w&eA5V%ZshTWUZPcW5AS&DH7yUK5(LZgoLl1GyTr3yZY8 z%Fz724t)IYtu1*3R0*_IuYdaLUn-7%DPc557VIA34g)Rb_J2$$2Y0DHqdT<|3UEU= zNch0X^l-TdX@tFI(@?EX}>J^XJ%UuL)NT=9L2Hoy&aZJ$W+ zmi*`R1dbXV*p0Mxc}q?uwLs$s|3v0GDUx2nD}aZm|2BhTrf{7n%vJsgkcrFIMUgyQ zXRH79lu?jiJ>uFNE3mfU4pBtj=ur&vv0+X5-P^3C+1Q~j(z_;iub=hE)n^WqO-t@9 zifGMZwDIEQ|1Fg(e*x1=)wjTFX_*+1_-RZ~!b{TOi~rU4z~NYZ4(Jq;F@msCTI^1< z&NGN*16z?JDM*(KLI;{_j_WjUFRs!Q-Ian-T4Pp^7wzh+|CPyFxa>n+6)Ovj#k0{L z3WlePkEU7shWxwcW`s-dYYV96S(2>3Rs#$4lJHGfU#_XQsV;9ueEqEG1&49zeE zMx$fJ8Gb#~AocH|`4=kK7;eYYIDpYjeDpsx;QIhL`wpP2$7%@q1w*sR z(?fI%9IYK3-zgxL?2u+XnXa^Q>Xds%jFc{F&hEln0O64NWUyuhC%tWA(@6E5J8jeJ z_2H}LYf5>UnGdh(H@nhmmR`Oj|BPYGhb&uNK^UJnetZJx|2MPa?}M8z7=UL*v<3cS zrEPExNXF$d%bC>mt!^=rrr+OW7zaJV>&h-QMglcYMXfVU(6%aPVabn|t+$ zguC(!R@Uv7Vo6HjtVMD{VlGiG+I&))VfJ=Z;8!gZbj~~Jq;cxU7w@v2l|!4(0wIBn z%*;%}7-H`}QMc8nEmN!q_6nAJC-8yG-Y+i$uC#!eZ9tb+Oa0h!Px4NQZnFRG9rso$ zi)7+jJ$U#~@hR|4hL&GXnbP@Ntb>pWUnlRF*azb;HCmI^Jc@w^Hx2CvX0x9jH%im8 zg^jo8gwj9uW9%`ay0bZR$Ysmm(a}L!elinuO;F_IWM}$*R`Q#b$KjzeToXd15t8pt zl07~7T;$|QkI{o0L~3BE=4;>DP!j(Yfw4U!h%Z5`^`(n%TS2z>QEZ)skh(}9%`SOO z?I@0p@ctDbYyQoh<-er9{qPwJh=2SuNdKqKQ_(4*vj9f9Vv+w?imk6&NU$)gHg0(0 zOC7!HzKsJJk!~UWP5b`(_O4bh-VoS*mEj`)WtDyrFGNf^@0MSHsn)Z&OVB$YPrYw5 zXI2Q$0D7;RbB6fk&TWaI9mIVS5(U?y#S7pqXMjdJCe?F;b|39SxJ3y3%LP`-kA}rm z#p!OtGX;RFm|W(fgv{bM`Y%r07G8BMf#ODG&5MIF^6d~Av&dz{)xr*npo<3ya;qp| zD$g7UO5(xfQO;a3fLHbHSZu+i@Fp@YSX5Y1(8;Ug@CBw)oDwoR|87zCB(hO0N{jC_ zEGG~#zB*wRyNX8;7hQdXYR3_uj`xc)sPY_H9_BPw9hG7>6an zJoqu0E&o-AprfG5Is1eNX!inB!76Ees83d`(T&+pgjJdeJlZZyHJDKAaPJ($Mkfo`mY&+zdLXIIxI%wqUN=?J+Ouh${ zR6bc|*+t%PuW40VvoR_n&vYR-4THo*2^&z`={l&I9a{rID|Bi;UjoOCsG%cb_ zv>-uIl+oL$QAQt(G1~CE?BigL2*kg^2&lAGHNBF{`?AIB=+`?%@jLFb zc9lKQ-fuhMS+wcs>@kXjExXKOY8x6lsiyCoQ;g3V#-73+m4}V#0 zX=)6$EGbSwBvnaP=7^%RS6AmID)E)7Hl{*dJ6jLWt|J6_wAAq@=PqXxGQvwWWQyY@ z7BH{5ljrxI>S>2Y&1kIMbb_3_XIV_7r=4Qn6VaIMq@9UMks!7fo$otBzQ}PL)&uMV z6vi^R2HSIagK+@g!{r)>m+2@q0k~3XY$vu<1I~asrCw4qFfN#dC(;)3rDY~h<|P*k z#QMndTN@)Da`-(b??3=e=_uAujL?amiv^K|L-xSw1?FM^obBol z@iZGAx+9+|+G|vjB*wZ!b#UtIZ{*I=7SWRj4*i|^Ta64gKrUw|^)3=P(0?vaJAhj& zlK7DfTqZr1R*xX($#afNT~$`d$(xh@hs*CUHrwyN1Nnk;pt?p5H^-Lr*E2TY*J~RK z9^ni~Tw>4(fRV7!U@5v9N}h(Pr%(S}tmpT8snR}j)}-20J+V(O?(J8H2LIBjV7@^K z$9L62o<%jxPX-ojP-j8g2Zyo^m^d%Jhy`Sj|4k^JfR;M^^w^}PRb6*PAE*>7;bw99 zq*BAc;2vgR>UzDn%XGZ+Ltt{|_Pf!;s`}8a4SWYMDcb+&`p@kNA7F_BB6kb-SEgM220}i3 zNTa^O^6Z)JE5jrtdT>zKDDY+bcc5vvF5UUTi4=Hih6y$r8=LG59d~Iot080967Dwj z4BU!|p(G$6XhcOtMU7=DUuImC(4e9!n%O0$u*fm0d|BicWq^5|rE4ND?F)U#8`TdwBIEofkayd+giYXq@5ygV&bRbK zWyfVXrKQDt&LYFYv@Pegk4`itqE%(}J&)gQwqZ~fPQRy8o9%x>HAU?Dgg2P;)l(!; z-ti%EwVgHR_QNk5YRY^)mbg3b%LS(UMFI2daOXx>mlj=+PE6qurZ!O?>(a$xA77tz z)QFrUjh03tz?cc-Vw0WbDZ@A~k3%?;uEtrww9as2tVG^6eL~0=S@X)PX$y@+mG?kj z^)~}Y+lZ=BoJQ6-!eqEYcMg1GtQ^a01)(-CRhFrSAhuo@rtl}-{5*6cy}&F`14wP7z}8^YJmHb? zC%~bfaG`l#3=?{hr>Sj8r)JN#hU+;W7$&6ucD$ie-z`@t)NQidH|`b}TChK2)!;w% zWYi2~#x9}sO6_+L*teg(Giq&PEzUzLQGZ{KNFWAa6_6AM0YD!a(}^$U?J5zv2|QvW zu({gF18wJt?0V=CqEcW-!=gVg06tPJ1{>Es|7u5uSp4nz)r0Yy%yyGOqD5dmkGbd9 zLen?Fv%V*?{N~j8PxTyDctETYftluoW(EePT6&3Q`l^60)gCPr8_jj5qw`$m@K2S8 zWNgFFF9)1#iuqKXUoAr}~j^Y#e)y2SuwcM1M1Jjin z)Kf8+A?>p>5z{)yo)8!?tY+{%g|}VnI&vu8n#-r`;40k>oK=pkVJ3ELH4@y_P?_u9P?{o z>!Yr(YG4(9&(bO6-zcDIG(iQwUgAp&d>S3-^@_}Fb#*#5WX;N)T%A@YH`Hgqhya%( zuJKeUOrLO_k$SS|XiLD!g^j1W5y-R?y4`qwx3ZXbx}|F4s4Gje!PDJsGC;c5E1Sjd zw0mUH7S}u(Vs~a!1I|Ztx8{;zo$1dV8cPe!@y5gY+R@`T4NS8Prrg~xJFM6^;naY^ zB_pLaf<7sD&*cP(JlWjOqz-0xQex((dLd9(OX$gUoX83GppixIe2y?VfHm(x$=&KNlonkzS(_ z>;92$5gVIGzZKIBd2D*3d#zMppbgiHdadtgn@~RhUjA-F{e*tSg|Te3#YSRSfOY5t zNgQ_BU@m!CAKX*B;fP{AuR_xa+>yKYN<*&rQyk~UPwB|%-LkD-m$^uZ*=%&nd3SYQ zuY~*!R=KBufgO>UJJW2QJAYdPF>!6~n=vhWwM&heV4A6qYjIgaZC?j7uTNJe zmNpzacmWz+q`&2hJwvNpC#67Cq#fiY^Ooip0EB_xgEy-;#DCSfOu%3@8`e_~ySOFN zCmzmiCvri3?_!(E;jU3g=i0eah4-OQ>W|12icK=A>J70sNQ-V~i0zC&hX43Dg*B<| z?L{t)&c)l@>us3C?Wbk`bN65HsS$HqdIDUhsG&?Z!BXfWu@x^~AZ_#d{V8*yhf4u2 zgklhSl?3I_!XoLW6qque-&;k+BQmLypt1NM>yX3srl^I5xsV>|Vy4HE_iSh+S=dyz zjGV7YqPzW1VS+s)V0`MTW2h3#ipXdqx_%$)_Rbst-_hz~C(-eSjh0ffm z*l*kCQIJ|!Fa=w@>GXFvTDN{mok!=uxcRKZPvD8`o|~VU z`PMC2i=rUXem80;5-#P0Sa=s)n6De`IQs34BN^jn$vGYdhn{Jg0SrpYk&LzO&Dscn zw1s776jrYTZ0#6-?Q98hyH15Qa6}V+aBf!ZD4`phnYiBH$TC`C%UgMB<>YtjxjLd~ zE6}W;Xy%hAcmCT1lN988FnP>lbl_kHS(F8oKXaqnx#&%$0RZ|k*+*F@K6vEO2x^RW zsarEQ)5G8LK<~c%GK-=rV)sB?Js-5u1i>OxGN;k5GY#ryjm@RjIYsMXg$6|do!iG} zC+Q<~_PjR*&7XQr@5WrYnrL_3dC?sZ^wk3y){(wix%>?m|1J;$dHhY&LLIt^_Ju}^ zY}j8NQelPYi@DaBq<>wMpU1%gba8_W)KfP|z|3#8u*h|Ia}uL5874+hErgdo&*7;}+Vazxoo&bVH?poKJEBKy6JX09zh*L>DubII z_2|JAL;}v+HP^@R>cb%}BF?b*z{B0~2qu-yJ!kLZ0d~!zSA*KV2f+;j`K*$!V=j<7 zX1c`{>tl|^WK0^s-^IS!?Fpm@y_8!-p#fY6 zXq?cF%PqmSQ@~eCyyOO!6N>6Q&NeZy>8@*Q3rFoMN3Qi-fujXTrE$~qZ*nJ_jsd&N zwGCVSow_;eWTj~Y1fi)|vLyUbSb_oouwysVUgy!P`|W}~DobySU5=7r9sfyZAcS(b zI<7wiflSwtyhN%MS!L-@RoI$5zI2WeyxMzgQIeQaJJ=DKA_C3wKxzQNN=cywynsi) zIGZyUALv?xj+8ietl7Gi0N*sm`Ytm6{L%4JIz6gE+#=UWDxtaR0^X>K-NNPg!q3a? z4RD382d}Rm7;AbhDgtd$TZ={7%PuW6NpA5z6o z3O#9U+rIVlR{7;daDS3CIOTY4bJpDx=kAHxj4~Q#sB|31O;fl%4i$dWSc)%YjR5zb z$Q>O+ap%)G9mVKAt+|Ln296r18A23@V19M=V`nrg(+2zG)xr~r=ioj7`=hZs>~)6_ zBKs)v{P&Zu4~j}5KW;FFlH;$N5K+(yYAIBkkcq)~=2KiB7kM&eXZU$K+KD<2a#Byz z8OlFx=89Ck^+FpF5~V^q?8Ym8{E&w1n!Gc^8b$1!!p;sK4JF-iwpJ4~vuWPCrI zVH$L*TmK-#C;YjM>_vQG--gj7g=z-feXE5MRsW~5cX8Df4v$5#QUSGri3eIcrkNv^ z?i`@JxYFTjl6R^|&I$k5=Q1j3Osv3AjYzCSX`6G;_3Y@Y-_FSf?OMHtwNGC!--b5Z zGJ2^>%vrBXCQ(9%-9HDRav7Kh8olJxq>YA%!fRcSLFmU23dHHpST0zK27;N~OI{R2 z=lzvCoPV$3=J$8OYN8K6gGrmx9ys3`qIdf~)gi2~V=6aXqQbh=lgN6Ni2P??O6`6* z$8Y0)og8E4Y@zN91@m|FKBey%%>i~vav}_c z$sjWOzOCn_@8D6!wtFTbxI74q39LrHd{)Fy`DS2# zfe}RNBxqdB7R4;%(gdTf+gkn1s-7IoXf=zY$#oFo9o%!91tveQVVC3Oir5ewc0Ieqf-sonwMo4PIA>4cYVo{TU)z{I-}D^u!+I9_KrhCP+sU6 z#flg9^J_Y$)x*X!@QU>K?5$fLjq}S`0icrIsT-Tk)J#?xPqYXCw8_YV+_N?r8WJ>V zx#F@NC?p0RS(Lp!q>(Cm)5V2lb+$P%?8ICxnsRRiqi55uhO_!1HJ5Ef%vWNz z@12yn&J{pqJ`1$P6fQ1(2=&t9(*;X`+D?9S>o~}bywi)C_EiW?)%itR0cBzhd-dwZ-K_Sk#^KPL!$pwKB<78ck(=jLY{fg2JNJt;gr+pPL9=5Th0Q9y<5ET zjJ&R<3N3v-mMzxedJE>aLZ)+S^5n$=1VXD4T zpRn>A$L#bRlt-f}6LeV`X5`QJg(d7OLNBOXT@e{UT$NkE%skIRXI5-T{>pjQa4CysuXv2K1L{7z{; zRE1YfCW-|w&uliGrGkE?)pTW6B>=;$on58W=*b*`%g1fc3~SA3fBD!+N6(|RS}^>g z_1j(cot;5;pyqiBzIx@#mB`nkzh%)&+{0M!t=VQ+NH*0yo4}!^uk_;nAB35kG?3iMg#d{pVK8|GvLVByRxWnlr=wTkzG=2@gx|Z)YY5FW>5>)b z)v(0-c{uxMV@N?tqJcs@uR&{)vH5&`f`@s0(Q}&4DCIzUzVMsJIMy3IPUJ~aex=)j z+$Q6tGkPZzmOC*EvDzJ>7AG9*{y(~0vF`{9&Vef}*7`klq3OaR7JUr=eo7E-S+p*w zb#~z|L>G?ot|7?8IKeP*GUxvkUE45_n;^R>PiYbv42v?YX1m4;P*LqlV#TK>_2G+kP6u zs@I^%mEQLcY>P7WUK3plbssn}AQH0=eyVqB<%}xDy@X18gha*Tx#oSQ++TLBErnw@ zBpy$NOP;d)o+zg|+^k&sG=(vh#_G4BZTl}+XfxdRxNWKFhhp1&@ETp@j?$*yLLZ1-&wT$6~ESHz4iepG(0|Ts>+B^xtvuI z-KbQinG9kE^hA*y36f5hy6t-%r1_=o?Uz>OECn;EC?$HC<-OJ+TV89^m^-M1Knm84 zD~M(col3!j(f#@?2lQ-!LsU|hb7QG5`OgMJ^)$7EyP6rl#)Qgp1nhnsTQvLLZ1hAc z_Zedsw#{`T%h@{+*9X1?)9Xu_TH4qU@os?f^<*f=OU=SPp&jb)M*Xov&*gKE)1*DF zr{5F#W{zJu+%h)QZOML!h!K_n^425`teIcbv^!DmzhF@_ZN&Km(3IqtDv2%@t8XY- zL5TpJ|DlN7IVeF|<759OB^LBRb6w)q6-`0Cb43*ZcA|4jMQnU(y_}_59@Te3!abZ+ zv;nkv+U-FdM%UZbm~D^;X65GcFF>kaml!#a%rA6p3=hQ5w`%7y6M%uO3z zHNUJ`x(H+1*g1XyX_A{ZttUBa*vo2YlW#9kp%}E9rcW9#jZ7_3w8ZpTzDO6o^`*z| zXHuK&a+uL7#8T$W|A=i+xgI|+w--tOQ8(k+3?Tou2+Sv%Jf-kov>zorH*sY(;P)kI zvf^h=lf;K|v>>{VhexP%b^{;?qVMET4d;I-ov~wTIXYFn;qer6gF%E{osk9z1;}5% zTmghJj8DJ+@SyQS8^%B1xI`mjK>NnimnR zimY)~sX$b+ad7CWsHq_lP+1~w?d;BvR9RxLQ=~9NOTB9!SW*4M17pZe3Nl83B2>mp zbb{Y7pQBKhY=Ap=8IvOJtPq>I9NRvoH$MJ@qd~;BKgvCKg+A+8k{YDBk*(mU{QH{!q2{jtFwb zumv3-%(*l@6K>VnZ$@$@gAsZX|4>;hG_ws^7EBu|26CmL04459dQ9E$hyG%mEZ(vy zfRpf5th|d0rx+^@_m94Nl05D>pk$ZZ6{k5AD;P#)Ohibqao<5Ti&Y0-Y6idTh4}3- zJ2D)=Pj${Leeis1wG?Ib%=Up;&xadEsn2KNr7XjccCgz1k+o%2Ftumj*sa7+7a)$5;C-ztd^%|w=HQpQ}CI1`oUx6B=6Dxjqx}yBQY&AymXBQ^= z8e+KC`Fc@*t8_9b1L#4s^Lt^w(1x$1y*Sx@{97M-hrr zv<^bq_MEhL*6y3MgY!&-%U$Qr=v{^GX^Q|N-Y@Jvai5GSEp;!$Gp%D)!m@03+;?5zEeTfifn#Q3SwmiK5>Hvv?_0Z#~Z9o%Vhd4H?Uhxes-i^qm|wpuztMn;L4 zA&u&FR|yH%Du+>H6;|BZFzcAq=9`^)h2~DLXlV0-L6zhItK>&1`CjGRUMcJxw0Sbc zw12}S|Hk!`gkk$!IE%D4cJ$*bmc!9;L;On%dv=gS6RUTNFrl}=saET*sE(2G3e1+Mee~&bLV3W6#tb2yp!uW0-+D29Q#YAqJle;FM=baXYPdAN zX0+Buz=8aF`{*dC6j(abZB+XB_8dgsC~t~I+=-+2!3#}VP_0+sRfN~(Gg=Vr`4Rp* zCf>dx%R0xHUewz#0!v51d_V@^SEvf6xinIB1BQt8$NFFQExrd*>l-Hr6Z1!5H;b%4 zOntumy|w%yJxHF05P|esjVjhBCnnkYhO%?Qvmv>k6rR+EpyYcM3rD^_FL&b?8w>3) zI;Y{%P!2&c(~u*=6N%rW>=0?1lpyrf7OCF3sdR&E=`jV$ZgADkg4Nb`8dG@%tD zedF?nPyhS&`2nR$1LZ}njxlO9Q|9{ty8jaJ~%$@#-NfK0>wU8=XajvtZ|SdmghES{0>%E%~eqXl%}GnP=c?!xz6V{7n&*-ul*$m{*f-Imv1*?-FcHF|ET3W4UD{a_8y z<9AS#u5?tvtsP*fVTeS z&(h9*t=1i~30W*&t?z=@WeWzXO(e)pMISZ?Fk#n6RRZVO46D4k8AWd>Rj;2hYV^E8 zok~(f^=}`})qV1H^(G}H>P@NrKJl13Z7XjIu2jHt+~kFx4IhL|H#<&)j|0AAOlkvF z8V}N9`Ay7=4eP}j_sz>Jsh){U!D&Dgi4k<%;Uk(AY@*IAg~uHvCGU{yZ|<&rXA*Py zRAtX!{$ef65c!brq0TQZ(wlC}dvBYYC32so1(DJTtb>Vn5`;y(w*9l8rpakt=vMBr zH&rd?M`H@9z(Yfb#g1vmmrGkq-NWA$-rphfu_=OYVpz(Z_RiT=A|Dfys*L1MQUbG< zGM-jXIe=6kl2di|LYn|BMc|I`D0+2p@sRIpJ!3on{_WV^F- z)N9mZF)+SrizF1a2Thl7e!NG^J$S!NsO=WFOi+ ztO$HPopEl^^%co`1M2v8JJ|GR&z;UXt1?r(4Tm@M&9am3MD=9}mUCcaSU1bY#=+L( zirmYa7RhJV=8uXe!Lb5noMcUH>{$k|*^i>@V1DD8QL0j^Y7ZoN(#tZHa_WRx2Lz^u z*_SPVP=$Gy_nQn)lIsxPH3t3)E@G&lQNwMciu%l_qtbOg8@MMhvwGi~L6Ih}l2Rskn1jxv-dC*!l%W7kBzerYqz{xtp(} zci{3bSHnh^_X4=MJk9fMRu3q^-rqhpOOsG^xPdRREskOlA?Mb4Ic8sx4)S4i=Ie%z zZ;ZMASmrgr&RzQfIWTgIzprJCmSwK)Gp=3B?n!^{Es!kiNIsPG;$faZ^BdeY{?+2s zV?1yH$ltBBK(yir5Mut*kmuE^F0|a*JRNe)nNubQ<8r?HNoViq#J^Z9CR)_6!8Pr( z5lL!x3P83+;(=}TPoTAWh*C21g`DwAutHJHL5E!A{ZEJD!iHInI62>9)yAGj6efNY z*9fb1ZSX!-%C{bHj+G$uf8G8e0fpU_uvs>`0c}|qhkJ-pVci*N!b2!_m}YH3&dmVO3y#%^vuWb!e@=|uAk(b z4H*EN@SS^<{M?r6PyXRLh?U~~xhVaW6-v(kor9o(a_Ir!Ttzj>{a5Mty`i-c0m*e|&w z{mak;gkRLy(GaL|{p!kc-7%_JDbZRDo;PgEsr&*mPE$USs$^7UznU>1lJf#H9mD|+vKopn>1i$Itnby%G z`V{k}xy4p47 zc}!?RDOg9#)PLPMNl^1$iMI!Td%nQNm=rwsya#)j$d8N}=7!6qtGQV4nO5~C9hiKx znDHfxkqxEU+{9!)O<0f349qD3(%8g~DNXl{c#4ZFx)gq~2Y?{0Y)c+E#i<+p(vMC8 zTEVfGHgxTapEnfCle2&d5nMu$Vh2|W7~AXK<%6*uQ?@R3k);oLcHGT|3dcI@9486U zEFvspH9lI68tiF2rX1YUC0o;$dB3DJBXfY<=PHiWbf(c&{^^&}!ec(c7DkXZ+(HE^ zbCSPE*U_KW=ppm#1YLyagUV{6$z0G&Uw|0NFSK!qJD*PLnHE@imfa$E2~*+maBnHM zD;@rpaApXOrh(4sgX`4B&4scnm0uwR8d&!abg|rzrpEsJ*;EfL_03Z;* zcFqHX<+ReymIxC~#P2dM_wm6^s$m2v2mx)^8-;zFC0F>xfsNa(dAUWH{VajO{3zUN z_qT1w&%1CInOs~JB6-FrTBjf{eyx06)2Qlst&7Nua`%q6K^B0cN(|hX$zvV*YOA>6 zY34rjqW@PL=~2MyK(B;l3Ij?3F!-<5Dn?(sAAHDbczKaSQHk4l1r|H7-PNjC zvd7+Tc3*Rw5ZfNHkB8~1VVem6U|N=lde=z~)Bq?Yhkx0!%$ce(J1LF5b@JrPr$9W> zLR+xfKj{u1AfHY6IBocUrSZ5P{c#HQ>TEhSGT6}1^EB9uV&qQ}bLFK$24LhAK?}jt4ByC*9UPs+qh%hC=T(NvlG1*6 z^WMuaO1R`?Xw+RJ5B^Z2IOasY?z<4Yck}A=0ypNR{>9+W_tY}VEEs5ad2ffi zX6S*?%>u{>FC1x7&DSfvBxe%8GA8AAS9A8=tEndD1|0ocUu;Ti_b6RUFmK!BsI0=y z>2|XpvBT8$ua(m6nxlDoPwd>+_Hqgxuh#k9(i@j#%|=*;y0$Q<33+cX1p^s<_Gl9- zAk{*iQPTL$4iS{EUBcov(Qqd{g$oF%Y1cXozrCoub*r}-%*OSU@fwY7A!Dpj8h61DRN0%% z&>8Vi{U{9xbPX zlSc&A%N}p2TtJ9}poFBP&*}ksq9A%b<>l*clZSnR6ndd&zL%dG%1?(RdQs`->x$@m zLpwN{3;ptdwLYnlx^Gd=S-iEE>COPDGFXH;pFr0SFPG^Ayh5*9^-|w~3Ry8~7YI{6 zuVoN0Yp^!$6@Im2Y51Z)C@i7y_qatgL#N5}O3I0)-n`?S`?u5Fvc52mn~h4w?S4UI zl`&@3Qw`-ZxlGqF6sQ9cVX%|9OIfv-iibzv+Q=Fykf~GH)z*uZ4HcONUAIKfUWK${ zCa)kwvb@qSm$!jUh|o`VOa;e~5Xyw2F+I$ER-h8|&U4E*(=60HKflyK8xm^D0@Nqt z->}@)T^yfg3r)@|Z^zJbs|&BE>;hv8o~+oQC-O-5!yLL-x08Pvs@;PuL$CKbgl*| zWJnty=*P#SG8eftvear@?rRl3@F)H9xb@L8AZPS1TxW8xu8+HZXb+8E#XaRXNfKVv zDmGMgIz391fcSkZKTo;DI`_`7+)X_@euesz)XPmJvcqZFun)S5-arfOB~>K_-L{!# zVu1dq;VoIZrsw&EzG0(hR>*s;dzS8p9|!Mi7vG+$Mt=;AW>J0@4Ir!@F_Dy@V;n1a5Y5Evnvy1<1f9K9Ofv55JX}Ages|ZNE z(3vK+(0s#!#}QOzR}%G$O+%#%1FjcAG@JaNelx9KXS~zX_UV`2Rm$793r{TE&rS(M z?Wd^Iq`gEQ%Xj7gd(Bp*?$^x!Hl>kudisS^yrReXL*_JAa?$pCHm(P$NBPECcbhPY zO0)jKx+b(b0b0N{-ZT75|H&~nrLZrg=#AM;mqOTHp*XK`@s*ZM3Hmrrb)ChgYe2Fi zGL)kIc=1;Pudmg^^RxHXA4(Q%0uE9Mi1TT(k@D$0;J|e4e8Zd?Mih;x(0OA3x*#w= zZz^Bx>~PVg%0`uE?+QWq+TIV(mXFi}e3zP?Ls{7mDo}wP}ju^r!bWkfM$4$+@(GuXvU0E%@b6hc01^M|=NOyAe#;8~k@yp)ByF>bP74j!tT60;xeIYx&7N+B!owc+) z21TK&adxW&N>ul3O^c%R@&ZW@Y44Zxa4gox6hbO*WlWSU*ewR4hREG!cr99}e;E`9 zM>0WLUs*8#Sf70zVT*7>x;Rtc^eDy2pgB*{B)F1V6b<0{q zAFD#_2zzPO@8BRXH5FqVK@w?d)OB|DrI@h{xAVO8*NMBQW zbG#P4Jpv5aox*NNYm2V;Wq-fN9hz;D7(Yg>${(X9#|25O=)5*){XS8lJ-0JW68YZP{<$YATK@Iw4%Oy#y-HqkVoew~oA~Lh%KH6+j2yYo z8fW`!m5lKrS1Xe4xO@KsUml}Esstu7x!K$J$d-Ci?s#uadE5rM(<5uqLni_~sLbtZ zKHJ2qWeJzrh#CpM5uEA|cy@D?v(pDS>NJ>C(sT2J^tN+AOP;jZfjYAmv-wM@#P2rt zh>0md2b1e{2|zEo-@(TIdWC_HX?h~K>-XEmPEG5L8fMAYJ2A6p8BlYeuBwIVI?k-| zz`AFQ*;muu2lic;0Y)vxY91(s0vLgH&*#D`S+h&B)nAluP6Jb3;UhA+valRhwi>OG zh7XaS{76{N_v9EBfoz(#bB#gtB;ak=_LQ7NxWgj`%iLt6$F{A;!uAJ@!*_2M3{YxcRX>TpWP!~m~yy{;HbE_1o*(!EoJ&dY4-G*FC! z#6>Ig#jm`fxRGPi41ieqrzbT&Op?LJN>FAfB974pAh-tsf}05)eZKZ;muW9zj+ zV*FDm&KfGo1kIb<#&vS<)EZ=T`9YD5>~=VfGE+I3DJbe^piz(`WLDGkYrNOFZ@eD> z*`R)l(ru%?2|Wi23Qn|PzE=@xqM-9&3*rJC&xK0_eQL)r#xZzk0EO*8&37XZ^AD&` zbU08nz@T^X4h0aCx>jVygV+#UjN)6l6-H3KsKY?X_>kG+pPh3oBMs zE5dDHou*j>3U4Iotdo3_g(mSDf^u=hRxeEvzs2-Tm4hLQYq2{Sz1!QK5$l>>MpoD{yy#y7!1~AoPHZNmhHWW*!8v3 znr{{Mp$RmtqyJio3JK34HC1rB{F!0!yH5y^w}&iR-^MTMXNH4ugNjwj za#Y>#n93pcKzKw`PZbym6dk|91^J^VM6vO?S-LD2BFVZ2%7nrJ|DuV*VRKT8=AlU58TEZd+?w)F1L2OsN{ z7cio`O(Z%ZutmEXiBoX%OB90h*wIPv08dKu^tEVs;JGr~{cM|g}+&-P~`CaZfIWOCyE~ZayP%Sk?T&Z7lW>W>51P-MxsD z<6>>`j=IZ5&W8Op*~N2`tm1xS9;g*|Z%5XhZL0o07uEtp&g+NE)9tyGaQta4R& zq6jfAKoWcly=FaHy5)TY)h3kp!BXoW(m#wI2Y1m^NFplS#I*&}gQsN1ZP2W>`?mv` zW&#c=v7M3c?#6Q?0m=Ns(_npTK}y{l`eEKDQdQ_$FB@O>-``|@1o#WArw#zY`$3Gj z>d1NhbLV=lhZWUr;uMX$uQw;<+!_v|_+wktbsjMu539|vFn;`^)48~U_d|my&>%>N zaOc4yCZRGm@TXH+l&oX?eejXVOKii}A6uA*3HAt&{)Go;(GAgHWTK-=Bw{r2eN9!T^W-F54elN)(Lx^-_Jq{;dG_G{Lv0cT97%xF1Qba(>OvUL<|h9j4!DqH8Nt| ztI>n>xzh&EzGMSYql;O4GYT=gmioD(`J|0ChKJ+1%21`|-;J@R{9?i52+zC^Q8I|1bTEs>{$Vc3 z!%noJKlVz8IVd~R6bq4vk3P(R{qTvk5faff^n`>-ro%Z z=qCVpiAo4+y)Gnqn7H5m-|#G7QVEc#H;4;8Ze%l%II@b!Ow=qExBupGRPFH~XSeLLnQ){=3W_~i$WefR)(%Gl6Js61qs zts8-}5VTMlyeu0Vwl?*5Mbvlx+(m7^gFT8F<4FEzFRq3CcOpdZQR*A5RmuTL1VnAc zWUW$ow0pyt!jpmy+z02B-P7eB4w*T~$s)NiBKe*BR&wRstADkG8X(62OnG_;YSC(7 z-v3s{9rb^$O~OMeD3|#&)%KI`?+ZRr{_}!X zF!pF&hyKlf8{QlY?6=@QzC{E?XF1y>_sm56Eq8_#-a>s&psYllGAsapZ|wF|bRHrd zvM(i2Fm)nuO7(@=gbmofe=WW)8xVN`A=y;`gSw&lKgZcza`E(BXCUTLjM!<4m4Ehr z{l~V%z;vuLy%R~qN2%f6J_1wJvjaDl_Xc8BRR%vtAA=%c=k_lbg;yYc6D z13LAE?A3wC{$t(~08i^ zZLJ9rRno9KZ3++563(-4Uj8M$z;p{X8nGqSSkqqUm@hzt1H}6u%j0YE&ns?0nH)4i z?^=>y{_nW4|#p zS3Z@j)UcB{l%^vQtzGByU-TzT@AWT1Hh6D4(HW*-~{wprQT|Q?bz&K z2TvG=p1k*~k^SqK8v*hXP7q{n$Fv!C%t7za_xA4Ff$0CXj`crTBLal|Y)6BJkr+eH z|NNDu=1AIs>`T`p29^HwpE)KHZ!l*8r$?I%<}HED8~y)65+J&tq4wn+slq^PK1y(*6^YS%wd5REF@mo&)p8GQ5O@^%!}%L^%sJdkK_NN(6ZmbJ|)LW^r!W3ssGk+ z_riMq5ura@A4T8`0bJWsYh+B5Z2*U>IbCX zd58Qz(IyQEq6k9qUMYnp`9HBcPrA_9|D+CB;J?vJ;h&I7={s<%vCJ0>7y3_XjzcdF z?4OVdkY`|F|6=8}{@4VrOHiUSVjwp-jZFRfvByvTuj^W@)f@27zV|)ruVt4@{=Y8D zd-^jVDgSEj@x=f2*#NBnuZzqUXv6#n-?<0{{}1a1Ajy9}L=}j0KLJ+?5Cto+3BZy4 z>k=~mXcIsk_4fy#8!p!Q$C&;f&Iq`g{ynsN9v4y|kKYH_f=vHEe(L|5YylPfzkBp_ z{bG-ZFI+c37j+>_|HjRFA5i-cLg@I{$N#g|fXx5@KUSrKVpgI6CZuCSK#tDwJN}>{ z-Sivb$MWAxm)QWV`d4PwY5osl>pm2O(oC282~;kHAPY~QrHGsPFy7)b4U5auSnQ7D zY6|+a*b*YqHjzXnSIyjg~k`5R!>ISLn+FS61M;dXyvg0hOIjRJqNt; z*n$jEr2ba5&t9lNjOi8`>Mj?c75{z}R`tUFTQB|3`AOS<$Mht^tas*NTKUfz=zDMP zOxmsO8a)65Aoy>Vt!?xo2Y%B{KH(DnS{$V08mXXQwM4LqtU>O9Uj2V+6>b zyc~$`{%oc#@sM5F2S*oH_tgP1_Qz2jKovsi$p-n8xLI<2sl80KalzNvb+OqlrGa`d z%F5pU6VPTmzqibL@e$iIjnULOyx&}!2_^jwrn&SE9LGzu>?`$Ozv+$!vPx$g$B%DS zMX<=;l5#Q=ay@e+lbaiF64cHn?tLJ&c!obgyB9C8!-G>&=m7fJq6|Jj+0Vlle6S=! zhnm56Ex%BK04w-cg8+svF=1z!i!%{b=d$-cTk6)Sa-Cx+7)r`~w0w!rq=g!C`o;jx zNmVW3ijP`ecg^Y1SCFnOUPXFYrVduaL=C+exUz?Em$fc^Y_jO5Lz3AZ+Oegn&3$aq zm~U1w4EXV>-r`e{z~1p!5mXGH(Swoe#6NP>5^1h^QoAkQ9``#N!aax0RvAo7px>l% zt#1fp!>h`xS{ui>TgKaibRV5y(75xPXi2pvkTU|MSHo~>vBw-UJ{fGbS~*XhQ>6nq z6#^1t#MQf|KUHdwNE*Xkde9Y-;Bcx%ib2@09DvU zU-LP)_nGXoLQT=fDJs`AFO1Va7i%C&1QHzoXsjv#S21t6Vy`Ze8Drs7YyrQFNSBYM zO64~xzYd*Mo?l;&0;2?!{Gu=6Y&xSBQR;9@Y_i7$&|?&MvQKs5skM$eJj76L;4820J+ifiv^IrIz%-uDeduo?jZvZ|IV`_GPBg3*p_)OG>;X zsrH3Wk?}pluP?A50MmVuj&1ONGeh4~ez1jv4g2@hTk~Lou75ZKfA65B)o%y>nMAYj1;g`i- z&j@&Ro3o3{s}mzA3WhI_=27)939M-rAPRKyvJ#Y!b&f55)w|3PI?c7{u&RBtd-fTp z2608!Ty-3G8Axw@yCU`(^YK(YiblX5zF64*Kzz(~ zBERDo6+W-W)m(*^2 zQcun-SKJ9UY`oou-5Ag6BV#Z*IbeQ{(I}AIIu+emNjcCvuzSH7H%LM;3r7 zUi3go0l3*9W_xwe!iL#muoG!BlV~#PZIUx()bF*}LDu{#_X!o8exW{ssea|jTo))| zBO`*ue&geoU}vnjJ+hQ^lNn<@Z=hIakf6D;yvU`QiG5$S3&42F9k9l-X9%cF;=J#l zjvt_hPOZwPPhxQ8GB`7c>wj$C1;bt{;8_4ux5jY=LGW<2be!704{W^?V|9o@gqFA9 z4$@@kgt|wDg?5Ty*yg0ut*$`}tpe?v`|H?yoEpg? z(jFUY2fwoV#R58t4Hu5$xHN^F4vBRO^c5AG^FO;EBovP5I?s~Uy3B~5;emP_y@U(f zX_)@#a&MeUb#kcEC6!)uwt{bkWs{_2` zyM(od1sZ35bjk%8Y1DSD$Ls5UrLil9z;F&ZAmjrf@-F_&?`IdD{C+D56 z9`rpMn^dgaf#i>A>bLQ`grMs#;RILMJrRo?1SrQ^?>tdtLHM*BLUM zZNHIAg3p=+O9=ABUjUQ=lZ?(X-#y}}23!q7^)R2N^3=SK)fV#4Z&HQ*frWf{5_W1r zw60GMN+!cAE5LMLJyNyP8NzC~*-c5OQWU`uTFkFce$63Vs$LdI;g?Ycq4;;vWCAQt zj})=R6%hBi*#FK4v9ewk`UQboO4zd4mJA`j$%VghL-=quH#!b9Ofhn#rYLM(`hppB z(3PVf!u_wcq7v~o>4m2nj0184j>3!0JMLVxUTnB$zK>dk2o*v?hyYs%AEx ztrQD`;gy9ksbdW9X1o#fzMeCzl5Jt|!gO_45PrNIOOhHY)otLmao&@t!QM4Y?St>> zIq7|$X@IP0+8C^bWIk?CJ^XfZ$6PU9y?P`SGx%xry&40F%hCKB-!9U(3TJLopBPF&#I`C^Q}f_Q`rbV0iNwiujv3%>)TU7I=x8>GklEU3@)A!G@{C* zv$Q5QSq}d0G7&BGslMxnGk3*ec|D&SEibYQ(rY1`HCVKR^Q~BDj2`DXYOI^PgToK| zp1%%mj-h2`B+I`)1vQ?&Gncmi;SR9AM6%k+i4i2{?okQi1hRSHNZxD>PE-%?NMcgt z`WCYq8XW{Al-N!eSh5X9XNMI{d@*9H6BWAs%a0B6L3^7+dObnN+Xj2@8F59YSpWyS zenCMe;Zl3a#v)SN0H_l4&5ELV)hqQ!xwow>TBW}Z?R9V&)$stS&(tgJby}>O;7GNQ zL|2x@B|a+=KDT2JTvmOhjty+eTryWqXPaNFV#1SpVLhd+PAy*f4%46Z^9M(p@`sFf zN4W-Tr837fN)czS<{c(fM6Op}p^q2ucqpN4b4*ly!oj z_QzSmiSUR->r(#fen86Mnu&Ng@+h6WPCgQ^ljrwjR;q81FE(OzXwPS~y&ry!1*7Bk zV8Pm97Ue(WOXip3pDiT|dxl5K*9|_DO60E~w%@I`gsQIzD_2_1K&UcXn6*nv@o-Wzd($i`0qVtEb;Ex^(X7=n|3J?zIYsGp^D??y9HH&uNBO z;8GQka`BL-1@vVpavjAVfUu<=+XQ$b%BcQKqDn6_A54G>OvjwXhK;O zkpF&!L9x-9zb{agQKR59<>o5hq33oN0~9=J6ElGX79Ao8_$BkOL%M-~*e;%gMp|S3 z70V+>EQO%N90PjXz@OL5+6@DMPCEKrxBHj4I<0D@G9z|I{M(5^Ic+K#Za~oo+}Ff6 zv2E^Ls`a&Q!40k$AdmCn3l8@=>ywpRk;B%-F|an^e?H{@WOY#Sd%s z2Gi0~@q@C(*D=kvHOHRhip4cC)$(CG&&B%r_<%TUXgpO|%uDVxvR1pLm#>L+4{T8I zqjQ#?w;5hJeMe*sNshYXWi)YPs#eV!h<%^8bBapWZ}YmLXnTytGGw&ra^#O*$3a+GO-G9==7_^-TmS`_pV zTFN8f6n35Hl2zam-_Kb{_9`*r-ky0dmSS2Ud=>&2(n%^nBJ?YCf2E|9yJ--fYfy{XoRO_pmCKu7PB^#`SJ zbUutzo~Eq-dsU|dJ|pdH$%Ii9LFt9yhGj607hH$)~d zTO@${gck+Wq_#bS2$%^znSK1p{ZpJI_;HlV^AbGgdSY0B$c~L?ZeqvL_vu4%vd`6z#Xb)MtyA`zx` zu66?Um}$qM01EGzdchFWr)qbHUVwR&S9qSycl1V6+f1~@X>L{%L|CGij^G~H5}ZkF zkg4XaPy3Hu9*+X5qV!G&%d&a=mpA{#r) z`zdu$o*AfQe#I7L(17L*&hF`hC}3S=6KK76*!=4$yHSnw`p|bCQ1+(muG$8;l#=Os zyY?#xG;KYeHC_3vy4~RVbR8BNn2=y)NhTJ&c6VZqDxHJ-8&w@GSFDg7Tx|Y%QNZ(1 zYUTGsA*vLUQqe~t8ME#~F6-#HW;n3z@fw@Q@LP0pYAbNolT;iP4MW*&2FP>yZ5@!} zON21Vr##H$S?vDgJcyahvLIURdAnsA3;jjI_`CL=ct$LAqdI~{&;6gwa&C0`Z3Rul z;-61E(n%LXKu$`H`c1~{FQChdka^6o{y`|^wAck^-6XpGEOlG|TA(&0-bLH)b@Ns=U9}^lX`@RxyKnO=_piIU zO`n}J?X*ASFeOJ~2BZa9+= zs8ef7Br7|PwcHvV(;vg|+LE$DpobxpF^Ng%KIVM&bz&(3Vg9o);M}&*X_1yQt(y#1 z-i2fHp445FtPP_+T)&2+6%`erv~9J~YENDPRPtde-9lLi{`3PwUjfM5FS$yL(lQ! zYHe64KW=gyg}UoL8|&vFB3LDT@v#0(iN=J#%O2U|0knN`wK1$6#n`-zc4NnrY zPVUNwj~;%1`|1A)d3PD-dyC}KT{Ef3A&WKJK)H^pT+D6waC0(P!?=Kh8hK2o`P-); z_`@|ZXJDqN(0JNYzT@kO4FfiVBiVPgPklhc*_F(!qz3RolLLO4t)nVQi60Pd2N#zp zw+wHqC@BGAv{Ew3m~c3-^#@R0;6v&nP##hPvfWE<_tnN9eePg$kI_~&SWo@+>{^cO(w`iMaq)aL#*flf3Bc{>o?MQr9@d#YY=|*1-GE1u<$h@6qiD z8iZ+stRsL)%Im?q?1@2fk9Y4InJ@oj#^0*W>AV*^e6Wuk$;1LAI*d%r%yZzbOqxiV zPuh!C%?u8DuN(kdD=8GmT%YXZ%0W8@XAyD!Y2v?v?G3mLrgEdV>z ziU};w>EVWgki!kmX>m{O$uPI4s-v!Iy~$@RsUj{fa05RJpsbQv{xSW0CiG-HaawJ+ z=EW8AD<4eYd!1Zx~SLnysMarRthJ zV8|aq*-RNDnJ?9=HEoEPU;mW-N!q)ZeE?H@Oth?VZ z;#XlAQU2_UKJJA`1C-n4U>Vhpyb}fj%w=C-R|j*e^#Qh%?OxMzrXF^Pl0%Qv@1gK> z8zzG>p_CPc@_H{U=Z}+_0v7Y-h@`3lr>hMDgMPe~l|(=n?ng^&rx?PP(?t9LG2lo> zaMz<{R4X2Dl~1i(q*75I66=Ek-EG6iTO6g*DB;?Yi)=n;{5cGwI0n~`J1)bL{{??p z4}D)aG_HTLf}@I_|6k$%h3our;7DW9?~$;`e*HU9mrc|R&O(*M0i|!;jbI=D4+ttx zc{FFzp#td>pcmnv^Gtt#hp}I{;yai7MA+pR_7lw#AuyAAO`)2U;<>20s;LuXI@|(qg8Y#ffOC6Y3!f`Oo5(7 z5rY9XAAiJJvc2r?GV?_*Hg?WtMcQ1wM!*wBvL=SizH{1~Z+^pipa39zX@3kgc&fUS83iuYx2zI^EWgnp==TXR!56 z&n(T&d!Vnj^=pL#2tfAmmJfK0>bU?-(BXC%6BU)D;0`3npjHtm8X7}5q$fJ}!gqX` zs{+aT&yDZPXSlhL!e%KQu;qE@5f6Q|UqwhLD;WHSAy6P=4o{-<2HnJjcp*Fx!tpl< z;zill>S}b}4sPp(qC1RiqheO|?pF*pM1`j;TmFPSz|e({uD02Gpifq$i#sloBg%=c zNI_M`mjCExf2Q}gY#E`xr>pm+J_l2Ex5@mj4^gSk06PO6NGrlQF}@k~0#GhPyR6p- zGdg_&{*+uHUM7G0OIpd{kuXAhSuoMU@jdO3B_+==0SypO@=-LZ+i?_8*4UR<1_oU~ zlWuRi|Eo$Ib7*9#3X;de-N6OJb%N9dz{f0YtdYkBC6^P|Hjfxey^$$fwfOS^ms-*< z6zG&5i1Q!?()?@3Jt=M8-?EjkA5`Q|{O>9Df9qTRA@2QOgI=IZDpwg?h$G9~;t?3k zf3F~bUg87QDmr@5R|nCQdaE4&km3dIdOOmaCj9G!qmU=^W!`0>wW42>0?k;zcy5Ls zudS(~3$?eqK3|%-_6m{wp_}9%VVBjO#}v>Q^BRN>pir&+1_vR8bev6G$VKm3zH`gi zQ$B@BSXh^vmxaLUwv^^zyjzeY*yw}yke{8FM^5a;-s;w-%4k{aOuPJI&8#EBuVPhK znfw^YYC{8qjEPSrFwOIW#EmKQ9J^oN2L+h(RZ?ff(!W@6A7rMVZ< zDvYBoA9(6L$I5&ddhHu|?uhH{U5xrNp57_@V5a-H7UCN1o60g@v%+?)iv=d?y9>w6 zN*5C>hONDmTsrU<1`#aIUnt2-L9kLnL^cmyMb+1ra~OH$DSvB7jS1B=2N{<%G}r24vdcH2omfh3T=LZ_dig z+iEA(risbe8FJy_5cDnHk%ib^$h#BQa&ZX*&u?fhHLBp zXy%&(G3&LKue;@HLEmvrI+gJ?y#M3I^y{RjN(^t2uhrmlPli5UPFY3y%)#c#d0QSHiJ8L1WJb&VPeGl7~ zoDGus|8)_}0Nb`+yf^P>iud_Uuzv*yz5df|0~>TIPoVrk$K~^Wc@ie<4S9_}Fc)m3 zq*U5*YsXY0CO7*#Je=L?7*9#n3ri89A0>hevBrdeanbv%Inx{t&k4YppphDwNEpF z-EEtAe6a<2rqQ;0>XpB$qkFL13(M43#Pkx(2+T&YSibdI8>buT2N=P#R}; z-ywd6S9r!HaK9pUs+I51|DQh0LXNc%%Bb+l3tbfmwCTy;eI`P`H0P$ zXCmA6(&rPQ6{hXXX{{YmFO;5qn7Ofndzq-xc-5!>p4)fmBVUNHJZbdmRUVRLk$?Po zM!oySXTm+kD2nvQkU8sl>33;2AK8c+)qm>}G<4eM1ogY0u1h|_qLtWboCIB;FWYWU zw-+|r-iIcU13dnG1kr4`tQohi$7Lk7(%D|9>bYhB%LS2mU>3ixIV9*JNR@Pn5b^QrGNv zNb>jj>5bJ&Cdd^@nEM%TuJq+Q6f)D8y}aBmPNG_MACF-3Az<1UrXA6GV6@Q*Z?%Fm z?(7N`iCHZKEVt*eA&~l)1bY3cImR%+hF%g)FCK%lKasEU_Tv*10Ae*k3+3TsPKoQo=-vAq%caxtUT5u_F5l48-|r_S4_9U(_?av95wEXX{)!XHoo7s< z$NQPlsg*KdknyMO@m?S!e}dfh1cn0Y7%;BU zWD0@&`+4|Pn#@Wv8 zmSc^k*KrO)3<4su&^**(praZh<&hc0b2dxC(CPP}?kE4CkKP(edIO&jcY+(9IaA7- zq0TZmQA`>3RW@NoWe0~0I+woSK~55=mpqw&v1l}0g58p(LE3h`xb@cSo{DA&W}VUO z{$^S9&0nu`lYW|xU%ONAoK}5B+meb_%_h=%zjnaTCcF3UVBzY&`=3Fvkv-{6BbZP7 zi^zGx0r}MI-e>Z-jmj?=fCwIVjPpHrccN(j1uaEp+ZG@#z-G_GNFC{62QBJpF(|H>`Z2pe2>3jJI zps!!6*cV685hGP4@+Br5p!2r7=#-y1Bi5Nz3qa2{E5Q0#^?j9Y==r%6@@7PGX#@;i zCmftz)-yj6-=UNvLIZ$nBt7jf_oq!}%NAaV%cuqhUmG?^JA3@Ek{n%8c0S~SIF9~IWyGf3^OqV z_CyZ@lZMg-D<;^ME{dTSDl4nO4%mPQ*sG%y2qEFN`8np=^iW1(x87c%zCLTok!z3x z^15wEh-*~Ef4$*_?ibo=^tpVans06lKKWeoh=oq^cfBq*h~peYMfGKNNS9^W&hq3p zx1B;(P{Fi zaLG7+SEJ;08oJ{XOs?S!pH(Y{*!aZ>k8>9UCI%J5m~@+^D!Vri_=F}Ru+t#W4PNh4 zIAYF6rO7_ab&f==8`bqa<~GGVSpe*=OA@RGe2pML;z_Am`iIbbGE3z~@Tr2s7v=+u z{&2*l3nn}}ZI+z&_wo7_dE7lK%Sz?i?C8{6lg5=4lun#6*of&Jn@yM~Ba z`6W5VANu`imZmAw0F%(0!?VYkOh93XR7Am8lX{Q5HLrDev z10z7sAEgPC@p<`qC4Fv^3K3|a!r4~(QXhYr(IxLGBF*N1(5VMNjKbZDOj5cGaO2p38?jd2A?F)Gm=W)(by82`9==pdEOY>>E>qv{$*YDq zhw4jdI_|tjnTkZCcP;Y#DU$$Qh@x+*QVy@uqv-@D~wI z#`?`ZQIcehv6{G`U(Yjq=nlG*nV7@kpxZm?q-PBA$mE_wuGhq8>62LkvBJx4l0T@_ z#Mi70ul8#`NxBRG1MMy;yvNz{%uf;e7Xhz6Z@0SoV{L1qdS31UM^4T#rRrS-Zdo|X zT9i_*v0lTs75W9&!qLR(4#^Fs9_Qs-AX)%DY+7Kn{sWjy=D^&A!=LFQ|I(D@xt~4% z){1n>!c=5gk)OV{?O()YR6_SeW9V7^gCQ@vs@UvcxihK<=jwNd2tc<~E6qe?@@F3N z_hBH<%luvY6L#Z-H?7zE@D&*YiUs}dcXfISK}Au*J)(ulmG2B?jSh3Jas*f=hGkg&9-r1vyxA?qHK5;mUY9)|y|l1XMK;9SZlM7@6LE z)gqpbF|(m%D^gKzw_<=!*$hez>AXO3{wEunGfs_`fq3xZOlevpjtR zANP5>u>l1o)^0sMB!GNWqY@SDs82lec1nh>!z(>c?7(U`Q-?${HdD540+szNQ%@r7 z(pppj{LZ)kUeINq?r}Ab3k8oM28N!NYjaPW!sb|tl@Jqr*;)_dqoW0_5R1Zq0 z@P4=ugO)L(&Yu6zr1fORBo2j9(QJ}MqkS)IS2{R6mIgl*kM*{!;FN$vTIWN%oP33{6 z69i!J^C~~=0ZeyEx3hQw_Y?UG0~XfwbXZup#24vPx!^n;Y_@Mt#5?c4%@GSCn4fJ7 zK*JpRKYsY#x81CH;fc_jN9Fi<_-&{9h!z^0OltZgIP|8pZ=}h7x8`laj8MrKiWhMN zM*Prw0%k{+Q47W`&-&4JUtxdx2wrJYfsaDuO<5ob>ly|;X$IHUq7U0w8#yFXM+8B ziAZH^Vk%aTEfRjYcd|vJ-Z%DdEe#p~{QwqUVLdISFGH{ynjKc0>jQo7tjgPv8*1BM$U1-6x^1i38a z%aB0P9`vxYniLuE?jpk;&-GY%ThneN#ixZ9B#+7zDJT6f=l9xCh)=7oyLnLwFP@K5 zj+3j(NvWV@eQJL7aaSez4;4Z`8JtePr#u9cjhl9;^f?DyM%3hPiw*VEW zVGR?0>gfw56tKg7ws;}{06l6wC8E?P#WbOQ7Fy zsN0(`GM&{yf2$;Pi2*d91}Ew<&FLhan@&5n{(e$rfmWb!(K9UBE?RI}UJ4w}y}Wik zoEOcf@E51RkwG>;@F=BPtoMKU45-T@xa)CO79<0aqNj^&-S1?x6OoFP*ZvtMC?~0> z3M@%DZSHW)CQ_3r->Ow??@j3BROpck^Sn?`T+WxW@@DWlqTJlDSgbZ99!7Z0m1!8% zA7S#gVI+U&wKW+0dN*Ho`>F<7A=pAOQBy8>l+j6mYH#3ha2e3T+!Aq^U0uem`l@O!vB!G~Ekb zNu$2laz=fBwt=G2b|ud2s{^sl%P!-Eh9vbgSa_@kFbRaGu%2wPo3&?+9t_1? z_kx9+x&dAY=W{ML9v-#b{6fBWtY2O`zSBCl%^kIN<1qr*F@E=PZ2Z$kgc|CghkS2U zS0lkU&c4ka`C5z!Z;H{2lT&t#uE#&>F(h+OL)qHh^LA`89ta=omkwvY3tSapJX^2D_UoWEXo zC-RSjkC@Z6!Xp>g6|KujZGAnq z?W5j3-0N$^UBPVL5t0e*M;eF=+4Sw~oBXw*k>R@}mDsg+-!W7`Q$~k`Em043bk`Pxb+>0ZFhy@d-n88NJ1h&1N%RNsZ%NkZRow5~ zI1+g$3G5boe7Y<=@>6wZy-Q(X-Yg8gm*0zicgu;`a7~%V z7;Si{j=R(FEvg;ZxI=?^r_BklVtBA+%Sk*>rO|NOw|!o3hCy5X$i2LYV4+uFOQO>n zpzP+}-RcUWRv_;&I`HAd@m-!Wp=-UD?T>LbpQVa-*KY~KmRHAqwLr*)5L&6P3jbV> z)>3FOl$}TM^6qKL?}pQc&tLn*$W7A)H;Nw9mtUr;cv9v$KLxe4TIaQ2U{;7BFi9b7 zG)5lZgXP4uMUGP|>{>CC$W_-@z$Dio;cw!kP>f}?L)ElUwvs%}TiWTaR%|p&A z{(cyFduq4?o7?};=&I8OXIm7N$cQ+;Rwo+M@CBg-UY_(bCcFDe*!_(69xg(}M!uT< zbIxUy*v3xnCwX`<-bokPBVjxbVqV~arsuk6r1Pm!ikG~3MD^MGy^_ibWCPT$&mNVp z>T?yc{{lt~>{)U_H@JnW4G}4+u=e|hpLD+xZv^}vKimLZd70$O%@R5p9I=m69>R1| z)KSGp!vLn<^!Xei(Nlg3_WbldIxYd1hWNv1zQw_8PRyS0=Y@10`|ummEQ~J$1k3%J@1VPS{eT?;EF`VN4wz z^9C9y*EPC0!i@46LTN0iAPc3=VkE%d!0seic*Edpo&~x01>0 zN&E(bJjpp_l47o~3y)QBs--Z6z;Y}+Zb-nDp^$W%`_SL3hO&cC(zXybePOYJ89taj z__!apjV-m&>Z_wKW1;1{Y!KI{fAQ(9uf%pDY;iW!$jGR+GNDfJgo{c)E18qKK2O|6 zSGNe3zDIIBaWG@POIq04`3^$T@m>UiH%EJYH|-j*_d(B|?lAKvw}w{b>9?Fqv{HI# z)i20a;aKv3E2NZESe)d5U;If*re|i9kf7gF7J}Vy0J!^{gy)W^m?;#XS+H%H7x+vd zbNiRC{hz_-e>MAT0HeTqy^V*3saVL(QYW$}bJU+;NYB>RzA#vWMa9l=MM0tzhm*9; zE^!&;iA4}OnMM6`Y+lSRd90jv{zL^VxI6|XXkm)py+!${JwJz`!I=q^5b+it^J{sfy?WNfLb=Tazwbfo+M?w% zx^Iz7wV6_A@|&-fV=^TlSNiRd0;p#xd>+V_BT4rJxq6+w6bU?bUPUNgxsEz*!fuMD za9&t;46|53VCu$fKrx!FgmC%0fic%wSVY1}B^ztUmS+jx{B(VA`#hP~_u*sG=~B3I zp5U@^`e&IZQw*owK)Y!JUvtl|-5INAc@$!Nh+=%+ta=^>=A)=ym%kBy5Q#^S6z$Mb zP#@x#ymo3&yl;trheZ%L<8E(so$Je%DL|&J-VsNu#_(Ftv7K^^V1&+kVAc2C4XJPc z4Z;CBuCvHvTSh)nW41sc+|h>d$d5HTfqo>u{@eKZiX>Jh1j!)kj|X@@jdtHV^Z8tq z_eg%gB1GR9pM+C883Z$_vm~up1VA_?0S-ZEDNEyM%`DD#oa7@DUq1?1H7T`t4m*ir zHFESXA&maI$WwLP^?hTOK*hY|(Ag?BM8`b({Bb&xZT7|?4$_!M>Li+tY?YIxj#G{0 ztg?_+)wjU_Qb6y1i}LLnNukETESdQwW93TNv+9y%=H0bFy&(-F)zmjj+gM-&w&7C< zb83X91}SZs!>!7Cxp@Oc?J{|qffiA^zjbG^hPv18FkfQy(CFj+ojZ3Tw9CIDYl$oA z#l@z!&F>6?Te}OIIK1As25v*{|1Lys;I)kW*L}`zBg`__19~zZcircYV?Q8TSU>vP z%>;pW@koI}vNM6E2ydvs;v$J$^g@g#nMF{SoXwNTsknPu7hgg%8zt*DOBlXIO@!j} zBE?$f1=C-_@^*q7Qh=-x9FtgsO)UMEesqFfNXL7p`{CQ}0630het7lNGRc;Jyv~^2 z$?|)T7x5;apzeyx@!NV+jGJexC!N)4xUrivr6Ym}t4X8BWC;&H!5!q%=EZ{WdhYR8 zSvZeb4c=wi*(`kR6nON-Epi6am>+9}mT!tI`0Nm(d_Ly=N*8o05_3HYJL4f0`tDAD zUGP+Vx`x3#4e?^|{aWNmw`m%~n^|xTYfZ#E=w91nm~;9akUT86jtVz3S}_Yhe5tx|~otiL19h;tfY$w)4Kr z=Y`ylkWcIebt3k!sLd3xlZ*7pEbm#-n`k1ty(=8yTouBe4!3)Vgc7~pfaocfZ504f zpwj<)J96suT+*1WF7TTgW}tSP6}+iXQz2g}pmX^148KXB5AQkQUCO$` z9P?GCo}$@Lt%9knWP2BTEj1XDP}V+iY#H{|gQCL+v`@Ya&WgC10Zp%aXQTw^^YBVtsDUOvt^;_SqUfW_ysxWQH;5 zpV~&g@~EFW35mp6VE%T@5PH7pIykU%q+4^9cAjoyCT?~@(3%EIuh|ybBMq$Jj})zz zBy*(xr@vXSpQCfdmzm`b=a&x_PTJ%?tAkt?!mc)Hogdt@*>EWta(F*{(q(~EnV9@K zXi45!$DJ6$NXEMKX2}{lDW97o~e~#ko@exghuHb3(ud0pn1)RsjC+Bq} zhlcQb_j>i9r8ep3l3-G74zb&f65zQU!#>Mg$zaT!PSqX<=hd&Y)djE@ku z*wNU93MmE7=|8xCUii!b+Hl=wh-h0b^u(enwp6&w29?ius=c{o82VznI?x${VP>(l z_0iz+JrB-FQA;Xu))>v2FKxF-AK?z=d6#qKr3doYs?B<@RG%M#Rl#Urt(QHy>kY!= zoe(5+|1YUdiAr^Qw~GzdU}0^BOyI$GsfBM%uxV{&Ln0REVIqv+g zr_FZ`IyfOI7xYAUQfCSnDW##fVx1YJPxb}!U*vQ7MZ)jxNgZ=r(C%ltQ`2a4bNSyB ziRHksAwzVbQq1Si!@fCO6{~SRm%vAD3F$KZ&PIJ9)?_LLbZ~Zr?TTmf$nf@fH?)nC zl8)WDdN9l&qMt?3>g{Q82wfBHu$COD%Gmq|bddX6Gm*K1WVFkY9n{ zXM*d3VH>NL=?C}fox9Iy=$bL5eMnzl$vj|*RSAv~K>|)oGuwbc*uky2O_a8s%jlS( zLyyhAsS%MwU_vte+q`5T!_}Pa3e9Gfu-A$_%2k*hO?J%-E$fL@GFxS{h-~@O>kL?E zyi3*1OC`NrF$i4MCvN?2E#IbjdVXXrW|7_28(YIO86Yg@bp+_S216ay=3(!iL}W%( zPGK*F5pmhl=b+4CD4u!D*zT1|mHy8<1M>`D2ZI(e@dxmFJH~R7!~u(|!tPN~!MhxP zlHuexvs<2~UqJj`vGH-2C1W{iSmSVyi9-bh=o#~OW3zBR%%&cbFdqBTmqA=s3Qg^$ z?loUD0HYAt-!FDb2pm^spoRW_NAMSCp%91`ULO>VT!;f+~wP5KKKaZ|X zCAIt=AgwtHH4t2L*oWaYkZ!BK#NVF25B+N1^60Z4S?#Y2kz_T<89TOgB0pL|?@o}j zzTlVv8N7b5T=%-GoYG8RIM6k$H( ztSYS{(8qrlU%3KpYif*w)TYJ1;3q8!1j7G@vr9mU2%L`rW8YI_k1W@V+7lv>kD8(BYJ~p545#ubLm6Q$_3kRNTdM_z2GuH^5m_76B8l0E^NyB_A z{?=|g)^hu&4CxSuAs@DCH-7Y)&a}B>750FxXLy|WcwFN{tc|>o8E^&j*mkjP^D2*uK%)Iv;X--y)k^tbABKW^53sFc@A3H zYFTXTdil~{!sPFLH^^0=%8ha2`>}94+~Q17>G_z=7rPTcw6*lo$6gI}LupwJ^73yH z8esI!S&?3DxI?JnZ#C>*U=t)e4mEKi90FS{#%&pYQxclmcEWn9HF;6VUyreGcavvy z`4eE()L6X}q&wy-#i$W$F^b`JdD7lv5k0EklDXsh_ZP;Ox|t)H9w=bxia zclG0-#HWS&s3X}IJH91X!rp`NTkVv_V@l)QeloP?2>Cj*M?LYcm_`K7-_Da0;F!DJ zc(S+%zG4WDOB{J+C^qV2zz^#2LsfO0_7!>--12tiKz9pmA;ODS#M_TPk+ zj8<<{t`I(|Dm99IWg&Y!oS=y#LnL4AqSbZ+C$#+t-8|6f?NiNp#`~M&3X%_xvK@i| z2V1`bEu3L&Y$#G_(&&#;z}RsB1gUqDqqv^C0FOxMMtfIr@P9IRcz7Y zZmrIB6SzJ(@b!wlD3f0#*VOPj9Dt++d|qWm!gz>w?VLrj-Ku4A8#Xbgf-uf$$y_P16DuUPxB>kW|9g3fi_b}UFTWtz zyJoEW7O&4v(tlp<`3Av+10YEfgdOZ6wtf_yZipL^jx#b`H~fEZz49}*S_dgic&Y{n zjWpP^opi2@JtqH0p;2D-1&9+%e`B0;7d&5IwXy^Qr;5(DHR=EC%QLR@pM#G64*yRS zHure0IbWrBKy)f*C_5eQX(qcwc#-*>J$-S(rdRWpkE^>+SG|MXEm2s0wIvwfwHZT4 z@-2jY^A>Nmsli_tD1=3vF*O>#jUflk6P<=VCA0pv^*&s;-C;YR$`7kU%o5NJ^SbV9 zt+K zX#06c$xY$&47Mz@-G|O`^lMiJNqkf|LW=g64y=?Aiwbk@n*m2 zV2R9K@NEeKg-BQ{F1wKZ{|`z30(x_ShJ#KG;qHm7;$+WNAPb}J;yP{!{DV0wi03TW zwPf>t4Wgh_X&`{>&MErY2(C3GX77BdO*9oVeNShqPVtf5n*n7B^LwMM@cvRtPR`=I z1?)pZVOL3(8zVktsN7ArGlTt5Fvp$c?7RI46YFp{b+?+}v&xEE1-v}EgGoP*pYeHH zby!&>VV#k(9tQG%R(J>-t>~}B^{)EeVe`JbP8;~pqB8L;72iYjhR@{FmGeS44lkC! zKcS@OfhEJ9nRiWtb>US;u~rfrxqEvU3Fekr^Pfvj+@Ahq+RwTtO7d@2N@qFo<42D< zC@p*n99LYN#=R#USCnl&b$Bx_TupLOg+13O>~_Ro4|9)Gx_)}}0VFwlzHQ>!2&4jG z%J6bK*q1O|8rB#c%W6dR4Botq?5o~^gB2b{_Y~>~)UreMVVuWLjtM?X-QBIRi(M29 zyjq64KRP@Le-UI}DQvcMI&AeM@%r5+EjL_BoSV3uX9fl2ozzpob-ZIY9K;1-co(_J zZuko1e47vazn&;5?`@zykN^I)X34)NS_b5<|GQeLRYL>Uv4s531p_=yXJu0#rY}_b zRGd_gff!ylxO=E((ju7^=c}bWX0~Vv4w`CUDr$$lXFVq2W&*x(FOiK#iZqYbEp?0P z<&6A{MIDxm;DJ%`>Xfs{MB145?DStN$^{feryUYF>dPO@P(idxmB(%1FI&zQtmRGR zeW^KZ*dk?zz%-IhaxAlWr)skD+X`}HCBK*Ai6)k#V~VFC&-kN{p^!Vn;K^!~;&f=K zt1y`h=59v0>3yMi+uqq)2~ya>|BJ7$j*F_@+E!E)K|mTL6hx4eu7Lq5r9&D9>CTak zQIKv)>5%Sj5J{;4>28Lup$C|6>p9PPpXc|U@BR6ovfZ0|uYKQZUF%xc)%n>HhoiPRvmaXY+vlt7TNm~~Dxd1WA*fjY_US#W9uLfiM$)Eld7d(MW0;n{$ zk&+pc$Hxt&Z|=B}{o~c?{C;(i8e!i6Jb>rH;+m?}EF<&dJ^F%^RshhrYFkNc$6S&b z@-lCjo%K2*Yw+!-FZ-6f%<9GPhAcq~aL*%}Gjo6ITAZRVh;-Kah`8&vFAPgua!C8! zDv>)iVtljXIdvBvp92tWLe!&$?S;>k6Gc$^!K&L9@1DEE(_V^|B;zgDjRgo9!aJe8 zpk?x)ORuF2FQBw}iBL6N*HuUoIhe1Mx4A&}Gjcworc-B?5+i;p@5DwI-n_P~b#e6C zRAR9RK*%Z;drkp6eZuCe_P@D-|4yjJTVDdX zJ?US$z2}l!dz85S?r=MdyX7uCC5~O`2a?Ohj``W{4Ej2ZV_PHl6Qau4NtwaQnm-E{ zZj&Rg^aCo{vYpobs695_!$Nega^uZ0fSc79qh#`u`^N8@%<9?iTWs9F%_#Xv--GNk zZe>%X5Pj_jh(ogTiWDsin|l4+NP>CQlV~?r?-eop>48Ll6#O-h^f&Z1E}V|;)%(=a z9jN37UVw8@_#m}aKW^6P`u<`)kc^%->*w6Wn!_uu8bE>-z%?aUEf@@Xt=?R|dyFN~(e(|xEbGe)G`#uM2uk#CO>-#L6y{E@$}Ui6HSdF$7w2L;J(GHR65tlhSIWsR>+IUOL3P>@B@Oo!0SQl=N*eqiN;;G@C(X|if1Pv%!G zjn|tzdzL2LKE@DXf4;89B2tE3E&b?IP3&dG`d!@_lw8L;p|lBjsN7@VFz!0vqUlH| z!Eo`%)TH<2=h@WDNrxrM!}_lkcE~^@BIlY<`{QAg(9I@mAyzBJ;ZsscVLaXD&wiQU zC!vl~wjuMDi@Hvsj)|wDDfy`nK;C51c98DaZajO{<6M@NE;VZbFUsAfF4|lzILjyK zSkHE{dh>A4969z`;9;|?LO3hc3IM`tA)sgby^QDNLcAH@3aP4C!$$N-=I!6Z$i;5r z_l=9dZtsp!P5>$ER_$vahqt3{=o4m5uhm-=il>p#F{Sn!KS?kyM3APjL%05D_1PX6 z=0B71ER))$O#N6~sN638rBT`1>TnEnkz0;rAc|eAhg2C90C>N9uU#zFbOH3okBDGW zEXS{B677fWOE7zOTu3X^wwG!ZIxzt(Eg^&vK_#=f-O|B5Xl`|(kNGrVEIy(bTOc2N|zW_BcmOttu4Pw?&+M=>fdW<+m& zXIvAkL%3SLanDA%n~5$($uohj>i%i8Kj3UOk?&%#Rqbe?Kd1~Ld=-Ezh@ko#N zjzTBgA0;t1KH!*m^?SUXOn|^FJ|jk{haooDbTL$x5#jTM!=69Pi5zPDYGNH4c1MU& zj3_xxMQhZvMHZBNx$ren>l1V9h{qo!!0)Je?iM%yMiwzB+yPEg8wuyWE1)5BZ^Nw! z)!PtnPZemoBE5`PT5i4yxQ`F=W~MZEoF{r7UyZ1RmYUxWvnhOOG}P`=k%!bAb#(dk z$;&bIxXJmN^WG|Q`Z&sojr+sX#88@N+gBrXkB1y;NYc*YoOrLk_dSond>Wr6kO)M4 zxCr5}X#sSJm6bcMfgW zfJXk?%L8_gw)XqK3-4EwmfQI(9yb~0kmHn_u$GJI<~mQP*dcP-%RS5v!Yl5H^z_C9D9yf5E-#;s-4G|9q<7tNOP^K&xjBTz|Jxoka5PIl$0RFE_v4_`xSH{XU2;P6*@!j8Hir-26Uo#IyqlT_G zPSQ!+Oj!dO-kksKhJT?O|LZay1h4*a>H2@W)&F-Lz zeA;RwUfA-Ao2R#d)%`E*2n_sM>;knHS245e%wXpKwzhv?C-nBgYy}?9{o~$-fxmK6 zec&nLu?9~Vr<+hmaDpQM`2+6uZ-msO_V2L|MBl#<)ZdT)CjiULk>SYnnO$U$4S=8g zPjsH-{{~1N;@}v&-eml)0R9~h|Nh+aI}fg~ImG@Iyy!|yfG7U@2mbx9X#fB7reGKI zk}oq&-NEb_|LtL$tUdlTxBk4W|5TeQAxjqsDw`69tbur#@_*ZM|AXfIzVJ7E{0Ab= z2HroMI2j%QiO(L|ZdnfT{co>HI@Rpp;M1Q70KU(^e`&TrvOgGXj*QiRzH0FI9jw>( zPbco^-}@BsdNE}EPIfN)(q?IZ)lWSzT= z)b6hu9s~O%8)QzJV~Me;#~q&tLxorh>!4&q{Y019h~HITZYf8qDnKxsE4RJw!kh2+ z^B3`(DJ$PFFb*#(D}#A90rRz)fHzy7W;y5jpw7q7pYN`@aB!5HFU;S)I6?pvBdv`M zlJB$i-k#B4m%@lwy@=07U>^Fj`gvDZUruW`V(JOTE2CYeP+dWqVut#xbl;aaE%rTM`x@BgOkEAOw$?V*gdbl} z^}FtHk<*s|lao!ScYb}ey(~FpWRTOU#xaUx{TyeqkJHDAe?4zB{?5A9o&@S*BPPON zwE;7LhCpr&J@(3kBYl@H7?fseQqp59UzrXhGPgOqyB{1p14d%~Y3U1|^7F6K(CklD zA^_z&oL==Jyohqb$Hc_hO?*#3>X%!3jTSEu*|tj=oF==~0b*7Q{3hvu9h%|T)~#d_ z=(?{_&!vlmm-p%hhxKgLy*5Q3bfx=jvj>e8#<(Fejta#12c1M4xg7aICfLE#|2+8? zi2k-$!Je=IOh1JY2E%T@N>>UhKR#HiZ0h|V9Ho#Y0HX=jY9#PWu}KFX^xfO2$NbQK zlQeO#J|}cHS>zLec#+!`*V1cW*5Mx>7!7O*LGCM3q7yY~M?yaHDeH3Wg?x4ZGvhw% ziZl6l|-JJ)b^QHv{Z6}eu}A_e|XqW(h-`w{e`$t36$H2vQ-S%huzCVkb&z> zX@QN7(dsyRfB^LRIsO|(Jgiig-LPM4me2V4kF&zv09E56&2kFBO|Wyu~;jSDWU;jHDA#b#hCHu2=#amfwM#t#LP-?b5hQP2lW9#HlGp0+2_ zVNnW`&uT*rlT9LYT4k@m_bsZAs4KLoW6KD4r8@Dpt_#G+!EbL zn2&<{HO27oaPR&mCG3I|V~=`8x#icHW|{p{5j{N{+$M!g&z>nCJJsdEJacU4H__Y_%mvuZi|H0L zmn=ay{I1t6@I2NkoYoxr4y*%d35lE;@YlyDipIt<{8Dl`HMuu|Br_^A7A@X+x|fh@ zw4u=oJ~?rAx#DJJU?5{cqE?nzd(LFzzG&}kcFUVw>S2xtcoFzB&2P^{w4833w}|Px zo{5Nvt(JxC>GReQ24^dY)+Koo>vMYyj8HOVrI@sAjjpX$%m4JS zF$o2I)$~b<`v{l3=w|ik)14BeIDIyhC>@ieW1XuxuCvZk%N^G(+uU3tL&Y5{eQ;HU z>6#jS)D$A?|k)qZvpQ| zT=C4S0T031IpK2SAaIF(>(})c4qwp^0VQafi4whd#Mv;QpvW`!B&CreL#EpO6<2K= zFdI{5*vSO)y2Js57CZX2pvIqhK1s`KH^0EIG=p~nR6U*rj|X6{Q{7Pu@*KsqXVa0+ zZZAQT>-y5R$893}hH`I<0II!ld3}R=9^5|K zQ#bWU;|0dO5L_A&3_CkulHmAmu0l$ik6#Ff;kWxasKGB@>6?(PrFQ8G;W%~YiiPQ$ zv3nw!HdS}gyE87MH4(wVU8lQ3k~3z80OwjRWy`lZ6PzaEmCCjVcG<&-V>{_SKa5@# z;W8cUN^M5byK1OeTNhEL+04|?aUZ{Q-kS|0=URWi{4M{%15F0Ry)nM$&xv$utfQUm z&&U`M-6@S{6(BRkWKbBOqis}#kZ=fnsoCO5uaGLp`l<58u;Y4F(e>vbRbwGfv;Hqe z;_TLWgYp;y9;Z8mPHAC)o_C+fY?%RJj@)6_WI5rg!`@tLsrspU;{HfZmfALMo?b2o zrciya8(E!m%5(_cJ0PcUDR*3>P&Q+C^FF^A=x({bKhh*23iV%q5XRd4)Ly3lC%qwM zSCB)xQL$d5Sc_-+P&7kZg)MIlZk|fBh`04sAqWNtZ3aH!yD)h1dqf4tNu13HGhHhb`XsmK%yKkjLRI? z)F7fUW^2`0jTv`&CGM1lmB;HvAoB<+;U{K#Zm>=yH&cIaw-a#>vn8OUlWIVDVC2zh zVch#jb7?1Kh@^6W?y^vO%xT^aWEesrBW_#t9kfDJ7Oao+*vXFDo4*&g;-fc79Dc5m zD)=4w(t|9&%B6Y}s)qg(F_YR)LzR4L-gb=BD$e~%qX}z%GMS|h16Rm$m~zXac?#Ua zB%K!CGIIRFx-^8f$L#m=Yh_%=mWz*!hb@HsLHu<+Irqug$^SW%LXQU1{JKE?&08D&Eqwk-mi z*eL6qt9o?=yWuZ6G&6H~)vPt&%4{!^Z|9iL2CVW{Kl z)@*OGR;N4Dg9)D>Di~gMBGecfvx3c4KRx4}ipxrV?@4T?QzYU|7Io;5P-(}2BLO3O zB?>f0zqUbx{Zn{m@TT29ymA~R)6~L~s6QfBZunr~aOWMR`=!aDSyBnV*JY^o-J|_t zp&*-HqnR42qnuw0uzNBwECHM*RvDHj?u^y#9LB>ZLJi-JIJ zX4RTR##)?Sjl7S5Nd$AP*C8z>TM3;~gPi=vX6)-A0(xNZPnn2a3lf@N?s+IiHUGl! z&=AILJ1rAx@KV|Ir2w`6g-nLy*kuBDr}S5Vccpo7EO4|DBN>33!eMd59mAxdN}2Wi z_(b+;ytCGDuCK$!kTQQG4xtkxa|`o5ulA#!q*6+EvPbwvokA2 zly@614Z~yNky(mqEu1R44yij}`v)l?yhq+X=p(f}9mP|J@` zl%@V*-d%un#swZ8$?qDDe4Te3dbrRVwL{lZCKAS{-# zk9OosdqjI``y@=HSElGm>&20R2f%$@!^2AE!LYQaQOS)>T*>#r+(GR+mw0>s$ zQ*3=%y9+@gGfbBUiv!z^3sw;{nVHdQ6i;=#=d@0{v6a#DmAT6s7nPZ<^Gq4hx;~no zG(-fUGJ9-}H1r_SYr86QlGlJ90*5)G^q}Q(?y`)`PI%7$>;c zu0BY^7N#FsxiFvN`t(#aj?P=ejT{p-Cac`_QyVKED|w`1ABnp!{&|D1Gda_d8U%ZY z3cKw}d4qA**&1f*`ZE^$$b?Rmea{fP^^OPrW19Q5dOGPQU{M=g0M!8BcNKA&e@(;& zx#zUSBbHP{wTP(22gc#M0kw}qt3I%Dx{{=(mJlrqi=37^DqL7OM~-S|vRpRxtnFC_ zr$$wFEFjXB9|BBeID0=jSH_JT?lWdt&g$}fDGOAJ4ni@K! zu8V5Qg-iMMIw@sa%2!#AQE-{De~)7oGA8nHm(3>p4tV2nzK}b9eIZjVi5ggwx%IB~ z;J|(j`*wSPN4mu3{!ra!X#(fi{rh-SZGg(baA_;~L()S3{cUcd6_lnxj>p+vw+qkK z&v^13%!Z#m2*5Pqhs(Mr_jx%z4z`Ba;`F0lLaomew{CsB^;Sacoyl=pPlx5wZ;;{v^7I%nCVp|MdsZ-ZtzBS(2vf-Oxx=mV0b$ufxQs0 zrV=3UoU5~tUG3AS1~$Hzbglby*guxqyNB{++3T%QSNCK)W}825PuRUg1we4qz53J& z*w>Y49Bl6fy3@1+)-=ksZR|Z7>0dSq>mA+Bja{kfKEW=6t2+for9z{96PP`W(;9H7 zd@T)a)e5J`p77d6tq?yUjVPS3eZN(;$*7uv>uq%spL~au4Wg2?!t!TB%g-8cGO4=Y z+;GyrkQYsb6Q`1LnZ?dl6TlJS zKhoE*4^t|sj;4}c&*ZRI?S5%v5tFHaJiIl)+_KFDCuKJ@T5SRN$3Rn^<8-brm~?pf zhzlb)#Z2DMx%bP)BdNy4(cLGm3@vou-PV56u>fa2!`_@!(`1?m*a`K5B8-AI9(6N3 zpv<-+yBPn}WqEsozz5<=^w}ZlPP&!1xMWj-Vj2 zRtwf(0WC4+d#NLc@=^l&1lScVPGhNFS6CzifoJq!kPU@v9aGjK5BuC4UUsrgIUt0E zg~U4Wmy&wlY7SmIQjpO~bcT<~GA_RdW&rfrLJ&V~Uc#<=z7zNCzt7RNZ92xcD&lX_ z-y=Ncz6+CclvWtF>wZbbv>uBJ(58fPg#% zWH@J|!`S|CV@=kYABGu%yC;Ul=OMsAbdVf=0eYc5m<6!1dRoF|gC0H#3Kpf0$l&bN zl@6WRI4P98hS16N(@?nwwfBwVxR z=p6Nom-7-8%NRl(HxD~~7mA>tB{+exc5x|>ZpR+baSV1DvRO_U@1KNhDarJ+wt5Lg zKHU;t>G6(ns75(Ert6&fWk~Kv)P5ZX`yx!6D-h@EUahxXA;eBF^Ox}>-bs$d(XtIR zLP66D6VjhBvPUyR)RqM1up=LEMw}#nt{}ED+MN!w2&ciXqgE^66Gh)1x8zHyqO8wa z!6#x-r{XfbG)X`v$KjW%6!R9q&V2742M4+~46+&frCp!-e(Bf7 zbhF(uo>BDRXRoRi43ku##7J3AmEK%^p$ot;tQqsJiu zY;}AvV%}>fW|!U-k|Escy70Sufr4`5XUWrlhR$ zw>Ogffrlei+|31fCBhzwK@|@TmRC0|>jy3TK2B9$AE>U7*i)|3g0r$$?+ZN=yLnA) zU~QeLE^@+mdtXXkn@(dMgWq8}h7-=;hV+qSUWt#RG66GoC2chtAs{i#`dGxwW08n< zl=oXAXctA8w>7PS_m&4Fpv{&MQjnOi5dE0s^mQ>c(TRTg8{(?-HAUU#I8f_z(7wdM zl}DXNs78`_X*9&B3dIj7Oc3uGy~tyI)ar&*cc2~x98u;jA|CzOOKAMpV`i?N1o5J0 zmjtHL5tbJ15Utvl$%xz3|7lMF015kBpk3dNw41+Mt)p-vA<-*tixD>AoA&;4{1F*{ z-xF|zbCf_0ayl_@Ev^LM>UZ1DMWU3B?P=^3iOM#x#FIqKLGG{E?#)F*1E2D+UMtkM z1hDBa!g7uJXDX>oDCIJ&%zRYBO%s}Id_#3#)LDx_3qmE-ze9h8B$(HgXnyut7mn1Y z6xXQBtbF7Q;b%rLOL{k6vWwpCgS^6DxCZrN#nasT>9z0YpFSqxa*=uKwjP$jd`suJ zYPa&67&FE#jqg>=>QwU#ts$~>dbRA;Yr=?(y7N~8r8|3Exk|l11qJNt$b7*J;Xj(qGAiITDMEe} z;=A-p;WBb^;WeID_>blS(m$%%t&-Yuc=P)}{OJ)(%?`v)o&3X^fYkPz+v6FJm&`YK zNbYsk5v7Y{fe`D$hoU>u=^hiE&>@PUU9T2<?2r3Y3Ugc3C4E9)M6i7204#YyF@bBXy}wxZAYNT}xUSdkEmMYs8P2gx6p zpOZO(<{}l3p(5qYYd^FCz+r*C&kC(IS26?RgXgdoH zP7-fbXCsIYM(o-QSNpXvNYqa`bI$-ix#vBZ9^FkXHTQ#L$7t2mBV9q0_eOjY7>mZ| zG1iuJW7S0)&}eUUN39Q?UKEn<%q7hP(=Fa%c^hQb(-mr=W@1&gb5ixfZAb!>7U>yh zVFzT+CBY7me?6W-hyu2HV;?Ii8=<61EuQy|cT9ZJ(*tO(=vF&|qR-1MtcX~Z#f@yC z!P70Q>-UiH|~P0pEcTb4B?N=U@-B+v+C2{0)fdJ4rgE zl;4##3XSi*mXj%OY!rIjNGvp=-PqWuQ%*bsI|G_yi!2UA_H)&#sD@N44FQBF7jmF& zKN;m=#-yA|*bD7N!qt)#v8QLQCZa(Lg&;FB{|perXZ+WUa2UO&n}XZmn@)$m7kNA* zqzK!EjwF};gF`slBIsgoMOv>MWHz$TDD;hV8S|%T{<$ru31uqg-G>3r*rrcSS14Y^ z)7ML~xQ$8^E^xTBn}3xB*hc>OW_LQ?b?1W}p8J;^_!=SN+&-OWt`Yb$Sg0@uHLl-!Z|nT8uZF}Yl^yYFlBsK z$7TI_(MQ=3%Rr=udz=PsP7qL#Dl1aajc!CLpf@XwDkidANRE z;DC;E-VIClqyyn_g_;n*8EblYtX7!I*P$bjmiwrS!x#!4AOYdhJ|<&)mq}qekE5r| z&M8pc+zjcx>K!Xg+ev$a1`JrAHXGr(^4O8)Z_8;}3Tu=sTY68t(jH^ulkd5bHAH;= zX5qL6GxJ%i&o(iXQ-!h^tJOX&r2GcgRnebK6Lgw>l?G;j1JCnqo`yp??~RtQKk{|* z9RZq3M88z~KLCF&_6UT`;k#eN-Ls+>D2u?(590S{e`Jxus}qP` zx-Xr|*C~?U=hE_NHX;0=Ex4exq-(TpJgNjPIiKpFT zaGm~RtS{Y)${s)7=($lP6>@o2?eudi**_{GXjEW$*Z1J1}OusfSxdiTjb z2X#OVH1}TbUOc8|(qkfgv%2cA5- z(!9Ow#{a=8tNRtx(N!@;;|5#oF%+i)at(`E(AaW@sCk9Vr^|dkx*Cj@&J%VEC7Jky ztb8n|&DS}~`iWqBG`6p2DyunBw)${G?IXlnzo41$?m)z%04*~$PBNcrT2@{!bThtE zP^vhePzmacn#soM*WkynvJU5^1lm|wq8dvhMz52Yh) z!z8`?Xy?qL^4Zu1-rJ=SqwpxlVSLba5HOesct9`tTOO};E7JNx4S@1ms?W&?u<#P? zLt0NAZPPEcGPtqHrg-q!g~gmI^dz&gSD3)_ztG7!(%F6D%+s5YPcv%1f4w_H#$$=o z8^elM=o}hz?$^py@exzj14eW7Sy{wXD>_{ek35o5IdkY}mMK+m z-p5{cF$Cn&t1c*WboDV&pTnq2#VCPLVX!_l6ozXISH5*G=hSY!-Yf_JmJvvWeQxH_pWCXxXg}g#F;vPT_W~9YcTO z_$bP;QJx~~0_C!D)GVa!4puH@b-WS}6o$;OCJ<|R0zE{jp&luZYSniJOS~igw2% z19T=`u9?2L*yYB{*D7^S!A`5ZVZ|bVZeg_9 z-psx365Vkxy&5Z_<=|vhn>nYOD(@DSdu(Hi84&$7iy=3+i%T9rEEO+AT^{F1cTj#E zms-j$#$E~GGBNeN0O&8_Za>^6u)G51eDmyB$eIlv$q572(f}k8FFchs!TQ8q?am4G zY%K%>0l#hx+*1*F?dCm#zAY0*T?E%Va3jJ3yR~ay3+9#vgX{v#(`m36w!Q_VFX+2`@S3u~ zXo~iLh#!R-rQi#Gt?)+O`b7KQWPMqytfMPG4mSw8um{R|PzsT*v36TbrUwJ9zmHkn z;dCD1Kgz11AJOg7QG1o|I3vh~l=Z&RkL}_TO@~F%TX|bgH+3G=ahVO$8O0g)L={Gs zsPlDQAQ5-s>De`@TNg*I)* z{mJ?nb3pL8AYLRMNDi@rY)0}#V!to88688`DnKoPvp`RhR{H}supwrP-2t7GuGS~B zPa#G08g|T==ANlcd<6_mB*FoOKmbY8`$R(`fS1H;^C=~TkboZgP}PkZ4>(x(nBZI{ z%qLoIEnJ}>u+wFcJFMe%KBwA;ldIgpPdl;Rk^?ku;LmZ0iS|BZ-@gtR(3gBp^0+ax zn_pw=F$kRYSHNH6zkUF0K)@fQo+MMrKqB90IqtKRwSAM9&?&I)DFN*8G^`{pwa%NB zAiw+a>M8$Rb*ubobO?HC zujL*!n6}kwJiK)MW~w@jdL#i-vQcf^QX=Plz2sXW1vt*qLR!4A(WLA{j?>Rs*X7i7 z&UE>+P-j;IR>>_csOZwxvtG$M`0*GshQxFD+UyJSIh^Qs=vB{o6`T0AeUJXM5@z%o z$ZT^#4SSDYIRXvJQ{e!iVU2S157qz(C zji(%s*mCvr`0#yEV1&c)h9xwCv>r$Q)e{eRs(SZTpqmaat-rJvAP?zw6G1L$&ArT^ z5aJ&;8>U{5S?RQm3z$_tSz{jWY7f9`4Fbh4?8_(CjCzEkE*09(v7!yk;&FTfLUF3W zF2<9UW;93ZtU)R!1zklh*;Gzal`9dTR?U*gHNkY_RkRPfE_YpLc$B_ zGwN&c-C7%#%LUL_06{hrZLzp*kyayw-H+x98L^dU19%Q^d1_zh9_fZRd!g7?2Q>&B zY*NBRq)yG_jf7nnOA%9k*zrmgt9E69N#h$$aVDD#7jlxXXF|;r+_UL-({}co^(p8z@U&yWNbP@|Fq&I0hUg8<6=QB?B>;0YaB7R_w+g zT3xf<#GmiaVUbJYN#!hgJuX#K&61VN-Q4gKj%Ua8e(StMDVH?iwm7c>6+w7&UK9x2?8Yla9q zJ-3p0n>1mbgOgNY$!3T4W5+Gbw?*D;oC1(1d>vMEIORIt@@{*W8GS>KOL%lEAZuDUJW>^)Kj)M7!dmqauo-W_#L%;g6|B6IPo zF1EC$?FAWK95JAUYpS0}(`twPPV8oD)EBSD$TBrTFC)oCD20Wah~Y5h_3xbolml!A z2OowpK$#LQrS}PI1rHMW?!ZSq^<0kezc^dmt0Z~!68;j7=BO~M{rZ)d)3`S*imuz@ zf{Y+47$86q92G;(%%#aG7iIZl{IZfz#)@>UcoT$JpAD=xZsI! zmBcHCu&Wr^e@1gi*vx)to1% zJtE#$M2_oBp|qX7D?s_}-`%R-+U@C?OQ)9D-xDLSpb?D#QhllE<3aPq>AQ-lphqW_ zfOuaUle5ff&)545V{SNct3H8;79(bD?Uj!JbLDh60x&pSOfJT+FoXL6;@0Gs%;?y) zJ)PQ3oJQkKw5nSP3_f2M^pJS#nR?MA;5e8Xx7HST9C!UgHl8hlWZuSieFVF%(c`Ie z6Os8?Lo><3vr_R0Dxpt?*#)$wEX-HsCP_Dq?C}V@F*5h|XL_bZ5TpnHp?;PE-8Utq zdzE1KGn3jy^ue>lTJvHJ??^Y62(*e@i~EW8^BSmBK4{?=k| z?ICjgQT1v}@Q10it9hiEd(aLzoO&GP!1q;fzRcQnMJxL8z{tJf(G@4qlkfG446gkt zKJFtgzULT0SNEoFHxAxYSLyq*G}Tap>lrQ>*R$*l%~{g=-uLDwH0Z0KLb1X1p-lDl zNu$fbh0z^?YUGejT#Zga_d9YhB@}Px(|*lB&s1rM<=I3O;ui!lpHhZ0Os~>(IZ9{| z5Itqb+k^e6*~{ZHVVOlJNK*57$32USQJiigM4s&p?baP`JY$=0%{`;pZ*(WmY#bYl zh%yS@#Wo)isbDj4!-Pt(|~>rL4lc z)Z+=~k;3z-cOmq;ksq}ZJlhP7uC?_n2SxO5JqjAVIEB>QvU``NPCXrfGdHuritl~7 zKo^5R54Zjltu)<@>=ohKp{mG7pLKl3B0T6G-_@-6a1ZdY4lWXS*!Ciyj_F-N7|_5X zXS;0i1I1^rRU&%)i1Jh3$Fs1`f|?Hje@(_SodQ2eN;AjaJE$tHNM<{%o%v?I*?YSH z*ECYZOKS23)Ze8~VY0$0;Ia{U+3K{-C;sF4rgEMu{dL`Y2?h0Hbq(3|-q>vepMBka z{l$P&D#53ApMLd3zGO&l$ppn;Plgbo9+7jBe%~mME_a&2ZDweSn{e50YcHh#@*Ey0 zqh^^up`T%wFj?(EgNaKK6gqhEo`CCR!x?o!yfmrUY~|Ul=WjM{m9E>Ad$*e=D_`q1 z+7iwR&lDn~iE%e8ZxBbi5~rc$l~E$9w$ByUiq_Z6cV=AhJ+p#ZklpZupF+Id#Q~o@!cyx! zu=grGl}lhQM4Kb`)-aDn=Yr_U(MR`$8Ga}BO-BA3TN_o)g72+{DwZ{V!Qubyy zonD!a4xYX1Or^beMtA?jO2qw;@=hG9gu~e0m9S9e=I1DW?_HVqt+u+-hhjh_Y%o?z z3wK&;%*F1yZc%BC+i?@I9TB|Z&0}ypF%M9=ZZKG^kZkyI(|mamvzzpOPrduV6T1}C zT-b~W(t;I3aRec}NXpjmXct-d)w(gDw^Vvb?}IuR>oZ>$!oBC=B5?zuQZ~^9v$mRe zvduMyIJTn-C!E4!K()AA`?p%%F3*oHj|5$SLzDaD%=FpmMqKx_(@|HjQGkzl&6IoP0|$NMl9Q;Emm^#%^3aRIAy0f6Y5j$e9c@cD~nWKX`!JS@?pW9gKE5 z`0;>;6q4tF)*JBXR$zTS$07zmlQ%s0KZQ;Ep9AfX+YW@^UWaO8MRBS*KYx>qJ1kGI zb=~-c?{HMsEI^l9Z*{RFsKs3oQNHrRFB)NjI z@h40fYxQ#;$Kp-rH3vwY)1NiLi+WA2mHjbDW~UC4g# zpT!+E%od$>eF^L#ggNqQLBC9xXw@w5kD(VH9eDyopN5B*va9v84~9w?@SiJwB`1k{ zxfnV4c*bRi`|4`BQ`Y=f(Y>7=N`s^h>ffbygo97ElZ^3Cb^Q5xs>4_2a94|v9XrTg zZDJrH-@Pxn_CW1WA4#j`zS^VuFpR?E2a~hzSO6zxz32Y?odlid0U@$?&QYjpI)KUNbzfbu53^r`<_xjJt1 zBAO>hIbq&+{GVC`%WZk)aX9q)o;e!5?g{ocBhCU*fY7aVhg-zpelh=MtgzI$;W>!a(Bx zQfKPO=0x|Wh`v1ppnQW9gRxX+YJ#1zmx zv&bbWGpDZah3ZYs&k{RH;i}kpTe~jwiEPlux3kUJZEDc~3MU zwFz~16Q8_3r2j^>wX&<5$cP4*t>Q>G?G6uv6#Lmo_c&CvTDgWW5?PX6@6M`~vbYe5 zxI8aTeE;im%sv0qhC%UZxCd1St^@M+Z%AxHBCbuu{h0rSd}=V8o=4kU7QMD>tvLF| zxB9%%3lYBLvcEtY05d$>^8&I?9Os?M{P^2=L0407J1nVi%3?qLgU;omDdghHiZ4*o z-!varyYB!Z)~}p*?y}N?10GCm49hEL8mlD>I<1GGJhxA&>UbTcXFppkWnEt9hiF}R zL^Z1B4oKAQ2@hA8Jo&IauF5_}-8AZ~xq21{ct$8+Z%$Rx02x3ykeF_pEFl0o?b~Qi zoUyH=OCjeSl@)GG#GuzyyyDFbUU`q*%S$}6Pynf0I#|len07+nc(G_M28eiFZkzPa zxNP`jh)Wx``wuJ)Ce5Zlb)4)rdnlOKU4391_Dnp$sL^tz9!aVuB%Lo%K3evn;oS|8 zm_F{)C>rIH`z*1#SGGh4$UbARwu;kodR6Qb^`~;-ozZ{-k3n-~4qTXpht?Amz{5M- zyGGgbeeF-+zy{F6p}v^_;qcI@Z*N;RvLUb^cWIjYyAf;tBrqhdij1|KoVVwPqXl(0vmza?nbGd zHkLKYN*HZC<3}ara_IX6C;}~3FN`&N=ht~mRhN}gxp3N*fP(SCvls;UZV@OKWcID1 zF6;>=i~eV%i=Bn?_13PAp4avIwb0!rx3HNA8iVl~`8OlutfNRnp2DyaAk#eT;kzVrEV{3l2IyUI)#ZjG zkJN5y=T8v1r8YvDPY@b)=<{b31IakhEP_IjCa>o6^0ofAAUxL~;6&ZX9vAOJPS<5mADmxs;Xo=W9j5ENoM#Pr>>iA;v6cgx zlpxpBcW07k`qyWEf;WfT8jHex8r2+4k-Tc%+Er$adkf+`>S4Kxzm9}fjf~N@kNZ;A z;T`oed?!|z5#>FG0_(@Su%~9FxD8vn-)v^b zzD`24PUb4i8ot@EC7YvC8A? z)t{A0g%3OnS{27usPk@GU}&#_J5gg5tschkR$XXs#r93Xep}xFTBm0lX4zycX99M# z=yQ~yVU&xIQ02#u)(6sN(1BI2-K+ooXu#DW_q3T^1_&Ibe^O?+} z?U3~mhK%kb$%HBgDybqJto`~CWut!l=N2pelLPG`Vyd+bJ>YbqCQ}8eG-B^dft0KD zA5Oo_pYM%ggmY^sfUroH#27>7!q%&N_xigZIi6~_Ot^2f6brznarqke86 zIjv4HOq1HOqU30A*I_ZnJ$kgNBKiMFd+V?$x33NO7zjuSh;)lIh%^i#El5arBhnp1 z3W!L9N=OWXf`p{fDJ?b9(#_B@L+7_g&pAJT*Z02P^t6So zdN$8e1;VVle6iADH%1E4Rg(dHN2E}NFc z%y&tBfZyws^?dwHGJ8ld@EBDqy+kYLveHAmIAri5puenn}!p#OS$Y zUb&76Jwx2W3qF)uuxnOZwR8$ zUmDr~ZMyF@B+NBXT#!P$@Wgk|l%ICWBd~WL5a{AQ)?=3C3#)7N4*RSfw_zRzSBq#@ z=Dpe33ZyOnSj=F6?_C{0;sVRjP&#tdEvUrN^*Z{?HMip3g(3;!Eds=xfyJ+)ZXdIf zY%q0c%XhtfSK_lj&Z1-#Ps3mcO0mLriSNmkK8LVHampOnlCOq?t~q<+3+lu6`{X<3EQzpFJYfEPa{cu}+A5T@`W9ti^+DmYT z^u@oMjQdemY8L1@6-Yj)`pTwY){WAf*7@8E_HYxoG}hmA!e}3lP_Cox3LMG9b$PewtOC z?qLhihz4IhuUV_MJ!ikzdJkcy2SsOpy~Lx%NP4;T$}I6}G)46S(1(B0@K-|q?zI1n z_ijlvfaH0-r`KYAsRDp7ekN)#%J9r_Kdx-NCE=F)xJ$wSnL$!ZLTNm7p?^jNn%gFtv{q8Lid83QK`pQg`)5< zD#?Gtirae|gZEZpz379z^(bX)l@+lcmxrSnKV6W3$x;)Tu%MV-XCd#zMNBtz5Zo*d&a_&|GM4q)|hDJ{P0ARzs2RvUD3zLiv`S$MsIwKu)ULfLCnss zt2~zQQxm;3r8>jmwERo>;Mc9BM^4b`QK1HFVBy8jHN&`n1@{{|q?xo0M<)x@^`~nd z+7>PC63&P|G2p{sRWhP~OhEm^KPCVd^=8v)+nCg#FRgji zK(G`rCERuSC~ZWbEnS@f2NF`2d54SCkbWQn{8`wO46!}^En5CU2)cgb4vqNp>26Yb zx5J8(om@ET(kXjMB6dHC>ynFPfiv|CB_LA@|2sEQEz3l!tC@bMs!|x^Pg^jO`mdq5 z$y&Od&^W|@QDy$=<$&P^zmYKozX+?Mmn=6qa8aH9P6<8rl*_y_6=$^1{V;5~1i-VW z1*pluZFYvLGv@BXz{202O=Ah9c;i|mTSLiPTT}{)#hT?`XfSS@tP7PZ{kQ#r+uyR` zR)SrAKEH_%=SQb-&^B7UYuDvQt6TQx0d#Ek9uh0^_Md4+J5d_H+$gi-^2KTb4mQ=7 zviszp1;8Qo4^s9V$2)y}}{PC>C+{4qdU ztUm|%Yu)@B6;Yyz6inwGZnv$vgLo1VfcXD88;`GK{%=F8UIMP4W_*3GwMN4qHY)*! z^2B;~zU?g{;}jY6>!UKk`HH47H6t~;P+j6Bxgr=K&E*pA`5zv|BJ*=3WDV@5J4@Go zSrov0exW`umJqKI>NZNGehU8Iz63_wwx0mi0e2e%a8NaCW0zA4BBqW%d_LoHg`pYi@%v}zUs{C^g z01M#XO&EM~Pa}=0POD1+?Y?)_?+#CKeaiye{apvo>t^!+=ogLK$t<>~O}T zzYXU%<{no9?CbA@DB=03-`WuQXQNsg5DJ|vk8;IkK)!*vvBaW-s}71<{!}Ya8+ds| zz;QAP#4R?$d-aS6i}s!|GCpq>8ZnRXtHfSCUnv9%Tchou05slDYp}J-uvkyrsC?q?^I&F9kAG^K?QPhW@E^N%VY0w6xjnymU*Vf zc`=P^62i+Jv9vsP6*XMtk6-avFweF{CdD_5MBm}Or7`#r zRRx^ildvQN35@Gt<}sjszis+A!%!Tgo3bGP>}J=5p-4W(q`auxdA#(>HTJq+pM4js z_Z0SD=7>mQ2`gL5_j8NCbSeqFq+jexpzTe8o>@Sr>#YOP$seUej?2;fpW9Qu&mV@o zY#J^n1og@nc#FrFxa1G61JJMSwH5mXFI3!rnMzk*-(qNG*Zbm3_G7Wf2ieFV^=x*~ zG%G{tKCQ4TF6aO(3nKVEWlA*qON&N-(Oc8sW)VmSm-vrmm|Zx)1pM#VZ&JlG>X_YC zo_jG;C_1~lz?|W)(z*VsG5EqEwvOtU`0{k-QXI-5W)y~oJz}Z`_OML`_mGEv<({S*`nI+}o_QcoqU?G>+=F@eu-l*kG>xokx zwTvwNOfIF1T~DZwCoD(^V=@C>WXT8FS0{{39XoG(#dV1{4IDwY*f;Z7@#=pboqYn=5+M z+=cQ2ZIUOeanOYN06dFLpQD}lljBDqcvHlYut}|p`MRkQ-|;eGbX&qtt)ng$G!8Ru z152p#@~^F_+D{X=hV5v^iqFT(?R;Wa0H1GuZfN4_ zwv=wQZD9&?s*6&us?Ft(Olb*L_>vH^O3e3QHN$C|qoXmelJP#5*X1X~CaZ_ICHV=b zL1iAw-Dt+o^f4@!M$~wFp9>!ER{6LuJH_AuX-nT!;}Vpl-o6$hw>-BtyuNqTJ?V3} zHMl@?>(+CCftoI~OYD(Rsc#bs7kx!&fR2;bt%rS`rnV_d{EpeEL<0DjrUr)#joQyy<4wzoN69%5G?_K6@CGt^GwurPbeZl3f66n zE9^Dkd2B@Ic7aS_Kba&1;qEq=JNVfLqz)GJ;`fgGd*$5vcLP5A@_JqsR9-0Qq?59! zY2tJaSOrLb*8P6pqJ6`8UWv^t(4-L?_^>jm73ueVdhP-^9a})40@S59_`sMbss!3Y zaY&`LU=g2hn;F!qFt)~KkcAUZG>r&gb8r;KH=42nl@+G9op|hUP|tspK-H<9sv=`% z-g)#sgKI}r>UEp~?;YEI{l<~d$RxEoZ=2yF?@S_Y9i2}s%5TN=?7x95gEM4h!s%6jc1n zVYmub8Q7~-Ul{cw+)p41GJ7{NW&lE&X*UW}NP$oj-BoyX_tA#Culj^&lULVcY1w^* z_rV7F+K5NLx~^l&4+sAXDt_A$Ib325j|+hh(w|@o@7PN4wDU9aG(L44g*U_-CmO#% z(gtqGB-rkoJ$20NFg7O|#1$9U(4_UDa}2&c2U4lq7uRkSxGjI|bMTvzShXIo%mal1 zY7lHUzxXDwN~M=Mpr%$K~qpiEUdNT4Eo4{LX9k8vfN;FuGG%%yZ*K zN~6Q@#O)%g%gJZL_WRa`)hC)HF%p*_g+bv5u2j&;~us5rmVL)jwsPw z2n?L_`>u=LZ#j%Gc8Tvk#M!Q>A%cAT+MUQL_&U2YcWD-EcXO5Atm9rA;jQ1(;`prz zjaF&=9Eh}Gk-DG_*A=3>k3%aQL>Gn&Ml;8mv?^$5IOFMp(u-^fZqKRScN>A=Qj1fx z$VK&&i@SclaH^ofh!atMTE*ww0&Wyf-DC3TdMM8c@sqYkMe4mlJVr7Q*wH(vkD zmO}K^?+q*w(syk2e~}={Ke)rDNo3KX+&T%x=`T2xyv>13rHEuuND}9roUFrj)pOKL z7V+nrZ~r2F5(ORJ^=$$O`yDPr+|hQVTQRr(T^szg7k(H|SX>W}h2`Zx| z++zJ;Dqp$H!-PW6CMHkAI)66K==6*;U1)!dEKkwRzPIWESlzQoHKInzgT63ZRrzF>#|cIXTOnuQ?@d zlCsAVWh)j4^T|bbOS!L36R5mH(L>ihlL>z7plewY?ugBhs=irnX7T0*|2m(xe{xPo znNp=VV89SO5Jsqhb(<@hL@D!eV3I43c>b>W3 zOEZduSQiC%Ce;1~ff!h1r4VZj>2@m z_@j%n+&zR8aDXc>4$;8~%@h$nIns3e>zMb^PNwme{^HL>&WX7=u50%8Z`QKgL&m<^ z@aAjgMFXg2VY$#i|91E5afWx40tdm>kCegV%DV@TMBrMmdWN*pGNusS@}M>b2!l|v!W2(v?2{R#g^xcawx&rKhY ziNy+c*qmD)X{LjlozH$4hCj8_)2V(G zpMW^9;bpWgk^+vsnwrFn?K_TyUIEY5A6ZtS{*&-s_47%idTpv5Qt211~Ux zfK2Fi&T=?UzjX=>j`TvN0CK#{ln>re=}Yn{#~?tvD!v8+LwfkWM2eHdZ2M-1Zu$8w zP%=dC87X(0<#z~+23V8=jB(#05XF-nwTIu3XWW0gI?JR<2!?J;Cv(COCM6vK1;gV2 zXlrNbD`IBW8wBKeOhy1V zd|I|@IR&wzsQ6XNM@8O32;SxfoxTjC0tr(BDn4Ba`NR(VM-!e#L)D5!OO8Jqx{o3| z1Dt0h4EGO#zdN^*On_&YIwzN$N|?m}S5|(r8t`(G*tmUrHnRonLM%uU7V5cy%`&P6 z<+8^tMin9R-`$qjBLNq1jgtF?LFHCc z%NHj5*H4>fGe5M{(*`ej@B9phRI1Fr53S%3qftC+^b1F7Q8Mbg6Z4N&lDF&hS#W6Z zTEg`ddb=@BCPQWVo_y*PyCoRFR&U<`Q8li8wj0rzP)P<_D&O7`V-Qpy${Uod*BNYa z+AUKnX4?K%Vy=I3-@n~{|0rr@q)=4|3gfZ3C-#&+K7sV(+H?;aKcrrN9ZKah-O&Cv zZ!rFcfIkbAD{$u1wj@7(=^QUqKle~&=j;vT6&46KI(IGj20`EGSB08o6`+QM%*f$N zMJ*ToX+$_k^^V@qsnb6^@jQZFw7Tvprfti|L}odof@3{xtg0Tmu)WmAXO zjR+F%SinN_SSr*k4OE0h-ZiOz%K7@IFrzh$;l?+P1Z1|Nj^s<+>fMe+hfKC5Y2;2n z`nI$&t@vv)kLWiUNS{LY}jXz2`a@hELrRbWI*t*n5X12c8_l7b85kHFol4 z;Y7FmllS)sXIHl84ijD!MrDTW_v4Eohq%m%R*}@?qurpY>c^agZn=-xOMKc$ust?) zvC$-Q8qnB4Y5X>s1hHRP0?nN|+d-}#tym9-jZjNg-mC^CVh@!ff56cr9|U?^TUf)Y z!pk7r{-n;^MYr|Q2Y5f?XRQ~ld20k_(o2|JQd?5$&ANI6dX+QFxdRA61O41VdaaPx z1%v0tcqB$-Vm@k&5YSho=CZ5+V2<5#M#bxS_4E8sMz@OKl@H4yooke~F_E|g>;d~Q zM4~5f_tmPzJ9(_g12i;vhjS8qLO;zm$j1`m$VTY2f&enw*r`~*TtC{1Z*N;d(j)`; zzE@TBZoW595SepK!}8lcBM8AHcmfX(0V$tQ+fQeW3W0jE^PT+*x|NP^);Q@~?D8LX;9W<`%0I998#W0z8l2CtVQuUJD0@5B|?j-Gq)~2YO$Yn*<)+1Z6L` zz(a0}N%!4{5Ynsm1OJs3-pCHUII{UG#-!?620vw7js;F~`7GHB}*`5=WtNW2^syHPV zUJ9Mk0Yiyb=T*!ODrfgDoKGCkcsAu_tfgvnyWHE{#@O20*rnF+08S0dV)rqfYPnvK zD&K8b|AnH~SFDD&d1Xz)ZYo9zR25T>j)ieQ?}-7ANN(JHY;R!XTfBsI&s(L-z;vIY z3~B?ycb=?ebS4c_HMH#q#GpGacUqcTu7wZTVs%uZW_m0c50O^>kc`}?GlhjXyj3bU z28-o0+ce}i5tU$;@2JC*mmzpN2aeHKL%W*j{qa9O%Aj85UFt7pUAW+1gzg|0=;Lj7 z0cVUMC)@u8(rw89 zNnE~cFW}y=1ZIal3j^}(Cw7aQjc^chzIRqLBICBr2d=J*b(yAlrJjOqlD8UHF%1A>~ecd>W z>lm67FwyjUYiGdfZ+NcuM*d#>qIRYZo&Gdq`g1%-J`s4Y?=wXK zu{8#qdFx#p{U4Z{#zoM?VNJEjPCg!`m&?8N?p0`;9R-zcodUWX(1`IVo;#7@ndWlW+nZiz3ntG1sVM>4!c09Tbkr>I%TXX8cx z&1oRSmscSa_8LJzbF{u@TWfj1qE>LNZxQfk> zj2GLf2M?s!7rOD6N7Kv;8#>pb^G#lZ1;z5rzr-Ab(N~ z0+SckC;=9Y3O4oZzQ5$S3>D=;T0Dei(!!5GBce{X`ZAAZt z<5Z1uy*x)hfQqLoL8+1rbZfj5^=1P9=;#RW;3;t__#R6v1C68;f6F&_`5!#(iUDe& zWDneLFXtVzo>q2`;C$B*$vf`nS4WH1d0_e9;gG7ue)9u{wy=QsujGPPG!hFnY2VK*K${HEd*1Oe3W>KR;TOc4v^}@8 zn!3h2CkD~Tz?-SVncfCC@9#}-c&S=pOD;N2IdrjOkhxYvxqijFQ~T8Tnb^PfNc$7X2DrZACpj@NzAk-d7Scp!kTDO|d9=NbjyX;GE zd=9|~kp3o>EiNo*>1UNzl#c-69Fbnxa!6=)g7LsvMyDGwvBqsKZ5kkIiQ|QOVTcJ& zZIsODei(9NeWBAY$!f7Xu{bE{%A6BT9BQP}Nn$_E8w~_p)Vd-1K0N!*_R7q>aJiLj z3PwPC74fMj9gu1np{@vpZnrTu$wWio0pN=_3`R}wm72BOyZdMk7*QDQ;GqxeSvWB> zV`elX6*cedk;{VMYe2*SR3R{;@U!5z^$(2eeM00_VGHA&foToin9O}s?WJG5q80@} zR;uOftFY^d0DfB4uSmA>vE{o1Vnp`*SRtQm)&`$53$g*BOVHV+nD6i&p-~<_T1?(sS^btwrX(W$cHxBQ zh>`|n?G2(;A&)hVkB<|sOmj}GTP?SAbpxovVn1cS%5@)=rFU|dq<6&Sb(XMP1sqatPzCRrAImc&+;9Tw|MT*yr=5(n@v&;dswZEmm-`rf{0hgHXw0rDE?biX zs%G*1!UT!jCWTJkc;Oq?#&zDZLQi`NJLJJSyIW?>oio8M_}7boScoPnSCoj!Wk?G0 z0BLxFj+K)2vgO8*{KLn!)N53L_Pl=RiPQur=I0xwR_mUhjSfX=#BZL{)cKzZxGr)< z+-5B^|8i1K=x1#8*z%&Y7a!mrpcseQMCz>m@L0Z1tFdHBCA&k_WVY{IzRDAIh0CNd zOi5>ZKC6$~u+9~e6l_PoU&*u&PTbN}W)YC%9_Ebmtx4aHZPkAF-ojr7C8!4mpg`YG zcpwqf*qYFug6knNr#lOFulrUd*RRvB@+|@=$4IAL|NbM-F2k9Khm5e3Ves``H>vn6 zrX8eJ6X|z1Lf`LitmO{UWJ^plGo~^1E$0AOc;CA3>PL^B&>0A1ueoV-piB_65y+XP5A#JX+87$(G zI#C&wNV15$K3(lb0F>T7#3T@QOL0(Mu*w`fb?wSNRLl_d$_&5*X`CvcKPMMM@#!#I z#XLy>?1fCzZp$36ZjuyObO6Oo+Ew2}nibE^W7p0&Vu0xa)B6NZD`*#`pA^%d2l6mg z0Sh?|Tf}qQYim~~EMv$7?B6y#nuo2yK?0`KZqyh6$~58j8Xp2eoVejjFGIxrG3kP$ z#qvjUi`#uG@2tIk!>pR=s-<(c>`}O7UK$>#Q`rEj5b;d{V)~U^@7YV2b{{wbJvpg` zdoUm4&`FdbjmC0izQSw1gcNCBcaN`Wr0+;k%bYUpJs4(h?q;Wi#a_lk0G1({&!-nB zs`^OGBsi9lsLpNFxFqHc*>HaCJn#lic9D}n7WrGdK%p^{8H(1x1<_{&**PTeQ%RzE zl|d(*!fzLJN^o_CU|?BMYpr<*LIcvqpF0ftitCC&Awcxxu_9e&q4dW?2q`)|Hmp`+ zG@f@~8Ln;gDO^9vxoy+32a0V#l7rvt3-B?JG&VnT$zR_q9~FGx>GrXBAO>K)XoSnz z*|`~nBY4xcoj(YljpcJzILycrDx$-J%ZW8q)6zr;#)%!o_d-3;1vxvXkxMc6eaT=h9Do&eB zzUi08)P7@<&)k@KBX~{hjqKj-c4dLz;T%uUD3KZ{&h2V zfV>)&ARYYTyIP@-f2-=-J4#ZMb#zh!@yZW4p9L1^;y6rrN=4prh-VIbWa-)!(W8g@ zpwQpQ4!7$5YYbRMc-45g*>+$RgBj_f>LsG0uAHOkeef6Y9N~}cvoCM&;3}y_XxB$me zdIFN#U>RrxT{OPuW}Gg-p&Ogo8eXr||JWE;=NF3LHlBOefBj$kzdz$375LJZ=Js(K zbH?%Fml?|AtT}(RwqwC*#NXtA23lx?`dHHhN;r+%L zO2LaDGM>jjav!+?sdAyOd;$9upj4+^&9KV3^dWwPZrXjD8$E^grshP%pnzNbvRtlU zq<&9)5hOx`?HE-7QB)TifG_O2Ed5=5$(`TjDF3L^`S4lcVeM9bLF1EmW#(-+r&Orl zyl=&#w&@1hZd`c$dQSZ*ewNx?*~u=Chp0vK<;oycF}QD$C9h@9Qw z#`kh4_u78A_+6*EBURe)|_eafJ9 zR=z$oh?+gwUw?=^tA-IjNcuP+zENceg5$Ob5_!Ji3wg6d)Ws10d~|9hkndrZiQVGif^?o0?E>A-36)S$5{j)*pJn;tSGa)UJKlY_E9vX!o>rDz~{pjKe=~>(m<5Yao)=NES^#RD+Hb<9Qp8RnCuDXwhQzUqjEyT z(a(Kzgdos<47gc?XpW2zM!2cWD|qU5*wBHEbhOPqgdv~_q$eW3p`=gLYGZI-Ggs=s z^;-RZU@sl(G{NDqNJtmS%H=8hib~$)s!69S^N-ruI$l`iKO6S_9LB84T0-j8hTD11NQM26k^G#Fj4i37PR`34V25(Xre&z+{wnlW^}B~e z+6rA6+mrQS3$~M1k2bfd^V*Xdy7RBa#}@?ox=LuCi-y+bXT(Ee(s1-*&W?78=MIuD z$2*0E^JCo3+oKiAmGj5BO&ao@7(fXyqf}k2=2nKVZ{IW7<0QJ~)3x&Ts`q^%_G7ok z5S>K(WsthPV~uU6W8Y&VNo;Vi>492+hPOw)^E z9$S;ST%7x6JKbWCcZ`{FEE zN|}p#Y@6%xh2_7#T)?XrM;wPb&KGChCB?^wCv8`MG135$G7y{l+SL+YxG{&}SF$Dc z;paxK){j#lM#VMKe~UNy?Izv+&tw(fn)N+1{!Lp1WQoSblZT1HT>4+Drd6jj+j>7F z7-yyatH}ZK4QL6dTS)0&+&Xls&BXQGc@;@=bo96_w|`;xbeqxzf!_wGpJ(=H%H40x zDD+<~V*bm>XuLkQ4xyv_i`(RebDk$aU>>1KmmtZX^NTf!{syT4&EKrM-%ugXZ^j>* zQ~CR<>Hq8MT-oM7S33!Q6ZHNr&T0QmGW`GgYPM@0@|>^or*<^AKI(kG>p73;2y_i~aqDQNM}J=qlu2m(QAhvu=MA&&_cG zt+wTVoLcnfS)eCg`xl}2*I%O;@=)Z@Te&?aJa?>$)2ns6IV?Bzo>_78E(^!O@d1**z5#vfm-hq6 z_Ipw19EPnc-HROA1EcY9PLe!d%F@KQoFU2+Yh$UI1xB0CD&P z0c_#(rPy`Dz^I+d_$M|~|MhfV=b9Fj2Bi(H-900o;n0=d{z_bdXPIGyT0#B2-@>!b#_jtsTW>7IP`-ycN;Gr{+J;k7M3?R|85?O%J8cxT)g5Dg^O=O?RCAU19SOvRz~ zxw*Od?WFHF6~EWiVc`*vMi9>ISIs%i%papn@$W>O#G==8p>ApL&Y6bET^@~!u*T@t z-cz`9#7^3;@iYHg_W_{2cmV58e9bOhAa8)_;@|5z@XR=1XNS;pYdjKU0?f`{JkqUm z$A|%1E^%=`Xy-)~q~<^Z&1|zK8Zb_F39cB^i2JD>VDYuP|6_6Lm1JoMe_pjdk5Pmgh{kcbSmtp@+{YY-=TMjbGK%V7(_4KPAAXBZrNuO^p^EYQ*SRd=wT2t(-!%o|54;) zJ-qv7&DSxiFYz8>jl2)4q_8%qa6H|pAQC_){o0rNj+hc+f3uexLOi2uIB(f?2rn*@E*s~L7dm!qe&Y7^fmR<72|z-Rs7aF9tP#h; zKYjoSJlP}nGpLk4**gL9K;i&6W-7vF)H3LNwr+BYFIlNkd6g*s@Lm<45 z3?PJcix}3efNol+Sx*)c0cpF}%SPv2sM#XzhyFTQgN^eWrre0p<=zRv_6P_3D+a!x z9m9j~pkYEBzkz2It%TM-@$YRO7P#Mw`Csdp@b69Ym~?+$mL=jgC-$iy29N)VeL{`s zy{f(16+j1@c*v|Q2V~|RKCD-LC`Hb6mUp;Ni~s7@L>GU>l1CF_*5>gm_796oF7wxF zk+3R1RY%k#?`f8su#AW+a4XHyIbQ z{fr6XrAh}$I=z0wSU#F!n#J%VUy(6h)L?__rW^S#6{`H_L~etILt;rojFMjmSBtVuW1SGl`&@QrZ-K)bBsfK$&ivTSzwdxiqzq1E8QO=w@HCLaHn_gmo>f#{4 z)@Adi@IDtoXZM!awHH9&9ExO?!&;A1e2^Cj23twEgXh8kQy>Inb7*0;RX~wMqbl$+ z=;s!Zfa5sZTD_9&NTbiW$dgUxhv$Gj=?wZsmLD9Adr&%`9X*2F-elYE?-=SesFa~5 znCnVl$HKwUK-66}J+%K>MGgQf^-Uye=!?203<(K)A`hc-+d}=wi|S`@SiFtbB6kes z>}f?ksepo()Vs1_wK_}L1ee*JImY}#OUi~jh=7wlJ*}L_S7`BatpaICPH$j{k#yi{ zPdH82kxW!Lk)Ge$`Toqdy3sxrD1a4MI8455B45pOKJt2SK^mUQPYDzV- zlzsvUIQaR~1;E|w0J$8;x)Qw+R^2EZz3p`Kv`!ytA#a5tRE2h6JvKo{H45Q!)cJ%* zo~AA6xQw$8CKk3&DPRT?ptez6m`^M%FH09XhM`@X=FO;}keA`&0r`oNLU-31%$IEW z9noC>eY#)>EX}RnXJ6Wpk#cWsiWX!{+5n=cS#J*O2C9%lKKtn0ON|F*Ta|d;$k)qC z^(~Fta{!ncgU6tSGAIm>KTtXm$FK#z}lrE>hbJ-l5gRE>h@hy9AMIClfcn?Ksmw{D}iQF|~Z-)gHMA|AdmLFiGng|N7U8 z`o%zUG>M`=YYQZq??go_XH}@a!SFx9(~;DbYNi(TY$<#uylX6?3>6Xa0oq8kj+0g7 zn7~Y`%y;LPo{eNh|5QON9Eg;Zbj7anK4m9MRda5zT)p|0OT&2GaNr~SWsP{Dv+!)h z$blPz^VasKprjT z<<4H*(ZJCB@4zIycm8_%bXg%)fVu~{9P&<9eAmlID<(;Y69cgw>5xDA`NN205`kc+ zMRynzpf%%CGgsGWuzld<8NX7l~)5GkpVV8}gaG_gzMfB6P zaHlMq>hhD%_SK3%8!w-a)B&uK^u~>sw!O$?1k-7G*t65e?;hAhR{{#SY2PG<^PPnP zuk8lgfU|4xO&;$~EDpme&8T7Xl=FdX_>27Eb@ml%YJPDj(sCbG6vXt|!6;5cFg6Eo zsfA0u%&>ae|3iE4*Kt9mMEhr}6Hmmjq)HUfPPfLdV`r~%PP5nL$tT&euO@5DzmMUB z+dF4}dpDP#cMJjZ*DAlcs~x-XT%0Uu)G2qg;q8T9@Hb&M(r-h}j%JWdzD zvA9gZqV!d@9Q`7dVG<~>ppbh9ms{-zH90Tk2iJ0N;#kB)WNnSD3+j$nH8pDm(d0h79^VSqkVlC!xco{BfAHo)!8^e-sBP|_odUp zoZcqx#*ZWpv%vzJHE2dQy6tHG=w47!Cf>NmNTYP8=F zu()rI8&S33w?VoY123{}E_RCy9qxvf+KB7w*P4S$pcH{Ord};Mg+GSfp2zEn583@N z6nlH$Wx)L{tIoA!q;N~MB+m+>Lg^Ny(R-KZCJ9^gZPo{N-`tFhjBayiJ|3#P4t${C zF}LMNi^tMq$nrS)H`t&GA*YO0#styDw=q548!sFwte997)!3?QRO=}=4isCce3n_B z9EuBAQ%j2s< z`A_!xUn202#boO9Gjruxhw?N`qN+oa@0+nv8FtYn!LZi^pK**|W{R{A+XdNvKH{^# z!i(~IHkCsRN)hjl`vvKr0-avr3$e%~X)YrrO;R_fL?JKoD<-9`L;6UgnuF#q*5A%{ zrT=5g?&>GzUCUK(^YG5_YMM+M-O5neRGV{2uYU)m4R>JjznzagRRZ|Z;SONw&dttV z*NF53NtdAD59@;Vtz&MaL;BsjtHCp}LP9=t#me+OurHF?1&MY-N7g0RZ*K=Rd}V zQ}Ww{8=frBL)P<#rXJu`ym&xFbtzyngzzv*%dk#Of=qH-FkJF!bq~E(A3&b5nGApgj{_`a}624PjE83s+Mo3|B_*R|1n5DXgg4z?*!Q^9n(q1P`ylk z;E|*Ff%`Cgt;6fcZO@yhVqa6y3%Z4@$8zDiqI0$Xy&2BlEd;U2GG+t=^k1o333g3 zs%D5^GpKaXkbJ=A;EO}{Zn*>W2;x`cKS{g$wJ&k-I^ZolS(gi36_Jw*HNmgSAK8hS zf<;pZ%B8r=+2S1}?-+7da@u+yKg*i?z@+g-aL18hoQ@XsuKsNN<}Z)M&VGe=510ck z{Y!%7d6?$5sW7u-`ttITHw^AD>e5r-JP~S_& z(LxloV0fTnpwKi`e_H&b|2qXH%wVE6QzjgF)4gM>68cd{wI~x z!hv~svak*>bJh&yedmv41$Wq*4aKX7P-e*;sxUf*x^@pZYcncCH3|aa^H!Hq+%@s{elt*@xmO}+ISSnmwoo}bE<_)fY@K~?#zJTan3o|H3hQczl^&<^~ zO~{uhHVrBrEr5#T{Rb7YgHH=xRO@IdiVU^0^E)JMTXTmS}!L9+oSXpb|dNQ>ii_2Dm+(|ZbO{#eZ!=Lymu6+o(C?i@h7NH zwR}hpKEtjTe;ub8?0Br!Gz~n=>$M>kgexnH-@3F4zRp$`W%8$#uRKpGWtd=OVCXNk z5VY(viw247yiwq(#y;(xXm0Jnn5fu}s1z?t-$aqfZPq9g-GCR*9I<&jI+{gBQ&zUJ zsvh`%S0@4esl*bHCNcEnvQFl69DQFL>OA5I?rG+#v3|3I1qy(~>|@y&yV&T;tf^53 zw?ryHO^zcgeljbp8C5`4Q_-0+b;}bUqvYB5o8{W41};V%#bFj~;@(^xG7m>zoBO>D z%b3abKDD?wYwOImz6~aJv6p+i)GMugZb{(FQ+ODxfquCs{Zr6kMhK#;2k^`2q+fFU z5d2DHQ2{yF7%OgnKN%|-57srbtrK7lXdikf?H?!;4H%{DoSDR3kLid>Z>riQe)8EH zYWdwZh*je$`wl0geZ4;gco;YIzPyvx(V?w&-!~t_x|4YPB+H#dS+nW|Ev3M=T3%f* z=SEwD@cUw?zW|p^cS5AO6+%lPU`rzbY0SriTYWvu_|VRvOu?9NQQ_abeEgC2$?So=`o*O(qa#vg%Xkw7Gc0=8rncK z5YMC}u(vLudkxOSJ67#Wz@}Az`KAsdj>$j;)Vk_Lm5QuA#3hfWp)RDRg%IH4$~ZC_ z^m}S8$i&GjJ+3dPpo->31ML0Wq`^C`iPTS|4M5F8U_~ry~zR;`4`9ye)SviX1=N7r}lhXWAy5S;hLzN z@~<3NP8o@8vbB#}wgyWGA3t{JuQ0MMzlZwxeQ?2NfUhTwpFlMjmTv*nSt-cN5L!=q zfIMy6Y$hX#&}4;wnEuQdU$Ii~Ty+!!jo4Ev_kqQ4@96{Y;=@F1b6*`+?VM47y8aA0 zZ>}KFQ+qR>vsT;?hg5!D!07%#F*aIB_h4%xcD$UDLH6dk_gwX)wp>aeA#A25TEv5* znLZ=cCyu`1WMizBmJ35nt=4fNkOU^W==SY^8ao65dcnPXm(5#qxIyVeE!BFIXIw8w z^vz9&r(r$>u|D5~dZtV5yp$h2=~{mPz9Q0%J-A12U#6Dt#Y~%0h9SxjW@2nd3N}YQ zE1g%x5{X28>JamVxQ{r-RZ8y{epYh5eOb(}~1$SqbwxJ>NHVbAoe z+hTDWzPh|PPKul!VQSga82dycu*!asqfq6I&fkfS9O`;!25l56PtdZWjA2qI1s)A; z(Tr~}@o?Ibcouv5lXxV8Nowcs!9j=H=Y+bJArQu@RG>j??f$rchpRVY$}#B)4wP-$ zp{|kb{Id%FB*#HJOFGC#pO=#cSm-R5Hrfi zEpvk%XLwyXSBX4ahK3jDUz2Yl4R82dAP&D*Kk!3gFBy$IP+$^pS-v>~?k|@FG{G;( z+A?*Rc*$A9!QFkYi{LMO=WpEWU)=LQWdhdd89>dRe|iooKwS88kYeooI7r+KNDu1|uP2d{u@4p)x8uRYcZ1}=pUIQ8>4!+!W7hK>8g*0#O@URE(m z94F8LQpd1q9w#xtpFm?dl(Uwt#)|~B8b`DkHH9b!GmN#-+&4$8qgWPdMgU9tBe+Oa zldxp{?&+eT1WPj@Op|UMQF>Xlch%}2vNS;n&g^quf?!-q&WDqZtV0Ur*={CuU?Ay^ zzeIV`8}=Nn1EUKyX2Gl#)Mr3fCwf8^@E!%uD4P&eFnPpbb!AZ47)OKvfORuKp==T1 zpJ@Xc+=2ku9m1d0K2@<Xl4J_(RoLL4`wAZ}+LE!n-dFYuB+R=ROfBup`4!+IU| z8Vdzp3s)5e4>r2-y>4;G5~{MC#&UumyaObxHFgaEn|y#aBAaz7DAvSA%^XP{eM7KD zxGQWi4tq~ekow6y%&_)B$OBap=`j=&Zcu?n7A_0J!E? zIq9#A^fFJ?p~=7?8S-C*-A{0h2a-i%H|98A{q%i9bGeHR?+Q!xDNhKMo3G;=NEXcA zU>Zk1J#7j#(+Z2EleDTJ@zRKc2tjE^X(hngB^&#UHi)dE1N@L5Q=TC3HxF+`Z5O>p zy6FL|_quX-A`HyaG}~zI4P6pGTEo=t7kNtgfg}8roCfRfZfP)ZU*FM*$YUn0TJ)bQ zC8I}ie&LdF#iEC6mL(?pFOCWC09+UuueSnr@!L)N7Wurz!2borZ}7enI$azkl$4Zs z2i2ek$K|2#iWd(4P*qpV(ZDzfx9X1S#TE*4BOxfbq$COUuR`7cD5Y~I} z1R{Kn1>KP=V1Ymyap6yLinQl3YL3Dd*5qjLlFH?}*c1kEPVmtp$A1#aXZD20<|}pZ zdfHk>PvT0eIfRnKoYp7M9+W-Q-T!KsIR2!bM&L7jXehOgRRvmZBH); zB7po&RJynp6Ssb!z(B|i7ymp&L{|GqGQkW)0K80mWgYRhD04xhS)_0n#*{Bczu?UI z<6RUt4RmMk0Tf2+eu?+H+5TKdgcE*WuRx`>%;3{mGg_w%c7ZZFXk- zb4m}^In}C6*~4`oZ&Zuq3xhncK)3VR_OqXU85z8BC>^4n(`jz!!RduAEn@?6I54p0 zr2E*JK@;O${hr%NGTPucJdDpCVa_THlAJ`Ut4Vx%prKtECWMd`zy7Q# z5U;|UrWaeQx8o?1g043sKrTVUfZ?z?Bzv44vxVSyO!`t2hoM#BE;>EC5O ze_zx<&+qT9-`|IrY3reNOlbLRy)$!Sl?6e>((0nxkH^>~2cnDVnIB8-SVL(9<-Fyb z;i{Bp;!6{y#Os5l0v}@X3M$#Rl>IdN(=w3IZ}2my+0Cv2wMn{$f8w(xt%v>{ zwk!J=CzPS8_GI7fMyJ+)X9l8kXw{K@`Bpdu+9*cYMqLETLSOi)Bn!Ts?74^M?%2=L$yxr(CqXM_&omKu>R&crBX>irH;CX;GQ?+X? zhhPh5hUZr2v=vO+4`oOQSCbyM2jC^Wng+r>@p^3{RE&tw=(p-~y|f%U9Kez23y>F@ z`(7;QRD5#;uVYq}vMy*DoTSvAVM#^lSGPetpx4uWw`~8tcv3IsDYqqm@WuSNi>x@C z6WR8>XbdB*OXRp2=x>6cEEcno0!l_S*H<>ybLm_FKutqiHk<}VxcH#m5SVR3tZGdF zx6Apn>I($SX|54-dn{T+0MNeHODWc<3GURxeOFCgDB!g7g*)d9zhgZYfrO}J)>hmD z;t#RULx%J_nHQ)vvvJ6;1~b*!V+i#X!|i!a<)3L5@z;Ldjp)6r{7PTr+hp%ytQUp0{%*2i96>v5#aJT2LB%+=(~ILcM`H|C#z z+SHq=W-@a>S>JR`IM?mdChoX;7AP!!?O#$7cO@tK7pdX@I)+j&wZ46q^{D_|;)SCG zdu;SZ;$rtd{BL#`8A`#JWKofkkzq(7?UQp%P6a@;0n*NkU+dMqsMU8}K>ptoj5h@E z($H|vv-K#S;r&r{tJ51|L`?j$X%F#bcOZ(v$OX7|y)u-bWn61RMb@7zMsJ_h)V_~@ zPD>xjG`(6oJ~S<+34fA4kUA;Ucyg|hSqDsMI@=bkkop zzmRWyIvx_YtZ|ZHF+MTxXY}aZJ*vsqBC9Jjp7Uzf>9D7_ zx93|{%k2tHylIqSWL;CMRioV|jac2ZOX8P|gjtFY3$uV<+UTuyKIDEXofrTZ!8y+k zOun+gRHiR0Zb zpf&%e-0|D%>@~f0MeR_QFy+?VHuRY72GRygnpN?oT^CfRG`o3Ao6TAx zaaJXUPnMLbQK`HaF)II&N(B$YGifl2k@;iox2!Rp*^ve}R3#NuToXu;=`^ zB?=+yWf;~J^Pb~G&AFnP;m!L7<+JJh@ur;cT)cZ_E!cJaodZJP92Pmq)6>GPwY-0* zj9E|avmBSI(>636-}sA``YDZgC--Jl-WAF^lvVE)|6dO)%aB0t4wwcUoE2V0MM4%9 z7K`ian3Bw(1D}k?)$mZSYoGyo_%u08Om6QOwRs6#=^)X0;tDh$WKWR2|XZ9~Cjn$i093 zzrOms6nqX>nTnszq}fOm0u67Pf3knUwe%9%HjP=&PxPe@FS}*FL~vX@8w;FGx15*f z??u7F5FT<_dtPzzLZxcbi-?(RL2FR5-Zj;M=jaI~CO%GAurJTToVmcyS1MWQ(+`4V zZmD0#ZT1w4-QDkitBlec!E>HBG-6K*0XwLelv#-o=DD+W+si@X$c*CN9h=?H>BX); z_S|!CeAL=XRC`WkHBJ>6!l!os`+w@?V`*?a{z*km?0shw{gipo{lql6Zl7vb_^QtS z%65-ZYR(SLzs~ZFi|tkU7RJ9s8*5z#Fipw+ou&@=C6cnu0+NJ0=`0XvSJX7;vxv_t zZG@1NdeXZ}f;QfF{yX`KZ}05Lb*8Df1@gt7>nh#DpgK(V!)uSGM&i^b*~P*lEw-!-jl<*W!QYngmz-73JWu!zxeo-hicUpN#F-;&$q=0XvgN?;0fXKWmi`(* zMm*l8m_lkAT&*A{cgAYrFl;zt@g#$TtN5#L>zt@w%K&qU zRTVz}z4hCo`nIGFBK%+5p=D#_Pof>A#QSIi(aV8*bp)8xyAkv^ zttR{~U^mlV(SB**Qiuv_5~{Sc_t8Y6C`p&6w0a<>U;;Sr1G0dB9J{vNqmM{(7fK@U z@87er`Q;Vg-ri}2Rq{@1<;;_-t~MXhK-^=utmp$8!<+q%<06M#YkaP@Uj93K{ASA- zAhHGVmDm3)eadF%;HQ|~6Ax*%0vJ!RA{N5B7K(v|J@{^nBG5*Q^XIsfCyan}6a~h$g!#Uj% zK3m(&0$2Jpxn;5vdmFLOg605>zc+3@vm*8+cI)`W@}5y&vM=yy)H_bBz!n0J^w;|Q zHaw!`bI@cZ_OUz3;IigW%dOKP(T0M_oK2}D^;-OPK>}Li@e?8#qgLp@{rH;z#{s;= z#p9r6WCVL8d9iKPw3NoXp?G%*@Hj7_TDkI$>krl1`ew+$^;uL@bj7&J^K^&0Gb}Zf zh3}wsswlF0LiHLvR4T+7g_zQ5Wss{AIDCvAi;i?`U~J9S5db1Ydq!6Ta(FWtpnP?-`0yNNFw{HJ2-UXUecBzvBmmU6#W%8Y zSbuG`Dx1j&)1TPAr}OP~1ivoV78{JMb;ew%<;R!m|0UafxO4dkB3n-3{O_q8y8sc9 zycaf~HAaf`9Ga$lQ~h;U8O0%S4>H*TEPhqiB(#_r%gyJ-GL>$UfdyueT7|og!N#39 zpNCF{j;+Sg1EmT7Hm)=FuqlHuPU8H)f3A&wOE;CAXf-0Tp z{=rgG55MgJ2_!gHf^PgbuiHws*=~1tx5450%|Z+6=o+hfWji2^e_eGN%#etXt59R5 zpit2TnKJbhvOfS+iffO90VrmqmI>8?1dmN(E+Z)T(2e0R{T04g9f0W^5!{2Dv%g9D z)kS#ldx-}^FfGrpjzE=A`Uo)RC187uBswf-boA8u7xC^TH_P-E=P&HV5%F_Zk255S{x z?1U?x*3M{Y4ffQ!v#DKN2cG6x4Xt)x`1v=GIN~6eDh2?lp*-CIsWdLMJ6a*@kR&#) zE<{MUd;FDmZa~vj2l4R^9wk@DaJH=Q7{b?x@*Ej&{=SK9`)O}z@wP!-1Mre4v!pzQ zXyw0Hp4NiFXX~7VTMv~m@$rL(O;IvYWPJ{v)zP^&Py-l8);7t4D0nZzfpeb8rMF~cu&J@ zMA@zXi<3fI)vivtSc}ob-L|HQMZyZ1%l)mGR>%%lNy&exT-AK03Hh;sY*uMM>`g+R z=0IV*h#k}dLbGHKEZcyZ$Uchanntf0IQ5oywqnboYDBSJ{?XQ*V(Oa zf)45;!q1W2u5Zo=FFwR35&Aap*QSbqHbx7DJv&DCLzX$r%StE zoaPN10~(wI=auJPXSrNPR<6D&PyG`?9G6PBq`Ab*BP{o)2L!YHpf6DdBrm|*V)CL;k7GO6->wDf z>DA{z2@5QmWb}ajZTw4PA<*g|XMCvD`cuD4R%bYiHPLa3UF)a|SU)ZbFAlYiJKJ!M{aY~9svlazpFpj-;p3FMk zo$mrBfE*4J?_%kjfGj#s6ZqEO@kI8T)w!PT=r{1fuQBlRUKmdICkhfoKb7@O&9Of! zeNryyx|2NRwDco4p3fftmZT=&w4{yDYP~WaFVaQcpyqklxnk|F?x!DLIdfD3e5B@( z>@8!)gS^q4;wii93%tFYA6Y4gcOgpSf}hq;%z5bFO=RoPo5l5 z6=^h(8E^f{xn-~g*X}6&+0+-A1-8jKhw)tIG)5u&b_)x1p9bnER=W?#SyV#7dC}HV zT4Dkh9+`;Le+SF<)3w6|I~Ni86~XM|t)-3=MYablfFbRO zFI|peZ=wY3k8tNh@;v(nxAPApB6rgXc!F!5#Ysgv3OemTzOku$H+4#rT-&NxGF)m7 zFz|KhGmDOZY)5^SdV>c~JqrR^M81(C5rf23EGxbt}45f%}clFFGjk*=wmEyDzo11Dd+xkEo)hO;ANmU}^kDgph>fhof|f#U!>t zUp#3_-3_+({AUi1K0JGSob`4jtWG73!1KrVG_+(4fLSczacPKP@u-eqmKl{Pb9^i`2t_an-Tnvs^wUjJokc5Wi za&Ol$Sk{ID_8ftmx)aGIMcc&0J%5c^pj11Tt=tTXHr9!kpqkGvUGw$^yYzRL{)@bN zK=ElQQ{-@IE#4LS3NnK^BB0q+q&!*Z)m@LQ{v#_5;;lE0$!Iz z8^!VN&suhup7hE!SCITP=+B&b#~)V|M8OzlyVj?QR`dkIs+CUC*Vn1vA}daIEui1oIy6b_a)I8>x-pR658+oPbm3o@CTJTYVEZ~}xVHaDQuEp2R_b~4>Jdu?VyC$}!RV-vw z<>u>7@cBe@9p{JGq8yqT8`h<}`oOlfKr#1snsYBcFWcH$Jm<2Gg`7%Qc7durz2k;P zj$9ecXS?*}T`GXbN+(r_T^#X!XY6dsHK_y9lV5&H>P;O@COR&zI=?cT(dg4v=Rz+O zy-Y|<2r7-a-53-NXB$rM)CMR$)67BvRVpfT(v_ajgS>MAmn~>TKUkxY^qJxDo4pll zji4tqRk3@I*j|8(z;Jf;teau&mY}l_LRt$0jbu`ULX23a8PvleOJz?Y&Tvb}rV-wm z(sF%?g4$s`e=WkS=!oGjHEGihcsoK^k~7J!m-y_)G@_B0&mguBycy9Nf1d{~tM;2h zUEgYmJU#&Ak2veamurN-Z^`}tihLMzf$r#bIgjy&?b*z@)O$X-nO!rNg2Hk;JXXv5 zPg=^Cbu9#Mo((b0w4u_{m8{%}^?&gin)#a2l4Z3598yhVZ^LE#3k9LO1 zPw>yVHkIe=opAtE3332vy1#PqWXU9@g}dHL4H}5>Hq2bkABhD^nM{?})S9%a+LdzYG81gLmLMwL;d9?Y_2Bor1LW>oUt z06*&yzWG{5$yg=^o3Orv{8tw=G#x`#$VVXPnue0c^3$eU@4G)9O?dY$O+w!m8FJ{a zLBQ-I0j+A$K$(U}NjM3(H;{E<|2_%~?WwAWO+X6<22XN%VW40LECK=Ml4K&lP(XTA z`Xtcr6riPIwYYq8`IS|{gL8GeUK-WxWlkNoP-fJm`w3}%VQ4$*FYLY{6<)Bbsx z08A2nf=S|5L7PKb@ZHCs4vxJ1HM&bbOwBAb>Q{>A;%rVMgu5O^$Z1KX3JJ()_v|ld z70~y@6oz|6L=7KY)j95F&^(NMG?T9+I9ds^kU|f+t?+Op$>S5$GXTeP;_-#k(9&wo^EI`L0CWE5)TYqV+ub?R%oK`jy*^9wa8y=>uT+6 zd?t0sub&2$W@RKK@&@7*-Gi71dZDCmD;_$QTFr`&dH{3spkN)(JNOvE*I{er?>zc#1=K9SdAV;Z;&IqpheM87|Q9g#~ z_~i?GKq1U{l`3W@#kFVZZNbOd&N&s#S&lCeka+RS8&TP|S1?0TF9;>|Y;sLqcSg$eok~ks(}j%7pFH{JHUoI)_k;f~ za{a8Zkq(wbE-SyCLimv+5csnz$zgXB!5aw8Jx2T$Pv{}pX%f4RC%{vGVZySd%tJxL zJpMuS60&4ucwbhKz?e}ITMrgDuD3=#Ax)LkW4PV`W$U~jTE*iZ0 zB-4TSW3nh>>aNDecf6&hS*s$#ABP7Zl(+}tIZU5xd1%-RxVT$kc_8{h&E8oR%pE7U zJ+$f6OET-Y!6iObom-~}NXDw#qqYFo`5AN)ne%eBab6H~chQ#x`^@ z5~c*@E0zUDg?-Vly>TD>{BAzJgZ)q<`&qG4;oW4MYW?{}Up4bDo;VPmvhD~#`_2tT zd(k88A|>d2-w!{<<+X4GE3io~lAme1RFQaK$(FGd#+uv?-RlUU(7BA>tA9_q{-fV} z-jkMalrf!(FKe}p6kQzO?2kVdadsY8bHA~_g9<<%a$6~110Vk<`{E-X;<MkBb;=T?R0%Rob#tzSr^$ycO;afCzl}VCjF#DHBqiQMqq-M zx|<83;I`JOilNqe2+0b6rr7;SY|Ue5in;Rf!uDK4MDXW0xz-^|UO@R73TXNcY#p(s zJaVaOzSUURYiny4Vj|WXUl1?1&&{C%v*O=?{@8L+?1i|0FB@2ajhdb1VDRZl!hAJ$ zHCweG+@-;*%l}kdym0P~I(3a?-GhjrYofL4_riDSu}Ro4>fDMFZ^rqH>rO!?2_6*s ztqrY7Qab>FryjyR5Ejh{H)~*2s8!C>pp=n;@qXU=q2zJ`M10K?6L-bMMEeAl0C<+QP7MvZv`FMsOVk(`8eQZo8XVVIG zW!rDDuaR*rBijah02$D$gd)n8Qq&}FuF!-b?hhSj8R9g6LWSq(xX@;OkMrjHKNlp} z&slTMJiHly^$2YbZ75q64{i8(h^+ge2qLYRfzx1-=|@3jr2B_|Kt+H@i~h}(V5Eas z`?*Fdq3lNQv?T-@x!-RKR*^x`Tm1 zK^fAXi{ZLfJ_+!~vC+NV3$8t?>e?#A2UIZzHo zOxjVTdzoVa{vHnSzfv3;wUcUZTt~iuuzUHSX)by)u@ziDgnF!W%l(i}eL!7zq_i2u zC<-!2;S|}B$+;KmUiK~bpQ#m4jTNYd0J|v@XAqgNl>EH8bKq0vAov{X0D7yAQ74D3 zz_$j^k6Zm`6(3&3Z!>3HSsuoEH8y&$wP2rnNp6hr1K0c)S<*>N9t8y>Bkyk-9RA3^ zvK&(j)s$dSt$jO8tL=kW`{oZ$xHRalt!Z=N(2rtE zeYUv!IR0`;V6Tt*M?o#-pULch(SJY0dwTKPeze2aOpB(N7IL@}mDKN=XWlBC+$kSx zr3_<=RV0_?4b9U%e2q4qUITu1ZPsO}4T8Ep=pB_{gd16KQl@ywp;=$!?Gz${kjL4B z1SqP5)lAH(A_U^!*SrW!f)b=1W6(Dm6qemZHLQ@1CLzU)XPmK-((JkvtrGWJSw+)-O*D) zN)?6&U*(dNZ$1Pfl4Z~`-)}x_E3tP7bQe;#Ws3q-u_r50<~CJkqYq_dtT?(Y(J_Xs z#6ny^EYJrVR(TXvdsV%G8PA0pDdxt{v*Ks34#ZkoGoxNATN(UHBwl4!>;i?7t!8qd zlApcLf-{}Q2YP6u?J}7`FO0Z}fP&1}*&B%h+l}}Hqml&pyrQMK<)+V-44aLEL-_LB zq7Fdhe^8-y8Mo-HP+b?-UeUa1yXbseYs0z~0KTfddEk=To<~C&v)~BwOq2CIy%j!?g zo_s>Du7-kg&=uf0l7^ArF>hHd7fn_7`c)qmHG^t_xMA*Gxy?SC2&!gBrNzo0+|-0J z;rSuXT1bp$o@;OsP&fbOzsTMPRuZ@Ov1c3gSXbM13!^Q}@A=6MV{)rx?I|9rwzc-W z*9R!0&FQnk*Kow*xlrG{EoCvoee&&Np(8Gut4gWlID*SKaDs{NX*_@Y_|@|Ym#6x` zO|Y?;(Ba@h1!PDmV4$=`_D9rz}TckJ#%3;xfPyK|n@5KpenN3hxm z@kq&5`3el@(2m}O%Y8Z)$a@5Gn^8B1&jv%@)%Jg0!~H3o0m*LoJvtn<=SZx*|A*$T zMyq|Qcp(W(RRw!aX>8a_NJ!k}_`>=8VicxYJqAKy#29&7vg5)eErB5P^9fsyd_}EK zz3Fh4f2JW3@Gq9JZy^U&&Yx&Q;C33r1lXSb&+(5LqZ{K=gLK}6LjYj8yqOt{OgDco33_n}} zf5o>FjIvZv8uj~jyVFj0*4D%#*);h<>XLeN=M61%j(-TOu=k*7W?AXW41DksVVY7k zb8>Zln70L_D@Fd-k{h^tRx2SA`^{=K{h+I<=qehKZ35kqZ)RYFcp|kzdu@Sizz;;g zwKMI4JgmX@(P*)GBku`;>1`YVBQHPcEfa7aA`yKb28fj!eejWIa$}NVU4G9W!g%8L zu6|Xu*(bE5%UnO@=uOf#%=l7FVEDmfGJ>d-Cg;noGW5Gdu}8O|3F!zlg662i}M<+su>KZqS7CKktytP_}VPf;sz#;6>{G z^&$yjk|v`+RLxf^*t|eh4FIW`_1WyErw=a`fV5z~%vksb+$?xN^TMQ1g4G)sBS`M$ zZHGxs_H&hR=Po)=+L1;r#q~|p8|I@>4asj-R_)$%Z>tB+VFt+~+rT11BK9F(6kk3K z0NvB>+8}rL-1-w8-$joR0&ebj@WwX;y5c*RB{XU2{yb4SSV!M{=4JYj5JO z_s@P2kK|2Lm=3RX0yjRc)%Wj`o?1SAwP%&?<)*h#!{T|kR(Q4==dfovEB3N;<+H4h z4~M}ztzLb+X^Z`2+>;N=-Wg>{c@llci|)dBo7xp6may;L>B2o>llb1<;= zwBy)RN(d7~=c|q^i|O+-Hkl&0tgc85`V2y!k8V80gh{?v+5VJGB-L@6FXc`9GNqbD z;L!qhXJ|?&Df8GPUfz{D@b7j>^T))%ovSaU?sfEgdbKy{{egH$92|1CPY)iZ_X0TX zv4urQI=GP7ZtQ{LC_DsczTW~R6~E)3w9&N#?S!!g^;50&p+n_Z!8Zfj(b)q!XM02O zT&_`LD&KZlRew?`efAfl9UX>@5RskS2WnTjsEyqw`VAmdxy9J6y$}^~-$lp!t36Kn z8jU&|-KoQ$_-23i7cnd!P(~9x7;VLrpk=W$3(;qDx9K+|q#>iAA&uSPz@?l0ww%IC zcH{>hDg zhfHRro`6MZfCzBTJNLKRU5iDjzH=`R7ryJ`GM^{W^5ehG$mLsL1>JUrqQ zWB#;=NDNlE(ynk;N(dpAs!ur`T>u1uHF9x8?GwXa1AOO|Ev6e4ym_2U^B!$~qh31f zC*oeRxBC4f*G$^!oX=@##nfSUlDrn(2>Xt(+u1vXHKYJU*k&(sy{py6jwuv`@-2Nu z8fpL`+wWi=(<$_C+-%*3I)&bdmX?4A>0#op$Qac5DNDGbjTHXfx?&oVF(X z0aJodzktwU3)`f{o)6!+KQUy+xj-(@;S-BmJuZU%EceyX{dRH|`TG~>Ss-aDT!~G&XPc~D~ezf9N|PMo*xYTgNi}Wsuqejc>?-Vin(LAO}H^=Ri&+$f2@7_LJedt zCikUo{4}m%&nE>AH5A<%q?YX#*d6KOidaE${1EXb+>;n4`K#cr(c0&|y@Kykaz=X9 zF03B~rG~2J?-&eNd4mR3D_|66)q|Xkmf^Dc4`o6gR|ZsqE*kyB=QrcchtD2)q69dz zEUYV%_y&eHb8>z&yzJhe%QUxM={CJ*ooZy(C;7V3qwT3dzu6sobLHbfza~$wpIe83 z*~*YzgmrVKU$CK2bFJ+uE%E1tmdSKww#zfGzo+Xi7MwgoBZ3vWch;fYUt?pt{c8Vo zsHB|r9;z$(I?nWDU2A^}H7V7Hbj!w#_&+1}8T(upoe#yJo|md4)^ncvH#oAIwhwdj z{};6EU%mF%wVfJb&A+rMRG-@PcA_~2)AT1MU1LavThp~0ble1&c=KwW8rD=z=yk|c zKhgAR@!S(1p3flG)2C7bGDbGh#_p^KlCl(S*drhZGz(FR!&{V*W@ zec9YRK?F>t#eTgQ6`lQ%vu<9Mo(aMji3$(@{N7gnA(fz%NhS--UED#MNNQ(TlIg;^ z<9@=%Y)OI-;zcmG(4eK2+#Mt56o8y*EKm15^{n5!nkZfFj%mAy-qWy}&v)`P=+;Yn z?J%TAy11i?^>pnVv_q(AV5^ZrB5XkLys(Uh#E@IVLNhBvyzouQ!1xfCAAXe{c7#h6 zDKd`^q5|3h-GZwNN=Zc5*@kJ*_mz??y}!^uTQ)WSQI;RV#rYsjr^V-ap{JWLg#lTN zP!m4cWl}!IU5kU7wh?-e8paeL5AoUSgMh(Xn_AHBvATOX`4(aJhg* zW45QIdqOoiB8}pupzZ07X5n(7S_4@?O_;95+L!vVf%WlS9_gfCm|Nogb&o8?6)2Jf zT7Oq^^s!*msejfCrQ*Zhp~+NK^7ZoaTyRDdHMC!Ke{sJg=nz1EOahK3oEHXt_diL< zHG!m-mZ=ua@=VeKOKiRIB6n>n$@`I)uPktRc;LL78NJS`nP#ljwPbRVPfXelG4yb2 zJG~JH8jMoO*u)Cz6b}(1z$F$~kdTg!yVPud&o>VNz-Ca*6+E%@MQxK)>U`9b5T-s> zO&vq!kA0bw*rD^DdxTGo@N?uc1`vzRJ)yw0I&*9j5KoA%W5q&=FnCFyXHGE|r%>9h zC!~E*w@nV;j|_67Xs&V;7+R~*G5Yi|%+QqK5T*NT8^!VF<-$OUD=mD#B2V^2gN%c- zR8c?hpXG)FXFi(ok$97)jR$8Wpg?8Zcefy3cFL=8zpH}XJ6_Q4B#Y6-kJ;dc)JEdQ z*IeMrE7SdtO9JCxXACV0tjY5Tr9}&8zuoK^Y=ySSHgef!#(@%jHxEzOZ29PVv4(b$ zk6N&?sow+CVXbDqD~J_*xI`$Gf1=hcu8m$e-lo;qUcDdvu}%iHFM>3djZ1%x3P}F* z#+bu*>GyzK$z<{;D`-gvfbiP%51-h9v8`0g-o(^TN9H`G!&aZ$W+1Hh%TSt#M7nN< zk=g9(v)8oY!2Bk|v`mTDW62=-z(CZNdH!T09+VRbuZ6P=+MVl8Qa;Uo2(JB!9SrlxSPb6DA zR&ra-_`gLP=9|Vl69iCwy<8gBn;H@4G*Jom0|RXd&Zlaw*8%qoFTcGNu};r4rj$dJ z_K*yhJDuz;>VKSV;09$aF_p;BjOEnln66xK0XxXNP9#6lP-u>`0oC zyxBXz0hh?#Mq`<4eF`)<$d)q`a2@J@({jk~jAE4Hw z;F}zV;UWw)>$rt#%}cg)4|mZ^L3sffnV7}>Wf0dQt5_tjFc$IxV{pQavKUUv@3Qqn zqj^CqgAE88{J`Zbr-E4Q3y;Z!Xn0B|4eJ7J$(ymTjk!8m&s7TN^NrXU+6M9dTHgGh z6r#~SK2FWUWhoTa6)S&rJ$c9P)O>nvC(6{{6`J71rsl(!AB;qc6 zX$&_I{hZNxFP8S{ov9eSbp0-?93?@ka3!bWkb$L#0C4-Qf{zj)8CUoXk*`=25bqme zNc(Zk`y0%X)1ccXjA=brbG#6T@VJP*O7shRFn6}aomW=R740Oh(P>_wXc6yabx@S^ zW^1_?QXbK?n3oRapM|ZCv=|mLeuxu*-9t#>#eY1_OQ= z+I9GrI5z8gapzm&-h9j9;W!2C<*P2H!f(7;qgR1$5NVub$#HbLKLYgakB-mtv@S_1 zdyD-M$Sll3A{7Rk>{{e`v9rxJ{b^lz6K&;ljoYB)9bwGNRm00-(Gbj?dCzC3NSOg& zfeKww(I02(HUx5Vzswmv#7TGkFi3mj_F9r?I#2!HW9LvPySHARvR!yaZC6+rK~SU2 zrD6U~{`JeXR+Bdq<5WN~J014e7pa zOr-}Dh_bGv_6r-0t{e5B14hWXy?V1IOX$HBqSgLla*Ut$d96~*zGmRcOla`+mmxYt zq?6|(ZlP-b^@#}KYnl?9YgI6^+->$`Asu?9_s?Z~7o9(J>_LLqX(?c3lfMEHX<_2a zyH!j`dSR~C!=?_}@I7CIja)w6=`QlP+R8set-n+|{};izmy!b3V(}H{_5q^#?@wz~ zzI_Sv4`7#RZmBvm?j$#Yn@z<2h;M6bIa=vt;kyE%AW4^QY1#Ft(d|BZOJg_H3Tm97 zXN$lQy_P2wIlSh%gae?ye^7(TxjN1%KIbao7(2;wEg-nL(Ijvy>EJ z4M7?OM7ICHf~ilgh^)$U*+9qdx{)15()St9SL3!>dT?N!_~+W3=+MSnsfV18vtN~C z9(~~{rQk(kI&5(K-``3I(xmjAeJ;5=W$FCe!^LyNx)T&)<(s2yzXPCfT=>5a^ZKtb zqJ#-J&iGMh(=H9i(FP(;oUvOAbyO{mC%t58^;c;S%sYO~dd7EtdZHon2Fm*hgqDuk z&-O1n+Q_X34Q6Q8M-a{fMqrF?XFqcq1uRDzpVHQ)rPA*gCdic&@8Wu(Lr*q;5;0H<+RUa89~n)gHtEb<;){qGl&^u7H; zRL-#WID*@!jhP^hAhSRb%HA?!UUY64L4ylJT)aPC3falsIc5HTzR#{@3c3eH=|(|( zTdF(+L9z$YhS7JMCF5k&;)$F^lS7f`FMV2f($*XMyT4-cD)Z`>5Ijto3F)SNH!o9_ zL>xy$FRiJ)&a)(B_H#QvY5uSbr)k{a5~r&cPj~ z)6r)K-r&Ea`~+9|qdK|^2a}$)=`Iyf0^Ck}1XeBz%=4TuBzyZx%zerx^##NjI`GdY zg#EP-tnJlD*=*wRKDdB%*vqsY&P8*MWeCrSc66UREgsvG2$y_Xra=TvKI+DR1y(U^ zC~P_PUn7d=n}6(b|1fQ~|IcQsL|+fKgy0T<4gRy<(j3(nCWBMX4%pCWZ#twe+WrVY zRSmddh}6B*w!;YBVw-9AWGNB2`~MoUpxYtC_+eGNh`KASyvGDuEe)QRG=bc^t;^E{ zD@bcm|BTMk6tD$ETiCE=!2etejOlG&ClNYDka%LbthX{46jgn@{QB=EKgN?v`2h4W z{yVKm_@$X=a=9Mt?h1*05W4gXatT~lb=^;2SA>*N2O`jr3{bWk9Z?Hj|454z=O%lw z=>;HecLh*)-p+ekLU*<`aTuHW-il+7e3j?<)q^1-IlF<`u<+Nj#IBK?6Fk2Zw-XDf z%h5C7v|^%Lfh4hX8`}~N37#k}_2Yd1zQY1*!MYa^*;3wve|J$Pr!USt_4vHOO&v4u zTYj{~DO>MZ^*Z}iownI01wp^*-m@}(5I)tBCnDTH2X4OK%e}qn`dCtIHpeVQn=bvL zHg}r`^@YnRw-m}eQruO3Zqv+pyrY32>DMlNpW5?nB!lU*$alMgtIU&t1%G*3kHL`@xJCsE1JzqUGvr{V43&8%R-&%HY*jTr<@NHsiS3Q`WWVe^|k3vhH%^nJr2&WT3czJ z>;aei*&$#brPz z!nkRC_c!}b>0d{O@I>p~`6lvIPNCe{svM-7`*@nhxtYn_j3is1ZsuBP%N`_isrx@~ zmtEtq)iE0eby{uN-N$&mh6)SaH?|o7_0->+N`b*&%NG$ZpE_MR?e|?l_bj=G+)FsZ zE#igm=eOA~f>Y|Gl%54=QPIc2WW~7l%;dav-(Jt57)J7VnpS^FJ*+12u1Lv0^Rzvp zQQR>Fi;Q)uUV8D0QKUn!?=h~NwrWyU)KSX)34*BsOx|N~VO|%dJ z1oXEQvSdiF`Bdg2JQVNsp>p3#H!jw?O-xf09x}H_xU>!B5(bkp2LV2=J>w6f6EH9x z{~Z`4rjB{|v=zfX-j&U(#Z1EXXCSt)#Zzn;En53S1yeM!9I41DAU$6l4rdzx_&14^& zgWK_S*p&3+^^4f_Zh%7JFQ4s4FR3mrTVGR&TUOcXKPu9|iXz-q{<7c%)!t(7M6;yibMs(Yo8gaFn_a>LGu5^@R#yH|UAZOk z46>eQ$+W_qdfkIVx1Nk+MR<{epN?H(j&(sa4v;zjX75#LzL2^4{|(491M?t>3ZOhUO(wy@hI(XRBGZ?=CYK zo04~9R;3C;-@KV#Tv+$Hi;B@(h?|q#uJ@InCsu(AC9&v@{e;#y8L)AS(HAN`f47^xFOhXJq$pE6Rzjp7#clbtV1GY)_fv!M5O2DjyzA4a57kG6 z%U>_6?Z1WYIjI~V;)&&$T(RvZHbDDkkE#rHs)e%<>KHq1* zAYLY2*R|TCJ)$>R-%`UyM^Cu_LzZ#R9wc6|Xz%zgGKimCXUcTd9*>X`L?LyCMURO!fFp7k*<4pdzzbdhPan)&Ocy&-kJdvuBYiDU2O3X$8aiU zz#DCE4~mwnwfMw~U=0Qy0bdToNcD}f)|Wy4ElemRWG7p2uI_aGNiQGV_3l72Up5iH zf`G!*X)LocU=~HK4`ozsWdLX4Uz~*#FOyC>VJ{5x>aDE0-sY%evWW-VN;K8kEH3e+ zI?)0F3jow~X8%9V-a4wvb&VHALZqb|L{u82Yto_8h=8;-(%p@8i8RQhq)Qs48&OKS zn@NY0u6bY9-e;eE?l||HG47v^vBnzu&F_2P=lSIiBtf%!YaD`Gp$8!GurCZhcX^l6 zzv=v-UAu&40Z2DdI`lHEYCk%P09FZ{yNKVl)J5VOpIlO1)&}ma0Q1f5;=LHh2T=M&zXaW zyZ0@Gea`9o%FwmMBW_%Tq$zV9Vc>4$sPqQ_-%50>i zKz8uq%W7xYcr*Q~=)M^03f*sxS_w9@HRRJ&4eEhD>cQc=7w9+7`=#6MwIg}l2qD^z{%-t2T?^RUy1Na1sa!MY{~$=l z=PKe@^vw_l@)`CgawsB9;4c{F(r>ZmF{ets)_QKJFQ_j0Tz)$ohpsGaTsKU;g;<+d z^r^!3q*pHS%QmM=HjbqhD<(eudjkAtTA67>5hsQBq zLvAO^@v=GHd|l1xs8*+QD5{xOG_|4eu0Jp=WqAk7y#~TxW_7E3V3#?2KCy%jRKXGy zzbngAAXk1F{Y0yt=GHJ>Sggu?ICwz4IQxC&+kR6$m9Jh%%$g;k&fC}teUDA3I31~? zxHj8C1eq#O3WFv-f`bEIL%U4$ndm{)A~_ULYQ^-E(HU7cI^DfvQdA7Q5m@JG(aT>d zzFTxzGU_+;aFK0DUdp+A_i*}5p{v_yiG$5$A|+qJH?TFk&~*MZHlF1#DKmXXr6lTt z=NZi-5fKz@3J4LvSQ1W;&h#TBZHcopJ5T}XEd`pR4UN8_l%vTTbpoMt(WxIc)K6A> z-`So;7dZ$$Ry+VqH)3t+*1Pj(>@Nq5B134(p3ke7G2ANnyf1Op0~`;>)inNwG`Zei z68~%q#3tM9^7>D1ITiWO^@#TD{x56{A+cmAuL^Fy*E%J6z4fZy4{kDQb{{r~5^~uI zpTeY%t)`SqWRJBpn&+`vd^>&|48fCO8%z0KGk|AIXa&MDXr9X@1-wzxNnq5&zk5JQ zaI$|1PhOtL=dLoV4+%2X9}QKP@rk4h{b>8n&CVaZ_WhhRo@+05ChQn{6_}<4*GczP zDAY@S-2(Ex=~x%Q=U zhG-B8oA490O)AIoKaf1m54J0en<~^)0`BiO^pP6vyQjOcZ?Ew1;dRZWZqHR;L`+Ui z@Z;M|KLVYPYWZ{LmJ90RWdwWFSmCWM@#itc6Z0!D7~<4vfK(T+B>=$nokDJO>pax~ zUeRf*zAAcAq}-rbR`oZFL8?!HGhlk&;jeKU+^w!IE=R0BXAbi%X5aK+OC=6!m;riA zLqo$LI;mHyr@K=Ff5#*lVPR znjkR0#RkAorI|Y4>hO~Skh{X$HS6_K-TsfR_7PMOud@!Q54vghF{oPPbU%9eO|c|2 zL~-`#)?C}h`DErAk`AtXjop%-?1w+lzNzxy9D}x%kx5SR{aKp7x8F)MrBt$Wgo}ZM z^iwBl7EqU?$Wv;9g+W?n1eg4Q&t0d?@3iXI;{~i#e~G>3lr zQ{wA2{Xt54p7hLI+Knn7w!V)#(tQe6d;qePNP#f6tI>({o_E6tFQR4o)2Na0!hS#p znML#J3C~GV6h#5!x!d7Vu2dh*%b$tpwU+$&`}mCXQJ`MnQ)=~Q*HHEoxujxN`Vr!s z+gf4gg8|!1_~-2053IfN z`ctcp&A{&}J)|0eS|F4u>_#(mDF@P~a0p)vwTBZHI4GXr42JF;M0dN_-4~DI!+C$C z^{Sw84pV1D8vbthSp@|Wi=H>fcE<#!A3lqsr<9qa+S?5<+EVe6wu4u zT&px@7^y(0jM{228nhzQK(N(1;UK#TFpp{}sal5J{i9qgD2X3IAmWk5r1m&>atf`F ziKSEH^2!~`-gG79B^Hq^D4X7B#E@(ACg;~MXv0k&U3&OX3*0xE4u5ff@X3BqZHc$GYI7eHnW__S*Y^N*Yxw^mZ0ips1 z`2tLTU#%wMgL(shyn5)Wm{uoZoBJt$Mz2d~YYs$zp5Z?@|2?#Cjl(p5n>8_lcPe`z zxwW=s0PVb$Sx1l3@*#PsI7NpuYzWSPK-jl3ouI6|L=^SncX6L%xt<-&RBPU_<7n4f z7AQO~d)!gaXCq8+u0&d@SAVzb+fO_CryAv}Uc^(2IoI0rvBh6fAu&mx6UD<{xGN3g zop@FfOn_BI2;=cYt0dtR8NUOGV@i|7C)kXFH9(N6iX>No8NT!PZw##}fo1S@{0xMV zXs{Qm6g?d3WkHdVoGMZy{dnSNG(=@*`omoD=&Q7Sv-9?B4zhrHZXh4M=_kqh7v9pyg z)zNACT;Ll}NvN8XCF=Rs*p3NG6k9P%4bCbqf2{yQ_37M)!yh>V%Q8x47f+8TI;&gA zgnKHz&P8h({{o@*6DKoaAVpMXk5C~_EOJ=m8yU6$!#V?tnv30h!<#^m2nL2COjO^YVXjYn$k+#uTFNjUv`|KfXus~600|Yu@<<@XY>s2 zP|gyz9e(TBg5yn+4j2dtrZbk+QGg1`A7qpXkyxzw$Hn3A?d_SQxq5Eq-;MtHL+sQN zpuk;zM!vG=@jHc6Q?`wzS4X`4p$TvH9{%Zg6?r!s#61LfzUd;vQAjxS%O4KAoN{HG zmM9Ha*ayeZh~TnVk&c&Y>Bx4o49G=3!X#yn>Xc!5)_}`4E9kpQbag_>e27+@!33>vgshxo6ZmoKdeLsG(VQ7E~FICL2wYp2eT)b`(^ zV+X|OZ;g20e6s5dlo*(WmZbkBo_ap51*a=QK(+g7}cz9W5u<9tJf-tliGR*k~gZ#R=b=m zY31TufsYOqwN!bzOsAfi&O>+h44WXvZie7nlBYp-Ph@gO`%4ztXmXBU;u|(g+32ig zaHFXMAB=(5sw43)hGk>E#vJK?m&SoolBF2^1mKJgxSFXGPFz^I@rw z(nd~;Bm0@Db6#%19Uv{0NqB`jpo~DLvN-AP0fP6FXfG{eqVBZ$t)-2KVOY@;A{)Up~Psxo-{g8 zlDGF2vOWH?Vf1^Ga9=?6&fvjtZA}d@mPeqTe%q%2r3fKLJuFY3J$ol;06t%)*@Bnz z5edogBORMJFT&(z7z%H|H}79=D^#joE9W7i{f^6M+#O?hs!XS1QaNo|u=e~2KR4Ul zgl%*F6DpSI>VeeMx)I5gB(eR*fckStg>G3)gI`iIIo7Hbh&lXzGbTP+`coU(E`OmO(Gu`hNxouS;qV@1EEDC|Tecp8R(A$$fOhW20lJwRF1 z!|H-#1T?HbUhCEi7JU2ibX`d~B0^URb z<ipvosMJF4t>P%`x%VqNJcV=&0Hn$wmSGX!mUn>wbh184r|v^M4o?BL>dN)U}S?O7mRio95%e7`<;u+e2A6aFg_4Q zGbl_~&{!=nr5$}p*KTHL7%VBn^7`nE&7H>WmUL^pu|S}JKFaBRwr>{+hW}T;c7qYc zWa7Mmcaqm<`xCQi^R-s|4J526$==T^fx`o%X|2o2^B`w@BayiYcoRvIG+YtyZ^UBN zd{YN>z`OA08>vent6}qtQQtEn&V>>;yJnwrCCMpp5qJLnBa=`m70C6aq-iQNJ5EqG zii{8sO8LZnE(Xvq6q=}#Y-R9lP*m+WU!-3FQaCIYbDme3)<51?!K0&_GWl@?ZcVzT zO5VDnZxc##OL)Wbowed0YopLH@k>vSwXi_kGtY?yoL5SU3c~->kUcj}J-5J=7L*ua zlXG?U)RN8CS~2VvYQu&ygmbMMXJ6v4*?{Y7Kx7;CTMvnn(;&KPHrpsKZELKamV`r# zP3^d3GF046ez>mRG?g@(qW-o09aO|CTXyj3d(BT4wWBuAQ*AV8K^K6e34bf5C4^B1 zw$X~olObNLv(w=H?ZYVuXfT?N$sPQE9rva|vZ$jzhJT(S2 zE4${%n&G@XpHo)rfr6sb%2p*|8xoXM0vLsOY%#egBr01UOvps1;Z(jqDM5ygO`igZ3b3_iNf$-*r_3Oya_MyS>c!0> zK!M?XH=?y;Q_j280Y@4Jh@WD^l(ql8Ypo}el=%oEGFiS*NDi%k?ymnfKT#PM!thv$ z>C$Sl6wS%W%H;P!7zokNaahaWnrva9;5+-2t$Icq2&=Krv$wau!}Q%-nEqxkUx8r|!^n<)cdvq9c7s$_PJ! zUtjgbLh{ym7>-x;xhCz?t9Or}6*JMow(;G9BA?P7zWs-bWd-sK)#<%VvhV{ew)2S* zn>re57*%h1ZoNJG36rg8L_56S&Hem1{%gEniNG10^i!XA`-1ssgYz{SZbnP!n^yDB z*}Ehb>e|6Ir`{jhZGM>{4BDL{WNl}gYZYvot!H*Bjv!P(uODo@_{CA#DdI(o_w#1~ z+vh~7MM&Vh{~J8P7M_OCO@Aeq7(?7TG<6`8$NVAp+gaNuGh zebRg7`uTwlZ{3O zLpCtHlLKx$Axo%|Qv&JQydB=q53lSuU5NsS%&#z?K^rGxz_AVNKYZj0d~IkmRj^(F z+4wzxa0ElxWn{#}l#XcRwgH1|Nk%r?UZMyyk%l7J&KZY4)1eF`RfPLo%Lp5^s5}4@LS8LumZk3 zMi=Bs#6w6M%aC4yt&ho1vw&6~bi=tiY5|7L9+XkP?l#72)CDYG*1r%;y{1#ytKWP8 z`DyI+jf#XQU-SOGN!SZI41ig>5T17fF}mRd1uc*DJb_l!{%3|*LZwW>^sKgjACdQH zkj=SttbExb)l`*pUzwb-ef^;IJneTPyuQ-jE)3C5t5t|E4o%ei@Fo$`W!JT$2GtLb z$}MY2`}h;__ve%TYcB+cZZ(4Kp5y^dl>{TEHZ2c`AKFUozPTRpTQrQ)_rxYl%KS`I zzb6pFE)Y00cAh&FruY4@nfxQ=g+$XNg$4ie~U0tK;fYn=P z1#Hh#nn&^_km;kTO;r1@cfgJ`gZhg2DE96^Ey5hRvDnK(?T4#er~*?ayh;6we^ks= z4eT-mC!pUHO3KzERJ{MHOaK8IlGdS}vqGK-3UK!fbDIxGtLFB;m=?>(I{2F?+W%z% zU%yO*Rn$jrEUrjN%=X~&?jgrwPdA&#>{v2Vwh zvAtA@GWdqqxkq2$_wNZVz8px>3H+d_lD_<0hD^<1lq~$Q4Ug+Q>K3Jz@$?ZV#*Qq4 zq0sB@Musy4TGHXj70pWM}S27S(cL%ZgE|w3iN;fAS~ZAPt9t zW?&j$tJ~uPy+2-1H8X4xdKOL+*o^EeV98m<^w4H1Xd{k)Q;Z*Vq5XB=kSiBwM!Rq3(n?0W%tG>fABme>vI!sc3-n z=QeAuUGqS_TxSaXhs!6cl9W5;iczobuj}J`)@PYm!zJ&EIC9^q%ZzLTMmR;xk8>cQ z=|6UW^@)e2Qh@SR<_ ze>th9PZds}HnPGE2h(ch`fns4ZN%(v32`y$T%2#P=xA zHAWQfS%B^ni791F=veK2KS@-o0;k!$Nh+V-X&Mp!;(Rq_2rexP9mnm#ULzKO z+%R%w6%(8k)kRzHjA(RnZEy1(Y1GJXZl~y9DKRumSv;#IFZ9*V@}&}S4NUpv^IbVg zO9*k^0dIJ;mm?D?IsBvq_566HG02!suz+|@l<{h7yr`N>N-89(cjrjAW*hv>aT>zt zFONiT1?w+;D_J4u0W4h}3BdO4sFV}%E2ge?O={(j}$AZF9zg9gvrCy*)Lz-T?ul+E8kXE|+rcDS8CoSzj%c)Cy<)X%y#Yd{ zR__DflPwJAKja;?;ush)ANQz8jLk3qsuO%JkT+-E+EZ#T_x8UTsc6U59 z-bo^gQVau_os-slKpW*>sQ4Teu*b{eKnW@7k;=<#o{-k7Xau&ibvQb_GCKNtz!WW# zM(;#I*f=GexB@KuT+c;o+4gUtbJ?GD8O322H10|paQ*K@GYWeqjTUh~M{^mzgRxk( z{wL$x(ywInb<+(FkyLZzea;5eb#`Fh35R0N7i3gaSe-B9pL3a2%P7GiuQNwwb*GRU zOaHDdXLk+24H_u|9XD9AEo&*{9xLU%KCHc?%*u_IN#I2Plg~?n*r9VW17>}SC=jtG z^-9wK2O3MkiyIOWf^jDCLY3JRJ@hSs&@*%>zwIpkC00`Bh@r7cvozWkMWs8pwzzt@<} z(fcVTfY@wMulV5Gk1cQkT8L7VLRn6L0e|WrhU!G;b~r9U(l~7LVZLzQ|92%#(IZwwpj=Jh``F2DY)vP~EO5awpSoOf7y#pI1`2-9&AB<>wS&8Nzm=0Q(OqEM%;OJ>rt!LM|fO z@K|0pB=1r1dJRIjDL($$kz=Gm-x7Vw=q@Z9MMWQAbi;-JHNPd~RTLS^15L-}#uj5b zatZH!&qC{p1Cgso=ZW|?Fb-WgBsb7iA2XWUXWSJMU4_G#!6Or;)_nu=iU^tkxP49b zd{H@WtoUPf(H^K7Pny~rEujU)CBb(nAs>GJA}f{V7&&O%bwr$ZpV@6VoRD^Ts3NNr?(Rd*=tl#(ZLEp_14t&ErM zIS{wDw(cerwG%(Bllr&;N}d?$vxrCkrd@^QG!$`Iumg`jA@CH3)So|{A477+cuXtm z{cgyOqC`)c96a2}#2h^_@0+Y+idbneg7G}AzEfkrC|WTsS;fAo02K7+39atOj)B9Z zHBU??ENOqI@sb)E#uBi43%K5VqM}Xs;|B)R0X`qZO3>Fi9=F2g1?ffxMQ8{(M+aK^ zi*y;3GnDK_26n!KL>6rP+oeW%4hIf*y5ToP#~%+se7DRZ;vF$HF&n8A7+R3^x_ycY zQVdQO;V_Qpg)FBtUmF+S*H|#=w|HhOy9^Y9xV>lAIJ5alg8nhNtoLe=_^GWqx6dhS zV?E6Ri61JQe#Ax**i?B9FG}~3bvY#!{~zv*chBU66`zEk9Vud-jZ`BAsCl;SKv&+N zByza_T@P2trbY+|nOkh(yernl)BfmeBIsL3boBL)fZYs5=_fy;9_7bSsFiPU*aK-< znV)C5HDhAe9!d{}e^g9l(0ls zH{5x`2RV-r*kS*Jw*BYI{Fh_upN;-sS16EXAuv@vcR?yi*Nh-77}{9I;s}wRrMywC zQY#n)6pT`#cCAuG_RM)FTuc@44J;ck-xJk!b!^zlqqC z8D!*yz?N_jfI)u0B<2Tr7HR2yz8?nLvx@Blsl7jy zY4DhaCMGQG^}!R8$Wyg@+unaYq!qN{xl;yW+PXJm`RAShce4hMfIQ zO6(S~n#W?XH%eq{Yx@Yxt2^VIQY0Q8DxhIW{GYGqUrxkX@XYUxB>ytD-9Pp_yJbO% zsk~nr?cLo(LPDMfHem`{Wgkq}kkz=+kNxNqr~YJvR>!cgux0dAgKwl9`Z?AcS0fV> zJ%Awjd$~PZt72ibd*9}(Rxm94n?w7z`0yo>y6;~1o39WLZAtG5*pI=0j()==QlTFY z?@xX2A>%yTp6=!ju%tMwZ#hWII`$%g74_cM`OgRV&#xJ|a@khl?~impXZ*9vD)}6J zMQR7nrV^BLSkKkz2{AH~fhc?5H94c)E_2zZZHy3l<;Uj`pFdk(%J zfEwIl4i_y=mLB(j_*e&#lU@$_1*NH05OMDat9v!{)zQV7cc645ms1e(d>l~ z)xt}Ou1pLTaxt)c2#H@7BhGDlIEO1g4I|>#3-~HO>5&!id+ntcj!Or-u(@F9ABjGqM?qba8 zE}|18`Z?^3f4sOX>lp``f<#S5%Ff6Rq#Sy^27Wf3kV7<349@QDJ#}BX7cR;}Kn4NE znW$2z$Q&imGIWB3(8p(64mqDQaJ*Oi{N zc=^hi(P&sChP#s=Anr#`I=HbX48amDDOtX4_ z4{80uSJ3YYK2=(cGVLI(YS_;+VV|o~)cBtPu-%&}WYlCw7g}_rXA*ygE^0&s(T)nkM8TM3sT{)F9vs zhz6^)Krxu<`vy96!r|6k`HBGqRlRoH$h84FfiDeeSoA`BJAbN~Kl6^MpcZyBycfZ@ z+G2q>giv1kl7vk^f^C&**r55fMkWF3Qj0lEsG-T2TQ-x`P{Jd9vv@N{DBn_Ph7p(d{H6OVFi;5{ox^*6kM~hF3JQ-ODSMj(5 z6IjIg)iTH!!5EUkqAT35v&QNCrCC8Bevl3-%7MEA$d|(e;hVn%Ww6KzbexaZx4AiE z-F_bPs@0r=jTKbnrdZ?k%tJu!irH{#DAz_w$3QPR;R{7#pVd(CW*29$Kp<4;c+~jD zZX#ykg3Tt|j(hX9l*&E47hSaA9{tzZ_YbUl?^L}%Nz|DCWg)3XaMxe$916zF#SR`p zINk5IjtN12;6n|KNiq>LWWcIP>FA&f2?=dYSLQ~=Fv{4(12*FFp(Ubr`)DCXxUNyz7_!AEmX@_|EU0AmcQA;Qr>202(istUS}FLuDL` z6(FUhs=Y;4#2R-#g5PfGMad$nG5A!FvEoyTho>N(YjUa-XjP?o`BJ0;wSLi`m~vW5<2YBV*c?aecY z-<_oCO2>UK5UpaGNBG&V7r*eQUd_Jbb?cqRP+Jc~CXkuXnfW29DMnekuWOh-r+#@C z9GTQ_6}s3E04Vbu-Ns_~Z&lL)18yhN7+_mPO|44l7tL7%sD{7%B0<$~5r_JO#z}B$ zBig11hje^?nMvwu%+F`DQ;!$CPI|!48oxIMmMh>Fv3<$K)mgJ$y|SaN{Z$Ij)APy* z+l}b<>fIR(wB4x`X^RL>*3^#mB&u@I%O$#mF3tL*8uKy>Q8;a~`)3&ATkMkg*zi*m zF{+S(SS^VqUzE7HMSM`Kd`EG6_eR$*!~(_xup+fUM*@=y^(H9UFJ1>A0k5r_YdS=- zoV0z`o6|GJSx(=VuQM9KrRaOp0+W&~&-M6K_d1@vscNCd<>Z559Bnns{nxpgMlkSD zg~Lt!Yi;yLGcS9FXV$&uYInau3_vvj8MiAkYiY*1KtwvJ(97RxltB+;#otfU-y!vb zY3H5}tD|(F{Qh6C)LYVjHfw~{S?6;$WsKDEg}1Fq5Tx0iUBT7gBY?DA*zs98m4Y?d z&Y;x)!s{ITgW@nz*dn)7_m*7vxMD5b>P*C;5 z%grdgrZdK2a+dNYlWnB_AD1piHb!wYd;5Sy{JZJu~7HVoGf1g6}s zB?QNK=>jTwcP+?aXUknB#_PU!J~HOw=wc;F(iBLNaaossS|bY9ZIqKzZG+f5u!Ryb zD6`Oh2?}z{0ulp8jT~gh^%@DV##buo(%bLyootCb2D(f@w?5r@obn7(@4|}7P>>_w z@i&2B?=7kaJs!vCW=xJ=vC-t-B>+|lLF%uxZRl$MmkFBqg5U4*Ma8@ho(^af1}SI_ zs%A^dlrDY808(dspGH)64|q7AbNiR)zY@0lFvdBwYZOjA9uTo>)9lLV9L}~V%Px*x zKDyE<(#BCQndn^WL$t#I)L3HD|b4>Jgrwq8Uaaa-V5tmNW!p*19$F zObQu%SgpPf1YYb&jogi7)8`m4cRUMaE1o)PH{!%akuh-HUBn}qMEj7^^AJpusyfc% zaq~%&?EN-}fQ**?X|b0m9@FjW{$$#2KhcL6khrU`A8ML{X(r6u%EUIJ}jI!asjjDFxyRAQFtIb0vbC6N4H7 zhOzoSHnVV$1Eh8@DV(2=_;B-S+sK zugfmhROGd0h3cQa5wCKJj;ZKTv{&Y=Uub4|YAar=4-`8lkHK z2YCO5W&L~Oy<>0R31*Sq$Jcg$**VMFS#7v`3*rxKweTu!rF_1fDZmWy||XRPvY z?y=()uI{*H!fPHc78-^m=B$S4QQ)1YTI22kY~%Vk(A`~~ zfnX$GEB?Za*nzo*@MH_xR{)^VZbTR806EPpbXIk`;A*W_m?(Q3u%=brY-qY4fs7Wx z9N_1;BIvP_WBR?4!`~A}`w7oG5#9^Hps#3uXWfaMDs1ICy$K&XvK%9(7Ai~*-cdH? z9sKx@EE)1ta;5@QcLlYyhyb=V05+Uia@q)F+)OuClf|Ldf_tO2RveAWKXPzUvs+qV zT|CKze>Yj4jD0TYgn!-iFc}w{xTdwgRh9KN|82rXcf8pi>zyD#~=y>StS)TXkFBrq0 zIB5ts5VIkqj=uKVw0P3=o5rC79i|2g<$i{!-J+xkcinw4=<%MGt^pOqnT-8XuD$HF z;3za1;n9}T$D=gowig0cp2DbGbc>NfGzA66<-Z#xmZqLSkX5W%P>Op6QmnpdlLP|| zYq-PM8(XQzekBh`?tz3`uK=foUMp4KBV1<#cM820?fz5Qf9PTJ#n7>lH#<(HL2sL&6cdW%f-uC@$rw=Om|d zI;~qvaqe0F6t;j|#{Z-l;Rq~;S=SKQUS?Wg6R#u-Zr`sSucMV-WA7MmtZrY87WtJ! zx5_XdihvXpI`$VHFaMjS9MV-3KTPgPg2V`-TNPzuG@cD*7&BJi3ZiT0jqx8(SwsA>b==& zQZRqVEw2KazyC8G76q>@lf?d#LBPXQUthAb*ZH;;8iSxqU{R@R5w(FSuQ8|9nP!&s zYW7s6fw%;lA^E8zt-ZQC^hFt`wrAa+M=+)|!1ZGQxftvD6U}HyeU(E;BxwZ!6(OnM zBIb{6?De~8rU2Nx1C26a`hrGofGjE4+ZO-&saD`{(a$N(cy>v0E)elUlhsy5Tz`i? zVw&Uq_h%G5)*%eanZEo$e=^8crmIpa{k&1k{pJJ^%e6avp#gD%U^`X|oMvC}?3$ld zKcJ%(`*AaraSgQIwCW3#{USf(QHG{dGJcR6m`M`QP5Y`w*~b`9 z8Fcb$<#)&aD*mpQA5)<OXS(Jry%-EtK1y*$`nYp# z0SRgMSSe4pXI&@H={7%1spxBoxeTkc;^=qMc1QaFdvl6r%io#vxolh8V&YVyV%uxu zns{o?V};XMJh!Ok=E6EUYh8x_mi&ArHVl_=o@Ur86d=h#2~V$D^rB-HU3jT+&8CM#_% z8XTrra5ReuBjaeE4!lKR4G#`xm@uDpOZSQT{zaXt2weq^tw-+>q#@B{L7hDVUBCZe z+@s!JAJr(1fi;>g1IqM5DK!Jh-qJH}&){6%O z65C&$%$SHIkBE){B%v!$KDiQb|rU0qm>)R<3VoeYngZY_^=K|QlpktGBKiJcyD${n0i|aa;c?>;^;~PjpE77>ldju8py&>YY@_1qp!1Ys` zHLI?FWrAhC*#!%Tgev{t(fq7BsH*saGR$q#|AbVm@Qhf* z@9luo*jIM@p9b1dMJO1(_-s1>LwieQ$24n}PF2EfHW=L6O8$>dL$8~3zi07V5h)c_ z#O-6qIUF9Onok8Z(!97-0*nJr@5{Usm;s(-`Lf?4+_*2s4{ecSC?9hasr&jS#l-?vbp7381+VO3E91Bap;Abz3gY@=4<{5C})Yk8Y}LS>~5`v zKR7tRU9vmGnP1kI%s_PW@A^P_1-@2$Wp)X|>ZfK$#;v20*mSZ@ca@$-x$;3^E8hW2 z9>J_W?r`wcqg5pa#h_Kpt>{l;R(^T#L!9xf;^OiTmOEvm`BEGZzhma%n}%~%^gLi- z1=PdNr1FgfTHgTu05%H;4I2 zhD8N?S_12B=7K>^h~YUDWeIKABjs$d1wB`%eo1=kC@>6VL_V2y=KYXtZm&@3LT%RQ zcYN)d5ebk^omD)E{No29KH0_E(amx;A)`SaJ}8-DIvdg8O9B}*vZFaY@7q`t-H)Bz z_1h%m=YROTG8DD9v&0>40?WIy(WP#SS@_isTXxX|CeFLW;apj5o0)l==rQx*NHmfc zR(x3ssc8Z}v+uT{yLS%)0ISF+KPHP0C1nE$oJQeO?_aYQeo< zJl*#-vrpn;vzqoX8BOfg1(9_3Goy@cb0QiCnqUBN#^FO4;$A-^2w%SR(_aT-+pl{C zbYC9m>af15()oOYex8Ebt=06@{o2JnRuHLTv}w6*nf2*mv&0w8??)6#^UVbD_?{OJ z_DYnVyWb(Gq1@FKMyTgYbx%KRqg@P&;=uTN_s%_$Kf@jl%Qy#Bu`c*WKYA&Ox3tZ$ zQrh^MomDs#RyhRyJBh8+zO(O&@+e+@H^uGO)0$SH-+H>=c|lNIC3SY?-pNm}}+N z)zvj!_V6N8%*f@LcQG+}rl5fcz$CU@-l~iT@1KZB%Sb)qr7F!ibQqB>ijvHsBblp; z&NFTeL>sW>>B_MNuS2?4?K!Y;9WIjo_>l{0U@{@55d#yoN7wVgPgz^5E?tLCkgW{R zSoP|(k$eQ*eRuuPhcC(O?G2i2ooYT#VS)JeVHX-L{kET*VQ)V3qUyVup-kjKQ=cvj{++!Hkp6|E;PB~y_$O~JPfHBcVNeL zqxiY}A$>CKQpHVI->`toED^wPWDyUfx}PQiJLbv^oD?kt)_gEJ%4u{RD4$$znE2B& zibjN_cGp6pMOB^(;+}E_VF0g;yV4o0Kg^rsuj3C`<-|P2r~}uWai!JDjJbhxh96;1 zXzEY*!pv9nT`W&C6U{{>?at-Lc^jTd{vdtst|Ti944Y5ETNemJc*Ma3JDjRAYbuzf z9y=YDP+=>_%8MTb%-LHulDQ^Eb^A^r`zCRH@>%$Jv{g-gOsh*Lt@yhK3CSPnjkLtO zOU&QC-yXi|=)3sj{}wF(Hp#q(%35o-;dE$hl{kvrPQvpW!qKN~Dy=b_t>qDY#Xd4% z$e~Q1*|!COFZW#trf$1?cyf8*3|kJXoFRHC@zq-OCLKH6Sxnx#10M_h-Y`2a=I-d=PfqRuO$bK?2U{~^3`?bx(+0e zgRBx#!x5+In-4eT3)d>m3e*aj$HEXu3+fGQ`+^%2uVbM~k(OstgQm`>H%cqVRGW)uxJxR z-5e@xahos6;JyZR*2I{kMIh-T)+}1J$|UL{Ct%XXH=z80Ebr(h*%k#8b@))kJxaSa zJqzJ)XtW}&aj751wuW2z`O0yvIL_g6CJAaeNdgk&2_ z*SNtJIbaM1u%KF<>)D9yB0ih8Yht?-6vX0)SPv2PZv4_D`ZGN}ool3SgH6Gufjn%t z)Dml1Tl2w!N9}h|!g%v;zI6a<1xF_3&=C9kDih+J@v+|VZKuR8Cyhfp4@f=S@gnE3 z9|8#|1$C{>UoO|j8CAH0lkon9L@XUO_g9!#?$5uH?vcwhxHvuPU?OWtS}=tp0wouj zf0!`}EmIBP^2n_Cn#=g+$!_2bV^!}K9%VT(&hKJlx*1Q#Yayx`�$Jm*QKF{Hs$v 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/user/images/kibana-main-menu.png b/docs/user/images/kibana-main-menu.png new file mode 100755 index 0000000000000000000000000000000000000000..79e0a3dca86587c9358b4e0d36a8c1b24bac7b3d GIT binary patch literal 369267 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 z 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�@dTOm{pFgM`7YjiEp%r+Nkm=s-LG!% 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 z zAy)K!S7UU(gHS(uEayVC!d`uB%+)=uLo>qlfWV%khM{tjf6~`h&xGJ9CF*6{y70}* z+-nOtxdEDB8BAIN7$zDwzN@*Qe*1RpHofp*LAaA}%O{#%V6wA^l;H5^Z#YfScp0%3 zfx#(LJF!Qx-XQ#>FJp*ecF)_8Box_y<(% zG4Sd&>3WOA8)cQi(Oy!AnCrU;2lsmAjqu(P=*rR!@4~+q&QUUZ!FWa{>&9lo4Caf^ z`S&c2NBma$US7TB3KunwRep4qwy;RGL^hysP;Kw2BM)UOq#`^=u(lY2>)6aFH@RxI zuGRD+L64j=qb3$xz%I`4lXX_<65ITv4CBcnLHLV4P`K-ZQNwW;L1uiwVPL83Apej( z-sLdrGr}5udl4Vxs8X^T?)s`nb>ju;BV&RHlQ`TN@y>Pofz|6Lu3rX{AhBMM;qd$h z*`f9oPfKGbF{FzM{{g0;KtiSz5*X2F{M!tXkPR{}ts2$mpS1C{a^A5@XzbKZcfOxm z^=D>nMjYIJFBoz`K-ICiJo+Qie54P$sHIJ{J$YkZ<~^=JS;g4QE+Z)a^rbhxzC(oc zfmq6UI7nRAnGpBjqoTh~9W+aV?U0drQ~c2L1IyZigM|8C>!n3rm7Fq(J$ zf$Gi+b&w!ROXzNaDaN|Mxw*PR-dyxy>E~gaeBk1{@?bhM zZ%w}L3*Ca~PaKA9cz}5C3FejGkl*mmON`vOL*-HVHsL1AE30|1EgqxKlQs7u=ar?* zmtpcvacMoKwT|6$KDd78JaQy$<7Tw!^XK+m;@6B$b%`(GpgzQpUm6=p~SzR0GPU zT8P3wMomH9e1|&X z*UR|OB_hZBkGOCP5~KT1&0ZjB_-8!a26~NO)N+=7mzBaPI|ZcWG|gt{gwazM?7_E{ z6bt%lO*X;!fNwzQQ(Nm{A7K!N8Cg$o(anH{yDfq_mFrC>G)gC;-bqq>uaxQWqe%R%!j4)ib7t7 zgMjn6@2`K~5=Ipml-S$;t=uJx zzYf)*#8(wS4NvuqLqy(o^HIH`=>9V^Bkoen}m)!%&Ge$+6TRBh~8*kxq=3Sj8j z`FMu!Fj(fb=KEIn1#{vuFV!xC{b{5N1U)!-sP{W|9V7i4DyuXZN6BOK@(LDYHs!9 zIS|8`IITcKjG+H)M9rhvmF0D)84&@q273;a+_gT5`wJR{^zX&(r2eIt0SiuEv8v+P zvm!klQizXrgzHwhzRfI>+6*OxQ10GV9p}n(E3bSWkt==Lp~=v}bnW(+C$8x$54L++ z{yLAtEsK;VR(MZO@71h0n5$`Tf9oDFxQHu?`jTOJIUuL{0nIrubo|>-4P$Hc^MsLf zQ?-JC#eGpuzrY|mr??S32lfnp?bA|%U!ayyd`xF&%GzsH37*MxvdSf8=S`FM{oYbW zZw0mjybQqzguKzl6|IW^Z{4=dbCbI0@jINa{)#M&H$72RIAPK__ze=115Y-iuY3y| z{~Olo=}bk-d0bmCsOWS3t(}Bm_3Wy{!<=ce_<%N_5TaYCyq(8}1zjGjL8?F%bQ)9i zmL$4y=?1s@V78drvT4g|uOFdjKTjfzjEveN*rvV<9zQl+$c8?od>@G3)8fLGd&YB6 zFSlGVS(5Mvi(N-Wgi0xwQzFMb4|ksHh%2Pt|6blfUmF}FB$2_OkF(1DQ(WLr#Tk|J zdz$~pEDn2u#VOPLQ5!*7-WHj&exu7v+Zt9%8ygoItuy4#&PoKGCwbj+e9Kq9+n6bz z+G}5IrqtTziOewsZ;k$9_&)ZwHt4=ILPQ=L%27R(sH}aady(BB2NlXh`3fUDYl`OR)51;MEzr36h)IN*Q(F>cNUl6u z6oxz*|L8W?2Jac>$m_GUYcvH`1%1eXHZ}NT^ zUc+-UpL4zHeR|zGsMy)c+?CO%vgqR5&`V6Fsh4o(P?2e;igJD17q9E&A4)VH)ErGB zlfV3YUCR6@bC#|t*E1)1165EI387sNen8Asl?*Dt-5cz&Z`Z|@ZI+GMJl(Lcd(#A= z%k>A9O7Rtx51!4=1q*|}PeIa+6D-+>OqgS15-R5%crC$!A0O~H@ND>FLq^4Ux=f#| z#^9P|(+xnQfQwsByYi_|%B=q;nW(QH>EK|La2;i?EHa?W>bqP1=fjjg=%TQsz#iX$ z#l{5g$JtJaKQRNC1R3V$%+LofF!;xe9w)RzWj!m%#N){}enE9}zX zsR@HxxQ|2?58Oc!58D2#Z?Uu4(LF^Fs8n}^qpQ3f z;%smC=H&L4-&SK`&;LW-yZdS9RK=la}k*Yyv4zc)Xa+w8U1^Z9xl z?~ljh{`CIQJ=Q_2Y(I`U#FouX4X@s5R6fMBK?i?Dn?)?_7;CU~i^4U{<$O2Da6ywl zg+mx-@?hoj#V5=4!VLt($2?AQRA0j7+a?r7DDu=!#@g`yyDj&FTYyX(4Hrqi_e(NR zp3LV|>M7UuNi?69sa0^Xf#(nTmOZmU_Yt_+_EI7II$>VZU;ZhZd}_{kp!gKG8i&&@ zw$7J%x@J{y(WjAcM-#j&D=78ps;g=TYvOu~HDZIpmDd$_uy%CYAaxFJO;dKS-59KU zih4vn$ZGe!*-WG}^D;l@2UK54tjjEUD~Yi)9YF0NKsUE$Kn;f4JN7I8v}n-P0&ea@*kr^oah9V)gKKBxJ&NZT|;EL3!Rao1E6 zV(Vz$-ozN5xxTEF=wl}{V5>QCW6Oi@jE3vu;w-|2rZj1U} z2I5iQ?fKe@4cehh?ybSPADqAu>h4=3dSzmJ1)s=aX zda)p6!VW>zU%EFcT0FCGy19*qMTi+6&F zIt{**XkDs0BfFl6UnR~|*jVCl#U%0I`dOu?3)qWzQkVNRYobTQ**hjUAlLPIn{ofI zIn@yfbcq<$3b}M_cWDP9d)2yd6@@HlwMdbyg`y|=7-yCRN~m}0IVp#CS}Vv~j03*; zu}8j21IL<=ox6>=i}YO*=KZ{GFu4G_F)v9Bzy5ECu*vbmlZoDikiAj7me_>ImP zib_(<_O%AnC(>Gqy(2*z^K6&Tt?3X`ubafhu4_jaesKEBP`#eq_MoseD-u57xW-n}EPzX+K5%_RhrR9PkgF4| zN|{%E{4Tg1Vfi<4|_Qfq-G5F>2 zshHT+TJS6>tK!sYZKPg7ZFzoP9?wmE%&o>wd4B(f;$LXy)v-*nwy8qvRDUSfa5gYo z;T6dg$9H|eu+Q=nzQ4971`HOM;^sXg?xwRQJz982N!(r5Sz9(ItV`Twv%481+MKd0 z+uc`I&cejb;U;%4llff#+X1wf*Uo*U<~8rnKU19t{ZIkWEWYfCDOH4^sUhwDNdt9^ zDS5%qR4?UH9_$wJ!(h{A#^vK&M-mh=ZvVYILZfK^G`{yz{rdGsn(>2*2_E3%Kouqe z%6e6(;bXsezHe$f?oCM)b9d0^Rp2PZZM~|<^P!fP*KOh>vGWU&oiUV69pbLcU=m^n z^o7nB=vSkwVYi&_YlZaP&8e+*Q6bmaFNyoJ8sisL@vBzj<}PEA3JrlDTyBYdpUoUN zKfoWhJsI3zU>#)ofwb0J&0K9)LfczDBEYrl?bgivIZ?6y&JSupodkI27*Js?lkj)pt24N)YNW zZhg5SQ1y8_GjX2i_3Jr5Qpr}~{a!!WO+J-N2QZb}MIpxLoJS;Yv8qHy$NIgIO73Da zmpdJ%WNBLe;y%ZN8OMv;?2yx-MvsTphxW@ol@A2@e>s~T{JFcMG|2&1ZAbI|U19_ZK?j?B65ei8{GsS3+a7Y= z(S+Yp;d+^WD8`qsmS5@EbhN6>E9ekj=ZJ<=fVR4c-X-AVi<>4IZEqYlx8#&OtnBan za)w*|JrQ~kx5HX!yF;%(7}Vvw$Jxo!zMFp3ENuZN7+FI~S=WcO z=8>L)hdNH!MUw%8mtkM$p7>-H@O*Zl*Pv8Wvje>iZgnCqa?GAwf~+<+17GhGjzaj-oBy+%#ezV7qSn@A%k816E}B)(A9Iy#2j|yM^on zUj;d2u>IuG(oa!?aT9)@KZauYEl=kfP^96r+Z29LTY=iyHb7es^sHZwQOVQ)gedTz z4Yo5FOgA9At1HU z$N5k>{ZcFMwlEOIZsl2tI6pU@oY3ym&0{D2%3D5ZM_O=iDZI_4;m+JN^;;;3%B{i; zFm5f^;4b1D&fjkA2swV$1b>h;>Oq}xN7!8(@Dw^YQ1`PQ*%&yPsx++M5Y4R9yltXr z*K~CY`I&*KE}H6j#1r#ce0gM~QgBnDJo2at0I8m^p<;vT&Y>*KPEFIAp`cfrZWqma zdp)TL^^G_Fl>fbzFMK&3#q{y<=|@Z{2rGLA3tugtZpI*=ogroZrE1qD;YB9S9zFR_ z@V#NB2y->}jl~*pauRdH$AJ@m!$kA_pu5Qrrdp{;-$Uqp7sz3mogp`lUPFgD=RP~F z+7HtQ8JnlYPpD^aXr6W+`RlP2H_3p1R*Q!JB>`15fU5Kw~zU{OD0N zE7AHrQFT=4ze|Ej6#2d|Pk(t188y-TY;YSrQFL04tI)Qh5Q^gbj@DLQ)nEE#vpCQ3 z%I21}Vd&f>W_V%{=c@VO<;2symAmINN;=DbYnaJD(mlzexh=I|v?Ri|wsfH6!v##8 z$cT5vjlpTYxT?RX$F|!y!H8!okp`c^O-p9x*&p~FK+EmEKCC{#voj5-%+=Is7e^#KL#kXWq z)wy$eBc#ZKHvE4pkf}_W9GT5&f4oQXIjHAdY5ScJGVe{O+FJcc^Ba12X4o7kZ-e?B z%}fG;Fo9;?JG+15(?esBG$Tl-&ft^HJjVO=Y{dtcg?4RJm+sZmP39(cXTuppSr9bq z^-eI6dTi~jY+1J|3SE7i&(ar(o5gX1f4AfUB8Aw&M)X@*?e+C;X7C&*uhM>% zpM1=Im^fIrOMJA&>f@Jra?Y0&Q-kJ`WlFQ#$`0UDDK3NQ=($m_^gUCFWt>pNDRUh)#ign5Y zV($^(MJRu`G{2NInKZK_xFKN;-rYA})K_zn-+9B+PZV;zHn2;3B(%mC9jA5 z3j(8SIlq*|iz%;OGjRj=GEPIj@U^?dli|%Nq90jPihKVJZ9kuOlSI4oYgh7XoiJs9 z(EcdejDN}|=iuN^JIO&$t-kcV1@yQm=J2hM8s6o%=C2gZwot~~j=hd%UL)ykdz(0+ z2Ccfy$d6YFX`YJscH)a{q0p9jRc)XBtu4!;ubI~5$&mmB38VQ@A91w;9B{nzt)fXEAH?@|nLR{Up zuUm4|?`YRUf}XT~09K=qs}4JT_fy%=)dp{GGV3w%%{M9KuDz`AdxDqG_WzCRIj(V@ znG~wN{B{2@5XMIzK$$JPoMSK(?_*jvmv#HqRA!wTURuv-VS)XR=_xidZ`n7i{fXE9 z^s7{FDNRC<3#>D5yaidCU0Pilto~`Fl6A_B>9M8~O+qIbt$p^a4oS+LCE&tlsg!g z6Y6{avCh_<>dDhA)4~AGyP?mhyF>1P3fNJsHO*ZM&f@7V70XI)8qHuX^(EMaJ*l-_DBiU^e4Y2w zdyBoGBq`b`BqRLdSWZBhGQ65|eA{yP?$(EJKM8<0t-W+nyoX>Whzf7FHOPu$Cfsld zS(}SOJ>$KHtv{M^#b)$b_shVzZTi{qxn-Ff!*1m$rDZ{G40{=FIdn z$x%h>;uZ4o*JC!ajzQ$ASW}G!%#66l?;=0F2Op{kpMrkl%}799FABxn5{_Aw` z@bm-lE6Zo;e?1$E1zIDNS9$L!qFYZGp8Tzo34Oz6(~cfCvZEBce->`mRQz4fqscU0 zknAbfxHr%x*#$Aq>j`?DrSq;ESL(rMeG6)gOW`gcT;2SZ8}_27!}?}CvkInPj-6ID z;eYWy)7Zc(62a?23}-!9)%Ki}8*ULlE=cHaZqyg)OwOzKd#(!SFBreoyVV>ZzNV?U z1QWuKgR63KH2w614>w*F6fv;q7oE2T!Zj8u86{&@#89lsRN8tw*N5|<5$}3(5B#&R z1FA~%B&~<+>lic_Y{Q9fAl{->&#G>|xQ?S~bj-D*m+)_3Z^O>kwX5oG&Q0Bg9JBvk zA-DWV?Ti0!{@FwRR!L_Hj~Ktcqox@A_opW}Q%pYxc6)Rm-w>;1DyQw!uNU(i!+28o zV;dQy-vt#$eC1@1}+Kkv!g^wExJW(;6$(ll=#u5QYEgHa(GFSkY{+-l&> zRsv_hADHr+a`q0%S(A_(=f=4m6p~I#Z=?76);T+!P$8>!a;+r$;s1FZ@#KBgwmBhIRB zd4RA`U6aKb5xewom$Cb-oATIQXL|LzVFl~q&!%C8wKRiK`(X6ciF|V#Xs+5T+!5!Z z{9NtB=t%uuNQvah+};cesqJ7;+ktbv_!wJ?&2wh|HNUGPVeNiJqDX|O2$BEuRpcfR zrX%P!NsU#)^33m9{P-<5NLzAOR_<@x(x}WiT*$!)oCUIcPx)vS`UAXMBc6NF1hmV*f_7YU*szi!6WY;}_z6H85L=XWT;j z;X+{FU7h>}aTjYjmYkQHZnrg550nTifLJ`u<*&*QmUmQ6x``cC@4EcR`L)mrz8P+d zUXjs~P@^|U45GCh8v_q;K|zH_d7n8wPi}*P6tC48r5MQG7|Ra0YZ>%7 z-iGlP`ofdP9gc@z<$?5YngM-on)rPiejM7gyX(vbhkSZlInM%HQ&30Nwj97%Q*H|H zvm(7LOj5A>|ld7Otc_X#|yCWelj-*=9*fhDy}?P)cm2L_#eK2|_ad=z-Kl)Uif16Fj#+%D{P*bj22@N+*Wfv5w`gIf-a>eEoA zEkYExq!ebx9MCTZV3{(w>5lM0_2SRk2AQ*&#Z-?DF3zi6bIA-J{ zy2-U%pyT-UR%}2=e8F@b(KirRs9FHe9H~T-Gc!k>*IBFX#`z3~y;!?>GH{cdmxl+p z$(=ftzWJ8Z_QL7gevw)M*LOt3-Od@~_o(VqRCx=Ssk{pEy-ye{V`r*+Y3YmSlgH=@ zVI<}9_Gt6RJT}3a? zNWXqx4D}ua@_@N*^Vox%*0iE<8AEyV^SYNAM@$W~(thNfE3$WYYr2~gxR=E{Xx{}w8k6s?g>G~0F-pp0R@jRcoA&K=IJHx6 z5piU)iCo-oxdGJltdkpit8-!gT@B>LoZ}J=<<8IP6z8|97%eh^;_dZ*ze9&BO+;xdc7Z7W`f3m#3_*maEc5SgvjX_ zM;cf7L!4&dE%;{r)eY53V2c9BH%Q#hGlF&Jom`N7Za6dJBoGaiRcVr9 zj^4HdpfqG5BIKmc!!ovGZh>x(dy&y~t(QNf+F668yzijjwCZ!KUrc=#2rxo>MBn{| zlEpW1xZ_#47gryx(Mp-H)l$H(?*dN7%UZJImc@wkW84}QnCFD?=KFkaC$_8#N!fw`+b$+WPjwtB9_}Kop61i^k zLYbrV;^zo4Q)$uKZAMy4rCBWA^@S{VQ!4wuIrk7mjgEfw4BKILKxDs?Pwo=WyN-`7@TCN&O80Nk@{ zie!-=N3V-BH72X0rsVjYUZ@}q#9U>5f!7t9)-cm(>+ax;y%7U?pF1&grKxUAIZ$}G zke)D&BiKx1AH}V8{cG#$|7neHc>=`C z?nJDm+N*pGc1NR<@Vuu|J!seqNaf;D{8*I_yNAM9g!`%KB|`>iDX36N%~CT2TVAlg zTs|+$Y>`#yuU9R~7ETK_ss;xo)tqwqM(Gl&Jvb+gFyI7h*-Yu7*A)W&H~(ZjdwvaK zgnW%4pcnDWD`xo1o}N>n^hdLIvlw<>8HidLnOHy5KR(M!cV&BLygbP+n||4nRAds? zCZE(wvwUUup}SR#nJf9KzC@e8v|Np(xG^0RvMyfFQw8o{G1H z_o}(Q7&@oj0hnSL-fU;b_^2YXvRWFoBn9xn)l>w(&a%~Wwanx0m5~HJ`LZ}0>9@19 z>^E5fap5m(Y6;kycKOv%TdiHIWKwD2e<1pFN1&!XDxhsw?6~GW#b5oc^Bf!v1Pj8f z{_^K=2!t@rV(~sz-j6pw^%8cgCG_WOW}@S;gTnxCHPIn^hy||UBoq$LH1Mp+PNUA! zIw*a3b?F=LI2jUj`S*KFPg>9$=SDJ7G*I9G|6pudF-~EKf6FQwfhG_3Xm;uAgVObi zg;%1outD2JeC7e|O®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 WlJ(0{i8 literal 0 HcmV?d00001 diff --git a/docs/user/images/login-screen.png b/docs/user/images/login-screen.png new file mode 100755 index 0000000000000000000000000000000000000000..7a97c952e1039889b8b8e83aed6be398473a59d3 GIT binary patch literal 47292 zcmZsDdpy(a|39~)JEuxcl|zZj`80|g>MkmW%9%MeXLHD5+^ejACLFLANJmLy{^~s`8r&Cbi-Jmd;gLBEG#VCSFc<$ zVPRpzv#|V?yN{juAG;G=EG*(IS1(;KyTiKBXql>FSzF$V6P>c9MELbvg)KA+B;=|{ zxuvw;?LL>2YpB@G9^+l971LEtOMd~e!I94*WsR(a^>cCDMBqQTz@`X529lWuf84@ zkUt>AHoYIx*RfaVF6$pp^cTguRw?M~E(s4#Sd(AaW)-{?HNI?=`$*;YI*Dp*aUe+E zHMGS{HiGJIseSC&u#O@N+kcP8Ef%yM!c+#|aVP>gXmS%RaxCx`3%ldLk6spQ`w_G; z(u$4%#ehDXJoft|<~!o9><6Ij8aRj%K3gWIAlxOTvh~#O?O(<0e7o9825RJz_P8gW z@hCU-}yQfVkfI0KBS}`A&lGq*xzsY!JqH2?;If%E&sH7=IvjC0{_vrLxA3| z6(}4^5>TVdN1QHeB$ho##FSC~JENjv9hW#h4kp!JcjGG_Y}Q@?K3L^Dtp59jr(Qd& z@Ife5c>WC=(_5$N-@0z2w|{Tts4A?BI7ogU2YX?N&yK&NDg#;Nr0@OBjpRNSNRpMC z9-_>e?7Zz*a(g7|_xb?C6+?GuC~B{}gwMY4UZ|bx_jiG9PPj7VpdyLyu?cwK`?Gu* zeSP@Up9@^wec=>d(xtI!qY z9YI^8T@R>8FP*d;PZf#$y}M(e5K{n#380wFBx9HM=;#R6-)gM*)NF_NK}esttJ(Nf zWWyQtg}471RLeKOR3z|Of@G7d3-4d2>KB{1I#ek9KE8O!j&x=^0MR5{`uSDdg17$n zZ5FP?Lp!8}pv!q6dL63Q`1xF-_5V0v^hqV=`f)Jy<7qq%{&6#^G6=D_O%XzNBqQ{NPm*h&J?0y zfYR|6mcG^!uc=ZmYk9BVjL;1Qgve%c8KXfM@g!`U-I+r!4`Ua=e{$k1Q%q20yb)Kb zkVr20iQl%~A0M*{eFI!V-PKZAXg4&dYDkr1R@(KLrINLj?fV`OEvMez3=E zmqK|DEPz+#ekev7W}4%>yvYFrJ zE*@Yl;flFpKsSBC71hfx-HotJ_R?wo!$9sv`}`t=hbvW@Q!DAOVM=T#&8+i(PP`*# z9+S&O8ueM81X{--EOC9c=x%j+*oum!m`|7jpx%rh03SJVF9LU>QVh{iDMoe1c zK*D5gq33XzhRj8t+Y6Sz8GkqW0$^nHr_#N)KeCAnpq(X3Zf6Nb{+~;nPOpK3IAQ`t zxrR}HF0*d{I7=*3ZJC5bu%dlJNZQh0meUl`5<&loNAYPm)3X951n0W(U9x%7Nvx9h zPcgp)&bw$@DjTq4(1ksJSrbB}Cr`^j()V7U?zwmL>)-$U$DXp@T_adbi8jfK#G8$k zf&KWlox7SZJf9V`tL=RtozpZJIi$R)BK|F}Xtv&2rG3k3n`vt;-v9?nJF{!;n1}%u z*85ZVGeq|Bg>KBio#|lq zU!wg85D@z>&#RTMm|bcIJjnB`=(=G{+Fa=sLg2iOiL`_9!FWYVFGG)M1r zYg0}3&uKpIAV@BKtsaj_{+6@*n=8Hv2nY}-CzHXpHvR?%{c^R_vEyWwh++>_ z zJ>Ur)7fJQ#)wY}wY!FiDPL&9Cw@&t!BjBfU9?=tnZd7LEB*^&eQ)`CfHW(H5!!bbA z=G!$HjXi8!j-AosMjU@(B##Z)ms;+FDQenLH0}CM4$vb%b0#Dl^a!MNxZ6n=%*()q zQ7)BK>i_{TSEggzKW9bbj`{(M-HrBA+2yj$k8H|&P?{S?=K5+>5UtszBq@4+Tkd|{ z`KA3G@ny2Ib*EJZiD`9y`;lN5bL-DKgv#N*d&C;8z+m*9aaIs^9CKuXtD}|R25mO88eSX;W-pCNNePLSyqWYwc;Wn=$`*CKrE;6MdYw=t` z)sJd~B*XH&j7!{xci@~NU<~A^zak(dDlgTr?gC`|8$r z`wK(Vb(%jYejE1e2`qz^Sz_))j;?9x3VtID->*>i~R(6G5 zs3oyS`)Zn&3-1VCyT8D=7O|z49O)eC5XAlFuQO_eZt$XKm1eSSX!x@1mkq+bAz0mUMn6 zBI%m^vJp1L=X`yyHiyrg;*b*hmK6P5m%XD>8pmmWe&uiSb)r!zC5&2+$CyutT9-Z} zj}L!Q*UM$nB3=*B!ght<8d`~;R)+Mko~-A1s=7$GF!5%T;Py1!kgX3#r@q_zEHyy1 z$1T|qUVBsf#u?tVr3_R=xHP9KXYo-k!L(hcm;DDZi>f_)3HN2)o<;kZAhqAEho8fl zcz>cy+YrFI87dZ{-~4N8cScnGe#EmmgJpyvlSk(xiU69b}e0TEFG?~LGZy!))R zq$1Y(R#o|(J5WiS)X<@8NvT>H3NF`5jXH6&HiUAaOk1$mPiZM(x$~m(>y3OnVdRPc zTO5_etRQg2HxGmCQ!kGf!$@@4`rIi7GgP!y9kGWgiUJzV0&}6@N!o4p` zGHyf5QlCX3B-$&z7qJajFyrv&e0;dv%7%;IH^IApc;#HP!LHO)|CJgLFJ~s6cEN9GY-7lFTV-}9Dc9?tL^K-hV;mI?|<&@jT ziiAAJALqOPZFElBg>53NY}RFGhwXXNue!9jSwQ>Jkn5EoR!fWx%_~2ss>1aO-E1Ka2*loXTbxTZ8XxM2-8?6xu0KUw|w z`B()2G!LW$!RD`$6Hp_(&8d`IQTN~LFE~2h;X=H(u+(QAx-wDP>oyy2e>m2&lwx5~ zQ!G75+tl$v53X!&Q_`gdJh7=(+8Ks27V%jS%LsYWLjyxF;!D__a6Tx6o9Qykx^}U| zTVYt{@fS)z>-AgthD!E~vC>3k2d^ zjqf4*qawKoP;<9^REi;o3{#VVOyIF@={o?U<6HDSN`;06cmh3~TjcM~RgYprLt2#K z$o`Bm+ZV?S9%4gYaOQN`t!GznmC#yaGv-}A7OC5?fuq^BG8R?q$<1fUIWa@*%2lpk zAnzdgdildr^H+D(K>Sxc`inh24kRcb)Z+D3xFWJyJHRdl>S9ARn}JDL2Cl4pAIFAz zlrOkGsrhUqx>^6QSbOEkSan`b1WkOnX$kh>vr{O_>WnM03Xcl|%gnT<@Qm~E?(Xmg zP&9pUb7ug9%79bw*X~g!Su)w#WdlYtk>xxM)4r%Qh1y4RwN}1p$&&o_AA7kkdf(Fu z__o(RDr5DSR)tlW4yM=cXo|6df(3Tm{wm-+|xUyoiu5MZ9r&o#P-$43*Vht8s6)u_ph(TVeG%E#C)*DRN(`>EpJ?by?N)-j5pRkY14z_O3 zE|I1=M96o*NYM>Tj@cn`cRw9fjT%UoxNGj=;xDfI#BL`RFb&ZU{c z6S2{2Li^&Y#|_FOFm-0%3+@iQ7a05c(+j~UldKtppDpw5v|*rd3L2+7W11|ZC^cg9 zZph&?506ON3JOQy>mtd>c@aJ@v6hhVTs|;lCwKc7KtGOSm9D}e-^b~E_wCZH5U$C- zOBke~JcxVai8twbIo?UtXBncF?}GV2K;c!voFt|K8(3IifN6^NH1BHu*eWBjY-EXww(v1 z1pD#YV!VCEjVzD&PP@F!)0t*hz$fI8vT72m_m=}_XgYVtuDp!44;%fyJ&ls^MyYf4 z25mQ~j{BuI2iY{DUqK2X528OpTAYnhQbUnH=PUKt;It9^qmpymd00Z+_LZV1H36R( zUA17wN=t8Wy~21UDGwXce-#t+(+ljTOxT!K)o*Q**)j#da|ta_AAjRL!lc)jf?B72lj*TX{M!hw6|wzD@kBnxP%* zTgp0n2&f4~-~1I3_1js5q0;o5e2M(nl)|CS`3(-@HmxyW?4u=ds%ZarVgXnpM&@A?Dzxc}4*8&nh)x-d zh0uo<27;3npu-2R?nHvi%6QcWv-d;43Mr!T4^WQVo-P&vM{2x^CRUTK*S}uD9h!cA zHaKki1-Y|wA@`{p~EWc@#4ivOk zL4CCf-z0?Z<|ZG>oZzk36!`Iv2CW7)kK#IIiC6r#Ea z+Ee4y%|)rfJ`!Dfh|^Tw%mI3p`ZjgI(BNf^iT6_Tj|{K`$r$_`^4dzeC?2`wL_CRy z;8y=0!_1}+A2`a?$}3U;vc0qeR;t+-UYsy=z5FiHU^Y_a(O_AAGW2MTA!d%^M(^Z};Ars$<45%s-)?0_Af*Zv-r{MN%`;O}zaZU6hAv^5eEl zx%dq_%+M|@Re@VYuVqG1%p-ot|E;69iYq-FMh+Z>sCcp4#KS_ydCLnEUL0!aE;)w{ zBRO9Rt!>Dh@d_;71{C$}K#iLElzl`SZn1_NI_6x6KV@^Tan>C>T(m^})Q?&ZSsDR- zGzYw)zFi2-|F}eWv5<6ndFu#c5ou((^&6#@$1zYY$25og6)@ChPv z>{ytK_~`&`C%dBuZ3SD++~|o^vvG=Og0>Tp+~P9pQ||h7DKq=__4*Cp#gE$!pLO7O za4w5+v5PRm{`Jx=dBfVmh_3n$4l&CCh=NhBb%rOmiL3RTG0InGYJMLqG~{Odm#=2y zU1sR&bk_M31;ZS)%P9Jjxi#dj^`3n6A8UF(oSf^rZhg+GFfaP~`)WQYG0NKHHEYew z@Yf!}#j@2+DkWQ8!#=cj8HF-~>h5ZLv6TS>;_PB=pO#|%81EP-j3SD28$-7~*(6>q ziXk=rc-31lX4s4<1VF_Z;SeV+|S~<(qgGb+ibpS|;^pb_0{1jtDH zm@PURonOvluh$t`lWU4P06KJ0lDKFS*%G8H8ezo``aw?o_bdzv@ z)V_JE#ZD%^-bHyfH4889GO#sWXkSCMw&m^c8XCVU*4CZX@MO;R#U(N52f# z7LW!%zOjP_G0%JtB0nCMTQ$I}_EuV1E)^{3??EN)&sT6aTdvH>?yLXKsPdtgGoC%+ z8kenFYNBkstHdRf2cg1bow0SE;Eyp5x3P<7`zX1!^&!>hE!4-){F(qRF*D=``(V?M zq$4mH`cY=uA|SF;BYympRr6V~{}&cEwz7e{3{h&S+YJO~M|?Br^Lww46!^PZkuWJ+ zv~A;F3tOp;{|uS^rn`}D_(M%sl1zUO@B;#h` z8gUUj?Y}mc-v{Iik*@q=;oP2c2McM5d;&w4%ho_b#`&-DD?|l6sLbi(AKNOiCgoZ) z0G@`c-Mi3>5U#Q3W9H=K?(F{oT-!_bzr_Ntrk8EZ3#p9hScLD6x&Www8L+JjmQAqqu0rXKBj= z>pffJD=%u9P3tdt^saqlg*LvJa3)0UnNMV6P%mn7^l-B+2xg5iQz^?5ZYF255PUj?kd;Zydx?b)~7G z#jL<702Oq9dl_JyHRBktSg~|{ta3cVlr00qS=lPW%YFSp$_DB~i%9-!If@Q+wIonnv!@gP~ zy=W{wGbd)-$t(CIgt5eMDu447Dud&t(8F?#Df%}o5r+YeDw%cKC?T&={So|Oc4NAT z60sE9j2l5~K)Li>^DzOZ?GzRLVbj7ESDb!H1*kwMD}uRTznd&2yln-E^RF? z)9a--hHQQeH%+K3j*(ctON}#wvf{Ddo~1qXEWaSfzR58eHmLv@8vkyhn)N+UaU%tW zSTGQ7D?SdCNc>Uk;v;H4K6#QxigY?%8`(j%teacEmOD;@jU*%(7%P2ZDiDdQVZ6Cc z<81DLjmm9dyO^1hvQVT6@pQ$%vjAn8JQj}P>s;+8@J=ZK^6vw-1GO;=bAjbTrG}%A zZ|z$VpYEM5E2!wSHbzRk*T$V6#hmRGz}WgN<7M%tRhQMZ(|D+$rd*$KhJP=@wVcEvJkReo>A7=Yg?@cvpYr${y`qWU#16qvt;l*cfF<-uLV|e@?1m~o#3sy{y>pFtY+VVSS~dMJ~R(0@jT*g$8mhm!@V+>+BIya+Y#OpNK+!*5;J@zd;YZ3-KM1)0qw4wwG{N@qI;JR1&<^+7Ces`&Ho%NJy+9BeQ*1b*H#S^ zHA)j3uW)%HV?INN4SY66S)G=e2P1_%SX4R?^0>>34t>6l>o}RqR_^peB=84$9EZlb zfF4Ut4?mhmP@QK6IW?nFT#YCpZd}C@OMR$iXO?i2I&3`Tn)IKS*$VuOH3Y^K>)CV)Zz)pW&Fg)6r?yxQNE^U8R2XXAjF6u3=0{WX~O}71*FtxTNj153#-+xj|1qanX=;*-t6U@OV zObDgOPMbac0pvspp7IXxxbN9zp`vfzV|gZCn$IiP+$6mz_l@K9i;iB{?d3BYh1n_F zhPj)B2MUeyZb-rKmFZA?-Ho8y+T@&W{%{=#(ZsW>oZ-`!PV||YA^O}puIVpVGF}ss zK?>^7Mc*KKcIYYXABXg+vsxBqd%>h44fg%BdOtd%UpqE>@iz;B(G}+w<=0!)Y84g% zcpIeTk2+K}yK+u<*t0QB?P<)?*eu?6bXv`4d`dt?F*~c-fjXlVnNv-Pj~1&y;P==n zmC}&q+-%ad-XUTZRtt%l+? z9Q9zIHh81%K2WqEE` z1YFGB119^o)ged-vzf!Sq|5$|n=l<{hD)|7F)=OO}p&1gV?*lyNOA?Qp6QSt(F zuuCwq`jS6cebIkqg|kep!s@dO&>qQo<2|b9U52p+O6aEr;;+J5D(Ag>Ht)Jbo?MoFV*`AB z;4lEDa>ZHc+Qh__#5 zjrFkzLtp@gk^d>s{KGxpX77r?Bw>_(g!HCojNH%xL|CYnUS(P=Mnu%=|F-N{ieA@Dt~&hcGUTl12M16SyD4%EYgJ) zSwBNz;#Jd9Upyxfbd|F4?+`6ZL@)LJQh*kvstz2SSj!DwtpOdy)XNdNY$Y!kwV7L_ zS-T9Wxr0+n(#@bpjY`wdmRyGu9)?*A?l~VI_^Q&@p@MQu0Vu}TG3^dT+k`lV8udY$he?8y+y^%}m@Zlw7@7hS zG0Ecv*Mz6~HYJP-B#ag~S1&7Y?Ww{~w=^Pg!NVH|v8m)yt@5r@uEc!w8EnK*BsPRD ziuIW?rnS_0`w)xld_vW9O#0d+KT8kRCDoU@jMWTg>>m3bt<)#oGa+`#lBl}B2I7rg zkX$ruTiStH9KC!M^k_AUoU6xa z2`A?sg})0?BBH^J@~*K?WZ2kflI9p2aO$ujuN7>Wjg{@O)jQ5q>YRX0olAPpOXU(T z=>p%4#;})CxY~JL7kshC(-J%eeC(bHQfRMJ#$JnIz4=SK)j?+8;o#M@yuj~AeD&_6 zPJvJNOW=ilwg$_Sr&m7euo&>f6x`2IoX6caL0N1M-*r`~eK{CVn%*dS6dt5WGp?a% z($<@L$++|3F?+)5>x26&u&`O}429G&6T6V9umYI-o+UZWuFX1g_O#0`Ce5B{Hvu8Z z8F&+%*wRK;*_{p~8rhY0TbR;Je-#tY0)C|&E<1YlFzSmu&$q99kB&qIWYM{o$4qpn_DwN*i0bpYC&XKqJ@2$9j#umJ+9{%=6-2F?pn5@4 znAq?F-e+_!=!sEX$=o^h428ODF0FkEvY~T4p4f4Nd}?0N$2ZyfE1tqO=^1m?1ct^N zd88@XVuyH_LF5}Vz~C;599yrK#>wPFaTIC@$qZlDrZ4=ndYl;$xcUKwe`fY(>c0z@9Bk{kGrro;YApiDKzz`FHO~sWpo*ad53iSQ zQxgd)+S7n@^~Y%s)5nScCESp!XNW_1y@$Vu+F*&iQb7(zn58iHvcZ-7SWHd5=_(Itev&OWW97*kJt1N;3YRPY0@|f z{`6e5(C}prP~H$7H|~IdYp^k`*3r@ISD*Hv(({z1o9nksd3cl+59CK%_&3UqaBf-H zZUxyUf3RWPVw9(?=iOY@uw0Hnohl9lms{)H$z(r302Rru-uF$G`%bKMLJ?{TM{UCy zKn$lm%=4ji<~8a3{D&M|10rKB3qy#j^I;K~Td{DxK~GT-Xh`{utlVIB7}Ug)O4 zr~^z>qYgl7(K^)`^Pb!O1Xr;qqvF1yaC7-#pZT-D9RRas0yv|9?jRji?czfZqrWOm zxKyh3c-&A#UcFN>Q0;qB4B%jMH_dds_QNDa@CgYvw`ry1jPGQU07fZ)1%d^W5?#Oyx9Xlwm)a zG`gNJT3Pz4WZ2lIy~DLH0M|H-?=M`68M-Y@Ky)DuJF_`C$2?EKxKw*#wI&qsv4vn_ zW@EV;FG~F&ib$)IAKBU(s&<`JIihRGmT$fktMz5+krtVLG3@5JgZEqSf?>G#&S* zUL$F1=zLuUM$XN@2tNi^&I9`_BEx(QR?V+tljj@GC2^xir}4I>{$jc;eKwF16=9hpe|9M`Jx%5_-I!2U+Sdr)$D9dK z2ITm8RJ(({WJdiHMg1*J@h@iGDq*ZE^hOpWvXanwH}Ad)`u6SU=$}dx)1#NF@aIEa zRy97*Y#0>=E4YIg%3tL~iMrKNS$}hkL1dth&T^KXfMwB0voyKkhc%;IEsyvK}|7B=#oT0F)+rmwBhacDlnksT~JeX@Bg$TIz7c{k=Hf%l#T zxr~yN488GYw8h>29dwXAH8yoCc&JQ_EoNYzpcy%&jhuFua~C=FYn}nnZK?Ufjp3fH8d=T zoow34)qI#Wk5}QX+>B zL3%xhBuAQ{-{*^~fk`BVC5UMUbyMJzb1D(qCVe|ORI z_L$!6u)KK?4AGqD%6yGEw9qLVROLLhW!b!t?G<*eJZYFRT{G7hK|1{r^vp+Quy@3R z&%O5sVY@)osYhRHr>v-`*uz8#S9W?*aUg!iY0ICf-gm;)I57~1?e|Zln#Rm*;g%iN zD4f?1e+%$i;~)Gs%jObXEC^# zR@L@vC`!MD#nms zFD8qBOz;2RjZ7DDZ3YT8rekPMFy?eE)bjZB4;S@3nFpa5u&Wsy+I(N`Xhcsz$=!m{ z4B1%5AEWVQy}GX;T59KJgPshmGCqg<^=71tVZuj?Zl(G{q)5RFvg3aH5$WFKIhr}) zaPY#0T#AvHWH3d#@_WfL{S8sMV8d2WKvsa!-)S z`^Hb;kL}T8i0iI99^Q%HJI2PoBl?46R*nktXOa+Fv?2NB%Txbzsi6QsE6n7FEgn|V z#<+D5%JWx2TT~W^-Dy}88tG;BK(pZ=U4w{1be4Xa|A7_ zi_lEkS1u+BG1@y;^4HA&MG^=?38JlOR?PkA=#S_9WuUW;+L$$}ndZYp7tUoJcQ!pNYyenY<`>XM|=~Y<%|GZ?ajq zUPUvj+KbW|aWHy4FzCkl-Ti z#mcJ9uVUfDu?zqFCj?cc10`1alGkKUMG8xH#&p$~ErypNRm~53adK$?`__)Y+=2OK zt@}-bR~9A?X~rwbW(XcjGobf8_|;ebSE`5|slLczd-8IDr^-+@c1HJE8rxpN-{jvK z`(fu6lpf+AW!>pdwf@Jo+jnxqU#AwA0W{}$p=V~+O*|;nsGIZhhNW9!va{y7sw&RM4)Lylrtgw&#=I0Y{xmvHvx= zor&yy>^*THiI)Ss_N)O1B>u3Ar#yg39xwqdESx4EZR9$~@2DZ&f>`L{jr=yXxV-{Svzb*j1`s?OoX_Rz+CFy<7yw8DyFF$o)AW6m)1z* zirv!E#`#H2lJ9BK>$vKk@42&$H7~pbV#q!vLnVOR zBChd|OnvMX_N#)!V*?XngDUQ@P(RTlcj{;=4qH!wDXe zN3TcDSB>$(H-AREw7oij=83xKqI3z-)=^>FsZj=FCx|?joYWiJopFu0hO4M5K=}St z{yKdvNQR3?pqog8Gd5q?RrM~>Tl64*pM&8GPp@Osmh3~6&HziU z>@E!KA&9k)nljX94nS76>~^~sW1=s{dN~M4b77QfIXVOS87YFdKpx)JVw@e7VGC36 zN&6p_mK#eB)NJ({eeI3!^6Hje=NhR5Z7Q#uS~#kxGV1*7UoW04 zYY+3Qo2w0z=BNN2#)zL!__oliE``%h(;5UD+6Tf~0vx{f z9z51^t#4XqWl8`?!)VXwr?QTqdK$s!7suHn&VSo#q*m7pTGtpwi1YzO{?5kS7Ims zAR%y6f?QJdgo_lSKIihy&FS*$(bJrUfv`$!QfK;$0pwlIxhl^SI!M^uK63cl%a_p~<(nsa_`QrVP>xBwY zX`1uj#OfrPgA>E3WS$=@*Roe1CxxLi4o9sOz?YRHhKHIdy|1UC;;5j$#i$>HaQd{n z#@};vo#mUs6J_>GI`rmk>g&fw%Q;xn$?fX4rs>PZz4V{6P2aYM+^`^O1U$d{95n4Y zCwH<@hp7oFL=q+F;6DPt9$M!sCBYGJ|#asmff*xn|h>S?UOO0qhS@EE+=r< zl))Z&@Cw|cR?u!UgMXT<^C>O&DRUa;R~!dCn&OSgEnKd+lo~cGXWKHtwCN66AVkJ#1fN9Z9oZA&iF;7p?TvVPF z>Go9LV)GVVNqa@^+~2OnX=$mdC5eX!6F*WtA~#gvQ_b{sg~zCb)S|3j!Mu%R14l^@ z&7eh(%%S$t%9;!3@If-19%QqJ+u2ce1jXi)4nK-K`nRUmxk(<3BP<4_lFKa&n$G9t zM&;KEn#-WAur!GpBjPxiOB1v=iWa8xErapsdIX*_R*k*29#y{y-+CD{`C~u^<*`;i zST}V53gtcy81|~>(VmyoGgT;|(aYj8@KvOIQ(PA3R-j*7c#j!0w68cpPwh!#5On{z zF@kb2*+y=U%9u^Ljn=o|mp`~PTHt9ZLxH*BRbnGg6rUOc(MtQ3=kUl)yn3kckBLWG zX%G#W4%H<~z66Vch-NZT?DqEhvVJ{-4<|d}CBLMS=^H|>x406QBpUh6lBX;xczj(j z#AE%lTl0-=#JTjIc!M$fl{$5hp_7TwVZN)$l^w~+uC;~zdy@s{7Cto;F z;fxLSQ#hTfr3G`NVG#W5nD$`Vqv}c=(nr0BtI(y9moYzdbf$WGn9(R{x0+?2cG*(O zq1PwPWk{9Yd5Wk4XPk#W(kTQOS|Wf@`gg& zx|{c05}tAGuKd`h(6F4m@w2Bz`l!6p!r|*jFB!$aM*^iDTtH(gBgxC2gMm4ib@R!W zvZxK>ye8;ldSdfWev}X4FnI84<;k^p{o9RFc%7|oH1!7vWt#kIJ@uK_R{xe|QRRBm zySmt_nfm$pC~V&K6*vrE8(EiPtc{!s#KyigaA|?R_c=bytEnwoT#d4GL}*GH)*n+Rf|G`<5b`At9K= zpk8hXyc$m{Mx=hU{5l!e!DV!`VV~plmo%sSOa%1`xY$(3TXG`ie5ml(TZ<_dOJM1- z`<1l#A)~ayjaSz+a`fpVOy{e0+x%U>~QbYxxnP22<|dojRa&A%J+R}kE;nq`m4O3qoezS}H=!U3SkaGxt|wK zgWNvkWW3nMowpk-y&h1ul%u19#-DFIpH|H)Uyx_TQS3prk3o7wwBosFL_D z4m;er>LL2t@Y9Kqb2A(|CA~38qUEg^S^0FkEykIs*HNw{P_vE7oFg!a{`R?*Ok456 zyqwvk)n=CmzO|Icya$ToI z{Q-8p9*{mT7({_Ph;HYEhG@uLj~Kw&HV&$;<~@QPE4W9{_*ts`MSVOlL6g3)67Jxk zB&(J~tCHXMZN;*AHOZJ>0p{Ef)^CP*D|xH;G|6RHKIUbD@uwopO!F7e&d%~TVQqSwup8aQv$jz?=AH-O z5}@4LMITGOMH-uI_`diU6wF4U7{=u$es`DLpn=*Nm$FBBgLrZ4}FG zQq+V-VyuOAL1Nfe3=p(s${Xsm8@pfoxD~QChK8_L5ttU7{>|Na(ZXf^XO>(UvBk1A zq^t8(xa_^Y5O46yek+|fu8j^E_bW2i+>a#bipu&H(=bG2`zWTw^6$@$O)zqaF}Z4K z@&FWOR6+18nQN%qySezvG}oumRnU+}XEIE#W3;FG)|5bT6MmlL)tl@NeXc|a&gr+O z^FtFnP=YdES`Gfkn8T{kKY<}68ZdL$YJ%Muaib5!t1i1fqHpWb$|ueEUKU?5^YW$>Nhs6H|{Eny(pI<}HD z?5n7Fp51lknvaGh7u7Z?fcCCKBSWEnq}GIVy-%I%>VB}cw`uM)a5Foi>;2?0w_2|e zOnIb3wH+q0#-0uoO-Q@}w{oHdfg~0Sj}N=UAB=|}FCc)Ei)}6!+us-krBqA3*NR2PoE7$n${aGHr~VE9G)h~-YJQ7q z4iRshp@LJkBiEk3YTkfxtWQIAu@lNWGOSM*ZFO16uiN=mkGDz{!BPDbQ8+jC{j>}> zbyIQm!dGVHNYHZ%GPM{Gro zdG?RV6FgSRmTK+?kTD`uSo;|~X>I0UWCYRq25kT@?q|_&Z!h=I^Dm-jD7P9dPG76( z3m}vtCP3X#4yvm-kgMQ{#@r-QkJw&cZiEzQuZ;$`TFqP7<10^`jaX&`H-)9bQ6aCw z9#x;^H9i`?jbO`P`pFep)by=!?5&}&KaBMQ0~@ol(i9Dr9Qkl$0n56}3qRPPc zm59x!1D|IHgZ<*G^$uF9gt{1MUd8i9{Zxs1Hu*!Q-0O5sS|g5}0mZ`icOE}W9Emzl z@cO0u+wwq2oKVm`sW|7At)#Aw$~CVjjC6k^=M)g(DMF^YD~hRY;}rhGbUWYTMqZy5Z}^b7Hjn#h6rHxxrVLO;jIa!$~tv# z@J*PnpxVh0iVe5>eeVtcf-vq8s>2Ysh&?}#@U=$IjUorMpV%PMkQwcsq`b&Zv7_>T zy{?W~M3!$2dEP^M|LuzCh|>=1HsB4M&v$hBT(S%QG`%QYG%ly_zT}Nutrq7`C zA|~pyrM=M?*v+?-uZkvc_l68t-zlY`)so9^!K5u8F}{d$sgXWGQ&qUt2Q~L2cAq~? zgtWR&Bin6OJm?*7W=uEjw>x7Z5@VDoFx6E@t~&#}xY&LF;@j0*i^a{*05L-37B5&E zq`v5;#V|H7UH<63U;9eOL#qJCWB1`_XSD#-(ZF?uCDi)KRq(Q!HFCyO#fe+D@O_C- z+R4_)$rSkF|6}XR)vzEJ@?$RK3~=2ZV6MR-5#Pyw6bvb zihKE__xRRy$c73`t9oP$Vcla?3Thy7-|okU?1|f9L-&&%Fm0RNX_IO!NVC25Hj;X= z$WBj%?!(l`R2FfECOD+YY22fEqSYT!Jcakl6 z?!RnWSZ*eJLnM)_^OyEmq^eWF*kFtq*8Lf*H(hW0*1HSc0T^gyJ}MXNZhI7Y$d>1k zXTB7oWls|KK$Jomeu_Tc{&1<4$!qAPXOqmU;|jF6Ln~qp;9m4~`Qzpa%s{sbcNwIv zu{&^Ec`x7L5X~5=aR~z&wXt^(CV&%*z}zuEvct;?G!T>KX=iWIwAw$RYn9)%T>6H_ z8~zddHDw2~K-vf;@D;gss?5KV4wyen^E4rtWiE|Ylc*>tLAN?xQT7j?fAF8eJ$@I` zL3KNVo=nt=iMv

sEoHMwjht_N09C9ne2xJ)!4u*s&>5&{L@b5WWh*c)=Zsh_TH#l8vINj ztQze63rFnzdl`UW0C|W>k;wAk1*8X9R@29~eiqC*{{Zn^Bhu3nHNn%&k0K zd046#DFybgT+sNR5^#pmBy7Cw!1&^-?|=CM?0ys2r&IQIxVGV|cPuCF{6pmdMhiN+ z3m}S}sp3}{2Ox((U-kX|?C%^A!|ABUzpuVnPNLa>@hdIESN{uV;{jO8G$bl_Eye-o z`L64Uu|$J;?kaKoZaMyI`A6uu z_QRfzu9K^SbpBeEn##Z_eEA>jGMW?dfabnJMLM&N2?1|UMe+V~aDk(V_&v8MT65i| z@1+xqY8ehgBaE}paR_;-Fy zI5WI^{PCokI`2v&Tm+P|z1&LjebrOE)-w-OZ0h7y6mFdLVtHZBVB~+|`@i>u27aVH zz|Q0T^QzoKJFx-Q`4!7kseHXrZB;@=9sf#&mIAyPq;k=~_T(S{%pL6TjXTko{$Y?X zpple`u&1=`XLI*La>C}@ucu`ED+4GCsN~l(-ZTL^ij*=hpl&`8IA8Q{VsrowC*)ZH zmYaSnn0v`eZ)R(6Nqj)~oW>=&vwzx0C&ZTU?q8P+?apAwnncNu18-jx(@0~` z{+orsk9+jLJ7Q^nqhwO~x8MJd;b1~P+_Rlo9`c+J6HdYZQEXx1GW5R-I-5S<`Y!c_ zH9t_%{%?t+-^+e~J@Dn$_b7abAEzKQH-z4*_a_m5G5`TiE-(6a+<5si>(swT(4gZp zikcx_Q!c+#6$AgH)M)GbbAG|jCa~?xqgV7^{w-j?8Kgzm>iRhVcT%XL(oe2gwW9^v9jZCQW6iGxc*1~Xwm!L^-}=rYTHCW$Rzwhtvt-!YP@*$w{+2U z=n}mcA9%<&xSRt2X%hDzl*n12j71A^QV1Oh%punCNL=Gjm=b!q1Wn7)w=kL=D-$Ni z%K7hd0-9|zMdzU)(A)82$I{1~Ng|)|-vS6!r_gUlZCeD-JBg`8mF3g|;*fcM{3L3> z5jG%BSJyzl8JlWCs(5Uz!$Ize2%jIYIi-V}0_V!;7Fc2n80&aX9xe=Y@ub$QdAFwZ z9M>=BEX6I_lptKzd~Kr-(eP%e|L!G!a+(1|U#g zwzo(VL)vS7IMf8r3z!A4h#CsuWCt3wQeS{VaywH27QB${Gohy%G8QCp9nXa8B75X~lCIVMo{)1g{-B#lUwG z=zdb>6FM`WCFS4>GPpId6Fq>LtA3Ri{K@6n3vgDBB<&_~r``#@=rq*~;%7SGJZkly5AWM$Ms;?+F+5#vC^1(mLS-Epzx_ORVJ7j# zwid8B`T)6tFEQ5PXPpfE`9e_(uowCM(5Ar;rAB&4OoEpm{RkKD zA2iW0JbUVCBg+~ggaNq_lKGnjL9t=cwF!JVDvsMz_c9hQkt>cP6Q1e-ldoS)h=K+K zp#`3>!kHKq$)U;n-;L!0Wzns<#tD=_q0#>we+2x= zvg-ujO~u5|>wy|)7eykZP67+irgIM?%6Nq#v1p6um%uyAZ6UM;NX?lPP1`lH_5o!7 zPUA{#_TV&Xg&b=6R_Jdy8%CaSGwzg{cyK`_*#7kq)at{tj62bAM% zKQmMhb9yvY)todD4KkHA|p_QP8UcJTD$r0NeRd zlnw;g>6zQvr)z!x;5ofF$te+cnjZ?H$@zC>nd$7rvR^|eJ-Z)Zd-+(t(BI<&sEsso z6kk8sI9Vc0IP~}jbsqmXuk>N&8;h`he;J1Js=R*FK zvG#kD_yKF=jE^f94<%8gLV1)=xczDz`6c@GyHs9y zGyJ;!IQ8{snpefDi7H<(ceyaQfGdPW5SqZV-v$h)J7rWvQSE}=#6 zV)bfiKrd@L>S4xEXKZ7m5=oJ>lIv!NGH0`|qrySt!dan}ZT2)~;|RL*fds&zzmQ9r z$v)0b6jM&@x2yFvKepoqJ#S#vO}|NYOLb1(36k#&GU+dzW5d?B&SCfz1`1`okn7;6 zCvaDwtmkB#kao*%lsrdD>b7v5oDzc8Lh=)$hg$h*a^Uq{i^?>xv?sOup;=R|MgVa? zOO+7U$e1W_nIje!CziWuRuHfWk}BZs>Q2cu?i@_co#Y@K$w&%gP$t2v#2 z_e{W|O#sQqHJ*|yX3*I5(xVgAO7A*2};q8(5z-dA_D`~l3g=!FL<=Ih?lIi1F$+W9z27Qt=27=RcE?( zohl#IO1nH5cY8s38gas(R_a45e)1#a5nx|20W;)Kj?S+=-j&6<{!gj>hlYi1N!ve- z^|wjJBwg%&e*g+M>@jtYwcKJ({nw8~l4s~Ihy0(m+Y<^+x2ap^qzP1o9jYDnYcBtn zQy{Hqsd|CvsAtvC=_ceaH znDg;2lOhXUM3Q2mjUN&vc7^6(F6W*~p?i$V0s}-{*B6_EZP#%6-R?bILDygAyRO^RA?8&#($e9D1`FJ7p0Q|>ojJO>>h|tao z`$Eg8;kT))V{?{XjXwsOr|aQQ~7!da}>tQ0AKK{iX2fs}8x&Zbu4d(EcQfzp#kdcYWHZ-hU# z>%%Kd0aZDX@dL0QcM07Fuzo@=T7B^9vfziwnZ0wM_?<)z(o2_B4$nDjz@9B;{@_IU>elMlvA)x;$xk^()+6Vec^ zDc2yi%7$58&>!%q_b-9lNQK`@i6EdZLj&;y94#YcksW>m-l0>e_)HA#OTdq=Hk-Gn zSr1?omI5Ik_c0xonYW{3WV1!?tdxIB7S zRxJeqL2zn-`>_gyD-!%`JI}X2Fw~-Ft1c)5T)*N#ULFW(Q;*eog>DVNWq32ZyguU5 z?A1d!1Nv;+Ovy)=(3>r;e7}W1&dsZL^5P?@d?AA?L5#)qAz(OQiRX zyfNXvRnbH|1Bw?OJ=b)=XEz`L5nEf;W!Y~KO7zX{=xtT6jzbaqv9nJ##?RqGMoIf2 z=*sC1qNF#>MeS45SAlSJRlgeUAgF9sP-)afbbld$A~z{joA>qRP{<}w_c^pQ%g8Fo zrb)X)6kdcdgQx)H z24j1=PVAX{w7j73;;j5(^i92RJWBS{&c*cJw!jK?oHjxaU7M4u5g7A7sx;wW`|qkN>)SG`?8}=PSKu0?vJGnN3SlGfXgDot0vpKC}a`_ zI%9vO=QH=Kk08rUNf!-2#KaED?IWre=L4 zbbY}3oUI*?cht3X;Nm0md$TJ+n1JpQnq!#+pv!mIJDHgM5G%VEjsJ#`j;+`W5Fc-w zPqNWxB$1l3!B$1Xg~MrHdeD(O<_A3wDk&+sChwX%S>tU$5XtK`U1MYFAe8LF+Bx(U zn-6b9zMNs0Jan7jjn-sH{b5IKLQ5CeAy1?ggC$1lO$Dd+Wg6Ft6Kjcyx9y>u*XB*? z$ePljk#OlG^i2zEr(!)3`COumDIe8Jq}$M>80B}#2xd_}UM-y`_( zc9WW6C^GrsjH)%PGQz;d!*Eb#-#v&4Pv&Y3bfPw6aH-EY?@TaO8juQC~=Z^0fux zf?Jl>*5!J|3!j|UGI=v1d^tFtrSe%%0aZS$UM-~${dwWV zLP?1$wbhahoE+HMZ+Xj)!@c&<*Cnhag?~V4fXfc&;K|MLc+%-)60+{r#u-zZVo=dq zwtiam{xv;T6yA~5-t+brlUhcH6xuMi^N&C!5oPJn?r?SSk}rj7o`>!?rzUrp^4!LX zlCnnmOt=j(Znp&si9)5}470y}7Fmf8u#m5KSs6Pr)#;CE0(fYdGSsA(R=7G+85qHDe|!6Wqd#ii{!-TA z`qa-m-?}V&P5dpfNq!S^3)y8t?pWVHjyWeT;8%R(gCxU^lYMwcnoI+O`oIcZ-zcL3 zwn0~JzjCzYmZ%B!jCHiw&H%fgi$5r(lwN}^F+14KQdg7|=AQ2cDv1s(gO&u=Rt?sH zE^I#I_i_A48NM~$q*#D=w`h8k`iJ5I_eu_bymiE=PSb_u${;)zlj%YpZ1d-UVeBlO zQtx6Z!z*Q#YbzpX%g?FiHfz&BmOe1tyrl+o)g>jltdzRG=;^iV|qdGxX(^LOh6a)t=aYH9);!k$~utk7St>m&ij^`20@ zn5K?)Gt)PIpua|crHmWMfSu>9-OjWRyz&(X03~f%oY9pPH^2By!fw!Y`uol&d?*kU zZ;C6sJSGcR2mzsCXap3X|H+al`5ICuz}**=U;6oVcab z4**jGoor8o)_L;zdKwphdYm`etFa-uY~8#xH%)H*>I4)o-Mr-R{3Z}M@DfLt`MeQ5 zB8sc_`5I@8c-@9c%Yus=$D4Hd7JMIHCAgWfqvFG_8GVnrs1w9zsUkAT!!rJIykhYT^G zIYJSVi%7Q6y-gF7(~-b+J{Wbo*+DL!mCFuzb(aTDd*wb->QuMMWMby%eX&k29}R@| zk5j3#^qrzlq1d(L)%BpwyOo~}i}qPg=LQy~Uja@o5iwTlxKnFd;S-BbgS3+euu=i6 zq*13N5Wmm1>AH~ny1(&us$*`FktJO~J_rG+3n3|gi3RzIM}p|Vj+4O6bMg{hxXiDY zzWw~VT>z-sVuH4p1$UzfvnCmC18;0rQApkxGKrx_jc)1FbU44(-V`$s#WJAL zQV2E|n<@)R5nsBJ?s*zgZroWs(6`84*60aD&_Za5 zq1lcm)kKp@=lN_rl=U`Z&V!?3wKgkT8^24y*EbD>?ln{PGH;@c;gtg-BE>h#opECg zq#Z&6^vy%1;r*=TxBS$JwTafz`Gu`@_ZiB)nO+N&Pb_KuGU|aH<vOi-d)58T z9hgVke6Uo)!sPlx(HuYL(ZHbG^-?%yX)6*kI$xV@|8;LtGJPQWn$d3#S%8?M-AR86 z0#+lswos%(H*#LfxTH`m;UkB8u4-wc|5L2M1U7L!_HNF_V`f2%%g2lx}j156m z@#{k-PAn(~_KGygNqq=lm1tW`T1WyxNunQYP8sqr<)SA2 zH`Lf~S<+a0S!-|q$X~1lQdT2_`%Bu55Q7`zPcNaBeNql+k4sqh#`tbjIV0N^CbBV? zT&(YcnDgwwz^WH6dj$odF@Xb&>nTG60mXTN;oL}v(1(vF8(hos>y&y!2(7G*Lhy~- zgg2G^^P$08<6d4;xZ>aYtNMGsFs^cJRd40aq|c8F;#Q2SQ?*@u z0Ut`%#b=1&GYP@`V5|Iq$poAAXI&IDZ1dggscyL9KyK)cCO!@y$Zfko*&>upv4-wg z5k|bJU&-4m*xaiAn8=^_Jksdy{Kd4G)^kDdt>s11m|GS7%vhSXFDL`GWT^QCbge_}VFbgBP)0^&DTHZ@4RQ^~y|I z3ZH*RB4$ByGPm${!KFs@+yt)|)FqGPVqWq^MW@cuqpKf7X{NJ0wCH{QP(RUFjZ}fC zO@op3>Q!{+v!K~i<|sU)f?RT0B^KdJox0jw?nEXLt@djs>u_7#2Wuq7>VKMBgi4xo zv$Z`MBk*=u7FddvrTi)1)ix%M>_MVwLC3j_+n75v>wHiqmd)3HHF zlZU81vW3?}x286aX|`}-+HI%`@Xq%y-X(Fn7b<7*lP;Ps>4;6bMc3*Jr?yfyvYJV5 z=cHbtRehXGB19YeAmupWZyauF{pvUlLM68J@uhC#tMa>wlz`%96PN#=-iM;odZh4^6SM79s~yQpK_0`sruC3<(&Y$$jl3Syz8j@-3&Zs zR5Z^pUweEDM_qbEg+FMDl_Edm_G(tVoBt{vTtA^+Zbe?IwHqK0IpH-Ab+*imVW{7g z?WS{^3F!1LUcu+T0DYg+8@JVX0?2mc|4wSfn`m#}HT}a{EB&T=u6jrN-Is>A}Rkb{&|UA8*SYz4o;mPefjTt$YK? z(g2wB)0Z;LGwxYx!Cf$8?^f2(VvmhIZt{prF?8_zdT=_ahPe~GE$^K#bu7nkLPLO2pRft>H@L-LHglS#(dugir?3td3Bp zvR+%c|9K44(PR_2a=fJVXbe6J$_oQ6eZMji2bCvHOXRgL3%^?dskOns!6kG|j6I|A zH{BJ862T9GI_*zG_^gW^ZJdS=|DpC6f}jFuV6(TWKQt$vn8Q$$(Rcym&;hq5Fa*{e z;j{oKX(O7FcGcR;DF*V9n0b`N`(?bSHsVlz^2 zR^RD1(q+l$dYA-ue;}nMjP8lj-58#-j>v}6Hg@wk%l^@rT4EIcbOGR=dte78^1?fJ z*OG2wxgaaX|e0Z0Cx-iE4=kD6|Q0q&aA zSIvZ0(vFHA|0(SEeex&$KJ38moMsy;NZoG3f)eZ=QFC~%Cs^=#Ax)k~H)!RblaX6|BoE;EMr#j`W68u6`=C-W!d$KIV74vXXyuE+~1od@=xCe;u3h^2ANVh^S4(6N^>v*15c( z%@GM}r;+zAL04jH4_QaVxY{e*OnnV=yN>kAL*VLbX7fCs zf-GoO`*_{?|DQ3X`L&gb1-N8n&H&S0V#D6Zyd%%qbKm6Ry&XnKF6E?s-E*3~ z%mxKaz@O&`A9KWISV<~JfrpQB12(^2k@62%4?Fg)fr7<3fR`EqWSR$5E*!<>^o6T9 z3i`OSsY9(aY30f7+7pWZCA0d1#1XMSN8@zwzQu9ib)3l)3=mp)t&VXFq%-&z?Ka;p znA|%&^YQaLAd_|yz=)%wT1jD57sQXN^;>$k*4m!Hb~-Udi(M}e)VX#_^ZMYM0p`bt zJc{gZ^J*^ z;$gRw4Imgnq`yk}6@zaT=KUQ_tLsH;(P2lTt`UT;A@I>n)1ZeTYYsW!dJRPRKBg`C zcBGSmGypa0)Wl5b4X7LQ@`DzNN{DJx~dO-J7t4B?rxo@WyeyXn+10ACIwo^1#>S-G+J|=G<^q z;%J4p%P7KA^4Hk(1++z+lpmgb^gzkjO8Hnv^WLTiy4OzY$;gY%2_!5qMtx`>k;?iMQ#_0MnyP521^FVFRxabJJ6 z3H_!u3fyay?)%|NE2IWKk}mfj9;{5dz+QGb+(B?BVuS-an&u#qw{7~PWdpzMs;p)F zhquL;fcrr+N#OL4qR91k^l@KYX_c*kWZX_6|I_K@ixuO$36WE_!x~4bLL_0ZA*R&9L8?*5M zrRC<2LR_35swX2X3!%*l){pq-szo~mI!B}Nufh)xwV4iIGCNeJx**mOWaMYQS+8FC z`~6yW0W{IQY_)mUYhTYH3_K%%E`q6CFUl3jE6Y0$vGH6SLen*KEb!n+{Q&VABrbgK zG?0cdMDr);xttzgPB^L8(gO(t2OxW~8NgxCcsT)Raq{yj;`zFWH&v%^St90$&wd=a z7`qB z@s*mVZ|NdzwAX|!ehP19x;36hkNB&5+RzSR(`nkslL(wpo1U`(F=5Z)krZ!SzY}>^ z?d)P=Mp6s)bi(%S(BO|gT#4jG<~a#2gw(mtZ4xf~5su=&jTTuijiW zkcIFuc*_dx=p9ZI1N3ODjcu1OZ4*a;(~DJjGvU3m;@yiOMO8h zbf~pjdao+m85f8n6`@{zaLLE7E-uWFQT&w8$58Vh^ahBni6O%d+vMOm-kFdeS_y$g zGyIOIJu6f)X0eU+vlnrszQ>C!U*8+LaWC=nG^vfizdMY;#j@tAHD{71j@zbSwx}IOGVcbiaYA6pv9|f;TZUSrKB*9!Pn2|Iylf{C z$fsLLwb%hrC(ikMUtG2^DsW23&IfL?7A3?HIp=~VbO!SiVW8*m9V`Cb-0@SZ+ek_u5iOC@x4^b93QUd2Ia=D8v>yHq{Ic3P<2rE1%8!{>0JX4Ion{Mr7H_%<`^}z z@l?SHw7o0pRNsR4te(XDkQ$c;iV?=`Wm>*(zaG3eF#YMOes0rMxSDKxQeeU&<<|fO zLY2GBsgf7mmrvOX$B|=EA)b_wmMe3(w7+Tr%v8^9kMV=vfhtMZQ5OT)#K639lXoyl z#l-;rN!qd1)*C*V4Bw~l?;Ez|;;7-z^we`ZYjC^kAZqOB!oUErEy`-uyyJ@O=ri~^ z%FVFw zv$KV70rqe6AoJIBD?KGHm~`gS!%7Bkn@`Fp?%GQ;OsDR=S!ZF?oR@Ndy_*Ys0qz=} zW9$B$VH>pUwD(NN7&huXmW(y0pv)HsI5UU%$;kXKkQLwk{ps7bY4EG+phWO93GFo1 zP{Cf_eAHs=dN!510n%I`xEfIR_TPY=RHykNypQ=o{M230!T{&cJJC*MTac^RZUp~U zs|9LipdMdp-YPPK=ELp(u}3W?pOig2YTgyvF?Wv8;w?39ZtXK4UO%wa_Bo~NYTmXu zD(HK4CtN*vZ+;KE3Dca49o^>w>__#39YOLpw4O|I6G9A;vpc4+@g*@iY&knt^`_oi z6(M7>g&or`rl~y0yy%7q_ST}ePg!Lzm7iyB z)nuF735%;fs~cUXrnK zK_OB_UED*9tSy&%t6Xu}P_f7+aSCoS46Zx>)4PeLULq6562cybAd- zHzlZzRT?%fUe(uE9-_Q+GXGb~zP~vV-s!h4)~PJJ2igOQ+;PD(%sX>z@Q&{{lN#H$ zddjLSX1IM~8}e!%S#-Ql1gxAr8b_l`ScO>}f&wJ2)8`{17kFfe#SYG0jS5U!+29NfV>rTXdvbsu+FZU!$zNQY;f$@|?6O{Mo}t}}01Jyp%)ctA=C9d^^9imgFBcsP zaOhArdvn}AM-dVhUUT~G4Mgv9UAez_B)IFKl}{O>yUS{tSQoN5Yx(neC)?;Vwf$$+ zDl_=(Eh^~yoaXL2tBZO}NPR?CVLn#!QP|@BfMK~a$u)Y%GJ;@KV!49dvz&})rin&Sb z#?8O4ugW&!N(e&@gx5zc*r#Pvh0A1{c7^Hz%GMGVX+nDXLyP>Q(PLEdom;C}kfD!V z3ApPpp5b<>XzU#NqP>8Y{ajo0VwJ4<;mRUjbDY_6q*S5jRsiBNA1KL(jF}0xXp;@N zqm#gigj2ZlGCe&#`NqZ@sD4u-SZAORY)6uHcV`u-BQT(3jyWT~z;+kkeb+xZZc%|u zSkv-XZd>#Lkvqth(aU38MZx8gW z1E$eDGAh_d8@<=(9g3x>cklyd!!&OM^8mG$d)-^k%VB4!ivs4VU)!QB)CRB>)J@Du zcO&mK=leC*f3P%3Tilz{?NF{7LR#9G8yx9+Q`Pl;74W=Pl=G1D@$uE;q#0`X>V&%i z4nCL}A5ShByNsCtq&DnGjU=GN=pNP{@=U_Oq7Ktg%cST~@~llZn0wrHs5Kk@uq-Uq zFqg&TJX#@eL9|-)9LjHSy0(6>eSem7pto%|kJ|M0wGQ9#Z0NVkYkOVrS)afKMcK)u z%aoEXwO2aLZgw<_+kOGCxM8WvBr#K%x$qgMqDjf2+L+qanwZMcp3nZx?Oe#bM*hYP zUWcSg+@r5Ix7?8qy3i>raHfV30ZFD(Hf%g)Z?(O$ z4dT^kOH@anA6e|bTFvcNNgw4VFp4XlGwZ8D&SKtBhq57{4z>1Tpo5K`2Wxh&YexT8 zG%CXZest~&95fcZh8S8AM^t$?1k5b&X(WLMxGP^*QOkS5l^iBB_XlL5_}6K*3v_q*fd`;gnkR@wguVi4j{1}8 zIvFmoTrlu0V@WHZq3QoSIEzKT%k?UYH_Fr6Ks02HZ$(!B$mCrlO%2DA`i z*paimK)??`Vi$p zcMAr78%qU9unB$^lytJ-Qhv9SfEOEROy&Aqol|GG$$rA(Y*73ogSfa(m#ix4&drG7 zQ~#G`iJ8Uxa%Vru4|DH~#Ht66qkw<`(!aUZvDszjURn8ZVe-PsQ1^?$hcM2A38##KHe-xECdKHK)$h%t|SJN-|w2?W!Q;4(riC@FqtE;UZ6H;&f9MDe! znxhJQRGZb_I`NApE{k+s&m_8=9>qH{bj%#Gn)`0e>(bnM&)>f+UDKtTuGMJVjrLv>-)gyWS5>&yI^#bS2=-lwz*Cv1kkp+JC8;N1sK1NgALv)oV-5q z6pz+(tVt<*{2o17kj+!0hI7!l{btlvY8AIyyNv3SidtdCd`}#a;FmS3)cg?of*HT>JB3k*CeVqLVurO4FO=}8#RAi zu3jd}O$KJOduk6R6iy@*`AzBFd7`&7NybPT=GChvb7R?7AUuRmJ+-bsWCW`TL;h?ab?sMYzk)f0p_P0J5GuUGSmoByFlfUxtu2KQBiPCHFDN30U#*H(W` zX!(zn;8%~E+oi)Or9b3pLpH)l;ky~txEC8o%oL}aTk_VYIXfye}h?S^)a#Mk%c-g*c@}&xX_>yy587AVW zP7EHMa_M`rkJ4gU> zA2CNLwB}9?Y3e-v}^>8P!=W$eYC{ooKc+?s{P&tQS_)Se7|<&*TCH4IgK!GS%n^bWYAs2IxDs7b!b zz0R7Vz#msRb#Ff6Ft_bG@_V`ijCu|NRl~7g#&YT_SIP6SuQSf^4;2E&JW*YzP}FW_ zY}H)$aERq$1>9q2BRfEZ~B#~Qgc4B>3_yW2ZT{Kf4WK;y_nKO)OvJ=C*{DHkHFY#erN? zk7R=xM&=9SP$^-AP&^qW7VMPx=d)oYGa8T11uSm`19=zQ@tS~mmN6Lat{Y)3582^!&N1;Q!a;Z!;LmLizLG0CM{! zM4{kgdda7zyU<$;JL?6rpf9u4&{c=YHlg##=^UO7%=C)cWZvQ#_t^9#yVv`4`Jjum zJ`+T;pzf#vhBV0pY(TS0H7{QpwL#v85AY@UBr@>)&MM?NW>D!E)v<#(fnUq~5uN<1 z|CrlW^Mb{G`&!+V!sOLmUe>=xH0gZ*z;C*AMy_XK!5{97Q@VQbv!s(rw8P!>N3FQ zA$nZ}sUJR_71F+JRI@~oAT{DmN zv$<{GXnYBtSdHE`W9cA_=~>qG&wdxah^|7#SLMy`o$L=M0-`P`J11bUh!bx{&eWx`S}0Z zy6!+I|3A+6>KfmZkercGA$tp1DWR;)vvM6r*_4%eD&fqmC|r@UoxMlSCZ!x_wv@~= zL->7;PbABZ6nWtA=0v07(;#K@PgcNgo=99G5Vo|&gB_C!yccUrRjZA8w-5;A-*Qekkyt`Tjpn^jL=yOw!-2? z9|9?1{Oxsq-rWxiM%M3j9V?R^tH?RKDC9ftNG)4BxkUKFw$OH|AiieqUk1CS9<~bJ z)#P|H@A^4zpD(g0UN4z9q*wE_JZ~(TC(nMKRP$SI<%eRl`fGX{_d}-8qg|zdihMp2kFV2I606=^1}NbN*g8bq4z=8YU~xl zZnpHpLq4SiBy#mcNAe#p`<~Eh-?8fMC`>HBPHC<$?pu+in)*Vq9f}@;N)YWS}CtS zHlI%|p$qP{^B*Sft^Y_R)q1^PzVUr+jN#?9pNi@?0l(!ZlL~Un8b(z@d!+jkIuOkx zBTFIJ!kLXVYHRbA&{A~iTCBSzgU>AizmewL!48!9!C~KdeYURT4Pw{iMy<#2OwZhN zSWLYr4A$Jfp!Q{|A$`$Tx4xY*U%IuI%bJmG-OIu&KgM0)#iT}@G|#|KQqs`tDBTxY z_s3fmmq0JeKsz*JC|b7>yU}d>J-M}uf2H)R2h(fGlpBl3YVEDr2`h!YQ&uBCnP=Ls zd@JetTp-OuJhSHP_Qu@XYq7xHhXZuVto>YDGba7yr*cT7q04TAwkFlif@OIgOZ3ZM zl)^U_K8`r0AvW%DSMzwaKg(@LExr-@*@-5t`Aw`X*W4-FD1BDL>9t|qWm$Rq2Ic{vdsdGc zk0Hk|;?Vw9|Jd60sG5%}bA*aVaJq@3=p)ZG4y34LMx1%v<}1K^mI`r(S$H&kQ1}%U zR4H4?e4ulEd0umIBxNOTlFrZ(9gSK`*Ghg_*b_bKQ#<1Aw-~7P=w-Oq)Xe(#{>hiM zE4dKYpFX~8J}My#rGxC<-{tTM=Ch%tpHqCA%*_+!)?DMK9iN?+jkhj7?e+0rQDT%v z%3xRD=PY@Tp7k3X)8^mj9NRg4W_{MDq3vxVLoTLni3>PF2*=44uL%@X}ShLhNg;X?i^Zk-nt z%q{sBztZ^4zUD6=MUSxc-nr+^23~aggOg3>Rj%wxY`mq=t<}-huEq(WgaQwyqYvw! zxM&~33Jb&I#BR@?WSm_Yw=B2CbCY-YL??viVbjHhNIizpPo?2|pu+hVp5& zt+>WNYLl<6@p>m0J+oALg9b8%!l`81w&uHO*d6QgzJ0RwiiJqz0uew z@5|1x@U_pki5{)xRGm9;-y^v9IH)27!@P{Jp#nOG?#ZSC^ z1+Q(yLHxdfP@H5H@^(CmV)-|rF~Cx}oOP*xRZt;%Xu5o+1w16MRunSzJjnbQXWVIryU7ysF9=KNp+ORHl`jfD zK6vRe95EH0lKQE6uJ3sWDC#L^V?$&d)Lt>`QJZ_KcpHAN_SJGcMNl>+$KU*26!r5V=or+)^ojO`Wzrqi{B2XCc^@b5+*1cv)lu4u88Zq*>xPiZ;ZtRhI@z)tRLIi|zhw#bSlS-i$QFui4 zncT2sNwjP-w)cbFck&Z?k1Sh8JT}LafNMlv2NR`_P4U`oB{OI+ry78&zR5Y}g$7?H z-f^69NcQ`ZY3XaaL9tg)w?!g^E+<6Urgm({EFhZ9Li+2upKDTeb#IFTrKl5)zx^hF zLQ2JDaHS2=fQ*O}?{Jy3BILF9$n_(vJ5i7LJ<0?dB@#W&RKN3K@QKM8nmT@cGX<>mfyJftbx_%=CC_`O% z{+k$?niS2`jlus4eE_mR{U@#wcUI(6XLe+TGlI?E`(n&_L?2CIZ|GL80sD_IIAAcf z;Y3;xzIq zN7eMo)Jwuu(vM{0fwo+uZ_aLB+Fs;&EJ>xX9mojhD?tYhvP?+hok{gCl;EP}*^w`2 zmkZB;+C}HkT~=rBLht5+8R_DZYA2T9g@b(JCrUe+%Jo^>Aiq?PIRYOm&oL0VUy}W) zb8fkFYW;;0A|8>KGGyk#)+^~|!&7M6%fK)}#b`^VdXOr}f*`%4FR7GrLs#%!!@J&e z6E1DBh<2HW7eV25JG=D9STMp7lvQ!r)*O|t>!8B9(ic7Q{7Dpg_mWMU%@9vN{|UYR z6MI&p;u+Chqdvnj5G+i4uM)7{`Aj>Z&cy!-T)V(<3-$O>O%9HZ{yjLNXj93xhn z6A}?KDL*f8#Q5}rxy$;DSYpE%icqMme6`-_oMS%;Mh}@y*0!)QP<>TkRjRdJ!*umyKilsriD~wq z$b1t!emEy-=)ICCoYvE-&>VXhbTGJ|7E}4rP0(IADRF*O;yj8cZX5GONf=30e;;*| z^%KnAD;+PDA}T)B+J}N^L;DJb9Vyf)s=>-~W$droKZNy<%zAJVIf2yKjEXl3c13}d zhMF3hC8r&h$J;SOfJB)eZ&x13G<2V`SD&uFBpA0Q5O0L4w&b+2bT5xG`%b5@eTIrU z3N;=>9fGdxK+}KdAr6U~@$D(jwj7dtT~K?S>d@f{PTtpop}8_pqkpzn5YCr^+meK;osk6a0seSsb!cPR9K)uc6%zE zHYdx5qmbW^I&SYVv866ozfQwXa*Yvk56qxSvzVDEwq7MwF;*b+tASLYv+1CsjAi>2nHrqD<2Ezy+qY7$tM!lEtPU`?80gq`V zXiY~{^AXq`*4D7Z%~aU*zGbaQmqZ8OoMbrkdgLTn#9r=%ws@nr;!1KK035l|hWY33zRsP>zEWsiNh@t_bh1X}#{Vi5zxbR+C z4W5e{fxTVRR-1#sh=D%mgu0l$LqWxXdgp$t(ScV=R`VSD{5GxqyPKaA1`%%#*zepQ z%m7d6Ql}I$m(+=w2mGy+@UDeD#dsi-lNr2~+#0-gT2&%#+f0${p3p!8$~@&Uox}Dh zvznq33^OW+L!*Yn{=*ZV#a5L3)|HrBj80wS;^^lGubv=YFE|g#6u)bPYGTOx0&(T% z2Q$L$X(z-zj}w`?)0lNg{nM)L9R9O;o3`rMg}~b)8P7V=7wx(3VDVSD=|k_y;s~^MhOyM>YT2g6J!@@SHnS zHA7E<`31Y)r_a3n{+7z!Mw6;-cwhSm((rn@`|ia+8I8v>rqcu&jYF0oY+=^kHN#es zS8Oc?Y)6$ggjDu}KA5u|a|94?qUpK*JC|Kn*-d`cH4w+ZTGAvN-8w!niArZ1pj@zh z8^FIZU5Z*I72wP4CUN+XHBQiHoQr)P^}Z(zhOO?~cJr_HKm*T_#(6EfB^_hbP(Bv? zIv-x>Ii+?(^>(OSQ4}_>j)}|3eSqmqI_uWiPgIJ~wKXY@=~|o?9_Tlkq)E{LjYv5I zvd+>u>|dNJ_leB;b)4*Q48}I=z~x4rW34WwepE$7SGN=kORo;Sn5 zI6P3qw#mWAWFZAT=W}h3fAL6c6bFCr29WgEt|&?4RtNTT=&_n@#!0_nK&I2*P9kdJ zNsDy4m9s{+L|T%ADtezm{ISDc_|2FR!zIjaGQ_{ke!DF-iyWqDWbcSw(VB70hI`nV zMh_+q>Mc1OQ+EGV8X)mfL7T5RSXGO{w9B54Oj&_Hp|jngxL0b>mbz9^_6w*x7#BdI zOx>E2X$6q1_CrCxG^a=hA8R*gC$;1a2@Ea{ETW^zn5-KwfS`mHx2zd}_v2i; z|HFU6DPh^6fBpS6IfzfML7w_4OQV5@XnnWQ*kfQ5CI5j!9A$uZGJDWd1PbZ1t3DJ~a$vKx`DJ5SXk0HF-6a(16x3~{!OqtDbu;dAv(mgZgU_Eb zxyA`;h%&LGNg*{p%SG5bEEU=kOOD=pWIS-3C~)i)neq%aq0)IMsP@OEkprkWVRJsK zq0MIdKh1Cuue*S!B7RqQjwPtgVT^zyaxAD3Dia|}85Z01zUh(OS0ZAY zgZwcDBEs;BMuSYS{zplA+0JYkvZgZsHrhDMl}nDzZ=gIy5$%^a$Vs*Rr^r8{XWUSS zy!q0{mx^CJk&Fiu-N#IvSEf$ke_$21`5_C1vu5&0eOeHf&{VLLKwiMU==ZGiH7rKp zY)O3j9(^M+Kc}nj?e&nX_-@*Bt!ySX_!qH>M^L`0dvhd?m$NLyo^C>18Zhi1XA*wJ zjG(GINK@45q8y2cXYMKi&!Yh4UsFX>CB{o=bG7Z+^zY-ANGX?@@sj-)##hOzsXtEI zAw~*Fm?ci3h{Czz1ZeCVN3#cW`PW20>&kJq&%Or;^>%_m!6GGIV z>3_!OBP`P`WLyU(oXL=)b{^1DS{>bM%lK$7pd1tB%Io0|^{tw>WUSz;rruwQ@ zcgsw23$4PSj}JgWOgj21T;ul4U2SeBss%NBYT$tgyEY^qf*E^G$0t&!IL8bdaVS(q=|=aZAIqD_h-U^FD^qvu=`?KU{^brA?wTLLY+Ft`3{w`X z_2zuTdgPiOyhW_1>Kc#GaUzBBL0q~-+oFmJ%`e$%LT~w?l0pd5FRiE$geREs&d~`C zaML8!oiiXuVUamjt#Pxesj?qvVLH`SdMdC@S=`x?g+ZJDs^K#ZXUl4%QT5DrnsF2| z036e>Kk_L1irI9qK=KDLr3P5X9)(ko{jxG1I9U1Mr{pEyEZzushmG#B*MUBOMf=ru z>oo>#irE)TV-@=xn4Ua;WG!T^W7b*62fRaW5r=)lShP=e`Yes1^JYZ?6F{jWUU?)q z79r5!|8=CNr}9rD%oha^(l`=NqnwhIlv4j{eg-9+7cv5DrlKIQ=pfF%u;Q{5p59~= zN6!N=Ex4#u(x_@S!G;DpsFSs_^6*bzW6-2Hvd{xP%0Fnp1Cd+G$bIU20w}^*F$;BP zKr)NXlg>h9Gd;m%G9pf{eikbzfjTOIJG$u5d653{gv&I+1mQw-aZT^c0wa;v&EK80}ioq~~wEjKCmGG~(p8_R}@7b+P6^kL2 zofr*_7H8p3W(zk-IX4db10>lxs!r?~bkC`|e;L}g)tF8fLB21TO$)(=j27MKxUoR2 z{!{7ThWHj*j+1xbh-AuAVx}Q3f+?RIQui}=ry}RQ#d!CQ;#f9GA9o14zh_p7GY|zA zQy7;r_i1jO`Ujj+^TMVGHcMK3VEdk=aax9)<9%NbO1_sf@|Xyr0g@&5M-umz%v{&x zOy!T+s>QKb)xAJ}4Z)rDpKbFME;{(*eFZvp132;D!vtecsT;DvBICFps5NyT`DQ$k zZFGm69y9M7<$rf8VshUWKycMC1kEEoE}jNX&5cr|(cHMgpRDjamXa?3gfi7q5a)F` z;v|v%mD6s7MPOvt$6ku<3m$U%)IZL3h`$BFJ)AE=b8+=;6^#`7iTaiV)S)dL(u6t` zY(B7{2TfE)J757wjW&m>HZ`ox3<9~k?W&_F9Ug~Neg}QSbXdQE=l7FDCLQkG8JXu{ zn0lv4RwAb$N?Gzv)I4H7Vt+jM3v}~+aIz%a4o-2B&_Nr%V>Bn%M&pX@TyH#;IY~ad zt*vT1D}6z=m7!pp%JOEIQYQ|s%rYe%&$~|0EvZ;u;Q7j2&^L~*3Ks6r23f5z{kfVcC*LWG0=H&bp!yAJ{wV=bS zOEliq+W_J?LUHofLiU8#^^!bMU8^>-D<=m8jSt#rsRwA<1;vmO-#+(R-y2~03(I#g zg4jq4pac;RuC1oYoFscx+v}>-5o166&hWv=oosRu9mxnKFU5g3vtl>sGXE?)N9uX| z1{l~G=iQWC0Cv+TkcF8fgvHdi*u1l6VIkYUF;ll>GRvZ;M5@mR!D%ugD_Y4jKnpzv zgr+(etgNOY$(8+2kmi_4o#kFIpm%|c^gZpm_5j7Idw<9oagiw#wehuTek7& z7JSs2C_KodJ40x|$h-|yi``~-1>eR92TwvXen2x19sPm1+s>PZ&15Pg?~3@sq(|*) z3*S|Ko%kfzv3zarMdAb{prY=Qw!t@1$I79~j0}#NQxY3RVM<8# zW3jcKIKL_3qDfFt&L3wt9(ef2*$a^<6e`MKXvpAFzK(ZoEh@jV(ggyk&Bx>4=aZEV z&CNwR)kcs~>}<{4-4#|>{9Ly20KtE#Rtt5$%*c4DOuQI12l?w~u|H<#1LfvtXJ?g~ z`*bHXdG(c*Cg2xZ3^3{r?U+os3eWny2ib6;Rpn+!hva|0uuJ-AXMpG}_18z9o<+nU z_Upss)`f-cAp)dEl_pd|r9T{i9QxAjtwKv*Azx@!^;-`b>*v4yGXBM)_;2zT6@R_r z=!1HBg8j@)X%`khzzOBZ_2qEnhPwNhNAWKue_!WPP*@%OA|Tx2$4=LO_w-O#rGs)A zY58l5GQj)&oSMUS0aInV+{vj#OExWzD(_1V3AtqUYN(k%2~kJ|{(gSKwr2P*(7 zs!{8-D2K5yO82#Os2cE{3&;f)nads4cCdJ;#9vG9#-7GW|6Fn(O4}Sf<=-ox21o`o zdxv~JknMB)ddb!PQrc1ZUBd_Mv}|!}|5|q1dq3^>pEf#!le(unoSCW1orpKf4x4w# z&eEgl*X5J&=kBWoba(DZ;i;(5{`c3q{hA5Dubu9v@-Jr#^gfXDVBrp7;s{_Z58Ame zX#?hTt1mkd?*F%EqhJlJrd(bo3!!GHIt z`0pO0jI*U$`A0_T8J67BEVBKJFtZ{x8bioJ?(aOfwc+ql;;S?N8uh}zM)f(9tvp<4 zfYaqpO(CB0-!4)~+tHVCHKeeMW_}mf-19CDx{*>5eREXr?|pi%<7NMfiw{Vu3e{#i zuvTp_AUN>+(rFo#;4YLqKAsYq3IuSvySHy|Y=tZQ$3znUnn)l6P%MMxodOJX1!R{@ zfwcl^*}D z_Bfv;4IHiZ<36=VFy)*Ofo*&dnJTBoa5=l(KQQqE=Je#GY(YiIg-x5={`36*Bp%Ro z%pZ1G#z@5L$$A^mbtFcS7#0~kI^WdOuZ{O++2Vx%S^1wg?@0zu6p&rA*~9%K_4*fj zkzpTP0qizjeQ!HLes2QM(fwsqs*S(qKc}|w?^DYsn`N3v11)C!FC+EJ;M#OkFz#IQ zx?R_!Fulm;Du1a{HKzNIUq-sokI1BwtS>vp{TuNA$%0mHIOU^M@RXIQt^n-H0131p z48iQ)yZ1S_-W@00yVg49--XBiJ^iWefEyAAT6sIKzO>W?BJly~*)}giQYBc(tabPC zS6GW_YX++SuM}GMfoy}T`+!X1XDd4(n0_TxyX>ZPU}X)(j!|7xed%~KROA246rcXd z6xk+E_#1th$*8*dJG}b3IunS#M+{#Cx*X^?@6~Adg_Vx(uY5C$XI8DPrC$6r>;G8q z@05{nMUAGUhm?27Zb}2nKahRt@+P=a-S`9mGK{=jN_8nHE_RxfUABhxbZXEx|L64n z@!RJBG_-F1VK=nZ6`+)}kd3-rNg6`l0_RsZABiN4)Z3ix%W*~!c&-eR5PwQ*eF2kx zs28!-+P;`qzwZACr-z{&y=?$R9$Y&JR$dk|U9no1n+{}JrbB}Qr-%?8-k{MDnwe_J ze_rRr-(hrvF5gd}=x8FD>_p%J=uI!7U3`~wNv%v zSNmuXb37M>A`mQA^?|=?Q zj1j0a>rR9(ZU77y9JvF_bNz@swR++t63n^EEBsZ5Emq>bTK6cI2oY#$UkX4!u(hXO z@^vckzwl4(+w!|}fQTJr##-<#YM$NW)0!L95U<*afR5wCnCZVanI~^nuRq&Y=A?{< z98ZnfPm8*fU@IYoIG(t2W)&!b=w%lY_f$9jFfAZh-T|jyLjB&tAG=q=e+>=V1Q{h; zm6Wja^Q+Ek2dJF=_o>@d8XbsE%F5RztVXffTAj`jNM;^4aV)* zv6F?@b*NCMxi~v}nVorA=F)fo=%VeNyk{x5rHmMK3vYr%8s8TLq>hcHO-DL4Udyv} zpg(_menWhe(Y>qSF3il@zG$ohnLRQ*=&?qdodB+cj~gF>CPHgIA{(LHO>JOuUr}Q) z23KLwQCjK0PW}EKq?5$fZvHH7skMl1QBXg@?CDYDQpDxOx<-pBl^+1VliU^`@b&Dr z2MyF4_m#oY`KNSrq+*~BGaT3Wq@?aQTwg!G^!xYE(!*`L`}+Da?wM~$h|+;fPeQf9 z6ZLLf1rDAMTloGpe(}tTAyukYA{aBdUUec25vrrckmEsb4EgN;$5(ipQ(0|kYC64u zN~d_quV7DncQ9@-M=izUuvK~`mF%ceT2lR#vkDHuU|V~I-k4uyURzry9qBFp)5A&^ zwf*FH?5~2&gF4s&2EMk~Fq3US7g%Ol$f&$YFg8NlLS+E_%QHxV9q{&QBZwNd>_#%b zp8MzYM%NAy&@=Ik&BaAIsy&uayEJu}dXCzHb>Titg=!?gGj| z)P|ENcnho|4qN*^)8I>w{0E=>Qvr6Xha9`OzP8%OX#NfOoYqUXTfFGalf8a?R2*DG zFC9=Jx3#K%P}t~g0NGu`sR^)hHJ(bHvJn0Dfc7hR_ftW4u=EY=uF(Qp1Zo%=}kLkk%tOS(=*9#vOQe3qr{B=o7= z2EZ=0Vn+3TRnum^ze9GZuAr6A+yOv91gbRxwL=Du=?JR)1g&roPNf3@iM;v~nb4wt zP~_D&<5N>RWd!a-w(RTcYOjc zf^scPaDoqMfC^Q*1%)9@XyLmI%DUPm4879t17iCs+ z?w!8eM_}x4veGw>%YxXXsR?^xodPz4`Qb9INcQjNH1i{pffidVSW}07A4P^g1fA05 z1F-o*Ge37^(?2!f&*Pn4{YUw`B7DbmjRebP1!ZOLmMNC76=9N5ta%RVT``Xyoyzzy z2sFdNuip0)H79f(q6rVOkU`PB!6*DvEF^IHBQm_{DvP`gJU`|EH8n6v{{_3JRZ3 zy8?QN0CszY=={HtpaW@Ilu(GgsY2CgnBD6{x*71f^|qX{Ggu_NMR1w%;mDdV7X=Ys zpWEPauAl!`fB98MeqNqUEq-lZ4MLR%?Ung*tovaxA%@>z&L9=JA*ND+m67_MrIlNkaZ=U( z+dk`JuUz|x8vpKK%l{x$O)g&0z7H5E zfvHlzlRO=WH6i^u7jU%adS(rxrEty1Deen7m?-m&Qtn7_q_`@ZDJY6qUgOh)zv_;JV0C;79v{KdT;J_AiRlE3es7?#;7 zc^5)1)e#~kz1wAg1jx+5=&XTG65)a7^ zxG>39{^dF|oJ7_+5YaM$^xE1B#U+H{F&4S{q)YiokW;lk`6<)Iv%A4O$aeGQn0IYG zWv(RW<$I?;0FwqS0;6MT(a|anW?IT#UaW7dGE$^NUzErHZeL18xJ53vM8Kz5!rCel zsl^?$)vb}k;p{v8)9rT4)bp7pCxg46dK>b)yLr#QZ?X(&zs{-MdXn>@XEeozRz8&{ zYHtgWwd1);tVBF}b|DV+D5T8)=#f)v&$~#dboHbHS|r4_w%*VUglG=Vt-qpR{{$i? zt+sY4`-gt2*pH5v!o;<@r+%16m5y3^uh{Nx!aLWb6-a6On}6FqfttAbSkiV&5tMFP z(%P!x6olos5b6wFU*uAhV^U{-)gMl$)>$YBJ&j9GrTh2rToqmo`Q@w~6$=|V=2LIjZaw8k|GxqJOvU^1NWl`OG^>6eD-o)~&wtDfssQ1K-+k?MXpyu{o*tp-PetsaU zqAf5ygCEd@6(j&|cYMF}v3ebb6R=@&uIWHtSqxKuiY)j#C*;%&!~gi%K3%n)eiuV* z&hUZVsK2E${^et)7JGD%uc3(_)CPRF8LW>#p7U?Af!iryEDfE+8B`Te#gY z1HpI`oV=~Qd_&r~qsLV3W1m=-8%GrKTxm}~6&`j1z8Do>hz~dOD|OnLdk^%fGdLeK zj(@CUHPTwBh(NGZrvNZ84U;+)X^exTNURBN*N(JjC?r9kf;lEOJ8YFPwjKDcY@Q~A zA;#WWnN9Ytb<dkQKlZaem|s(NU*@onuk)F+O+ zh*0q)J4iR-SMp-$D!Sp=r*o+kd(O! zcWMosJ9OmA<7eBzkh5c)Q=%a-{>%!cNyA>m=X(#g_T|hALmG{f-!FwlV*dOgaFo&* z*Yk=?l#%%%d%fO1kMm7G$zLj`7)rnQZJ>7PO)3jzDYK=lE--2;%aKuxpQLb>SLr7Z z^%d&sTyn}56sm##VX~UuS!8M$fitl7UfM%m#L$s+U@2S>n^9EfIh<%>kuV(TzI|>k zxyT$rng8=a(|I(=6g`fPZZ%bWg#}o@_gHE7T-&->YiqkVnCqG@ZI$UeBvD6S|5tx< zoVR<1=So-3)OC32Vr_l1Pi?oq_I(N-KeW7O?xOdNulIX*+OOR2$EiBS>cQ$z};xj*hh?Y$8$w4!h1n z*EnCc`7gk5b@W6oi!9kUaXQH%u;h#Xw3g(PF?^Rxuwd?zOL*Pq&waHe*_&gcJEUjG z2|)RXOqqq~&jhX+NM(pa?Q&g&O1rmYzP%!sfY`b}Xz1qH?7WC_VSV_ir)8;}r9U3X zzrVxjMqIt}vA$+IjHbUL^mKkxq)0=!OtRS>z#XHSk`iZU{9Ls1pNP{=#MF_bX7b3~ zQdiCah1I$?x_GM%UbD{%hBGOJa66;?wHtjhH{Sw?eVi#PVut{v$TQkBfoTTB!%DvhG*59T@A+3Z~U zV?tYyH!Sm@+k?NL&2D6@Ba*41BYSYPkl?a^cdW|oyv+k{HkT;O@_pQsW~LW%pkak& z_3ySM9ImtCjGr#4E*7X0-{fNq=RDJkuDivPYUFC|D@*TMRb{UNyc;O}z2ku2=CGk% z#Xr{KlLS*P=yAoq<>}RQdj{7ey^1c4vz1k8iY^sJt8LR%8Zcy!%58-Q^b>f4?Fi<{ z<8<;loX%nv=msyZl-x9_0YUi9B)&%dc@fv05j9Lqtx&)~hoSWe8&la32Nbj=4oobc z8l>Xk&SM?5fAX)iqqTZGls` zwKHQbEO&ALiDf1%Y}jP1ez}B9Yu?!mzx(AsFK&%%+Fl94gw~$4mRO5rJc4pDyAOQs z!}ak({?N+3&44(BL{5j$uP8ONE!u*5Ct-#5Eaje1 z+%I6T5ed?JfHO`XfP}^H*YP5*Ht@mEVh_;Rb~ySo5aFoNb0Q|4`hC+4-g6njfWc^@ z*dUNI9G7qCP}(t9iIDWMD4XP;2P=g#`j!(0Ae5GB!s@De(g{)aB%ihT<=&&D+kF9{F8Dm&=gN$$Z_6`(@1^}=gyrIqqHVbq)hlBnUMZ@VEiN{rvSsPney?&!;Opj zd5e4TXy(px^>Sv;_(DM?u6+JdF5*hX0U8JXt_U~}!}{9gP#dgf4Wu_cYOk1k2^a2- z*3PL98_ur`V$7jP1jQA@&I#&l<|O7~q@O(%pi(U>%?;cU;j_?g^p{~uK{+Y-R_#-mV*6{ED)*w=Yl&d(hFbjbV4?N@8#U zNwWTqQb_LiwZ-e?_MNflw3JEs7Aw%fNWo> zZdfb4?pHTDe>Fyp<^sA`UUfiFQYLEnRSHmR<&NfT?T2$$-g6fPq57?$C;3bzCmAaZ zu!E?{yWBE8a<{AOof##YmTNS+9*Q@d&u?!K9qu_RdKaUf>=IOGgF`JjIlcb68Sb7I zH){Jo2WR`k!x4OJJ?NdhFW;?Np6Lu71hANLS5$xMZ6Q|B+(UyZZ^qN;!Y7^&GfcnL zFp+eBt}2nD?nfu+V2Er7H#c?nNK$hIRjr13Al)%1xL?MhjUq7EPpn_6UEGItK8lrC zV>^j=CbhO>SGuzu%DfDZIJhF8Wn)^_?ix(d-!(?dhO)N zF)CWCepvyt)W5Mma>j4+mWg`RTaLE=XL= z__IK0O9|i0&#sDZfa<|bRQr0t&!o%`*pviQAWQlY$M2(VLUkI4ui}MU|!ZceejEN>D5{jn$%)E#${h%l*X;3)HC~Nryc7Jv(0zgB z@QoiX#fDZ2TtG;!T4=Nqau2EN!9`0&lZO$MIC&$d!1@V$r&@of*3@x~h;%lvc2Rqe z(TpRVG&Lmn{>dsyPowcV;fleA985#!T;}>GNin+FP3T1CG( z+4SW?7Q{#+Sq{vg`MK&Ckj8kCl;i^tKvZx7+5 zEI1Ld*A5);k3%?19hjZp<1`geoP$U7Mz7-<_Q>-6o8fZJv-V$oN+GVa30}0Rfb3=) z`*1&4{}aZFK`y}?PT-HS$Tq7Z%D{NtuvH2Y!~|z?;B!<+^K(z~TmtLf+d37t=3qoD zXUhs+MDL_l#hn@2?ZFx4Pcn|w9}P_nh3}kQt|(t`OIxESsNejo%rbN_6C27rr(QdH zk}vXPErM{tQzcpox^Qa|BVG=AN4xw#zr)K9hSeEE>_?D4dFVbFx>J22kb*#fEW8K|+O!FD5%9 z$~)`D$yP?1-5^GDAfp#k>2C^E9P1Z~QWQ>gdQ=J;Bp#=0PJH>EXSxuN|6U`+3BByV zndDNFJf_>V300QdFR10l?bCN-iofRNSKGPg_0nr$Ex53y7i~Y-L8oAwtP_Ey;~$d> z+D7w}t&%y_!%B~63={Rb<(vGg#s@==jZD>XYpA_zZI&!`XJ3~W!v7Kl@CKu+r3KZ! z)GE0^S6~qoBHE$km?c5%fDd$d##0w-*rrbL~-bbsxVqK+cmo7CDcr zYu*TQ3G0x!XCg?ePksrFY<(v@KX2t$yeu#$109PmO3z^?PGBgM+i`#;Fq*zhX5)mc4rEY<_7n{&ykC5}Z!WmOQnjpc z_aChwq*np36*k6mV0rUa5xHZ&tC6{)D9_~&E$S2aCf4f{CRy+kXp`hJ{mf{jf>B&|$*{8>}&9AAdI-CMTSMLs+DvDA-wnN*qH#IIVmHO>L)#n*} zNen;|*hl~-XvTzn>783#5|dWm7`DHpcS>6sdX!Wu%KuSLO>}Ta8@$vN4>(H<5t(rA z^OmMI1A*aw{usq$VAvh=zT+GFZ9^oXVIJkDZ)D|+(-3-)WA(0Qvom{X1~&)KgYIn7 zPFiX$C1$V@0t3ObH*QVx25)y5ePH-N4BkuXbEZ(-2BCs~VbmP~MZn4mYD@<6@1G$W zUp&6v>F=V-G_HvB6)`_UJpsdWUH`&f#Ya^}CNfX$m z$=yQFj!e?+K0TnVv4+uwQ19ozH#w(pkRBzSO*Hy3UC!$=$#DkUM$c_P%+2w>o)d2l zDw>iCK;mMdbP0J$QW|I3ty{v`*4xC7O9VtQ8V&vp+-#udW4h}$zOys=CaH9&ag%@% zSl0s)Pv%Z}U`MQgXEnq;v4F<$T#B`^&eUG>(?RwFJl6s*H2Qslq$>(gqT93kovS4! zC-;M+B{WhVk8+VXQ=xYYk5{D#)$AYLQQ^4;eWHLzHi5XSDLVspVik*5{|DmTv(*SB z?gcD-0}qPRr4>Shg1FU@@jqfyfPgEbWEmNt8X9zC)(jj@CIVUr!{RbRa#BU2D(#sq ztkT{xGq}X;a)<8XDZU^hAr2Zwct6uqXl7xtjK=rYJ10*0zcq|}=gB{Q*FiKf>`N-K zsWMtxNFKu}Zgb9hP9B&wo(bdNS;q2oKfq^`#QGe$7QG(#+$B0w+WG!%| z>o7lNigFE4C6++xek0&?2(D(7+Il^epsy!it;>n6%x|u3Y9m31>yeen*YBP5RzM_s zDEnMPb#)&kCn&6A&JbkD#tctcTh?Tuzh^f_4nDjzRv#ZVWn+*o7z`J`k}^D>48UNp zLLOuB)l(_T6(k~rWo(qos^{$Jy5LCW^)1m|b9II2h!UC;|5fAV}tt>WMYTZGl z5C?}(JXg#ZYzoERN3)+r&8WLeSZ(_&x|7M&%EPp9Fl3|o7m>{Ge}K}JKnH9TqZ$<4 z$fkh96-5@Z^3>9UK5jrBINg8#xj2eyC|HvYh0MS4t%z83--8J}%v*kbsVPI^YVrY) zUYQ4~kMyVv)N#p1gN6FXsvQVPeDyaUs_xoPXn}1)>{;O; z`DPs1@k2IW+f=4BJuc1?xiW_=JV}of_XF7qb0oN1WH0pV3!VdDBj(g7U81$&`Oe&G zq2V_b2AI|rS$Y%@$_}V3gB^&rP<`9dqFMUNfv9JCtx6wA9gzo-gj5dANNnBRbTSd@ zFneYLHjg2SW#jp(PE0g+ZGYg4XZ4$gqvRt`dH2GrN6CiSx2WV@kKOY)Z9v14b2n!U z;S9m@{!ANL`m<@!!7T<#&C&)ZWld6s`^ste3QZ06*p4(#k7Vbw7Bw10W2lanqU>vs zStU4O(#Kb6*-$7yI_dpFYLjkP)Mt`R9lTKq-k@S6TBt;>5c)_VBeNO27G?gPm7gk) zyIra@#O2PTNUgVHV`bIrh_lV^qsW_2Z5i23El?=nmX5ltzJ! z2+~BMY)QpG9bJ613wCp8voglwup(^^+9jd(gLWAyhwnLk>M42(x$BX_Xy?lEkih6e z1iE^3aTB|~vQA@sE&^<03#zPAl+4mHZzAesgzI;?-7HkQMF8}atW z`H3sUnj2LvO-tCYVI3fTk90-aF93J^Cdg8XkgPm=El>DjFK)3a5hyO`?6G&3_*9`r zn|n?ietn{tzuyTd&l%hsz3xHB#@~Nowj?Om4CL8%0u#-A+;dPB0STHNx)XaoYgkJ@Z`YJa!Cbvi0MJ?U0~uaiAgGhghf-t z21^f1m4xZ;XgM2a<(pn|&x~U>6mS~Fm3d{ZWR;gtBUY7W9u+RXc}PGXzE$ZLiO3SAL%Zs~!{Q5wMcy{!tjhxzovT?IH5sLApsvf=6 z>p4n}q}M|Qc61;DBCdSc)>kYUw_D#a1K)Gtl$I9D6=$^cCAqK_h9U5qG#0L3YZbaR zNm30kuTd}tP6JlkJ_kpC`2%xQ9@#tTS?I2k-#Yay^EI*<)8naB=Iv zg?nvqbgfgy&3r9(b?ioDBp)Ak-Zonw*xNXf=2Cg^QFkgJUsR@6C-Ev)pFq{QW^>Pq}@8-@WJ`)kopNOZd04H!B!)j3a99GOl1SCW`2DRAG*7IxVGXruy}!%4WI(&GNQMS zt|Ey0Pz7hCyjeD24r(SeXcDE=?``}v`=$us=_nH4${%>!%HMX7M(Ga}!pyVbcC>Id zr>dk9n-4Pa2Fyr+QU!eNyXhG2M0X3zL%6Ozs&UT9MUWA#mHJ?S5dJYb^>+K{ck3j; z`S&uZHTkV=aQ9?vXusw$ns|xLt%|>AxbCIk2t+`tF7K1{p&1I!I!=$7;<%WvFMT!4 z=-fvAAKMKT8idrLc~Ffy2Ol9B$6u%b14=}!pLABV4M=o%bleRmqj0*am2;Ucb%C%! zM9X6+JtscrxZYsJ$`U)sG<-Oyzd~tQ4<&*C1{&(>!B{bf$sOIdPUgCH1Axg^;MBeP zD@573It~Be0qR^7)XE)4@1@sOo5ZJ2(fuW_v#&;nHO|e?c^#vXu73&-oB$3;{hUr9 z8*H&4A@n_lKMod#Rnde?b`*;|SF{a~1BIq0t^5Rp)jcTvRleO@;h$@z+SDaqqQ-LY}o?QD0+Ncel-ReoTn=pZ9NabaIbP_QkLli#H(kCqh~JvLKS-M!yo~gZvBTH)sjwW6 ztu|G%M*UhVr*kDPgUqqmQB)MW=f-;?`_9l-os7k{o|boy+-s@NlyMP|`mN4SgTyP1 zqRI!$yf&h*oI5(03{cf&V#Gjmz@o(bM@Iu~z0cfdOuTd_-RHqH~_bY*lfU`JI`a~Ma zfrp0#@<7pe8P&|4m60B~@_6eB_Z(20sX@F&sjko6{!*~+W$SEQ|V zLk1Q!bS!bU4Lcsuzf&#=hO-Qyah7rg1A6%oD1!d^?ZW+&@R%$OVyL`}ItbAhu zxl<~nlb1)5lFFLl_)`KZr~nCXUi+)Ya#!`(P^DnQ~cz~*y5o9(x(67_{GD<<_fMK4Oz+gUA zlM2-z+-c~*TmEJuZx-&PqQfD763=nvMnPI{k2aFY?7B7($-qiT>djsRHGJJq=)ErV zE*S?mX*Z(5{2*;`Sn?5+≷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\j=|eP6`cs1--;>iC0m6{n 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 literal 0 HcmV?d00001 diff --git a/docs/user/images/select-your-space.png b/docs/user/images/select-your-space.png new file mode 100755 index 0000000000000000000000000000000000000000..887e8eea27c5c4afe02657df447bc267d8a5d178 GIT binary patch literal 105696 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 literal 0 HcmV?d00001 diff --git a/docs/user/images/tags-search.png b/docs/user/images/tags-search.png new file mode 100755 index 0000000000000000000000000000000000000000..67458200c50d1c896f411859d7ae1b558eddcae2 GIT binary patch literal 37555 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 literal 0 HcmV?d00001 diff --git a/docs/user/images/visualization-journey.png b/docs/user/images/visualization-journey.png new file mode 100644 index 0000000000000000000000000000000000000000..ef7634485bccdb72cd609da005043cb70b2cb7d3 GIT binary patch literal 152548 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

R8MCRF(U)BB_y{k;pZ--6My|^wJW6&H}`9 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 is Kibana? ++++ -**_Visualize and analyze your data and manage all things Elastic Stack._** +{kib} enables you to give +shape to your data and navigate the Elastic Stack. With {kib}, you can: -Whether you’re an analyst or an admin, {kib} makes your data actionable by providing -three key functions. Kibana is: +* *Visualize and analyze your data.* +Search for hidden insights, visualize what you've found in charts, gauges, +maps and more, and combine them in a dashboard. -* **An open-source analytics and visualization platform.** -Use {kib} to explore your {es} data, and then build beautiful visualizations and dashboards. +* *Search, observe, and protect.* +From discovering documents to analyzing logs to finding security vulnerabilities, +{kib} is your portal for accessing these capabilities and more. -* **A UI for managing the Elastic Stack.** -Manage your security settings, assign user roles, take snapshots, roll up your data, -and more — all from the convenience of a {kib} UI. +* *Manage, monitor, and secure the Elastic Stack.* +Manage your indices and ingest pipelines, monitor the health of your +Elastic Stack cluster, and control which users have access to +which features. -* **A centralized hub for Elastic's solutions.** From log analytics to -document discovery to SIEM, {kib} is the portal for accessing these and other capabilities. -[role="screenshot"] -image::images/intro-kibana.png[Kibana home page] +*{kib} is for administrators, analysts, and business users.* +As an admin, your role is to manage the Elastic Stack, from creating your +deployment to getting {es} data into {kib}, and then +managing the data. As an analyst, your job is to discover insights +in the data, visualize your data on dashboards, and share your findings. As a business user, +you want to view existing dashboards and drill down into details. + +*{kib} works with all types of data.* Your data can be structured or unstructured text, +numerical data, time-series data, geospatial data, logs, metrics, security events, +and more. Kibana is designed to use Elasticsearch as a data store. +No matter your data, {kib} can help you uncover patterns and relationships and visualize the results. [float] -[[get-data-into-kibana]] -=== Ingest data +[[kibana-home-page]] +=== Where to start + +Start with the home page, where you’re guided toward the most common use cases. +For a quick reference of {kib} use cases, refer to <> -{kib} is designed to use {es} as a data source. Think of Elasticsearch as the engine that stores -and processes the data, with {kib} sitting on top. +[role="screenshot"] +image::images/home-page.png[Kibana home page] + +The main menu gets you to where you need to go. Like the home page, +the menu is organized by use case. Want to work with your logs, metrics, APM, or +Uptime data? The apps you need are under *Observability*. The main menu also includes +*Recently viewed*, so you can easily access your previously opened apps. -To start working with your data in Kibana, use one of the many ingest options, -available from the home page. You can collect data from an app or service or upload a file that contains your data. -If you're not ready to use your own data, you can add a sample data set -to give {kib} a test drive. +Hidden by default, you open the main menu by clicking the +hamburger icon. To keep the main menu visible at all times, click the *Dock navigation* item. [role="screenshot"] -image::setup/images/add-data-home.png[Built-in options for adding data to Kibana: Add data, Add Elastic Agent, Upload a file] +image::images/kibana-main-menu.png[Kibana main menu] [float] -[[explore-and-query]] -=== Explore & query +[[kibana-navigation-search]] +=== Search {kib} + +Using the Search field in the global header, you can +search for applications and objects, such as +dashboards and visualizations. + +Search suggestions include deep links into applications, +allowing you to directly navigate to the views you need most. -Ready to dive into your data? With <>, you can explore your data and -search for hidden insights and relationships. Ask your questions, and then -narrow the results to just the data you want. +[role="screenshot"] +image::images/app-navigation-search.png[Example of searching for apps] + +When searching for objects, you can search by type, name, and tag. +Tags are keywords or labels that you assign to {kib} objects, +so you can classify the objects in a way that is meaningful to you. +You can then quickly search for related objects based on shared tags. [role="screenshot"] -image::images/intro-discover.png[Discover UI] +image::images/tags-search.png[Example of searching for tags] + +To get the most from the search feature, follow these tips: + +* Use the keyboard shortcut—Ctrl+/ on Windows and Linux, Command+/ on MacOS—to focus on the input at any time. + +* Use the provided syntax keywords. ++ +[cols=2*] +|=== +|Search by type +|`type:dashboard` + +Available types: `application`, `canvas-workpad`, `dashboard`, `index-pattern`, `lens`, `maps`, `query`, `search`, `visualization` + +|Search by tag +|`tag:mytagname` + +`tag:"tag name with spaces"` + +|Search by type and name +|`type:dashboard my_dashboard_title` + +|Advanced searches +|`tag:(tagname1 or tagname2) my_dashboard_title` + +`type:lens tag:(tagname1 or tagname2)` + +`type:(dashboard or canvas-workpad) logs` + +|=== + [float] [[visualize-and-analyze]] -=== Visualize & analyze +=== Analyze your data -A visualization is worth a thousand log lines, and {kib} provides -many options for showcasing your data. Use <>, -our drag-and-drop interface, -to rapidly build -charts, tables, metrics, and more. If there -is a better visualization for your data, *Lens* suggests it, allowing for quick -switching between visualization types. - -Once your visualizations are just the way you want, -use <> to collect them in one place. A dashboard provides -insights into your data from multiple perspectives. +Data analysis is the core functionality of {kib}. +You can quickly search through large amounts of data, explore fields and values, +and then use {kib}’s drag-and-drop interface to rapidly build charts, tables, metrics, and more. [role="screenshot"] -image::images/intro-dashboard.png[Sample eCommerce data set dashboard] +image::images/visualization-journey.png[User visualization journey] + +[[get-data-into-kibana]] +. *Add data.* The best way to add {es} data to {kib} is to use one of our guided processes, +available from the <>. You can collect data from an app or service, upload a +file, or add a sample data set. -{kib} also offers these visualization features: +. *Explore.* With <>, you can search your data for hidden +insights and relationships. Ask your questions, and then filter the results to just the data you want. +You can also limit your results to the most recent documents added to {es}. -* <> gives you the ability to present your data in a -visually compelling, pixel-perfect report. Give your data the “wow” factor -needed to impress your CEO or to captivate coworkers with a big-screen display. +. *Visualize.* {kib} provides many options to create visualizations of your data, from +aggregation-based data to time series data. +<> is your starting point to create visualizations, +and then pulling them together to show your data from multiple perspectives. -* <> enables you to ask (and answer) meaningful -questions of your location-based data. Maps supports multiple -layers and data sources, mapping of individual geo points and shapes, -and dynamic client-side styling. +. *Present.* With <>, you can display your data on a visually +compelling, pixel-perfect workpad. **Canvas** can give your data +the “wow” factor needed to impress your CEO and captivate coworkers with a big-screen display. -* <> allows you to combine -an infinite number of aggregations to display complex data. -With TSVB, you can customize -every aspect of your visualization. Choose your own date format and color -gradients, and easily switch your data view between time series, metric, -top N, gauge, and markdown. +. *Share.* Ready to <> your findings with a larger audience? {kib} offers many options—embed +a dashboard, share a link, export to PDF, and more. [float] -[[organize-and-secure]] -=== Organize & secure +==== Plot location data on a map +If you’re looking to better understand the “where’’ in your data, your data +analysis journey will also include <>. This app is the right +choice when you’re looking for a spatial pattern, performing ad-hoc location-driven analysis, +or analyzing metrics with a geographic perspective. With *Maps*, you can build +world country maps, administrative region maps, and point-to-point origin-destination maps. +You can also visualize and track movement over space and through time. -Want to share Kibana’s goodness with other people or teams? You can do so with -<>, built for organizing your visualizations, dashboards, and indices. -Think of a space as its own mini {kib} installation — it’s isolated from -all other spaces, so you can tailor it to your specific needs without impacting others. +[float] +==== Model data behavior -You can even choose which features to enable within each space. Don’t need -Machine learning in your “Executive” space? Simply turn it off. +To model the behavior of your data, you'll want to use +<>. +This app can help you extract insights from your data that you might otherwise miss. +You can forecast unusual behavior in your time series data. +You can also perform outlier detection, regression, and classification analysis +on your data and generate annotated results. -[role="screenshot"] -image::images/intro-spaces.png[Space selector screen] +[float] +==== Graph relationships -You can take this all one step further with Kibana’s security features, and -control which users have access to each space. {kib} allows for fine-grained -controls, so you can give a user read-only access to -dashboards in one space, but full access to all of Kibana’s features in another. +Looking to uncover how items in your data are related? +<> is your app. Graphing relationships is useful in a variety of use cases, +from fraud detection to recommendation engines. For example, graph exploration +can help you uncover website vulnerabilities that hackers are targeting, +so you can harden your website. Or, you might provide graph-based +personalized recommendations to your e-commerce customers. + +[float] +[[extend-your-use-case]] +=== Search, observe, and protect + +Being able to search, observe, and protect your data is a requirement for any analyst. +{kib} provides solutions for each of these use cases. + +* https://www.elastic.co/guide/en/enterprise-search/current/index.html[*Enterprise Search*] enables you to create a search experience for your app, workplace, and website. + +* {observability-guide}/observability-introduction.html[*Elastic Observability*] enables you to monitor and apply analytics in real time +to events happening across all your environments. You can analyze log events, monitor the performance metrics for the host or container +that it ran in, trace the transaction, and check the overall service availability. + +* Designed for security analysts, {security-guide}/es-overview.html[*Elastic Security*] provides an overview of +the events and alerts from your environment. Elastic Security helps you defend +your organization from threats before damage and loss occur. ++ +[role="screenshot"] +image::siem/images/detections-ui.png[] [float] [[manage-all-things-stack]] === Manage all things Elastic Stack -<> provides guided processes for managing all -things Elastic Stack — indices, clusters, licenses, UI settings, -and more. Want to update your {es} indices? Set user roles and privileges? -Turn on dark mode? Kibana has UIs for all that. +{kib}'s <> takes you under the hood, +so you can twist the levers and turn the knobs. *Stack Management* provides +guided processes for administering all things Elastic Stack, +including data, indices, clusters, alerts, and security. [role="screenshot"] image::images/intro-management.png[] [float] -[[extend-your-use-case]] -=== Extend your use case — or add a new one +==== Manage your data, indices, and clusters + +{kib} offers these data management tasks—all from the convenience of a UI: + +* Refresh, flush, and clear the cache of your indices. +* Define the lifecycle of an index as it ages. +* Define a policy for taking snapshots of your cluster. +* Roll up data from one or more indices into a new, compact index. +* Replicate indices on a remote cluster and copy them to a local cluster. + +[float] +==== Alert and take action +Detecting and acting on significant shifts and signals in your data is a need +that exists in almost every use case. For example, you might set an alert to notify you when: -As a hub for Elastic's https://www.elastic.co/products/[solutions], {kib} -can help you find security vulnerabilities, -monitor performance, and address your business needs. Get alerted if a key -metric spikes. Detect anomalous behavior or forecast future spikes. Root out -bottlenecks in your application code. Kibana doesn’t limit or dictate how you explore your data. +* A shift occurs in your business critical KPIs. +* System resources, such as memory, CPU and disk space, take a dip. +* An unusually high number of service requests, suspicious processes, and login attempts occurs. + +An alert is triggered when a specified condition is met. For example, +an alert might trigger when the average or max of one of +your metrics exceeds a threshold within a specified time frame. + +When the alert triggers, you can send a notification to a system that is part of +your daily workflow. {kib} integrates with email, Slack, PagerDuty, and ServiceNow, +to name a few. + +A dedicated view for creating, searching, and editing alerts is in <>. [role="screenshot"] -image::siem/images/detections-ui.png[] +image::images/alerts-and-actions.png[Alerts and Actions view] + [float] -[[try-kibana]] -=== Give {kib} a try +[[organize-and-secure]] +=== Organize your work in spaces + +Want to share {kib}’s goodness with other people or teams without overwhelming them? You can do so +with <>, built for organizing your visualizations, dashboards, and indices. +Think of a space as its own mini {kib} installation—it’s isolated from all other spaces, +so you can tailor it to your specific needs without impacting others. + +[role="screenshot"] +image::images/select-your-space.png[Space selector screen] + +Most of {kib}’s entities are space-aware, including dashboards, visualizations, index patterns, +Canvas workpads, Timelion visualizations, graphs, tags, and machine learning jobs. + +In addition: + +* **Elastic Security** is space-aware, so the timelines and investigations +you open in one space will not be available to other spaces. + +* **Observability** is currently partially space-aware, but will be enhanced to become fully space-aware. + +* Most of the **Stack Management** features are not space aware because they +are primarily used to manage features of {es}, which serves as a shared data store for all spaces. + +* Alerts are space-aware and work nicely with the {kib} role-based access control +model to allow you secure access to them, depending on the alert type and your user roles. +For example, roles with no access to an app will not have access to its alerts. + +[float] +==== Control feature visibility + +You can take spaces one step further and control which features are visible +within each space. For example, you might hide **Dev Tools** in your "Executive" +space or show **Stack Monitoring** only in your "Admin" space. + +Controlling feature visibility is not a security feature. To secure access +to specific features on a per-user basis, you must configure +<>. + +[role="screenshot"] +image::images/features-control.png[Features Controls screen] -There is no faster way to try out {kib} than with our hosted {es} Service. -https://www.elastic.co/cloud/elasticsearch-service/signup[Sign up for a free trial] -and start exploring data in minutes. +[float] +[[intro-kibana-Security]] +=== Secure {kib} + +{kib} offers a range of security features for you to control who has access to what. +The security features are automatically turned on when +{ref}/get-started-enable-security.html[security is enabled in +{es}]. For a description of all available configuration options, +see <>. + +[float] +==== Log in +Kibana supports several <>, +allowing you to login using {es}’s built-in realms, or by your own single sign-on provider. + +[role="screenshot"] +image::images/login-screen.png[Login screen] + +[float] +==== Secure access + +{kib} provides roles and privileges for controlling which users can +view and manage {kib} features. Privileges grant permission to view an application +or perform a specific action and are assigned to roles. Roles allow you to describe +a “template” of capabilities that you can grant to many users, +without having to redefine what each user should be able to do. + +When you create a role, you can scope the assigned {kib} privileges to specific spaces. +This makes it possible to grant users different access levels in different spaces, +or even give users their very own private space. For example, power users might +have privileges to create and edit visualizations and dashboards, +while analysts or executives might have *Dashboard* and *Canvas* with read-only privileges. + +{kib}’s role management interface allows you to describe these various access +levels, or you can automate role creation via our <>. + +[role="screenshot"] +image::images/roles-and-privileges.png[{kib privileges}] + +[float] +==== Audit access + +Once you have your users and roles configured, you might want to maintain a +record of who did what, when. The {kib} audit log will record this information for you, +which can then be correlated with {es} audit logs to gain more insights into your +users’ behavior. For more information, see <>. + +[float] +[[whats-the-right-app]] +=== What’s the right app for you? + +{kib} has a wealth of apps, each with its own area of specialty. +Scan this table to quickly find the app that gets you to our goal. + +[cols=2*] +|=== + +2+| *Get started* + +|Get {kib} +|https://www.elastic.co/cloud/elasticsearch-service/signup[Sign up for a free trial] and start exploring data in minutes. + +|Don’t know where to begin +|The home page. If you’re looking to explore and visualize your data, follow +the <>. + +|Add data +|The Add data page, available from the home page. + +|See the full list of {kib} features +|The https://www.elastic.co/kibana/features[{kib} features page on elastic.co] + +2+| *Analyze and visualize your data* + +|Know what’s in your data +|<> + +|Create charts and other visualizations +|<> + +|Show your data from different perspectives +|<> + +|Work with location data +|<> + +|Create a presentation of your data +|<> + +|Generate models for your data’s behavior +|<> + +|Explore connections in your data +|<> + +|Share your data +|<>, <> + +2+|*Build a search experience* + +|Create a search experience for your workplace +|https://www.elastic.co/guide/en/workplace-search/current/workplace-search-getting-started.html[Workplace Search] + +|Build a search experience for your app +|https://www.elastic.co/guide/en/app-search/current/getting-started.html[App Search] + + +2+|*Monitor, analyze, and react to events* + +|Monitor software services and applications in real-time by collecting performance information +|{observability-guide}/apm.html[APM] + +|Monitor the availability of your sites and services +|{observability-guide}/monitor-uptime.html[Uptime] + +|Search, filter, and tail all your logs +|{observability-guide}/monitor-logs.html[Logs] + +|Analyze metrics from your infrastructure, apps, and services +|{observability-guide}/analyze-metrics.html[Metrics] + +2+|*Prevent, detect, and respond to threats* + +|Create and manage rules for suspicious source events, and view the alerts these rules create. +|{security-guide}/detection-engine-overview.html[Detections] + +|View all hosts and host-related security events. +|{security-guide}/hosts-overview.html[Hosts] + +|View key network activity metrics via an interactive map. +|{security-guide}/network-page-overview.html[Network] + +|Investigate alerts and complex threats, such as lateral movement of malware across hosts in your network. +|{security-guide}/timelines-ui.html[Timelines] + +|Create and track security issues +|{security-guide}/cases-overview.html[Cases] + +|View and manage hosts that are running Endpoint Security +|{security-guide}/admin-page-ov.html[Administration] + +2+|*Administer your Kibana instance* + +|Manage your Elasticsearch data +|< Data>> + +|Set up alerts +|< Alerts and Actions>> + +|Organize your workspace and users +|< Spaces>> + +|Define user roles and privileges +|< Users>> + +|Customize {kib} to suit your needs +|< Advanced Settings>> + +|=== + +[float] +[[try-kibana]] +=== Getting help -You can also <> — no code, no additional -infrastructure required. +Using our in-product guidance can help you get up and running, faster. +Click the help icon image:images/intro-help-icon.png[Help icon in navigation bar] +for help with questions or to provide feedback. -Our <> and in-product guidance can -help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[Help icon in navigation bar] for help with questions or to provide feedback. +To keep up with what’s new and changed in Elastic, click the celebration icon in the global header. From 4d7941928d27a5f6c3397de324453a9fe45bb179 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 28 Jan 2021 14:25:54 -0600 Subject: [PATCH 097/163] skip flaky test --- .../public/ui/query_string_input/query_string_input.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index eca9d90a7500e..d9066a7345fb8 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -113,7 +113,7 @@ describe('QueryStringInput', () => { expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language); }); - it('Should disable autoFocus on EuiTextArea when disableAutoFocus prop is true', () => { + it.skip('Should disable autoFocus on EuiTextArea when disableAutoFocus prop is true', () => { const component = mount( wrapQueryStringInputInContext({ query: kqlQuery, From 9480e2f1929c98537cd12917ab22d01d058b1172 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 28 Jan 2021 15:03:15 -0600 Subject: [PATCH 098/163] skip query string suite --- .../ui/query_string_input/query_string_input.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index d9066a7345fb8..50e4f7d22a99a 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -84,7 +84,7 @@ function wrapQueryStringInputInContext(testProps: any, storage?: any) { ); } -describe('QueryStringInput', () => { +describe.skip('QueryStringInput', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -102,7 +102,7 @@ describe('QueryStringInput', () => { await waitFor(() => getByText('KQL')); }); - it.skip('Should pass the query language to the language switcher', () => { + it('Should pass the query language to the language switcher', () => { const component = mount( wrapQueryStringInputInContext({ query: luceneQuery, @@ -113,7 +113,7 @@ describe('QueryStringInput', () => { expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language); }); - it.skip('Should disable autoFocus on EuiTextArea when disableAutoFocus prop is true', () => { + it('Should disable autoFocus on EuiTextArea when disableAutoFocus prop is true', () => { const component = mount( wrapQueryStringInputInContext({ query: kqlQuery, From 1ab03d0cb089aed5a8c1fbb6955b248cfff18362 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 28 Jan 2021 14:56:04 -0700 Subject: [PATCH 099/163] label skipped suite with relevant issues --- .../public/ui/query_string_input/query_string_input.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index 50e4f7d22a99a..b7d9be485a303 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -84,6 +84,9 @@ function wrapQueryStringInputInContext(testProps: any, storage?: any) { ); } +// FAILING: https://github.com/elastic/kibana/issues/85715 +// FAILING: https://github.com/elastic/kibana/issues/89603 +// FAILING: https://github.com/elastic/kibana/issues/89641 describe.skip('QueryStringInput', () => { beforeEach(() => { jest.clearAllMocks(); From 608efb0a3d41eaa1c744d6ac7e2f433046ff1060 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 28 Jan 2021 16:59:40 -0600 Subject: [PATCH 100/163] [build] Remove file architecture from docker tag (#89605) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../docker_generator/templates/build_docker_sh.template.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index 93c5f82aa1e42..d896e9cfa671c 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -19,7 +19,6 @@ function generator({ ubiImageFlavor, architecture, }: TemplateContext) { - const fileArchitecture = architecture === 'aarch64' ? 'arm64' : 'amd64'; return dedent(` #!/usr/bin/env bash # @@ -56,9 +55,9 @@ function generator({ retry_docker_pull ${baseOSImage} echo "Building: kibana${imageFlavor}${ubiImageFlavor}-docker"; \\ - docker build -t ${imageTag}${imageFlavor}${ubiImageFlavor}:${version}-${fileArchitecture} -f Dockerfile . || exit 1; + docker build -t ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} -f Dockerfile . || exit 1; - docker save ${imageTag}${imageFlavor}${ubiImageFlavor}:${version}-${fileArchitecture} | gzip -c > ${dockerTargetFilename} + docker save ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} | gzip -c > ${dockerTargetFilename} exit 0 `); From 55afba4a4d3cc87074031de2d1deaa97f44188fe Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 28 Jan 2021 17:15:13 -0600 Subject: [PATCH 101/163] Setting up and documenting Presentation Util (#88112) --- docs/developer/plugin-list.asciidoc | 4 +- package.json | 1 + src/dev/storybook/aliases.ts | 1 + src/plugins/presentation_util/README.md | 3 - src/plugins/presentation_util/README.mdx | 211 ++++++++++++++++++ .../components/dashboard_picker.stories.tsx | 27 +++ .../public/components/dashboard_picker.tsx | 59 ++--- .../saved_object_save_modal_dashboard.tsx | 163 ++++---------- ..._save_modal_dashboard_selector.stories.tsx | 57 +++++ ...d_object_save_modal_dashboard_selector.tsx | 132 +++++++++++ src/plugins/presentation_util/public/index.ts | 4 +- .../presentation_util/public/plugin.ts | 33 ++- .../public/services/create/factory.ts | 42 ++++ .../public/services/create/index.ts | 82 +++++++ .../public/services/create/provider.tsx | 83 +++++++ .../public/services/create/registry.tsx | 89 ++++++++ .../public/services/index.ts | 31 +++ .../public/services/kibana/capabilities.ts | 26 +++ .../public/services/kibana/dashboards.ts | 39 ++++ .../public/services/kibana/index.ts | 34 +++ .../public/services/storybook/capabilities.ts | 29 +++ .../public/services/storybook/index.ts | 28 +++ .../public/services/stub/capabilities.ts | 18 ++ .../public/services/stub/dashboards.ts | 37 +++ .../public/services/stub/index.ts | 22 ++ src/plugins/presentation_util/public/types.ts | 9 +- .../presentation_util/storybook/decorator.tsx | 28 +++ .../presentation_util/storybook/main.ts | 19 ++ .../presentation_util/storybook/manager.ts | 21 ++ .../presentation_util/storybook/preview.tsx | 29 +++ src/plugins/presentation_util/tsconfig.json | 2 +- .../show_saved_object_save_modal.tsx | 13 +- src/plugins/visualize/kibana.json | 4 +- .../components/visualize_top_nav.tsx | 3 - .../visualize/public/application/index.tsx | 8 +- .../visualize/public/application/types.ts | 2 + .../application/utils/get_top_nav_config.tsx | 74 +++--- src/plugins/visualize/public/plugin.ts | 3 + typings/index.d.ts | 7 + x-pack/plugins/lens/kibana.json | 26 ++- x-pack/plugins/lens/public/app_plugin/app.tsx | 1 - .../lens/public/app_plugin/mounter.tsx | 39 ++-- .../lens/public/app_plugin/save_modal.tsx | 6 - ...ed_object_save_modal_dashboard_wrapper.tsx | 6 +- x-pack/plugins/lens/public/plugin.ts | 9 + x-pack/plugins/maps/kibana.json | 23 +- x-pack/plugins/maps/public/kibana_services.ts | 1 + x-pack/plugins/maps/public/plugin.ts | 2 + .../public/routes/map_page/top_nav_config.tsx | 11 +- yarn.lock | 2 +- 50 files changed, 1357 insertions(+), 246 deletions(-) delete mode 100755 src/plugins/presentation_util/README.md create mode 100755 src/plugins/presentation_util/README.mdx create mode 100644 src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx create mode 100644 src/plugins/presentation_util/public/services/create/factory.ts create mode 100644 src/plugins/presentation_util/public/services/create/index.ts create mode 100644 src/plugins/presentation_util/public/services/create/provider.tsx create mode 100644 src/plugins/presentation_util/public/services/create/registry.tsx create mode 100644 src/plugins/presentation_util/public/services/index.ts create mode 100644 src/plugins/presentation_util/public/services/kibana/capabilities.ts create mode 100644 src/plugins/presentation_util/public/services/kibana/dashboards.ts create mode 100644 src/plugins/presentation_util/public/services/kibana/index.ts create mode 100644 src/plugins/presentation_util/public/services/storybook/capabilities.ts create mode 100644 src/plugins/presentation_util/public/services/storybook/index.ts create mode 100644 src/plugins/presentation_util/public/services/stub/capabilities.ts create mode 100644 src/plugins/presentation_util/public/services/stub/dashboards.ts create mode 100644 src/plugins/presentation_util/public/services/stub/index.ts create mode 100644 src/plugins/presentation_util/storybook/decorator.tsx create mode 100644 src/plugins/presentation_util/storybook/main.ts create mode 100644 src/plugins/presentation_util/storybook/manager.ts create mode 100644 src/plugins/presentation_util/storybook/preview.tsx diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index c4be7a7367c16..0ab1c89c1d8f7 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -150,8 +150,8 @@ It also provides a stateful version of it on the start contract. Content is fetched from the remote (https://feeds.elastic.co and https://feeds-staging.elastic.co in dev mode) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely. -|{kib-repo}blob/{branch}/src/plugins/presentation_util/README.md[presentationUtil] -|Utilities and components used by the presentation-related plugins +|{kib-repo}blob/{branch}/src/plugins/presentation_util/README.mdx[presentationUtil] +|The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). |{kib-repo}blob/{branch}/src/plugins/region_map/README.md[regionMap] diff --git a/package.json b/package.json index d6850a50c046f..920e0c8ba5192 100644 --- a/package.json +++ b/package.json @@ -393,6 +393,7 @@ "@storybook/addon-essentials": "^6.0.26", "@storybook/addon-knobs": "^6.0.26", "@storybook/addon-storyshots": "^6.0.26", + "@storybook/addon-docs": "^6.0.26", "@storybook/components": "^6.0.26", "@storybook/core": "^6.0.26", "@storybook/core-events": "^6.0.26", diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index d1ebcfa1e8399..675b5a682f272 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -18,4 +18,5 @@ export const storybookAliases = { security_solution: 'x-pack/plugins/security_solution/.storybook', ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook', observability: 'x-pack/plugins/observability/.storybook', + presentation: 'src/plugins/presentation_util/storybook', }; diff --git a/src/plugins/presentation_util/README.md b/src/plugins/presentation_util/README.md deleted file mode 100755 index 047423a0a9036..0000000000000 --- a/src/plugins/presentation_util/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# presentationUtil - -Utilities and components used by the presentation-related plugins \ No newline at end of file diff --git a/src/plugins/presentation_util/README.mdx b/src/plugins/presentation_util/README.mdx new file mode 100755 index 0000000000000..35b80e3634534 --- /dev/null +++ b/src/plugins/presentation_util/README.mdx @@ -0,0 +1,211 @@ +--- +id: presentationUtilPlugin +slug: /kibana-dev-docs/presentationPlugin +title: Presentation Utility Plugin +summary: Introduction to the Presentation Utility Plugin. +date: 2020-01-12 +tags: ['kibana', 'presentation', 'services'] +related: [] +--- + +## Introduction + +The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). + +## Plugin Services Toolkit + +While Kibana provides a `useKibana` hook for use in a plugin, the number of services it provides is very large. This presents a set of difficulties: + +- a direct dependency upon the Kibana environment; +- a requirement to mock the full Kibana environment when testing or using Storybook; +- a lack of knowledge as to what services are being consumed at any given time. + +To mitigate these difficulties, the Presentation Team creates services within the plugin that then consume Kibana-provided (or other) services. This is a toolkit for creating simple services within a plugin. + +### Overview + +- A `PluginServiceFactory` is a function that will return a set of functions-- which comprise a `Service`-- given a set of parameters. +- A `PluginServiceProvider` is an object that use a factory to start, stop or provide a `Service`. +- A `PluginServiceRegistry` is a collection of providers for a given environment, (e.g. Kibana, Jest, Storybook, stub, etc). +- A `PluginServices` object uses a registry to provide services throughout the plugin. + +### Defining Services + +To start, a plugin should define a set of services it wants to provide to itself or other plugins. + + +```ts +export interface PresentationDashboardsService { + findDashboards: ( + query: string, + fields: string[] + ) => Promise>>; + findDashboardsByTitle: (title: string) => Promise>>; +} + +export interface PresentationFooService { + getFoo: () => string; + setFoo: (bar: string) => void; +} + +export interface PresentationUtilServices { + dashboards: PresentationDashboardsService; + foo: PresentationFooService; +} +``` + + +This definition will be used in the toolkit to ensure services are complete and as expected. + +### Plugin Services + +The `PluginServices` class hosts a registry of service providers from which a plugin can access its services. It uses the service definition as a generic. + +```ts +export const pluginServices = new PluginServices(); +``` + +This can be placed in the `index.ts` file of a `services` directory within your plugin. + +Once created, it simply requires a `PluginServiceRegistry` to be started and set. + +### Service Provider Registry + +Each environment in which components are used requires a `PluginServiceRegistry` to specify how the providers are started. For example, simple stubs of services require no parameters to start, (so the `StartParameters` generic remains unspecified) + + +```ts +export const providers: PluginServiceProviders = { + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + foo: new PluginServiceProvider(fooServiceFactory), +}; + +export const serviceRegistry = new PluginServiceRegistry(providers); +``` + + +By contrast, a registry that uses Kibana can provide `KibanaPluginServiceParams` to determine how to start its providers, so the `StartParameters` generic is given: + + +```ts +export const providers: PluginServiceProviders< + PresentationUtilServices, + KibanaPluginServiceParams +> = { + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + foo: new PluginServiceProvider(fooServiceFactory), +}; + +export const serviceRegistry = new PluginServiceRegistry< + PresentationUtilServices, + KibanaPluginServiceParams +>(providers); +``` + + +### Service Provider + +A `PluginServiceProvider` is a container for a Service Factory that is responsible for starting, stopping and providing a service implementation. A Service Provider doesn't change, rather the factory and the relevant `StartParameters` change. + +### Service Factories + +A Service Factory is nothing more than a function that uses `StartParameters` to return a set of functions that conforms to a portion of the `Services` specification. For each service, a factory is provided for each environment. + +Given a service definition: + +```ts +export interface PresentationFooService { + getFoo: () => string; + setFoo: (bar: string) => void; +} +``` + +a factory for a stubbed version might look like this: + +```ts +type FooServiceFactory = PluginServiceFactory; + +export const fooServiceFactory: FooServiceFactory = () => ({ + getFoo: () => 'bar', + setFoo: (bar) => { console.log(`${bar} set!`)}, +}); +``` + +and a factory for a Kibana version might look like this: + +```ts +export type FooServiceFactory = KibanaPluginServiceFactory< + PresentationFooService, + PresentationUtilPluginStart +>; + +export const fooServiceFactory: FooServiceFactory = ({ + coreStart, + startPlugins, +}) => { + // ...do something with Kibana services... + + return { + getFoo: //... + setFoo: //... + } +} +``` + +### Using Services + +Once your services and providers are defined, and you have at least one set of factories, you can use `PluginServices` to provide the services to your React components: + + +```ts +// plugin.ts +import { pluginServices } from './services'; +import { registry } from './services/kibana'; + + public async start( + coreStart: CoreStart, + startPlugins: StartDeps + ): Promise { + pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); + return {}; + } +``` + + +and wrap your root React component with the `PluginServices` context: + + +```ts +import { pluginServices } from './services'; + +const ContextProvider = pluginServices.getContextProvider(), + +return( + + + {application} + + +) +``` + + +and then, consume your services using provided hooks in a component: + + +```ts +// component.ts + +import { pluginServices } from '../services'; + +export function MyComponent() { + // Retrieve all context hooks from `PluginServices`, destructuring for the one we're using + const { foo } = pluginServices.getHooks(); + + // Use the `useContext` hook to access the API. + const { getFoo } = foo.useService(); + + // ... +} +``` + diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx new file mode 100644 index 0000000000000..cb9991e216019 --- /dev/null +++ b/src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { DashboardPicker } from './dashboard_picker'; + +export default { + component: DashboardPicker, + title: 'Dashboard Picker', + argTypes: { + isDisabled: { + control: 'boolean', + defaultValue: false, + }, + }, +}; + +export const Example = ({ isDisabled }: { isDisabled: boolean }) => ( + +); diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx index 8aaf9be6ef5c6..b156ef4ae764c 100644 --- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_picker.tsx @@ -6,18 +6,16 @@ * Public License, v 1. */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox } from '@elastic/eui'; -import { SavedObjectsClientContract } from '../../../../core/public'; -import { DashboardSavedObject } from '../../../../plugins/dashboard/public'; +import { pluginServices } from '../services'; export interface DashboardPickerProps { onChange: (dashboard: { name: string; id: string } | null) => void; isDisabled: boolean; - savedObjectsClient: SavedObjectsClientContract; } interface DashboardOption { @@ -26,34 +24,43 @@ interface DashboardOption { } export function DashboardPicker(props: DashboardPickerProps) { - const [dashboards, setDashboards] = useState([]); + const [dashboardOptions, setDashboardOptions] = useState([]); const [isLoadingDashboards, setIsLoadingDashboards] = useState(true); const [selectedDashboard, setSelectedDashboard] = useState(null); + const [query, setQuery] = useState(''); - const { savedObjectsClient, isDisabled, onChange } = props; + const { isDisabled, onChange } = props; + const { dashboards } = pluginServices.getHooks(); + const { findDashboardsByTitle } = dashboards.useService(); - const fetchDashboards = useCallback( - async (query) => { + useEffect(() => { + // We don't want to manipulate the React state if the component has been unmounted + // while we wait for the saved objects to return. + let cleanedUp = false; + + const fetchDashboards = async () => { setIsLoadingDashboards(true); - setDashboards([]); - - const { savedObjects } = await savedObjectsClient.find({ - type: 'dashboard', - search: query ? `${query}*` : '', - searchFields: ['title'], - }); - if (savedObjects) { - setDashboards(savedObjects.map((d) => ({ value: d.id, label: d.attributes.title }))); + setDashboardOptions([]); + + const objects = await findDashboardsByTitle(query ? `${query}*` : ''); + + if (cleanedUp) { + return; + } + + if (objects) { + setDashboardOptions(objects.map((d) => ({ value: d.id, label: d.attributes.title }))); } + setIsLoadingDashboards(false); - }, - [savedObjectsClient] - ); + }; - // Initial dashboard load - useEffect(() => { - fetchDashboards(''); - }, [fetchDashboards]); + fetchDashboards(); + + return () => { + cleanedUp = true; + }; + }, [findDashboardsByTitle, query]); return ( { if (e.length) { @@ -72,7 +79,7 @@ export function DashboardPicker(props: DashboardPickerProps) { onChange(null); } }} - onSearchChange={fetchDashboards} + onSearchChange={setQuery} isDisabled={isDisabled} isLoading={isLoadingDashboards} compressed={true} diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index 58a70c9db7dd5..7c7b12f52ab5f 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -9,18 +9,6 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiRadio, - EuiIconTip, - EuiPanel, - EuiSpacer, -} from '@elastic/eui'; -import { SavedObjectsClientContract } from '../../../../core/public'; import { OnSaveProps, @@ -28,9 +16,9 @@ import { SavedObjectSaveModal, } from '../../../../plugins/saved_objects/public'; -import { DashboardPicker } from './dashboard_picker'; - import './saved_object_save_modal_dashboard.scss'; +import { pluginServices } from '../services'; +import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector'; interface SaveModalDocumentInfo { id?: string; @@ -38,116 +26,50 @@ interface SaveModalDocumentInfo { description?: string; } -export interface DashboardSaveModalProps { +export interface SaveModalDashboardProps { documentInfo: SaveModalDocumentInfo; objectType: string; onClose: () => void; onSave: (props: OnSaveProps & { dashboardId: string | null }) => void; - savedObjectsClient: SavedObjectsClientContract; tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); } -export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) { - const { documentInfo, savedObjectsClient, tagOptions } = props; - const initialCopyOnSave = !Boolean(documentInfo.id); +export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { + const { documentInfo, tagOptions, objectType, onClose } = props; + const { id: documentId } = documentInfo; + const initialCopyOnSave = !Boolean(documentId); + + const { capabilities } = pluginServices.getHooks(); + const { + canAccessDashboards, + canCreateNewDashboards, + canEditDashboards, + } = capabilities.useService(); + + const disableDashboardOptions = + !canAccessDashboards() || (!canCreateNewDashboards && !canEditDashboards); const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>( - documentInfo.id ? null : 'existing' + documentId || disableDashboardOptions ? null : 'existing' ); const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>( null ); const [copyOnSave, setCopyOnSave] = useState(initialCopyOnSave); - const renderDashboardSelect = (state: SaveModalState) => { - const isDisabled = Boolean(!state.copyOnSave && documentInfo.id); - - return ( - <> - - - - - - - } - /> - - - } - hasChildLabel={false} - > - -

- - - - ); - }; + const rightOptions = !disableDashboardOptions + ? () => ( + { + setSelectedDashboard(dash); + }} + onChange={(option) => { + setDashboardOption(option); + }} + {...{ copyOnSave, documentId, dashboardOption }} + /> + ) + : null; const onCopyOnSaveChange = (newCopyOnSave: boolean) => { setDashboardOption(null); @@ -159,7 +81,7 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) { // Don't save with a dashboard ID if we're // just updating an existing visualization - if (!(!onSaveProps.newCopyOnSave && documentInfo.id)) { + if (!(!onSaveProps.newCopyOnSave && documentId)) { if (dashboardOption === 'existing') { dashboardId = selectedDashboard?.id || null; } else { @@ -171,13 +93,14 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) { }; const saveLibraryLabel = - !copyOnSave && documentInfo.id + !copyOnSave && documentId ? i18n.translate('presentationUtil.saveModalDashboard.saveLabel', { defaultMessage: 'Save', }) : i18n.translate('presentationUtil.saveModalDashboard.saveToLibraryLabel', { defaultMessage: 'Save and add to library', }); + const saveDashboardLabel = i18n.translate( 'presentationUtil.saveModalDashboard.saveAndGoToDashboardLabel', { @@ -192,18 +115,20 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) { return ( ); } diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx new file mode 100644 index 0000000000000..2044ecdd713e1 --- /dev/null +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { action } from '@storybook/addon-actions'; + +import { StorybookParams } from '../services/storybook'; +import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector'; + +export default { + component: SaveModalDashboardSelector, + title: 'Save Modal Dashboard Selector', + description: 'A selector for determining where an object will be saved after it is created.', + argTypes: { + hasDocumentId: { + control: 'boolean', + defaultValue: false, + }, + copyOnSave: { + control: 'boolean', + defaultValue: false, + }, + canCreateNewDashboards: { + control: 'boolean', + defaultValue: true, + }, + canEditDashboards: { + control: 'boolean', + defaultValue: true, + }, + }, +}; + +export function Example({ + copyOnSave, + hasDocumentId, +}: { + copyOnSave: boolean; + hasDocumentId: boolean; +} & StorybookParams) { + const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing'); + + return ( + + ); +} diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx new file mode 100644 index 0000000000000..b1bf9ed695842 --- /dev/null +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiRadio, + EuiIconTip, + EuiPanel, + EuiSpacer, +} from '@elastic/eui'; + +import { pluginServices } from '../services'; +import { DashboardPicker, DashboardPickerProps } from './dashboard_picker'; + +import './saved_object_save_modal_dashboard.scss'; + +export interface SaveModalDashboardSelectorProps { + copyOnSave: boolean; + documentId?: string; + onSelectDashboard: DashboardPickerProps['onChange']; + + dashboardOption: 'new' | 'existing' | null; + onChange: (dashboardOption: 'new' | 'existing' | null) => void; +} + +export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProps) { + const { documentId, onSelectDashboard, dashboardOption, onChange, copyOnSave } = props; + const { capabilities } = pluginServices.getHooks(); + const { canCreateNewDashboards, canEditDashboards } = capabilities.useService(); + + const isDisabled = !copyOnSave && !!documentId; + + return ( + <> + + + + + + + } + /> + + + } + hasChildLabel={false} + > + +
+ {canEditDashboards() && ( + <> + {' '} + onChange('existing')} + disabled={isDisabled} + /> +
+ +
+ + + )} + {canCreateNewDashboards() && ( + <> + {' '} + onChange('new')} + disabled={isDisabled} + /> + + + )} + onChange(null)} + disabled={isDisabled} + /> +
+
+
+ + ); +} diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index baf40a1ea0ae4..586ddd1320641 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -10,9 +10,11 @@ import { PresentationUtilPlugin } from './plugin'; export { SavedObjectSaveModalDashboard, - DashboardSaveModalProps, + SaveModalDashboardProps, } from './components/saved_object_save_modal_dashboard'; +export { DashboardPicker } from './components/dashboard_picker'; + export function plugin() { return new PresentationUtilPlugin(); } diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index cbc1d0eb04e27..5d3618b034656 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -7,16 +7,39 @@ */ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; +import { pluginServices } from './services'; +import { registry } from './services/kibana'; +import { + PresentationUtilPluginSetup, + PresentationUtilPluginStart, + PresentationUtilPluginSetupDeps, + PresentationUtilPluginStartDeps, +} from './types'; export class PresentationUtilPlugin - implements Plugin { - public setup(core: CoreSetup): PresentationUtilPluginSetup { + implements + Plugin< + PresentationUtilPluginSetup, + PresentationUtilPluginStart, + PresentationUtilPluginSetupDeps, + PresentationUtilPluginStartDeps + > { + public setup( + _coreSetup: CoreSetup, + _setupPlugins: PresentationUtilPluginSetupDeps + ): PresentationUtilPluginSetup { return {}; } - public start(core: CoreStart): PresentationUtilPluginStart { - return {}; + public async start( + coreStart: CoreStart, + startPlugins: PresentationUtilPluginStartDeps + ): Promise { + pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); + + return { + ContextProvider: pluginServices.getContextProvider(), + }; } public stop() {} diff --git a/src/plugins/presentation_util/public/services/create/factory.ts b/src/plugins/presentation_util/public/services/create/factory.ts new file mode 100644 index 0000000000000..01b143e612461 --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/factory.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { BehaviorSubject } from 'rxjs'; +import { CoreStart, AppUpdater } from 'src/core/public'; + +/** + * A factory function for creating a service. + * + * The `Service` generic determines the shape of the API being produced. + * The `StartParameters` generic determines what parameters are expected to + * create the service. + */ +export type PluginServiceFactory = (params: Parameters) => Service; + +/** + * Parameters necessary to create a Kibana-based service, (e.g. during Plugin + * startup or setup). + * + * The `Start` generic refers to the specific Plugin `TPluginsStart`. + */ +export interface KibanaPluginServiceParams { + coreStart: CoreStart; + startPlugins: Start; + appUpdater?: BehaviorSubject; +} + +/** + * A factory function for creating a Kibana-based service. + * + * The `Service` generic determines the shape of the API being produced. + * The `Setup` generic refers to the specific Plugin `TPluginsSetup`. + * The `Start` generic refers to the specific Plugin `TPluginsStart`. + */ +export type KibanaPluginServiceFactory = ( + params: KibanaPluginServiceParams +) => Service; diff --git a/src/plugins/presentation_util/public/services/create/index.ts b/src/plugins/presentation_util/public/services/create/index.ts new file mode 100644 index 0000000000000..59f1f9fd7a43b --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/index.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { mapValues } from 'lodash'; + +import { PluginServiceRegistry } from './registry'; + +export { PluginServiceRegistry } from './registry'; +export { PluginServiceProvider, PluginServiceProviders } from './provider'; +export { + PluginServiceFactory, + KibanaPluginServiceFactory, + KibanaPluginServiceParams, +} from './factory'; + +/** + * `PluginServices` is a top-level class for specifying and accessing services within a plugin. + * + * A `PluginServices` object can be provided with a `PluginServiceRegistry` at any time, which will + * then be used to provide services to any component that accesses it. + * + * The `Services` generic determines the shape of all service APIs being produced. + */ +export class PluginServices { + private registry: PluginServiceRegistry | null = null; + + /** + * Supply a `PluginServiceRegistry` for the class to use to provide services and context. + * + * @param registry A setup and started `PluginServiceRegistry`. + */ + setRegistry(registry: PluginServiceRegistry | null) { + if (registry && !registry.isStarted()) { + throw new Error('Registry has not been started.'); + } + + this.registry = registry; + } + + /** + * Returns true if a registry has been provided, false otherwise. + */ + hasRegistry() { + return !!this.registry; + } + + /** + * Private getter that will enforce proper setup throughout the class. + */ + private getRegistry() { + if (!this.registry) { + throw new Error('No registry has been provided.'); + } + + return this.registry; + } + + /** + * Return the React Context Provider that will supply services. + */ + getContextProvider() { + return this.getRegistry().getContextProvider(); + } + + /** + * Return a map of React Hooks that can be used in React components. + */ + getHooks(): { [K in keyof Services]: { useService: () => Services[K] } } { + const registry = this.getRegistry(); + const providers = registry.getServiceProviders(); + + // @ts-expect-error Need to fix this; the type isn't fully understood when inferred. + return mapValues(providers, (provider) => ({ + useService: provider.getUseServiceHook(), + })); + } +} diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx new file mode 100644 index 0000000000000..981ff1527f981 --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/provider.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { createContext, useContext } from 'react'; +import { PluginServiceFactory } from './factory'; + +/** + * A collection of `PluginServiceProvider` objects, keyed by the `Services` API generic. + * + * The `Services` generic determines the shape of all service APIs being produced. + * The `StartParameters` generic determines what parameters are expected to + * start the service. + */ +export type PluginServiceProviders = { + [K in keyof Services]: PluginServiceProvider; +}; + +/** + * An object which uses a given factory to start, stop or provide a service. + * + * The `Service` generic determines the shape of the API being produced. + * The `StartParameters` generic determines what parameters are expected to + * start the service. + */ +export class PluginServiceProvider { + private factory: PluginServiceFactory; + private context = createContext(null); + private pluginService: Service | null = null; + public readonly Provider: React.FC = ({ children }) => { + return {children}; + }; + + constructor(factory: PluginServiceFactory) { + this.factory = factory; + this.context.displayName = 'PluginServiceContext'; + } + + /** + * Private getter that will enforce proper setup throughout the class. + */ + private getService() { + if (!this.pluginService) { + throw new Error('Service not started'); + } + return this.pluginService; + } + + /** + * Start the service. + * + * @param params Parameters used to start the service. + */ + start(params: StartParameters) { + this.pluginService = this.factory(params); + } + + /** + * Returns a function for providing a Context hook for the service. + */ + getUseServiceHook() { + return () => { + const service = useContext(this.context); + + if (!service) { + throw new Error('Provider is not set up correctly'); + } + + return service; + }; + } + + /** + * Stop the service. + */ + stop() { + this.pluginService = null; + } +} diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx new file mode 100644 index 0000000000000..5165380780fa9 --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/registry.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { values } from 'lodash'; +import { PluginServiceProvider, PluginServiceProviders } from './provider'; + +/** + * A `PluginServiceRegistry` maintains a set of service providers which can be collectively + * started, stopped or retreived. + * + * The `Services` generic determines the shape of all service APIs being produced. + * The `StartParameters` generic determines what parameters are expected to + * start the service. + */ +export class PluginServiceRegistry { + private providers: PluginServiceProviders; + private _isStarted = false; + + constructor(providers: PluginServiceProviders) { + this.providers = providers; + } + + /** + * Returns true if the registry has been started, false otherwise. + */ + isStarted() { + return this._isStarted; + } + + /** + * Returns a map of `PluginServiceProvider` objects. + */ + getServiceProviders() { + if (!this._isStarted) { + throw new Error('Registry not started'); + } + return this.providers; + } + + /** + * Returns a React Context Provider for use in consuming applications. + */ + getContextProvider() { + // Collect and combine Context.Provider elements from each Service Provider into a single + // Functional Component. + const provider: React.FC = ({ children }) => ( + <> + {values>(this.getServiceProviders()).reduceRight( + (acc, serviceProvider) => { + return {acc}; + }, + children + )} + + ); + + return provider; + } + + /** + * Start the registry. + * + * @param params Parameters used to start the registry. + */ + start(params: StartParameters) { + values>(this.providers).map((serviceProvider) => + serviceProvider.start(params) + ); + this._isStarted = true; + return this; + } + + /** + * Stop the registry. + */ + stop() { + values>(this.providers).map((serviceProvider) => + serviceProvider.stop() + ); + this._isStarted = false; + return this; + } +} diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts new file mode 100644 index 0000000000000..732cc19e14763 --- /dev/null +++ b/src/plugins/presentation_util/public/services/index.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SimpleSavedObject } from 'src/core/public'; +import { DashboardSavedObject } from 'src/plugins/dashboard/public'; +import { PluginServices } from './create'; +export interface PresentationDashboardsService { + findDashboards: ( + query: string, + fields: string[] + ) => Promise>>; + findDashboardsByTitle: (title: string) => Promise>>; +} + +export interface PresentationCapabilitiesService { + canAccessDashboards: () => boolean; + canCreateNewDashboards: () => boolean; + canEditDashboards: () => boolean; +} + +export interface PresentationUtilServices { + dashboards: PresentationDashboardsService; + capabilities: PresentationCapabilitiesService; +} + +export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts new file mode 100644 index 0000000000000..f36b277979358 --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { PresentationUtilPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../create'; +import { PresentationCapabilitiesService } from '..'; + +export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< + PresentationCapabilitiesService, + PresentationUtilPluginStartDeps +>; + +export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => { + const { dashboard } = coreStart.application.capabilities; + + return { + canAccessDashboards: () => Boolean(dashboard.show), + canCreateNewDashboards: () => Boolean(dashboard.createNew), + canEditDashboards: () => !Boolean(dashboard.hideWriteControls), + }; +}; diff --git a/src/plugins/presentation_util/public/services/kibana/dashboards.ts b/src/plugins/presentation_util/public/services/kibana/dashboards.ts new file mode 100644 index 0000000000000..acfe4bd33e26a --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana/dashboards.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { DashboardSavedObject } from 'src/plugins/dashboard/public'; + +import { PresentationUtilPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../create'; +import { PresentationDashboardsService } from '..'; + +export type DashboardsServiceFactory = KibanaPluginServiceFactory< + PresentationDashboardsService, + PresentationUtilPluginStartDeps +>; + +export const dashboardsServiceFactory: DashboardsServiceFactory = ({ coreStart }) => { + const findDashboards = async (query: string = '', fields: string[] = []) => { + const { find } = coreStart.savedObjects.client; + + const { savedObjects } = await find({ + type: 'dashboard', + search: `${query}*`, + searchFields: fields, + }); + + return savedObjects; + }; + + const findDashboardsByTitle = async (title: string = '') => findDashboards(title, ['title']); + + return { + findDashboards, + findDashboardsByTitle, + }; +}; diff --git a/src/plugins/presentation_util/public/services/kibana/index.ts b/src/plugins/presentation_util/public/services/kibana/index.ts new file mode 100644 index 0000000000000..a129b0d94479f --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { dashboardsServiceFactory } from './dashboards'; +import { capabilitiesServiceFactory } from './capabilities'; +import { + PluginServiceProviders, + KibanaPluginServiceParams, + PluginServiceProvider, + PluginServiceRegistry, +} from '../create'; +import { PresentationUtilPluginStartDeps } from '../../types'; +import { PresentationUtilServices } from '..'; + +export { dashboardsServiceFactory } from './dashboards'; +export { capabilitiesServiceFactory } from './capabilities'; + +export const providers: PluginServiceProviders< + PresentationUtilServices, + KibanaPluginServiceParams +> = { + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + capabilities: new PluginServiceProvider(capabilitiesServiceFactory), +}; + +export const registry = new PluginServiceRegistry< + PresentationUtilServices, + KibanaPluginServiceParams +>(providers); diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts new file mode 100644 index 0000000000000..5048fe50cc025 --- /dev/null +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { PluginServiceFactory } from '../create'; +import { StorybookParams } from '.'; +import { PresentationCapabilitiesService } from '..'; + +type CapabilitiesServiceFactory = PluginServiceFactory< + PresentationCapabilitiesService, + StorybookParams +>; + +export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ + canAccessDashboards, + canCreateNewDashboards, + canEditDashboards, +}) => { + const check = (value: boolean = true) => value; + return { + canAccessDashboards: () => check(canAccessDashboards), + canCreateNewDashboards: () => check(canCreateNewDashboards), + canEditDashboards: () => check(canEditDashboards), + }; +}; diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts new file mode 100644 index 0000000000000..536cad3a9d131 --- /dev/null +++ b/src/plugins/presentation_util/public/services/storybook/index.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { PluginServices, PluginServiceProviders, PluginServiceProvider } from '../create'; +import { dashboardsServiceFactory } from '../stub/dashboards'; +import { capabilitiesServiceFactory } from './capabilities'; +import { PresentationUtilServices } from '..'; + +export { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; +export { PresentationUtilServices } from '..'; + +export interface StorybookParams { + canAccessDashboards?: boolean; + canCreateNewDashboards?: boolean; + canEditDashboards?: boolean; +} + +export const providers: PluginServiceProviders = { + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + capabilities: new PluginServiceProvider(capabilitiesServiceFactory), +}; + +export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts new file mode 100644 index 0000000000000..33c091022421c --- /dev/null +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { PluginServiceFactory } from '../create'; +import { PresentationCapabilitiesService } from '..'; + +type CapabilitiesServiceFactory = PluginServiceFactory; + +export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({ + canAccessDashboards: () => true, + canCreateNewDashboards: () => true, + canEditDashboards: () => true, +}); diff --git a/src/plugins/presentation_util/public/services/stub/dashboards.ts b/src/plugins/presentation_util/public/services/stub/dashboards.ts new file mode 100644 index 0000000000000..862fa4f952c1e --- /dev/null +++ b/src/plugins/presentation_util/public/services/stub/dashboards.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { PluginServiceFactory } from '../create'; +import { PresentationDashboardsService } from '..'; + +// TODO (clint): Create set of dashboards to stub and return. + +type DashboardsServiceFactory = PluginServiceFactory; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export const dashboardsServiceFactory: DashboardsServiceFactory = () => ({ + findDashboards: async (query: string = '', _fields: string[] = []) => { + if (!query) { + return []; + } + + await sleep(2000); + return []; + }, + findDashboardsByTitle: async (title: string) => { + if (!title) { + return []; + } + + await sleep(2000); + return []; + }, +}); diff --git a/src/plugins/presentation_util/public/services/stub/index.ts b/src/plugins/presentation_util/public/services/stub/index.ts new file mode 100644 index 0000000000000..a2bde357fd4c0 --- /dev/null +++ b/src/plugins/presentation_util/public/services/stub/index.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { dashboardsServiceFactory } from './dashboards'; +import { capabilitiesServiceFactory } from './capabilities'; +import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; +import { PresentationUtilServices } from '..'; + +export { dashboardsServiceFactory } from './dashboards'; +export { capabilitiesServiceFactory } from './capabilities'; + +export const providers: PluginServiceProviders = { + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + capabilities: new PluginServiceProvider(capabilitiesServiceFactory), +}; + +export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/presentation_util/public/types.ts b/src/plugins/presentation_util/public/types.ts index ae5646bd9bbae..7371ebc6f736e 100644 --- a/src/plugins/presentation_util/public/types.ts +++ b/src/plugins/presentation_util/public/types.ts @@ -8,5 +8,12 @@ // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PresentationUtilPluginSetup {} + +export interface PresentationUtilPluginStart { + ContextProvider: React.FC; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PresentationUtilPluginSetupDeps {} // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PresentationUtilPluginStart {} +export interface PresentationUtilPluginStartDeps {} diff --git a/src/plugins/presentation_util/storybook/decorator.tsx b/src/plugins/presentation_util/storybook/decorator.tsx new file mode 100644 index 0000000000000..5f56c70a2f849 --- /dev/null +++ b/src/plugins/presentation_util/storybook/decorator.tsx @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; + +import { DecoratorFn } from '@storybook/react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { pluginServices } from '../public/services'; +import { PresentationUtilServices } from '../public/services'; +import { providers, StorybookParams } from '../public/services/storybook'; +import { PluginServiceRegistry } from '../public/services/create'; + +export const servicesContextDecorator: DecoratorFn = (story: Function, storybook) => { + const registry = new PluginServiceRegistry(providers); + pluginServices.setRegistry(registry.start(storybook.args)); + const ContextProvider = pluginServices.getContextProvider(); + + return ( + + {story()} + + ); +}; diff --git a/src/plugins/presentation_util/storybook/main.ts b/src/plugins/presentation_util/storybook/main.ts new file mode 100644 index 0000000000000..d12b98f38a03f --- /dev/null +++ b/src/plugins/presentation_util/storybook/main.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Configuration } from 'webpack'; +import { defaultConfig } from '@kbn/storybook'; +import webpackConfig from '@kbn/storybook/target/webpack.config'; + +module.exports = { + ...defaultConfig, + addons: ['@storybook/addon-essentials'], + webpackFinal: (config: Configuration) => { + return webpackConfig({ config }); + }, +}; diff --git a/src/plugins/presentation_util/storybook/manager.ts b/src/plugins/presentation_util/storybook/manager.ts new file mode 100644 index 0000000000000..e9b6a11242036 --- /dev/null +++ b/src/plugins/presentation_util/storybook/manager.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { addons } from '@storybook/addons'; +import { create } from '@storybook/theming'; +import { PANEL_ID } from '@storybook/addon-actions'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Kibana Presentation Utility Storybook', + brandUrl: 'https://github.com/elastic/kibana/tree/master/src/plugins/presentation_util', + }), + showPanel: true.valueOf, + selectedPanel: PANEL_ID, +}); diff --git a/src/plugins/presentation_util/storybook/preview.tsx b/src/plugins/presentation_util/storybook/preview.tsx new file mode 100644 index 0000000000000..dfa8ad3be04e7 --- /dev/null +++ b/src/plugins/presentation_util/storybook/preview.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { addDecorator } from '@storybook/react'; +import { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks'; + +import { servicesContextDecorator } from './decorator'; + +addDecorator(servicesContextDecorator); + +export const parameters = { + docs: { + page: () => ( + <> + + <Subtitle /> + <Description /> + <Primary /> + <Stories /> + </> + ), + }, +}; diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index 1e3756f45e953..a9657db288848 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -7,7 +7,7 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*"], + "include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"], "references": [ { "path": "../../core/tsconfig.json" }, { "path": "../dashboard/tsconfig.json" }, diff --git a/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx index 6702255ee2e2c..f87169d4b828a 100644 --- a/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx @@ -31,7 +31,8 @@ interface MinimalSaveModalProps { export function showSaveModal( saveModal: React.ReactElement<MinimalSaveModalProps>, - I18nContext: I18nStart['Context'] + I18nContext: I18nStart['Context'], + Wrapper?: React.FC ) { const container = document.createElement('div'); const closeModal = () => { @@ -55,5 +56,13 @@ export function showSaveModal( onClose: closeModal, }); - ReactDOM.render(<I18nContext>{element}</I18nContext>, container); + const wrappedElement = Wrapper ? ( + <I18nContext> + <Wrapper>{element}</Wrapper> + </I18nContext> + ) : ( + <I18nContext>{element}</I18nContext> + ); + + ReactDOM.render(wrappedElement, container); } diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index 7f5c7d0dc08a2..2256a7a7f550d 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -11,7 +11,8 @@ "visualizations", "embeddable", "dashboard", - "uiActions" + "uiActions", + "presentationUtil" ], "optionalPlugins": [ "home", @@ -22,7 +23,6 @@ "kibanaUtils", "kibanaReact", "home", - "presentationUtil", "discover" ] } diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index b0d931c6c87fa..02da16c9e67ca 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -69,7 +69,6 @@ const TopNav = ({ }, [visInstance.embeddableHandler] ); - const savedObjectsClient = services.savedObjects.client; const config = useMemo(() => { if (isEmbeddableRendered) { @@ -85,7 +84,6 @@ const TopNav = ({ stateContainer, visualizationIdFromUrl, stateTransfer: services.stateTransferService, - savedObjectsClient, embeddableId, }, services @@ -104,7 +102,6 @@ const TopNav = ({ visualizationIdFromUrl, services, embeddableId, - savedObjectsClient, ]); const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>( vis.data.indexPattern ? [vis.data.indexPattern] : [] diff --git a/src/plugins/visualize/public/application/index.tsx b/src/plugins/visualize/public/application/index.tsx index 455e51a8f58d4..ae11e1de486ea 100644 --- a/src/plugins/visualize/public/application/index.tsx +++ b/src/plugins/visualize/public/application/index.tsx @@ -30,9 +30,11 @@ export const renderApp = ( const app = ( <Router history={services.history}> <KibanaContextProvider services={services}> - <services.i18n.Context> - <VisualizeApp onAppLeave={onAppLeave} /> - </services.i18n.Context> + <services.presentationUtil.ContextProvider> + <services.i18n.Context> + <VisualizeApp onAppLeave={onAppLeave} /> + </services.i18n.Context> + </services.presentationUtil.ContextProvider> </KibanaContextProvider> </Router> ); diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index d923851a68d9c..5d884889367bc 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -34,6 +34,7 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; +import { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public'; import { EventEmitter } from 'events'; import { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; @@ -93,6 +94,7 @@ export interface VisualizeServices extends CoreStart { dashboard: DashboardStart; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedObjectsTagging?: SavedObjectsTaggingApi; + presentationUtil: PresentationUtilPluginStart; } export interface SavedVisInstance { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index b4ac98b672ee9..d782937bce40a 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -20,7 +20,6 @@ import { } from '../../../../saved_objects/public'; import { SavedObjectSaveModalDashboard } from '../../../../presentation_util/public'; import { unhashUrl } from '../../../../kibana_utils/public'; -import { SavedObjectsClientContract } from '../../../../../core/public'; import { VisualizeServices, @@ -50,7 +49,6 @@ interface TopNavConfigParams { stateContainer: VisualizeAppStateContainer; visualizationIdFromUrl?: string; stateTransfer: EmbeddableStateTransfer; - savedObjectsClient: SavedObjectsClientContract; embeddableId?: string; } @@ -72,7 +70,6 @@ export const getTopNavConfig = ( hasUnappliedChanges, visInstance, stateContainer, - savedObjectsClient, visualizationIdFromUrl, stateTransfer, embeddableId, @@ -88,6 +85,7 @@ export const getTopNavConfig = ( i18n: { Context: I18nContext }, dashboard, savedObjectsTagging, + presentationUtil, }: VisualizeServices ) => { const { vis, embeddableHandler } = visInstance; @@ -397,39 +395,43 @@ export const getTopNavConfig = ( ); } - const saveModal = - !!originatingApp || - !dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables ? ( - <SavedObjectSaveModalOrigin - documentInfo={savedVis || { title: '' }} - onSave={onSave} - options={tagOptions} - getAppNameFromId={stateTransfer.getAppNameFromId} - objectType={'visualization'} - onClose={() => {}} - originatingApp={originatingApp} - returnToOriginSwitchLabel={ - originatingApp && embeddableId - ? i18n.translate('visualize.topNavMenu.updatePanel', { - defaultMessage: 'Update panel on {originatingAppName}', - values: { - originatingAppName: stateTransfer.getAppNameFromId(originatingApp), - }, - }) - : undefined - } - /> - ) : ( - <SavedObjectSaveModalDashboard - documentInfo={savedVis || { title: '' }} - onSave={onSave} - tagOptions={tagOptions} - objectType={'visualization'} - onClose={() => {}} - savedObjectsClient={savedObjectsClient} - /> - ); - showSaveModal(saveModal, I18nContext); + const useByRefFlow = + !!originatingApp || !dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; + + const saveModal = useByRefFlow ? ( + <SavedObjectSaveModalOrigin + documentInfo={savedVis || { title: '' }} + onSave={onSave} + options={tagOptions} + getAppNameFromId={stateTransfer.getAppNameFromId} + objectType={'visualization'} + onClose={() => {}} + originatingApp={originatingApp} + returnToOriginSwitchLabel={ + originatingApp && embeddableId + ? i18n.translate('visualize.topNavMenu.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { + originatingAppName: stateTransfer.getAppNameFromId(originatingApp), + }, + }) + : undefined + } + /> + ) : ( + <SavedObjectSaveModalDashboard + documentInfo={savedVis || { title: '' }} + onSave={onSave} + tagOptions={tagOptions} + objectType={'visualization'} + onClose={() => {}} + /> + ); + showSaveModal( + saveModal, + I18nContext, + !useByRefFlow ? presentationUtil.ContextProvider : React.Fragment + ); }, }, ] diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 111ee7b0041ed..8d02e08549663 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -20,6 +20,7 @@ import { ScopedHistory, } from 'kibana/public'; +import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { Storage, createKbnUrlTracker, @@ -62,6 +63,7 @@ export interface VisualizePluginStartDependencies { savedObjects: SavedObjectsStart; dashboard: DashboardStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; + presentationUtil: PresentationUtilPluginStart; } export interface VisualizePluginSetupDependencies { @@ -204,6 +206,7 @@ export class VisualizePlugin dashboard: pluginsStart.dashboard, setHeaderActionMenu: params.setHeaderActionMenu, savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(), + presentationUtil: pluginsStart.presentationUtil, }; params.element.classList.add('visAppWrapper'); diff --git a/typings/index.d.ts b/typings/index.d.ts index 782cc4271a06b..8223d85d53289 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -23,3 +23,10 @@ declare module '*.svg' { // eslint-disable-next-line import/no-default-export export default content; } + +// Storybook references this module. It's @ts-ignored in the codebase but when +// built into its dist it strips that out. Add it here to avoid a type checking +// error. +// +// See https://github.com/storybookjs/storybook/issues/11684 +declare module 'react-syntax-highlighter/dist/cjs/create-element'; diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 9df3f41fbd855..d473d728dc361 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -14,10 +14,26 @@ "dashboard", "uiActions", "embeddable", - "share" + "share", + "presentationUtil" ], - "optionalPlugins": ["usageCollection", "taskManager", "globalSearch", "savedObjectsTagging"], - "configPath": ["xpack", "lens"], - "extraPublicDirs": ["common/constants"], - "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "presentationUtil"] + "optionalPlugins": [ + "usageCollection", + "taskManager", + "globalSearch", + "savedObjectsTagging" + ], + "configPath": [ + "xpack", + "lens" + ], + "extraPublicDirs": [ + "common/constants" + ], + "requiredBundles": [ + "savedObjects", + "kibanaUtils", + "kibanaReact", + "embeddable" + ] } diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 28e1f6da60742..c7764684029c7 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -707,7 +707,6 @@ export function App({ isVisible={state.isSaveModalVisible} originatingApp={state.isLinkedToOriginatingApp ? incomingState?.originatingApp : undefined} allowByValueEmbeddables={dashboardFeatureFlag.allowByValueEmbeddables} - savedObjectsClient={savedObjectsClient} savedObjectsTagging={savedObjectsTagging} tagsIds={tagsIds} onSave={runSave} diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index e769e402ff0e1..c4961b80c5122 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -3,7 +3,7 @@ * 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 React, { FC, useCallback } from 'react'; import { AppMountParameters, CoreSetup } from 'kibana/public'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; @@ -39,9 +39,15 @@ export async function mountApp( createEditorFrame: EditorFrameStart['createInstance']; getByValueFeatureFlag: () => Promise<DashboardFeatureFlagConfig>; attributeService: () => Promise<LensAttributeService>; + getPresentationUtilContext: () => Promise<FC>; } ) { - const { createEditorFrame, getByValueFeatureFlag, attributeService } = mountProps; + const { + createEditorFrame, + getByValueFeatureFlag, + attributeService, + getPresentationUtilContext, + } = mountProps; const [coreStart, startDependencies] = await core.getStartServices(); const { data, navigation, embeddable, savedObjectsTagging } = startDependencies; @@ -196,21 +202,26 @@ export async function mountApp( }); params.element.classList.add('lnsAppWrapper'); + + const PresentationUtilContext = await getPresentationUtilContext(); + render( <I18nProvider> <KibanaContextProvider services={lensServices}> - <HashRouter> - <Switch> - <Route exact path="/edit/:id" component={EditorRoute} /> - <Route - exact - path={`/${LENS_EDIT_BY_VALUE}`} - render={(routeProps) => <EditorRoute {...routeProps} editByValue />} - /> - <Route exact path="/" component={EditorRoute} /> - <Route path="/" component={NotFound} /> - </Switch> - </HashRouter> + <PresentationUtilContext> + <HashRouter> + <Switch> + <Route exact path="/edit/:id" component={EditorRoute} /> + <Route + exact + path={`/${LENS_EDIT_BY_VALUE}`} + render={(routeProps) => <EditorRoute {...routeProps} editByValue />} + /> + <Route exact path="/" component={EditorRoute} /> + <Route path="/" component={NotFound} /> + </Switch> + </HashRouter> + </PresentationUtilContext> </KibanaContextProvider> </I18nProvider>, params.element diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal.tsx index 4fa35bd914889..a3ac7322db31f 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal.tsx @@ -7,8 +7,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { SavedObjectsStart } from '../../../../../src/core/public'; - import { Document } from '../persistence'; import type { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public'; @@ -29,8 +27,6 @@ export interface Props { originatingApp?: string; allowByValueEmbeddables: boolean; - savedObjectsClient: SavedObjectsStart['client']; - savedObjectsTagging?: SavedObjectTaggingPluginStart; tagsIds: string[]; @@ -51,7 +47,6 @@ export const SaveModal = (props: Props) => { const { originatingApp, savedObjectsTagging, - savedObjectsClient, tagsIds, lastKnownDoc, allowByValueEmbeddables, @@ -88,7 +83,6 @@ export const SaveModal = (props: Props) => { return ( <TagEnhancedSavedObjectSaveModalDashboard savedObjectsTagging={savedObjectsTagging} - savedObjectsClient={savedObjectsClient} initialTags={tagsIds} onSave={(saveProps) => { const saveToLibrary = saveProps.dashboardId === null; diff --git a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx index 087cfdc9f3a8a..b191b8829347c 100644 --- a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx +++ b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx @@ -7,7 +7,7 @@ import React, { FC, useState, useMemo, useCallback } from 'react'; import { OnSaveProps } from '../../../../../src/plugins/saved_objects/public'; import { - DashboardSaveModalProps, + SaveModalDashboardProps, SavedObjectSaveModalDashboard, } from '../../../../../src/plugins/presentation_util/public'; import { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public'; @@ -19,7 +19,7 @@ export type DashboardSaveProps = OnSaveProps & { }; export type TagEnhancedSavedObjectSaveModalDashboardProps = Omit< - DashboardSaveModalProps, + SaveModalDashboardProps, 'onSave' > & { initialTags: string[]; @@ -48,7 +48,7 @@ export const TagEnhancedSavedObjectSaveModalDashboard: FC<TagEnhancedSavedObject const tagEnhancedOptions = <>{tagSelectorOption}</>; - const tagEnhancedOnSave: DashboardSaveModalProps['onSave'] = useCallback( + const tagEnhancedOnSave: SaveModalDashboardProps['onSave'] = useCallback( (saveOptions) => { onSave({ ...saveOptions, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 3fb7186aeac59..9848551e7873f 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -17,6 +17,7 @@ import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/ import { UrlForwardingSetup } from '../../../../src/plugins/url_forwarding/public'; import { GlobalSearchPluginSetup } from '../../global_search/public'; import { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; import { EditorFrameService } from './editor_frame_service'; import { @@ -71,6 +72,7 @@ export interface LensPluginStartDependencies { embeddable: EmbeddableStart; charts: ChartsPluginStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; + presentationUtil: PresentationUtilPluginStart; } export interface LensPublicStart { @@ -172,6 +174,12 @@ export class LensPlugin { return deps.dashboard.dashboardFeatureFlagConfig; }; + const getPresentationUtilContext = async () => { + const [, deps] = await core.getStartServices(); + const { ContextProvider } = deps.presentationUtil; + return ContextProvider; + }; + core.application.register({ id: 'lens', title: NOT_INTERNATIONALIZED_PRODUCT_NAME, @@ -183,6 +191,7 @@ export class LensPlugin { createEditorFrame: this.createEditorFrame!, attributeService: this.attributeService!, getByValueFeatureFlag, + getPresentationUtilContext, }); }, }); diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 2536601d0e6b1..744cc18c36f3e 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -2,7 +2,10 @@ "id": "maps", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "maps"], + "configPath": [ + "xpack", + "maps" + ], "requiredPlugins": [ "licensing", "features", @@ -17,11 +20,21 @@ "mapsLegacy", "usageCollection", "savedObjects", - "share" + "share", + "presentationUtil" + ], + "optionalPlugins": [ + "home", + "savedObjectsTagging" ], - "optionalPlugins": ["home", "savedObjectsTagging"], "ui": true, "server": true, - "extraPublicDirs": ["common/constants"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "presentationUtil"] + "extraPublicDirs": [ + "common/constants" + ], + "requiredBundles": [ + "kibanaReact", + "kibanaUtils", + "home" + ] } diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 99c9311a2a454..56e342a95be51 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -49,6 +49,7 @@ export const getSearchService = () => pluginsStart.data.search; export const getEmbeddableService = () => pluginsStart.embeddable; export const getNavigateToApp = () => coreStart.application.navigateToApp; export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; +export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider; // xpack.maps.* kibana.yml settings from this plugin let mapAppConfig: MapsConfigType; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 4173328a41d57..5bd0bd7346ab1 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -55,6 +55,7 @@ import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { StartContract as FileUploadStartContract } from '../../maps_file_upload/public'; import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; +import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getIsEnterprisePlus, registerLicensedFeatures, @@ -86,6 +87,7 @@ export interface MapsPluginStartDependencies { savedObjects: SavedObjectsStart; dashboard: DashboardStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; + presentationUtil: PresentationUtilPluginStart; } /** diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 7010c281d24c6..803b9defe9a24 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -16,6 +16,7 @@ import { getSavedObjectsClient, getCoreOverlays, getSavedObjectsTagging, + getPresentationUtilContext, } from '../../kibana_services'; import { checkForDuplicateTitle, @@ -185,7 +186,7 @@ export function getTopNavConfig({ defaultMessage: 'map', }), }; - + const PresentationUtilContext = getPresentationUtilContext(); const saveModal = savedMap.getOriginatingApp() || !getIsAllowByValueEmbeddables() ? ( <SavedObjectSaveModalOrigin @@ -195,14 +196,10 @@ export function getTopNavConfig({ options={tagSelector} /> ) : ( - <SavedObjectSaveModalDashboard - {...saveModalProps} - savedObjectsClient={getSavedObjectsClient()} - tagOptions={tagSelector} - /> + <SavedObjectSaveModalDashboard {...saveModalProps} tagOptions={tagSelector} /> ); - showSaveModal(saveModal, getCoreI18n().Context); + showSaveModal(saveModal, getCoreI18n().Context, PresentationUtilContext); }, }); diff --git a/yarn.lock b/yarn.lock index befb729569945..1b8cc2f8dc6e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4445,7 +4445,7 @@ core-js "^3.0.1" ts-dedent "^1.1.1" -"@storybook/addon-docs@6.0.26": +"@storybook/addon-docs@6.0.26", "@storybook/addon-docs@^6.0.26": version "6.0.26" resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.0.26.tgz#bd7fc1fcdc47bb7992fa8d3254367e8c3bba373d" integrity sha512-3t8AOPkp8ZW74h7FnzxF3wAeb1wRyYjMmgJZxqzgi/x7K0i1inbCq8MuJnytuTcZ7+EK4HR6Ih7o9tJuAtIBLw== From da6501973f112a3d5d0ffd238ca0a8f3b6032863 Mon Sep 17 00:00:00 2001 From: Tim Sullivan <tsullivan@users.noreply.github.com> Date: Thu, 28 Jan 2021 16:33:21 -0700 Subject: [PATCH 102/163] Update README.md --- x-pack/build_chromium/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md index 51c034e510024..7c81e46318a1c 100644 --- a/x-pack/build_chromium/README.md +++ b/x-pack/build_chromium/README.md @@ -15,7 +15,7 @@ gain familiarity. 2. Click the "Compute Engine" tab. 3. Ensure that `chromium-build-linux` and `chromium-build-windows-12-beefy` are there. 4. If #3 fails, you'll have to spin up new instances. Generally, these need `n1-standard-8` types or 8 vCPUs/30 GB memory. -5. Ensure that there's enough room left on the disk. `ncdu` is a good linux util to verify what's claming space. +5. Ensure that there's enough room left on the disk: 100GB is required. `ncdu` is a good linux util to verify what's claming space. ## Usage From 67014a7970624ebf51be050e16428023b8995247 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger <scotty.bollinger@elastic.co> Date: Thu, 28 Jan 2021 18:14:04 -0600 Subject: [PATCH 103/163] [Enterprise Search] Update apps to use a service for docs links (#89425) * Create DocLinksService * Set docLinks on app start * Update routes modules to use service * Update component and test to use service * Remove legacy files * Add comment Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Add new line Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Refactor test * Rename class and remove extra route segments * Update test names Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Constance <constancecchen@users.noreply.github.com> --- .../enterprise_search/common/version.ts | 11 ------- .../engine_overview_empty.test.tsx | 8 ++--- .../public/applications/app_search/routes.ts | 4 +-- .../shared/constants/documentation_links.ts | 11 ------- .../applications/shared/constants/index.ts | 1 - .../shared/doc_links/doc_links.test.ts | 30 +++++++++++++++++++ .../shared/doc_links/doc_links.ts | 30 +++++++++++++++++++ .../applications/shared/doc_links/index.ts | 7 +++++ .../shared/setup_guide/cloud/instructions.tsx | 6 ++-- .../applications/workplace_search/routes.ts | 7 ++--- .../enterprise_search/public/plugin.ts | 9 +++++- 11 files changed, 86 insertions(+), 38 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/common/version.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/constants/documentation_links.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/doc_links/index.ts diff --git a/x-pack/plugins/enterprise_search/common/version.ts b/x-pack/plugins/enterprise_search/common/version.ts deleted file mode 100644 index e1a990e5c4710..0000000000000 --- a/x-pack/plugins/enterprise_search/common/version.ts +++ /dev/null @@ -1,11 +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 SemVer from 'semver/classes/semver'; -import pkg from '../../../../package.json'; - -export const CURRENT_VERSION = new SemVer(pkg.version as string); -export const CURRENT_MAJOR_VERSION = `${CURRENT_VERSION.major}.${CURRENT_VERSION.minor}`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx index 6c46c849c79bc..1b6acf341c08e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButton } from '@elastic/eui'; -import { CURRENT_MAJOR_VERSION } from '../../../../../common/version'; +import { docLinks } from '../../../shared/doc_links'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; import { EmptyEngineOverview } from './engine_overview_empty'; @@ -24,10 +24,8 @@ describe('EmptyEngineOverview', () => { expect(wrapper.find('h1').text()).toEqual('Engine setup'); }); - it('renders correctly versioned documentation URLs', () => { - expect(wrapper.find(EuiButton).prop('href')).toEqual( - `https://www.elastic.co/guide/en/app-search/${CURRENT_MAJOR_VERSION}/index.html` - ); + it('renders a documentation link', () => { + expect(wrapper.find(EuiButton).prop('href')).toEqual(`${docLinks.appSearchBase}/index.html`); }); it('renders document creation components', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 41e9bfa19e0f0..7f12f7d29671a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CURRENT_MAJOR_VERSION } from '../../../common/version'; +import { docLinks } from '../shared/doc_links'; -export const DOCS_PREFIX = `https://www.elastic.co/guide/en/app-search/${CURRENT_MAJOR_VERSION}`; +export const DOCS_PREFIX = docLinks.appSearchBase; export const ROOT_PATH = '/'; export const SETUP_GUIDE_PATH = '/setup_guide'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/documentation_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/documentation_links.ts deleted file mode 100644 index 7e774616ff598..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/documentation_links.ts +++ /dev/null @@ -1,11 +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 { CURRENT_MAJOR_VERSION } from '../../../../common/version'; - -export const ENT_SEARCH_DOCS_PREFIX = `https://www.elastic.co/guide/en/enterprise-search/${CURRENT_MAJOR_VERSION}`; - -export const CLOUD_DOCS_PREFIX = `https://www.elastic.co/guide/en/cloud/current`; // Cloud does not have version-prefixed documentation diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts index 8fa3ccdcb863e..4d4ff5f52ef20 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts @@ -5,4 +5,3 @@ */ export { DEFAULT_META } from './default_meta'; -export * from './documentation_links'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.test.ts new file mode 100644 index 0000000000000..3bee87dbfda3d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.test.ts @@ -0,0 +1,30 @@ +/* + * 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 { docLinks } from './'; + +describe('DocLinks', () => { + it('setDocLinks', () => { + const links = { + DOC_LINK_VERSION: '', + ELASTIC_WEBSITE_URL: 'https://elastic.co/', + links: { + enterpriseSearch: { + base: 'http://elastic.enterprise.search', + appSearchBase: 'http://elastic.app.search', + workplaceSearchBase: 'http://elastic.workplace.search', + }, + }, + }; + + docLinks.setDocLinks(links as any); + + expect(docLinks.enterpriseSearchBase).toEqual('http://elastic.enterprise.search'); + expect(docLinks.appSearchBase).toEqual('http://elastic.app.search'); + expect(docLinks.workplaceSearchBase).toEqual('http://elastic.workplace.search'); + expect(docLinks.cloudBase).toEqual('https://elastic.co/guide/en/cloud/current'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts new file mode 100644 index 0000000000000..3ecb28d1d4729 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -0,0 +1,30 @@ +/* + * 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 { DocLinksStart } from 'kibana/public'; + +class DocLinks { + public enterpriseSearchBase: string; + public appSearchBase: string; + public workplaceSearchBase: string; + public cloudBase: string; + + constructor() { + this.enterpriseSearchBase = ''; + this.appSearchBase = ''; + this.workplaceSearchBase = ''; + this.cloudBase = ''; + } + + public setDocLinks(docLinks: DocLinksStart): void { + this.enterpriseSearchBase = docLinks.links.enterpriseSearch.base; + this.appSearchBase = docLinks.links.enterpriseSearch.appSearchBase; + this.workplaceSearchBase = docLinks.links.enterpriseSearch.workplaceSearchBase; + this.cloudBase = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/cloud/current`; + } +} + +export const docLinks = new DocLinks(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/index.ts new file mode 100644 index 0000000000000..a926efd59a574 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/index.ts @@ -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 { docLinks } from './doc_links'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx index 383fd4b11108a..26bbc8814d108 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiPageContent, EuiSteps, EuiText, EuiLink, EuiCallOut } from '@elastic/eui'; -import { CLOUD_DOCS_PREFIX, ENT_SEARCH_DOCS_PREFIX } from '../../constants'; +import { docLinks } from '../../doc_links'; interface Props { productName: string; @@ -73,7 +73,7 @@ export const CloudSetupInstructions: React.FC<Props> = ({ productName, cloudDepl values={{ optionsLink: ( <EuiLink - href={`${ENT_SEARCH_DOCS_PREFIX}/configuration.html`} + href={`${docLinks.enterpriseSearchBase}/configuration.html`} target="_blank" > configurable options @@ -115,7 +115,7 @@ export const CloudSetupInstructions: React.FC<Props> = ({ productName, cloudDepl productName, configurePolicyLink: ( <EuiLink - href={`${CLOUD_DOCS_PREFIX}/ec-configure-index-management.html`} + href={`${docLinks.cloudBase}/ec-configure-index-management.html`} target="_blank" > configure an index lifecycle policy diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 1e4b51e157724..ef1bb03b7921c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -6,8 +6,7 @@ import { generatePath } from 'react-router-dom'; -import { CURRENT_MAJOR_VERSION } from '../../../common/version'; -import { ENT_SEARCH_DOCS_PREFIX } from '../shared/constants'; +import { docLinks } from '../shared/doc_links'; export const SETUP_GUIDE_PATH = '/setup_guide'; @@ -16,7 +15,7 @@ export const NOT_FOUND_PATH = '/404'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; -export const DOCS_PREFIX = `https://www.elastic.co/guide/en/workplace-search/${CURRENT_MAJOR_VERSION}`; +export const DOCS_PREFIX = docLinks.workplaceSearchBase; export const DOCUMENT_PERMISSIONS_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sources-document-permissions.html`; export const DOCUMENT_PERMISSIONS_SYNC_DOCS_URL = `${DOCUMENT_PERMISSIONS_DOCS_URL}#sources-permissions-synchronizing`; export const PRIVATE_SOURCES_DOCS_URL = `${DOCUMENT_PERMISSIONS_DOCS_URL}#sources-permissions-org-private`; @@ -42,7 +41,7 @@ export const ZENDESK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-zendesk-connect export const CUSTOM_SOURCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-api-sources.html`; export const CUSTOM_API_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-sources-api.html`; export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = `${CUSTOM_SOURCE_DOCS_URL}#custom-api-source-document-level-access-control`; -export const ENT_SEARCH_LICENSE_MANAGEMENT = `${ENT_SEARCH_DOCS_PREFIX}/license-management.html`; +export const ENT_SEARCH_LICENSE_MANAGEMENT = `${docLinks.enterpriseSearchBase}/license-management.html`; export const PERSONAL_PATH = '/p'; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 632bb425f203e..5f467c872447d 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -6,6 +6,7 @@ import { AppMountParameters, + CoreStart, CoreSetup, HttpSetup, Plugin, @@ -27,6 +28,8 @@ import { } from '../common/constants'; import { InitialAppData } from '../common/types'; +import { docLinks } from './applications/shared/doc_links'; + export interface ClientConfigType { host?: string; } @@ -153,7 +156,11 @@ export class EnterpriseSearchPlugin implements Plugin { } } - public start() {} + public start(core: CoreStart) { + // This must be called here in start() and not in `applications/index.tsx` to prevent loading + // race conditions with our apps' `routes.ts` being initialized before `renderApp()` + docLinks.setDocLinks(core.docLinks); + } public stop() {} From 7465976c2579b319e44d7a1a5224eac3a0c059fc Mon Sep 17 00:00:00 2001 From: Spencer <email@spalger.com> Date: Thu, 28 Jan 2021 18:13:56 -0700 Subject: [PATCH 104/163] [dev/build/version info] convert to integration tests (#89511) Co-authored-by: spalger <spalger@users.noreply.github.com> --- .../build/lib/{ => integration_tests}/version_info.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename src/dev/build/lib/{ => integration_tests}/version_info.test.ts (92%) diff --git a/src/dev/build/lib/version_info.test.ts b/src/dev/build/lib/integration_tests/version_info.test.ts similarity index 92% rename from src/dev/build/lib/version_info.test.ts rename to src/dev/build/lib/integration_tests/version_info.test.ts index dc0bf4ce6a833..36d052ebad937 100644 --- a/src/dev/build/lib/version_info.test.ts +++ b/src/dev/build/lib/integration_tests/version_info.test.ts @@ -6,10 +6,11 @@ * Public License, v 1. */ -import pkg from '../../../../package.json'; -import { getVersionInfo } from './version_info'; +import { kibanaPackageJSON as pkg } from '@kbn/dev-utils'; -jest.mock('./get_build_number'); +import { getVersionInfo } from '../version_info'; + +jest.mock('../get_build_number'); describe('isRelease = true', () => { it('returns unchanged package.version, build sha, and build number', async () => { From 8e57b63deb39a8245960d68c0c72394384970e2f Mon Sep 17 00:00:00 2001 From: Oliver Gupte <ogupte@users.noreply.github.com> Date: Thu, 28 Jan 2021 18:17:09 -0800 Subject: [PATCH 105/163] [APM] fixes incorrect values in service overview throughput chart (#89348) * [APM] fixes incorrect values in service overview throughput chart --- .../apm/server/lib/services/get_throughput.ts | 11 +++- .../services/__snapshots__/throughput.snap | 58 +++++++++---------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index 29071f96e3a06..bde826a568da9 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -27,12 +27,17 @@ interface Options { type ESResponse = PromiseReturnType<typeof fetcher>; -function transform(response: ESResponse) { +function transform(response: ESResponse, options: Options) { + const { end, start } = options.setup; + const deltaAsMinutes = (end - start) / 1000 / 60; if (response.hits.total.value === 0) { return []; } const buckets = response.aggregations?.throughput.buckets ?? []; - return buckets.map(({ key: x, doc_count: y }) => ({ x, y })); + return buckets.map(({ key: x, doc_count: y }) => ({ + x, + y: y / deltaAsMinutes, + })); } async function fetcher({ @@ -82,6 +87,6 @@ async function fetcher({ export async function getThroughput(options: Options) { return { - throughput: transform(await fetcher(options)), + throughput: transform(await fetcher(options), options), }; } diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap index f23601fccb174..eee0ec7f9ad38 100644 --- a/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap @@ -8,7 +8,7 @@ Array [ }, Object { "x": 1607435880000, - "y": 4, + "y": 0.133333333333333, }, Object { "x": 1607435910000, @@ -16,7 +16,7 @@ Array [ }, Object { "x": 1607435940000, - "y": 2, + "y": 0.0666666666666667, }, Object { "x": 1607435970000, @@ -24,11 +24,11 @@ Array [ }, Object { "x": 1607436000000, - "y": 3, + "y": 0.1, }, Object { "x": 1607436030000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607436060000, @@ -40,7 +40,7 @@ Array [ }, Object { "x": 1607436120000, - "y": 4, + "y": 0.133333333333333, }, Object { "x": 1607436150000, @@ -56,7 +56,7 @@ Array [ }, Object { "x": 1607436240000, - "y": 6, + "y": 0.2, }, Object { "x": 1607436270000, @@ -68,15 +68,15 @@ Array [ }, Object { "x": 1607436330000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607436360000, - "y": 5, + "y": 0.166666666666667, }, Object { "x": 1607436390000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607436420000, @@ -88,11 +88,11 @@ Array [ }, Object { "x": 1607436480000, - "y": 2, + "y": 0.0666666666666667, }, Object { "x": 1607436510000, - "y": 5, + "y": 0.166666666666667, }, Object { "x": 1607436540000, @@ -104,11 +104,11 @@ Array [ }, Object { "x": 1607436600000, - "y": 2, + "y": 0.0666666666666667, }, Object { "x": 1607436630000, - "y": 7, + "y": 0.233333333333333, }, Object { "x": 1607436660000, @@ -124,7 +124,7 @@ Array [ }, Object { "x": 1607436750000, - "y": 2, + "y": 0.0666666666666667, }, Object { "x": 1607436780000, @@ -132,15 +132,15 @@ Array [ }, Object { "x": 1607436810000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607436840000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607436870000, - "y": 2, + "y": 0.0666666666666667, }, Object { "x": 1607436900000, @@ -152,11 +152,11 @@ Array [ }, Object { "x": 1607436960000, - "y": 2, + "y": 0.0666666666666667, }, Object { "x": 1607436990000, - "y": 4, + "y": 0.133333333333333, }, Object { "x": 1607437020000, @@ -168,11 +168,11 @@ Array [ }, Object { "x": 1607437080000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607437110000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607437140000, @@ -184,15 +184,15 @@ Array [ }, Object { "x": 1607437200000, - "y": 2, + "y": 0.0666666666666667, }, Object { "x": 1607437230000, - "y": 7, + "y": 0.233333333333333, }, Object { "x": 1607437260000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607437290000, @@ -200,11 +200,11 @@ Array [ }, Object { "x": 1607437320000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607437350000, - "y": 2, + "y": 0.0666666666666667, }, Object { "x": 1607437380000, @@ -216,11 +216,11 @@ Array [ }, Object { "x": 1607437440000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607437470000, - "y": 3, + "y": 0.1, }, Object { "x": 1607437500000, @@ -232,7 +232,7 @@ Array [ }, Object { "x": 1607437560000, - "y": 1, + "y": 0.0333333333333333, }, Object { "x": 1607437590000, From 46c9e64278fa32fac1d95586cd2b45fa0c0b9dc1 Mon Sep 17 00:00:00 2001 From: Tim Sullivan <tsullivan@users.noreply.github.com> Date: Thu, 28 Jan 2021 19:19:31 -0700 Subject: [PATCH 106/163] Update README.md --- x-pack/build_chromium/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md index 7c81e46318a1c..9934d06a9d96a 100644 --- a/x-pack/build_chromium/README.md +++ b/x-pack/build_chromium/README.md @@ -20,6 +20,7 @@ gain familiarity. ## Usage ``` +export PATH=$HOME/chromium/depot_tools:$PATH # Create a dedicated working directory for this directory of Python scripts. mkdir ~/chromium && cd ~/chromium # Copy the scripts from the Kibana repo to use them conveniently in the working directory From 9ba3ee32a72ab56f1acaee2c634b25702fc06543 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko <jo.naumenko@gmail.com> Date: Thu, 28 Jan 2021 20:18:55 -0800 Subject: [PATCH 107/163] [Alerting UI] Fixed a bad UX for `xpack.actions.enabled` is set as false. UI should show the proper message instead of the endless spinner. (#89043) * [Alerts][Actions] Changed isESOUsingEphemeralEncryptionKey determination. Set ESO plugin as an optional dependancy for actions and alerts plugins. * fixed faling typechecks * fixed faling typechecks * fixed health framework status message * fixed due to comments * fixed faling test * changed approach * fixed due to comments * fixed due to comments * fixed tests * fixed tests * fixed tests * fixed wrong commit * fixed lang issue * Fixed to remove eso check * Fixed tests * Fixed due to comments. --- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../components/health_check.test.tsx | 21 ++- .../application/components/health_check.tsx | 152 ++++++++++++------ .../public/application/lib/alert_api.test.ts | 8 +- .../public/application/lib/alert_api.ts | 6 +- .../sections/alert_form/alert_add.test.tsx | 9 +- .../sections/alert_form/alert_edit.test.tsx | 56 +++++-- .../components/alerts_list.test.tsx | 8 +- .../with_bulk_alert_api_operations.tsx | 4 +- .../public/common/lib/health_api.test.ts | 24 +++ .../public/common/lib/health_api.ts | 12 ++ .../server/data/routes/fields.ts | 3 - .../triggers_actions_ui/server/plugin.ts | 18 ++- .../server/routes/health.ts | 41 +++++ 15 files changed, 280 insertions(+), 88 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/server/routes/health.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1e81795eb2328..ca58c43ba3f98 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21446,9 +21446,6 @@ "xpack.triggersActionsUI.components.healthCheck.encryptionErrorAfterKey": " kibana.ymlファイルで", "xpack.triggersActionsUI.components.healthCheck.encryptionErrorBeforeKey": "アラートを作成するには、値を設定します ", "xpack.triggersActionsUI.components.healthCheck.encryptionErrorTitle": "暗号化鍵を設定する必要があります", - "xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionError": "KibanaとElasticsearchの間でトランスポートレイヤーセキュリティを有効にし、kibana.ymlファイルで暗号化鍵を構成する必要があります。", - "xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorAction": "方法を学習", - "xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorTitle": "追加の設定が必要です", "xpack.triggersActionsUI.components.healthCheck.tlsError": "アラートはAPIキーに依存し、キーを使用するにはElasticsearchとKibanaの間にTLSが必要です。", "xpack.triggersActionsUI.components.healthCheck.tlsErrorAction": "TLSを有効にする方法をご覧ください。", "xpack.triggersActionsUI.components.healthCheck.tlsErrorTitle": "トランスポートレイヤーセキュリティを有効にする必要があります", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4d21a05cab09a..ae148b9a0c133 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21496,9 +21496,6 @@ "xpack.triggersActionsUI.components.healthCheck.encryptionErrorAfterKey": " 。", "xpack.triggersActionsUI.components.healthCheck.encryptionErrorBeforeKey": "要创建告警,请在 kibana.yml 文件中设置以下项的值: ", "xpack.triggersActionsUI.components.healthCheck.encryptionErrorTitle": "必须设置加密密钥", - "xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionError": "必须在 Kibana 和 Elasticsearch 之间启用传输层安全并在 kibana.yml 文件中配置加密密钥。", - "xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorAction": "了解操作方法", - "xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorTitle": "需要其他设置", "xpack.triggersActionsUI.components.healthCheck.tlsError": "Alerting 功能依赖于 API 密钥,这需要在 Elasticsearch 与 Kibana 之间启用 TLS。", "xpack.triggersActionsUI.components.healthCheck.tlsErrorAction": "了解如何启用 TLS。", "xpack.triggersActionsUI.components.healthCheck.tlsErrorTitle": "必须启用传输层安全", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index be6c72eef6f9a..8c6a16dcd4a02 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -56,9 +56,11 @@ describe('health check', () => { }); it('renders children if keys are enabled', async () => { - useKibanaMock().services.http.get = jest - .fn() - .mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true }); + useKibanaMock().services.http.get = jest.fn().mockResolvedValue({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + isAlertsAvailable: true, + }); const { queryByText } = render( <HealthContextProvider> <HealthCheck waitForCheck={true}> @@ -72,10 +74,11 @@ describe('health check', () => { expect(queryByText('should render')).toBeInTheDocument(); }); - test('renders warning if keys are disabled', async () => { - useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({ + test('renders warning if TLS is required', async () => { + useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ isSufficientlySecure: false, hasPermanentEncryptionKey: true, + isAlertsAvailable: true, })); const { queryAllByText } = render( <HealthContextProvider> @@ -104,9 +107,10 @@ describe('health check', () => { }); test('renders warning if encryption key is ephemeral', async () => { - useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({ + useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: false, + isAlertsAvailable: true, })); const { queryByText, queryByRole } = render( <HealthContextProvider> @@ -121,7 +125,7 @@ describe('health check', () => { const description = queryByRole(/banner/i); expect(description!.textContent).toMatchInlineSnapshot( - `"To create an alert, set a value for xpack.encryptedSavedObjects.encryptionKey in your kibana.yml file. Learn how.(opens in a new tab or window)"` + `"To create an alert, set a value for xpack.encryptedSavedObjects.encryptionKey in your kibana.yml file and ensure the Encrypted Saved Objects plugin is enabled. Learn how.(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); @@ -132,9 +136,10 @@ describe('health check', () => { }); test('renders warning if encryption key is ephemeral and keys are disabled', async () => { - useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({ + useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ isSufficientlySecure: false, hasPermanentEncryptionKey: false, + isAlertsAvailable: true, })); const { queryByText } = render( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index 66f7c1d36dfb2..3103d8f2a817c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -14,18 +14,24 @@ import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { DocLinksStart } from 'kibana/public'; -import { AlertingFrameworkHealth } from '../../types'; -import { health } from '../lib/alert_api'; +import { alertingFrameworkHealth } from '../lib/alert_api'; import './health_check.scss'; import { useHealthContext } from '../context/health_context'; import { useKibana } from '../../common/lib/kibana'; import { CenterJustifiedSpinner } from './center_justified_spinner'; +import { triggersActionsUiHealth } from '../../common/lib/health_api'; interface Props { inFlyout?: boolean; waitForCheck: boolean; } +interface HealthStatus { + isAlertsAvailable: boolean; + isSufficientlySecure: boolean; + hasPermanentEncryptionKey: boolean; +} + export const HealthCheck: React.FunctionComponent<Props> = ({ children, waitForCheck, @@ -33,12 +39,24 @@ export const HealthCheck: React.FunctionComponent<Props> = ({ }) => { const { http, docLinks } = useKibana().services; const { setLoadingHealthCheck } = useHealthContext(); - const [alertingHealth, setAlertingHealth] = React.useState<Option<AlertingFrameworkHealth>>(none); + const [alertingHealth, setAlertingHealth] = React.useState<Option<HealthStatus>>(none); React.useEffect(() => { (async function () { setLoadingHealthCheck(true); - setAlertingHealth(some(await health({ http }))); + const triggersActionsUiHealthStatus = await triggersActionsUiHealth({ http }); + const healthStatus: HealthStatus = { + ...triggersActionsUiHealthStatus, + isSufficientlySecure: false, + hasPermanentEncryptionKey: false, + }; + if (healthStatus.isAlertsAvailable) { + const alertingHealthResult = await alertingFrameworkHealth({ http }); + healthStatus.isSufficientlySecure = alertingHealthResult.isSufficientlySecure; + healthStatus.hasPermanentEncryptionKey = alertingHealthResult.hasPermanentEncryptionKey; + } + + setAlertingHealth(some(healthStatus)); setLoadingHealthCheck(false); })(); }, [http, setLoadingHealthCheck]); @@ -60,6 +78,8 @@ export const HealthCheck: React.FunctionComponent<Props> = ({ (healthCheck) => { return healthCheck?.isSufficientlySecure && healthCheck?.hasPermanentEncryptionKey ? ( <Fragment>{children}</Fragment> + ) : !healthCheck.isAlertsAvailable ? ( + <AlertsError docLinks={docLinks} className={className} /> ) : !healthCheck.isSufficientlySecure && !healthCheck.hasPermanentEncryptionKey ? ( <TlsAndEncryptionError docLinks={docLinks} className={className} /> ) : !healthCheck.hasPermanentEncryptionKey ? ( @@ -77,7 +97,7 @@ interface PromptErrorProps { className?: string; } -const TlsAndEncryptionError = ({ +const EncryptionError = ({ // eslint-disable-next-line @typescript-eslint/naming-convention docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, className, @@ -90,27 +110,37 @@ const TlsAndEncryptionError = ({ title={ <h2> <FormattedMessage - id="xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorTitle" - defaultMessage="Additional setup required" + id="xpack.triggersActionsUI.components.healthCheck.encryptionErrorTitle" + defaultMessage="Encrypted saved objects are not available" /> </h2> } body={ <div className={`${className}__body`}> <p role="banner"> - {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionError', { - defaultMessage: - 'You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. ', - })} + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorBeforeKey', + { + defaultMessage: 'To create an alert, set a value for ', + } + )} + <EuiCode>{'xpack.encryptedSavedObjects.encryptionKey'}</EuiCode> + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAfterKey', + { + defaultMessage: + ' in your kibana.yml file and ensure the Encrypted Saved Objects plugin is enabled. ', + } + )} <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`} + href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`} external target="_blank" > {i18n.translate( - 'xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorAction', + 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAction', { - defaultMessage: 'Learn how', + defaultMessage: 'Learn how.', } )} </EuiLink> @@ -120,7 +150,7 @@ const TlsAndEncryptionError = ({ /> ); -const EncryptionError = ({ +const TlsError = ({ // eslint-disable-next-line @typescript-eslint/naming-convention docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, className, @@ -133,38 +163,26 @@ const EncryptionError = ({ title={ <h2> <FormattedMessage - id="xpack.triggersActionsUI.components.healthCheck.encryptionErrorTitle" - defaultMessage="You must set an encryption key" + id="xpack.triggersActionsUI.components.healthCheck.tlsErrorTitle" + defaultMessage="You must enable Transport Layer Security" /> </h2> } body={ <div className={`${className}__body`}> <p role="banner"> - {i18n.translate( - 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorBeforeKey', - { - defaultMessage: 'To create an alert, set a value for ', - } - )} - <EuiCode>{'xpack.encryptedSavedObjects.encryptionKey'}</EuiCode> - {i18n.translate( - 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAfterKey', - { - defaultMessage: ' in your kibana.yml file. ', - } - )} + {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsError', { + defaultMessage: + 'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ', + })} <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`} + href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`} external target="_blank" > - {i18n.translate( - 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAction', - { - defaultMessage: 'Learn how.', - } - )} + {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsErrorAction', { + defaultMessage: 'Learn how to enable TLS.', + })} </EuiLink> </p> </div> @@ -172,7 +190,46 @@ const EncryptionError = ({ /> ); -const TlsError = ({ +const AlertsError = ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + className, +}: PromptErrorProps) => ( + <EuiEmptyPrompt + iconType="watchesApp" + data-test-subj="alertsNeededEmptyPrompt" + className={className} + titleSize="xs" + title={ + <h2> + <FormattedMessage + id="xpack.triggersActionsUI.components.healthCheck.alertsErrorTitle" + defaultMessage="You must enable Alerts and Actions" + /> + </h2> + } + body={ + <div className={`${className}__body`}> + <p role="banner"> + {i18n.translate('xpack.triggersActionsUI.components.healthCheck.alertsError', { + defaultMessage: 'To create an alert, set alerts and actions plugins enabled. ', + })} + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html`} + external + target="_blank" + > + {i18n.translate('xpack.triggersActionsUI.components.healthCheck.alertsErrorAction', { + defaultMessage: 'Learn how to enable Alerts and Actions.', + })} + </EuiLink> + </p> + </div> + } + /> +); + +const TlsAndEncryptionError = ({ // eslint-disable-next-line @typescript-eslint/naming-convention docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, className, @@ -185,26 +242,29 @@ const TlsError = ({ title={ <h2> <FormattedMessage - id="xpack.triggersActionsUI.components.healthCheck.tlsErrorTitle" - defaultMessage="You must enable Transport Layer Security" + id="xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorTitle" + defaultMessage="Additional setup required" /> </h2> } body={ <div className={`${className}__body`}> <p role="banner"> - {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsError', { + {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionError', { defaultMessage: - 'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ', + 'You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. ', })} <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`} + href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`} external target="_blank" > - {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsErrorAction', { - defaultMessage: 'Learn how to enable TLS.', - })} + {i18n.translate( + 'xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorAction', + { + defaultMessage: 'Learn how', + } + )} </EuiLink> </p> </div> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index ea654bb21e88b..f3d49c52855ab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -25,7 +25,7 @@ import { updateAlert, muteAlertInstance, unmuteAlertInstance, - health, + alertingFrameworkHealth, mapFiltersToKql, } from './alert_api'; import uuid from 'uuid'; @@ -801,9 +801,9 @@ describe('unmuteAlerts', () => { }); }); -describe('health', () => { - test('should call health API', async () => { - const result = await health({ http }); +describe('alertingFrameworkHealth', () => { + test('should call alertingFrameworkHealth API', async () => { + const result = await alertingFrameworkHealth({ http }); expect(result).toEqual(undefined); expect(http.get.mock.calls).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts index 52ab33566da74..f774b3d35bb29 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts @@ -282,6 +282,10 @@ export async function unmuteAlerts({ await Promise.all(ids.map((id) => unmuteAlert({ id, http }))); } -export async function health({ http }: { http: HttpSetup }): Promise<AlertingFrameworkHealth> { +export async function alertingFrameworkHealth({ + http, +}: { + http: HttpSetup; +}): Promise<AlertingFrameworkHealth> { return await http.get(`${BASE_ALERT_API_PATH}/_health`); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 889dfe8289b13..3c32b5bc729dd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -25,7 +25,14 @@ jest.mock('../../../common/lib/kibana'); jest.mock('../../lib/alert_api', () => ({ loadAlertTypes: jest.fn(), - health: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })), + alertingFrameworkHealth: jest.fn(() => ({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + })), +})); + +jest.mock('../../../common/lib/health_api', () => ({ + triggersActionsUiHealth: jest.fn(() => ({ isAlertsAvailable: true })), })); const actionTypeRegistry = actionTypeRegistryMock.create(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index baf0f55c415db..df7729bb407b3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -18,11 +18,25 @@ import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { ReactWrapper } from 'enzyme'; import AlertEdit from './alert_edit'; import { useKibana } from '../../../common/lib/kibana'; +import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; jest.mock('../../../common/lib/kibana'); const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>; +jest.mock('../../lib/alert_api', () => ({ + loadAlertTypes: jest.fn(), + updateAlert: jest.fn().mockRejectedValue({ body: { message: 'Fail message' } }), + alertingFrameworkHealth: jest.fn(() => ({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + })), +})); + +jest.mock('../../../common/lib/health_api', () => ({ + triggersActionsUiHealth: jest.fn(() => ({ isAlertsAvailable: true })), +})); + describe('alert_edit', () => { let wrapper: ReactWrapper<any>; let mockedCoreSetup: ReturnType<typeof coreMock.createSetup>; @@ -48,12 +62,32 @@ describe('alert_edit', () => { }, }; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.http.get = jest.fn().mockResolvedValue({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: true, - }); - + const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); + const alertTypes = [ + { + id: 'my-alert-type', + name: 'Test', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, + actionVariables: { + context: [], + state: [], + params: [], + }, + }, + ]; const alertType = { id: 'my-alert-type', iconClass: 'test', @@ -79,7 +113,7 @@ describe('alert_edit', () => { }, actionConnectorFields: null, }); - + loadAlertTypes.mockResolvedValue(alertTypes); const alert: Alert = { id: 'ab5661e0-197e-45ee-b477-302d89193b5e', params: { @@ -145,19 +179,15 @@ describe('alert_edit', () => { }); } - it('renders alert add flyout', async () => { + it('renders alert edit flyout', async () => { await setup(); expect(wrapper.find('[data-test-subj="editAlertFlyoutTitle"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="saveEditedAlertButton"]').exists()).toBeTruthy(); }); it('displays a toast message on save for server errors', async () => { - useKibanaMock().services.http.get = jest.fn().mockResolvedValue([]); await setup(); - const err = new Error() as any; - err.body = {}; - err.body.message = 'Fail message'; - useKibanaMock().services.http.put = jest.fn().mockRejectedValue(err); + await act(async () => { wrapper.find('[data-test-subj="saveEditedAlertButton"]').first().simulate('click'); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 8ca3edb1c68df..bd50bf3270f1a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -26,7 +26,13 @@ jest.mock('../../../lib/action_connector_api', () => ({ jest.mock('../../../lib/alert_api', () => ({ loadAlerts: jest.fn(), loadAlertTypes: jest.fn(), - health: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })), + alertingFrameworkHealth: jest.fn(() => ({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + })), +})); +jest.mock('../../../../common/lib/health_api', () => ({ + triggersActionsUiHealth: jest.fn(() => ({ isAlertsAvailable: true })), })); jest.mock('react-router-dom', () => ({ useHistory: () => ({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx index 5656aa9de7795..f44f6e87c7a19 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -29,7 +29,7 @@ import { loadAlertState, loadAlertInstanceSummary, loadAlertTypes, - health, + alertingFrameworkHealth, } from '../../../lib/alert_api'; import { useKibana } from '../../../../common/lib/kibana'; @@ -131,7 +131,7 @@ export function withBulkAlertOperations<T>( loadAlertInstanceSummary({ http, alertId }) } loadAlertTypes={async () => loadAlertTypes({ http })} - getHealth={async () => health({ http })} + getHealth={async () => alertingFrameworkHealth({ http })} /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.test.ts new file mode 100644 index 0000000000000..d22fd538ad0ca --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.test.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 { httpServiceMock } from 'src/core/public/mocks'; +import { triggersActionsUiHealth } from './health_api'; + +describe('triggersActionsUiHealth', () => { + const http = httpServiceMock.createStartContract(); + + test('should call triggersActionsUiHealth API', async () => { + const result = await triggersActionsUiHealth({ http }); + expect(result).toEqual(undefined); + expect(http.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/triggers_actions_ui/_health", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.ts new file mode 100644 index 0000000000000..752f5b3e2ca08 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { HttpSetup } from 'kibana/public'; + +const TRIGGERS_ACTIONS_UI_API_ROOT = '/api/triggers_actions_ui'; + +export async function triggersActionsUiHealth({ http }: { http: HttpSetup }): Promise<any> { + return await http.get(`${TRIGGERS_ACTIONS_UI_API_ROOT}/_health`); +} diff --git a/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts index 17a2b2929f0cf..2cda40c18db0c 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// the business logic of this code is from watcher, in: -// x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts - import { schema, TypeOf } from '@kbn/config-schema'; import { IRouter, diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts index c0d29341e217b..69be37f665887 100644 --- a/x-pack/plugins/triggers_actions_ui/server/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -5,12 +5,21 @@ */ import { Logger, Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { PluginSetupContract as AlertsPluginSetup } from '../../alerts/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; import { getService, register as registerDataService } from './data'; +import { createHealthRoute } from './routes/health'; +const BASE_ROUTE = '/api/triggers_actions_ui'; export interface PluginStartContract { data: ReturnType<typeof getService>; } +interface PluginsSetup { + encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup; + alerts?: AlertsPluginSetup; +} + export class TriggersActionsPlugin implements Plugin<void, PluginStartContract> { private readonly logger: Logger; private readonly data: PluginStartContract['data']; @@ -20,13 +29,16 @@ export class TriggersActionsPlugin implements Plugin<void, PluginStartContract> this.data = getService(); } - public async setup(core: CoreSetup): Promise<void> { + public async setup(core: CoreSetup, plugins: PluginsSetup): Promise<void> { + const router = core.http.createRouter(); registerDataService({ logger: this.logger, data: this.data, - router: core.http.createRouter(), - baseRoute: '/api/triggers_actions_ui', + router, + baseRoute: BASE_ROUTE, }); + + createHealthRoute(this.logger, router, BASE_ROUTE, plugins.alerts !== undefined); } public async start(): Promise<PluginStartContract> { diff --git a/x-pack/plugins/triggers_actions_ui/server/routes/health.ts b/x-pack/plugins/triggers_actions_ui/server/routes/health.ts new file mode 100644 index 0000000000000..1ea9cb748bcd7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/routes/health.ts @@ -0,0 +1,41 @@ +/* + * 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 { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'kibana/server'; +import { Logger } from '../../../../../src/core/server'; + +export function createHealthRoute( + logger: Logger, + router: IRouter, + baseRoute: string, + isAlertsAvailable: boolean +) { + const path = `${baseRoute}/_health`; + logger.debug(`registering triggers_actions_ui health route GET ${path}`); + router.get( + { + path, + validate: false, + }, + handler + ); + async function handler( + ctx: RequestHandlerContext, + req: KibanaRequest<unknown, unknown, unknown>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse> { + const result = { isAlertsAvailable }; + + logger.debug(`route ${path} response: ${JSON.stringify(result)}`); + return res.ok({ body: result }); + } +} From 827446bfcf37b679af2455ec598a4038042f9f56 Mon Sep 17 00:00:00 2001 From: Pete Harverson <peteharverson@users.noreply.github.com> Date: Fri, 29 Jan 2021 09:15:45 +0000 Subject: [PATCH 108/163] [ML] Stabilize accessibility tests for data frame analytics pages (#89423) * [ML] Stabilize accessibility tests for data frame analytics pages * [ML] Remove snapshot test after opening index pattern modal * [ML] Remove snapshot test when index pattern modal opens * [ML] Add back snapshot test at index pattern modal step --- test/accessibility/services/a11y/a11y.ts | 6 +----- .../custom_selection_table/custom_selection_table.js | 9 ++++++--- x-pack/test/accessibility/apps/ml.ts | 6 ++++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/accessibility/services/a11y/a11y.ts b/test/accessibility/services/a11y/a11y.ts index c6a194ace9c25..d29d17484486c 100644 --- a/test/accessibility/services/a11y/a11y.ts +++ b/test/accessibility/services/a11y/a11y.ts @@ -61,11 +61,7 @@ export function A11yProvider({ getService }: FtrProviderContext) { exclude: ([] as string[]) .concat(excludeTestSubj || []) .map((ts) => [testSubjectToCss(ts)]) - .concat([ - [ - '.leaflet-vega-container[role="graphics-document"][aria-roledescription="visualization"]', - ], - ]), + .concat([['[role="graphics-document"][aria-roledescription="visualization"]']]), }; } diff --git a/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js b/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js index 274a5ff0ffbb4..935f1f7f54df8 100644 --- a/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js +++ b/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState, useEffect } from 'react'; -import { PropTypes } from 'prop-types'; +import React, { Fragment, useState, useEffect, useMemo } from 'react'; import { + htmlIdGenerator, EuiCheckbox, EuiSearchBar, EuiFlexGroup, @@ -25,6 +25,7 @@ import { EuiTableRowCellCheckbox, EuiTableHeaderMobile, } from '@elastic/eui'; +import { PropTypes } from 'prop-types'; import { Pager } from '@elastic/eui/lib/services'; import { i18n } from '@kbn/i18n'; @@ -179,6 +180,8 @@ export function CustomSelectionTable({ return indexOfUnselectedItem === -1; } + const selectAllCheckboxId = useMemo(() => htmlIdGenerator()(), []); + function renderSelectAll(mobile) { const selectAll = i18n.translate('xpack.ml.jobSelector.customTable.selectAllCheckboxLabel', { defaultMessage: 'Select all', @@ -186,7 +189,7 @@ export function CustomSelectionTable({ return ( <EuiCheckbox - id="selectAllCheckbox" + id={`${mobile ? `mobile-` : ''}${selectAllCheckboxId}`} label={mobile ? selectAll : null} checked={areAllItemsSelected()} onChange={toggleAll} diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index 799911cd77a9f..b1fd96c4d160f 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -12,8 +12,7 @@ export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const ml = getService('ml'); - // flaky tests, see https://github.com/elastic/kibana/issues/88592 - describe.skip('ml', () => { + describe('ml', () => { const esArchiver = getService('esArchiver'); before(async () => { @@ -239,6 +238,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('data frame analytics create job select index pattern modal', async () => { + await ml.navigation.navigateToMl(); await ml.navigation.navigateToDataFrameAnalytics(); await ml.dataFrameAnalytics.startAnalyticsCreation(); await a11y.testAppSnapshot(); @@ -261,6 +261,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); await ml.testExecution.logTestStep('enables the source data preview histogram charts'); await ml.dataFrameAnalyticsCreation.enableSourceDataPreviewHistogramCharts(); + await ml.testExecution.logTestStep('displays the include fields selection'); + await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); await a11y.testAppSnapshot(); }); From 5f8f21bce5e1e0df6bd468f652414c032eb2ff2b Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens <jloleysens@gmail.com> Date: Fri, 29 Jan 2021 10:22:56 +0100 Subject: [PATCH 109/163] [ILM] Basic a11y tests (#88445) * cleaning up unused types and legacy logic * added new relative age logic with unit tests * initial implementation of timeline * added custom infinity icon to timeline component * added comment * move timeline color bar comment * fix nanoseconds and microsecnds bug * added policy timeline heading, removed "at least" copy for now * a few minor changes - fix up copy - fix up responsive/mobile first view of timeline - adjust minimum size of a color bar * minor refactor to css classnames and make trash can for delete more prominent * added delete icon tooltip with rough first copy * added smoke test for timeline and how it interacts with different policy states * update test and copy * added basic a11y tests for ILM policy list view and create/edit policy view * remove unused import * remove old svg file * remove old _timeline.scss file Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../policy_table/components/table_content.tsx | 2 +- .../apps/index_lifecycle_management.ts | 78 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/accessibility/apps/index_lifecycle_management.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx index 09c81efe163b5..21f028d1fec60 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx @@ -301,7 +301,7 @@ export const TableContent: React.FunctionComponent<Props> = ({ style={{ width: 150 }} > <EuiPopover - id="contextMenuPolicy" + id={`contextMenuPolicy-${name}`} button={button} isOpen={isPolicyPopoverOpen(policy.name)} closePopover={closePolicyPopover} diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/index_lifecycle_management.ts new file mode 100644 index 0000000000000..744a21cf381a8 --- /dev/null +++ b/x-pack/test/accessibility/apps/index_lifecycle_management.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +const TEST_POLICY_NAME = 'ilm-a11y-test'; +const TEST_POLICY_ALL_PHASES = { + policy: { + phases: { + hot: { + actions: {}, + }, + warm: { + actions: {}, + }, + cold: { + actions: {}, + }, + delete: { + actions: {}, + }, + }, + }, +}; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common } = getPageObjects(['common']); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const esClient = getService('es'); + const a11y = getService('a11y'); + + const findPolicyLinkInListView = async (policyName: string) => { + const links = await testSubjects.findAll('policyTablePolicyNameLink'); + for (const link of links) { + const name = await link.getVisibleText(); + if (name === policyName) { + return link; + } + } + throw new Error(`Could not find ${policyName} in policy table`); + }; + + describe('Index Lifecycle Management', async () => { + before(async () => { + await esClient.ilm.putLifecycle({ policy: TEST_POLICY_NAME, body: TEST_POLICY_ALL_PHASES }); + await common.navigateToApp('indexLifecycleManagement'); + }); + + after(async () => { + await esClient.ilm.deleteLifecycle({ policy: TEST_POLICY_NAME }); + }); + + it('List policies view', async () => { + await retry.waitFor('Index Lifecycle Policy create/edit view to be present', async () => { + await common.navigateToApp('indexLifecycleManagement'); + return testSubjects.exists('policyTablePolicyNameLink') ? true : false; + }); + await a11y.testAppSnapshot(); + }); + + it('Edit policy with all phases view', async () => { + await retry.waitFor('Index Lifecycle Policy create/edit view to be present', async () => { + await common.navigateToApp('indexLifecycleManagement'); + return testSubjects.exists('policyTablePolicyNameLink'); + }); + const link = await findPolicyLinkInListView(TEST_POLICY_NAME); + await link.click(); + await retry.waitFor('Index Lifecycle Policy create/edit view to be present', async () => { + return testSubjects.exists('policyTitle'); + }); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index cf13a009c2821..67bfdd7a07b9d 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -27,6 +27,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/roles'), require.resolve('./apps/kibana_overview'), require.resolve('./apps/ingest_node_pipelines'), + require.resolve('./apps/index_lifecycle_management'), require.resolve('./apps/ml'), require.resolve('./apps/lens'), ], From 749c01d898a96f01803943d134747d0e22a045bc Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Fri, 29 Jan 2021 11:01:24 +0100 Subject: [PATCH 110/163] Add tsconfig ref to vis_type_vega (#89551) --- .../public/vega_visualization.ts | 9 +++++- src/plugins/vis_type_vega/tsconfig.json | 28 +++++++++++++++++++ tsconfig.json | 2 ++ tsconfig.refs.json | 1 + 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/plugins/vis_type_vega/tsconfig.json diff --git a/src/plugins/vis_type_vega/public/vega_visualization.ts b/src/plugins/vis_type_vega/public/vega_visualization.ts index ae4d23db48ee4..26647ecca93ec 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.ts +++ b/src/plugins/vis_type_vega/public/vega_visualization.ts @@ -13,7 +13,14 @@ import { VegaVisualizationDependencies } from './plugin'; import { getNotifications, getData } from './services'; import type { VegaView } from './vega_view/vega_view'; -export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizationDependencies) => +type VegaVisType = new (el: HTMLDivElement, fireEvent: IInterpreterRenderHandlers['event']) => { + render(visData: VegaParser): Promise<void>; + destroy(): void; +}; + +export const createVegaVisualization = ({ + getServiceSettings, +}: VegaVisualizationDependencies): VegaVisType => class VegaVisualization { private readonly dataPlugin = getData(); private vegaView: InstanceType<typeof VegaView> | null = null; diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json new file mode 100644 index 0000000000000..e28839612bca7 --- /dev/null +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*", + "public/**/*", + "*.ts" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../maps_legacy/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../inspector/tsconfig.json" }, + { "path": "../home/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/tsconfig.json b/tsconfig.json index bdd4ba296d1c9..2647ac9a9d75e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -53,6 +53,7 @@ "src/plugins/vis_type_timelion/**/*", "src/plugins/vis_type_timeseries/**/*", "src/plugins/vis_type_vislib/**/*", + "src/plugins/vis_type_vega/**/*", "src/plugins/vis_type_xy/**/*", "src/plugins/visualizations/**/*", "src/plugins/visualize/**/*", @@ -109,6 +110,7 @@ { "path": "./src/plugins/vis_type_timelion/tsconfig.json" }, { "path": "./src/plugins/vis_type_timeseries/tsconfig.json" }, { "path": "./src/plugins/vis_type_vislib/tsconfig.json" }, + { "path": "./src/plugins/vis_type_vega/tsconfig.json" }, { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, { "path": "./src/plugins/visualizations/tsconfig.json" }, { "path": "./src/plugins/visualize/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 211a50ec1a539..fa1b533a3dd38 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -49,6 +49,7 @@ { "path": "./src/plugins/vis_type_timelion/tsconfig.json" }, { "path": "./src/plugins/vis_type_timeseries/tsconfig.json" }, { "path": "./src/plugins/vis_type_vislib/tsconfig.json" }, + { "path": "./src/plugins/vis_type_vega/tsconfig.json" }, { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, { "path": "./src/plugins/visualizations/tsconfig.json" }, { "path": "./src/plugins/visualize/tsconfig.json" }, From 9733d2fdaa7b28cec563eddfb2291f58f53f25c9 Mon Sep 17 00:00:00 2001 From: Marco Liberati <dej611@users.noreply.github.com> Date: Fri, 29 Jan 2021 12:09:26 +0100 Subject: [PATCH 111/163] [Lens] Use datagrid with resizable columns for datatable (#88069) Co-authored-by: Joe Reuter <johannes.reuter@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/expression.test.tsx.snap | 119 ---- .../__snapshots__/table_basic.test.tsx.snap | 534 ++++++++++++++++++ .../components/cell_value.tsx | 31 + .../components/columns.tsx | 186 ++++++ .../components/constants.ts | 8 + .../components/table_actions.test.ts | 235 ++++++++ .../components/table_actions.ts | 115 ++++ .../components/table_basic.scss | 3 + .../components/table_basic.test.tsx | 425 ++++++++++++++ .../components/table_basic.tsx | 245 ++++++++ .../components/types.ts | 67 +++ .../datatable_visualization/expression.scss | 13 - .../expression.test.tsx | 310 +--------- .../datatable_visualization/expression.tsx | 409 ++------------ .../public/datatable_visualization/index.ts | 2 + .../visualization.test.tsx | 77 +++ .../datatable_visualization/visualization.tsx | 47 +- x-pack/plugins/lens/public/types.ts | 9 +- .../translations/translations/ja-JP.json | 8 - .../translations/translations/zh-CN.json | 8 - .../test/functional/apps/lens/smokescreen.ts | 35 ++ .../test/functional/page_objects/lens_page.ts | 65 ++- 22 files changed, 2107 insertions(+), 844 deletions(-) delete mode 100644 x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/constants.ts create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/table_basic.scss create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/types.ts delete mode 100644 x-pack/plugins/lens/public/datatable_visualization/expression.scss diff --git a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap deleted file mode 100644 index 23460d442cfa8..0000000000000 --- a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap +++ /dev/null @@ -1,119 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`datatable_expression DatatableComponent it renders actions column when there are row actions 1`] = ` -<VisualizationContainer - reportTitle="My fanci metric chart" -> - <EuiBasicTable - className="lnsDataTable" - columns={ - Array [ - Object { - "field": "a", - "name": "a", - "render": [Function], - "sortable": true, - }, - Object { - "field": "b", - "name": "b", - "render": [Function], - "sortable": true, - }, - Object { - "field": "c", - "name": "c", - "render": [Function], - "sortable": true, - }, - Object { - "actions": Array [ - Object { - "description": "Table row context menu", - "icon": [Function], - "name": "More", - "onClick": [Function], - "type": "icon", - }, - ], - "name": "Actions", - }, - ] - } - data-test-subj="lnsDataTable" - items={ - Array [ - Object { - "a": "shoes", - "b": 1588024800000, - "c": 3, - "rowIndex": 0, - }, - ] - } - noItemsMessage="No items found" - onChange={[Function]} - responsive={true} - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="auto" - /> -</VisualizationContainer> -`; - -exports[`datatable_expression DatatableComponent it renders the title and value 1`] = ` -<VisualizationContainer - reportTitle="My fanci metric chart" -> - <EuiBasicTable - className="lnsDataTable" - columns={ - Array [ - Object { - "field": "a", - "name": "a", - "render": [Function], - "sortable": true, - }, - Object { - "field": "b", - "name": "b", - "render": [Function], - "sortable": true, - }, - Object { - "field": "c", - "name": "c", - "render": [Function], - "sortable": true, - }, - ] - } - data-test-subj="lnsDataTable" - items={ - Array [ - Object { - "a": "shoes", - "b": 1588024800000, - "c": 3, - "rowIndex": 0, - }, - ] - } - noItemsMessage="No items found" - onChange={[Function]} - responsive={true} - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="auto" - /> -</VisualizationContainer> -`; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap new file mode 100644 index 0000000000000..a4eb99a972b9b --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap @@ -0,0 +1,534 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DatatableComponent it renders actions column when there are row actions 1`] = ` +<VisualizationContainer + className="lnsDataTableContainer" + reportTitle="My fanci metric chart" +> + <ContextProvider + value={ + Object { + "rowHasRowClickTriggerActions": Array [ + true, + true, + true, + ], + "table": Object { + "columns": Array [ + Object { + "id": "a", + "meta": Object { + "field": "a", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "terms", + }, + "type": "string", + }, + "name": "a", + }, + Object { + "id": "b", + "meta": Object { + "field": "b", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "date_histogram", + }, + "type": "date", + }, + "name": "b", + }, + Object { + "id": "c", + "meta": Object { + "field": "c", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "count", + }, + "type": "number", + }, + "name": "c", + }, + ], + "rows": Array [ + Object { + "a": "shoes", + "b": 1588024800000, + "c": 3, + }, + ], + "type": "datatable", + }, + } + } + > + <EuiDataGrid + aria-label="My fanci metric chart" + columnVisibility={ + Object { + "setVisibleColumns": [Function], + "visibleColumns": Array [ + "a", + "b", + "c", + ], + } + } + columns={ + Array [ + Object { + "actions": Object { + "additional": Array [ + Object { + "color": "text", + "data-test-subj": "lensDatatableResetWidth", + "iconType": "empty", + "isDisabled": true, + "label": "Reset width", + "onClick": [Function], + "size": "xs", + }, + ], + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": Object { + "label": "Sort asc", + }, + "showSortDesc": Object { + "label": "Sort desc", + }, + }, + "cellActions": undefined, + "display": "a", + "displayAsText": "a", + "id": "a", + }, + Object { + "actions": Object { + "additional": Array [ + Object { + "color": "text", + "data-test-subj": "lensDatatableResetWidth", + "iconType": "empty", + "isDisabled": true, + "label": "Reset width", + "onClick": [Function], + "size": "xs", + }, + ], + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": Object { + "label": "Sort asc", + }, + "showSortDesc": Object { + "label": "Sort desc", + }, + }, + "cellActions": undefined, + "display": "b", + "displayAsText": "b", + "id": "b", + }, + Object { + "actions": Object { + "additional": Array [ + Object { + "color": "text", + "data-test-subj": "lensDatatableResetWidth", + "iconType": "empty", + "isDisabled": true, + "label": "Reset width", + "onClick": [Function], + "size": "xs", + }, + ], + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": Object { + "label": "Sort asc", + }, + "showSortDesc": Object { + "label": "Sort desc", + }, + }, + "cellActions": undefined, + "display": "c", + "displayAsText": "c", + "id": "c", + }, + ] + } + data-test-subj="lnsDataTable" + gridStyle={ + Object { + "border": "horizontal", + "header": "underline", + } + } + onColumnResize={[Function]} + renderCellValue={[Function]} + rowCount={1} + sorting={ + Object { + "columns": Array [], + "onSort": [Function], + } + } + toolbarVisibility={false} + trailingControlColumns={ + Array [ + Object { + "headerCellRender": [Function], + "id": "trailingControlColumn", + "rowCellRender": [Function], + "width": 40, + }, + ] + } + /> + </ContextProvider> +</VisualizationContainer> +`; + +exports[`DatatableComponent it renders the title and value 1`] = ` +<VisualizationContainer + className="lnsDataTableContainer" + reportTitle="My fanci metric chart" +> + <ContextProvider + value={ + Object { + "rowHasRowClickTriggerActions": undefined, + "table": Object { + "columns": Array [ + Object { + "id": "a", + "meta": Object { + "field": "a", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "terms", + }, + "type": "string", + }, + "name": "a", + }, + Object { + "id": "b", + "meta": Object { + "field": "b", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "date_histogram", + }, + "type": "date", + }, + "name": "b", + }, + Object { + "id": "c", + "meta": Object { + "field": "c", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "count", + }, + "type": "number", + }, + "name": "c", + }, + ], + "rows": Array [ + Object { + "a": "shoes", + "b": 1588024800000, + "c": 3, + }, + ], + "type": "datatable", + }, + } + } + > + <EuiDataGrid + aria-label="My fanci metric chart" + columnVisibility={ + Object { + "setVisibleColumns": [Function], + "visibleColumns": Array [ + "a", + "b", + "c", + ], + } + } + columns={ + Array [ + Object { + "actions": Object { + "additional": Array [ + Object { + "color": "text", + "data-test-subj": "lensDatatableResetWidth", + "iconType": "empty", + "isDisabled": true, + "label": "Reset width", + "onClick": [Function], + "size": "xs", + }, + ], + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": Object { + "label": "Sort asc", + }, + "showSortDesc": Object { + "label": "Sort desc", + }, + }, + "cellActions": undefined, + "display": "a", + "displayAsText": "a", + "id": "a", + }, + Object { + "actions": Object { + "additional": Array [ + Object { + "color": "text", + "data-test-subj": "lensDatatableResetWidth", + "iconType": "empty", + "isDisabled": true, + "label": "Reset width", + "onClick": [Function], + "size": "xs", + }, + ], + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": Object { + "label": "Sort asc", + }, + "showSortDesc": Object { + "label": "Sort desc", + }, + }, + "cellActions": undefined, + "display": "b", + "displayAsText": "b", + "id": "b", + }, + Object { + "actions": Object { + "additional": Array [ + Object { + "color": "text", + "data-test-subj": "lensDatatableResetWidth", + "iconType": "empty", + "isDisabled": true, + "label": "Reset width", + "onClick": [Function], + "size": "xs", + }, + ], + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": Object { + "label": "Sort asc", + }, + "showSortDesc": Object { + "label": "Sort desc", + }, + }, + "cellActions": undefined, + "display": "c", + "displayAsText": "c", + "id": "c", + }, + ] + } + data-test-subj="lnsDataTable" + gridStyle={ + Object { + "border": "horizontal", + "header": "underline", + } + } + onColumnResize={[Function]} + renderCellValue={[Function]} + rowCount={1} + sorting={ + Object { + "columns": Array [], + "onSort": [Function], + } + } + toolbarVisibility={false} + trailingControlColumns={Array []} + /> + </ContextProvider> +</VisualizationContainer> +`; + +exports[`DatatableComponent it should not render actions on header when it is in read only mode 1`] = ` +<VisualizationContainer + className="lnsDataTableContainer" + reportTitle="My fanci metric chart" +> + <ContextProvider + value={ + Object { + "rowHasRowClickTriggerActions": Array [ + false, + false, + false, + ], + "table": Object { + "columns": Array [ + Object { + "id": "a", + "meta": Object { + "field": "a", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "terms", + }, + "type": "string", + }, + "name": "a", + }, + Object { + "id": "b", + "meta": Object { + "field": "b", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "date_histogram", + }, + "type": "date", + }, + "name": "b", + }, + Object { + "id": "c", + "meta": Object { + "field": "c", + "source": "esaggs", + "sourceParams": Object { + "indexPatternId": "indexPatternId", + "type": "count", + }, + "type": "number", + }, + "name": "c", + }, + ], + "rows": Array [ + Object { + "a": "shoes", + "b": 1588024800000, + "c": 3, + }, + ], + "type": "datatable", + }, + } + } + > + <EuiDataGrid + aria-label="My fanci metric chart" + columnVisibility={ + Object { + "setVisibleColumns": [Function], + "visibleColumns": Array [ + "a", + "b", + "c", + ], + } + } + columns={ + Array [ + Object { + "actions": Object { + "additional": undefined, + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": false, + "showSortDesc": false, + }, + "cellActions": undefined, + "display": "a", + "displayAsText": "a", + "id": "a", + }, + Object { + "actions": Object { + "additional": undefined, + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": false, + "showSortDesc": false, + }, + "cellActions": undefined, + "display": "b", + "displayAsText": "b", + "id": "b", + }, + Object { + "actions": Object { + "additional": undefined, + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + "showSortAsc": false, + "showSortDesc": false, + }, + "cellActions": undefined, + "display": "c", + "displayAsText": "c", + "id": "c", + }, + ] + } + data-test-subj="lnsDataTable" + gridStyle={ + Object { + "border": "horizontal", + "header": "underline", + } + } + onColumnResize={[Function]} + renderCellValue={[Function]} + rowCount={1} + sorting={ + Object { + "columns": Array [], + "onSort": [Function], + } + } + toolbarVisibility={false} + trailingControlColumns={Array []} + /> + </ContextProvider> +</VisualizationContainer> +`; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx new file mode 100644 index 0000000000000..a8328f5eefdca --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import type { FormatFactory } from '../../types'; +import type { DataContextType } from './types'; + +export const createGridCell = ( + formatters: Record<string, ReturnType<FormatFactory>>, + DataContext: React.Context<DataContextType> +) => ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { + const { table } = useContext(DataContext); + const rowValue = table?.rows[rowIndex][columnId]; + const content = formatters[columnId]?.convert(rowValue, 'html'); + + return ( + <span + /* + * dangerouslySetInnerHTML is necessary because the field formatter might produce HTML markup + * which is produced in a safe way. + */ + dangerouslySetInnerHTML={{ __html: content }} // eslint-disable-line react/no-danger + data-test-subj="lnsTableCellContent" + className="lnsDataTableCellContent" + /> + ); +}; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx new file mode 100644 index 0000000000000..83a8d026f1315 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx @@ -0,0 +1,186 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import type { Datatable, DatatableColumnMeta } from 'src/plugins/expressions'; +import type { FormatFactory } from '../../types'; +import type { DatatableColumns } from './types'; + +export const createGridColumns = ( + bucketColumns: string[], + table: Datatable, + handleFilterClick: ( + field: string, + value: unknown, + colIndex: number, + rowIndex: number, + negate?: boolean + ) => void, + isReadOnly: boolean, + columnConfig: DatatableColumns & { type: 'lens_datatable_columns' }, + visibleColumns: string[], + formatFactory: FormatFactory, + onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void +) => { + const columnsReverseLookup = table.columns.reduce< + Record<string, { name: string; index: number; meta?: DatatableColumnMeta }> + >((memo, { id, name, meta }, i) => { + memo[id] = { name, index: i, meta }; + return memo; + }, {}); + + const bucketLookup = new Set(bucketColumns); + + const getContentData = ({ + rowIndex, + columnId, + }: Pick<EuiDataGridColumnCellActionProps, 'rowIndex' | 'columnId'>) => { + const rowValue = table.rows[rowIndex][columnId]; + const column = columnsReverseLookup[columnId]; + const contentsIsDefined = rowValue != null; + + const cellContent = formatFactory(column?.meta?.params).convert(rowValue); + return { rowValue, contentsIsDefined, cellContent }; + }; + + return visibleColumns.map((field) => { + const filterable = bucketLookup.has(field); + const { name, index: colIndex } = columnsReverseLookup[field]; + + const cellActions = filterable + ? [ + ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { + const { rowValue, contentsIsDefined, cellContent } = getContentData({ + rowIndex, + columnId, + }); + + const filterForText = i18n.translate( + 'xpack.lens.table.tableCellFilter.filterForValueText', + { + defaultMessage: 'Filter for value', + } + ); + const filterForAriaLabel = i18n.translate( + 'xpack.lens.table.tableCellFilter.filterForValueAriaLabel', + { + defaultMessage: 'Filter for value: {cellContent}', + values: { + cellContent, + }, + } + ); + + return ( + contentsIsDefined && ( + <Component + aria-label={filterForAriaLabel} + data-test-subj="lensDatatableFilterFor" + onClick={() => { + handleFilterClick(field, rowValue, colIndex, rowIndex); + closePopover(); + }} + iconType="plusInCircle" + > + {filterForText} + </Component> + ) + ); + }, + ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { + const { rowValue, contentsIsDefined, cellContent } = getContentData({ + rowIndex, + columnId, + }); + + const filterOutText = i18n.translate( + 'xpack.lens.table.tableCellFilter.filterOutValueText', + { + defaultMessage: 'Filter out value', + } + ); + const filterOutAriaLabel = i18n.translate( + 'xpack.lens.table.tableCellFilter.filterOutValueAriaLabel', + { + defaultMessage: 'Filter out value: {cellContent}', + values: { + cellContent, + }, + } + ); + + return ( + contentsIsDefined && ( + <Component + data-test-subj="lensDatatableFilterOut" + aria-label={filterOutAriaLabel} + onClick={() => { + handleFilterClick(field, rowValue, colIndex, rowIndex, true); + closePopover(); + }} + iconType="minusInCircle" + > + {filterOutText} + </Component> + ) + ); + }, + ] + : undefined; + + const initialWidth = columnConfig.columnWidth?.find(({ columnId }) => columnId === field) + ?.width; + + const columnDefinition: EuiDataGridColumn = { + id: field, + cellActions, + display: name, + displayAsText: name, + actions: { + showHide: false, + showMoveLeft: false, + showMoveRight: false, + showSortAsc: isReadOnly + ? false + : { + label: i18n.translate('xpack.lens.table.sort.ascLabel', { + defaultMessage: 'Sort asc', + }), + }, + showSortDesc: isReadOnly + ? false + : { + label: i18n.translate('xpack.lens.table.sort.descLabel', { + defaultMessage: 'Sort desc', + }), + }, + additional: isReadOnly + ? undefined + : [ + { + color: 'text', + size: 'xs', + onClick: () => onColumnResize({ columnId: field, width: undefined }), + iconType: 'empty', + label: i18n.translate('xpack.lens.table.resize.reset', { + defaultMessage: 'Reset width', + }), + 'data-test-subj': 'lensDatatableResetWidth', + isDisabled: initialWidth == null, + }, + ], + }, + }; + + if (initialWidth) { + columnDefinition.initialWidth = initialWidth; + } + + return columnDefinition; + }); +}; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/constants.ts b/x-pack/plugins/lens/public/datatable_visualization/components/constants.ts new file mode 100644 index 0000000000000..4779d42859a79 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const LENS_EDIT_SORT_ACTION = 'sort'; +export const LENS_EDIT_RESIZE_ACTION = 'resize'; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts new file mode 100644 index 0000000000000..dad9aa30b7712 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts @@ -0,0 +1,235 @@ +/* + * 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 { EuiDataGridSorting } from '@elastic/eui'; +import { Datatable } from 'src/plugins/expressions'; + +import { + createGridFilterHandler, + createGridResizeHandler, + createGridSortingConfig, +} from './table_actions'; +import { DatatableColumns, LensGridDirection } from './types'; + +function getDefaultConfig(): DatatableColumns & { + type: 'lens_datatable_columns'; +} { + return { + columnIds: [], + sortBy: '', + sortDirection: 'none', + type: 'lens_datatable_columns', + }; +} + +function createTableRef( + { withDate }: { withDate: boolean } = { withDate: false } +): React.MutableRefObject<Datatable> { + return { + current: { + type: 'datatable', + rows: [], + columns: [ + { + id: 'a', + name: 'field', + meta: { type: withDate ? 'date' : 'number', field: 'a' }, + }, + ], + }, + }; +} + +describe('Table actions', () => { + const onEditAction = jest.fn(); + + describe('Table filtering', () => { + it('should set a filter on click with the correct configuration', () => { + const onClickValue = jest.fn(); + const tableRef = createTableRef(); + const filterHandle = createGridFilterHandler(tableRef, onClickValue); + + filterHandle('a', 100, 0, 0); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: tableRef.current, + value: 100, + }, + ], + negate: false, + timeFieldName: 'a', + }); + }); + + it('should set a negate filter on click with the correct confgiuration', () => { + const onClickValue = jest.fn(); + const tableRef = createTableRef(); + const filterHandle = createGridFilterHandler(tableRef, onClickValue); + + filterHandle('a', 100, 0, 0, true); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: tableRef.current, + value: 100, + }, + ], + negate: true, + timeFieldName: 'a', + }); + }); + + it('should set a time filter on click', () => { + const onClickValue = jest.fn(); + const tableRef = createTableRef({ withDate: true }); + const filterHandle = createGridFilterHandler(tableRef, onClickValue); + + filterHandle('a', 100, 0, 0); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: tableRef.current, + value: 100, + }, + ], + negate: false, + timeFieldName: 'a', + }); + }); + + it('should set a negative time filter on click', () => { + const onClickValue = jest.fn(); + const tableRef = createTableRef({ withDate: true }); + const filterHandle = createGridFilterHandler(tableRef, onClickValue); + + filterHandle('a', 100, 0, 0, true); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: tableRef.current, + value: 100, + }, + ], + negate: true, + timeFieldName: undefined, + }); + }); + }); + describe('Table sorting', () => { + it('should create the right configuration for all types of sorting', () => { + const configs: Array<{ + input: { direction: LensGridDirection; sortBy: string }; + output: EuiDataGridSorting['columns']; + }> = [ + { input: { direction: 'asc', sortBy: 'a' }, output: [{ id: 'a', direction: 'asc' }] }, + { input: { direction: 'none', sortBy: 'a' }, output: [] }, + { input: { direction: 'asc', sortBy: '' }, output: [] }, + ]; + for (const { input, output } of configs) { + const { sortBy, direction } = input; + expect(createGridSortingConfig(sortBy, direction, onEditAction)).toMatchObject( + expect.objectContaining({ columns: output }) + ); + } + }); + + it('should return the correct next configuration value based on the current state', () => { + const sorter = createGridSortingConfig('a', 'none', onEditAction); + // Click on the 'a' column + sorter.onSort([{ id: 'a', direction: 'asc' }]); + + // Click on another column 'b' + sorter.onSort([ + { id: 'a', direction: 'asc' }, + { id: 'b', direction: 'asc' }, + ]); + + // Change the sorting of 'a' + sorter.onSort([{ id: 'a', direction: 'desc' }]); + + // Toggle the 'a' current sorting (remove sorting) + sorter.onSort([]); + + expect(onEditAction.mock.calls).toEqual([ + [ + { + action: 'sort', + columnId: 'a', + direction: 'asc', + }, + ], + [ + { + action: 'sort', + columnId: 'b', + direction: 'asc', + }, + ], + [ + { + action: 'sort', + columnId: 'a', + direction: 'desc', + }, + ], + [ + { + action: 'sort', + columnId: undefined, + direction: 'none', + }, + ], + ]); + }); + }); + describe('Table resize', () => { + const setColumnConfig = jest.fn(); + + it('should resize the table locally and globally with the given size', () => { + const columnConfig = getDefaultConfig(); + const resizer = createGridResizeHandler(columnConfig, setColumnConfig, onEditAction); + resizer({ columnId: 'a', width: 100 }); + + expect(setColumnConfig).toHaveBeenCalledWith({ + ...columnConfig, + columnWidth: [{ columnId: 'a', width: 100, type: 'lens_datatable_column_width' }], + }); + + expect(onEditAction).toHaveBeenCalledWith({ action: 'resize', columnId: 'a', width: 100 }); + }); + + it('should pull out the table custom width from the local state when passing undefined', () => { + const columnConfig = getDefaultConfig(); + columnConfig.columnWidth = [ + { columnId: 'a', width: 100, type: 'lens_datatable_column_width' }, + ]; + + const resizer = createGridResizeHandler(columnConfig, setColumnConfig, onEditAction); + resizer({ columnId: 'a', width: undefined }); + + expect(setColumnConfig).toHaveBeenCalledWith({ + ...columnConfig, + columnWidth: [], + }); + + expect(onEditAction).toHaveBeenCalledWith({ + action: 'resize', + columnId: 'a', + width: undefined, + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts new file mode 100644 index 0000000000000..38534482b81fa --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts @@ -0,0 +1,115 @@ +/* + * 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 type { EuiDataGridSorting } from '@elastic/eui'; +import type { Datatable } from 'src/plugins/expressions'; +import type { LensFilterEvent } from '../../types'; +import type { + DatatableColumns, + LensGridDirection, + LensResizeAction, + LensSortAction, +} from './types'; + +import { desanitizeFilterContext } from '../../utils'; + +export const createGridResizeHandler = ( + columnConfig: DatatableColumns & { + type: 'lens_datatable_columns'; + }, + setColumnConfig: React.Dispatch< + React.SetStateAction< + DatatableColumns & { + type: 'lens_datatable_columns'; + } + > + >, + onEditAction: (data: LensResizeAction['data']) => void +) => (eventData: { columnId: string; width: number | undefined }) => { + // directly set the local state of the component to make sure the visualization re-renders immediately, + // re-layouting and taking up all of the available space. + setColumnConfig({ + ...columnConfig, + columnWidth: [ + ...(columnConfig.columnWidth || []).filter(({ columnId }) => columnId !== eventData.columnId), + ...(eventData.width !== undefined + ? [ + { + columnId: eventData.columnId, + width: eventData.width, + type: 'lens_datatable_column_width' as const, + }, + ] + : []), + ], + }); + return onEditAction({ + action: 'resize', + columnId: eventData.columnId, + width: eventData.width, + }); +}; + +export const createGridFilterHandler = ( + tableRef: React.MutableRefObject<Datatable>, + onClickValue: (data: LensFilterEvent['data']) => void +) => ( + field: string, + value: unknown, + colIndex: number, + rowIndex: number, + negate: boolean = false +) => { + const col = tableRef.current.columns[colIndex]; + const isDate = col.meta?.type === 'date'; + const timeFieldName = negate && isDate ? undefined : col?.meta?.field; + + const data: LensFilterEvent['data'] = { + negate, + data: [ + { + row: rowIndex, + column: colIndex, + value, + table: tableRef.current, + }, + ], + timeFieldName, + }; + + onClickValue(desanitizeFilterContext(data)); +}; + +export const createGridSortingConfig = ( + sortBy: string, + sortDirection: LensGridDirection, + onEditAction: (data: LensSortAction['data']) => void +): EuiDataGridSorting => ({ + columns: + !sortBy || sortDirection === 'none' + ? [] + : [ + { + id: sortBy, + direction: sortDirection, + }, + ], + onSort: (sortingCols) => { + const newSortValue: + | { + id: string; + direction: Exclude<LensGridDirection, 'none'>; + } + | undefined = sortingCols.length <= 1 ? sortingCols[0] : sortingCols[1]; + const isNewColumn = sortBy !== (newSortValue?.id || ''); + const nextDirection = newSortValue ? newSortValue.direction : 'none'; + + return onEditAction({ + action: 'sort', + columnId: nextDirection !== 'none' || isNewColumn ? newSortValue?.id : undefined, + direction: nextDirection, + }); + }, +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.scss b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.scss new file mode 100644 index 0000000000000..5e5db2c645809 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.scss @@ -0,0 +1,3 @@ +.lnsDataTableContainer { + height: 100%; +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx new file mode 100644 index 0000000000000..df5dba749a60c --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -0,0 +1,425 @@ +/* + * 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 { shallow } from 'enzyme'; +import { mountWithIntl } from '@kbn/test/jest'; +import { EuiDataGrid } from '@elastic/eui'; +import { IAggType, IFieldFormat } from 'src/plugins/data/public'; +import { EmptyPlaceholder } from '../../shared_components'; +import { LensIconChartDatatable } from '../../assets/chart_datatable'; +import { DatatableComponent } from './table_basic'; +import { LensMultiTable } from '../../types'; +import { DatatableProps } from '../expression'; + +function sampleArgs() { + const indexPatternId = 'indexPatternId'; + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + l1: { + type: 'datatable', + columns: [ + { + id: 'a', + name: 'a', + meta: { + type: 'string', + source: 'esaggs', + field: 'a', + sourceParams: { type: 'terms', indexPatternId }, + }, + }, + { + id: 'b', + name: 'b', + meta: { + type: 'date', + field: 'b', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + indexPatternId, + }, + }, + }, + { + id: 'c', + name: 'c', + meta: { + type: 'number', + source: 'esaggs', + field: 'c', + sourceParams: { indexPatternId, type: 'count' }, + }, + }, + ], + rows: [{ a: 'shoes', b: 1588024800000, c: 3 }], + }, + }, + }; + + const args: DatatableProps['args'] = { + title: 'My fanci metric chart', + columns: { + columnIds: ['a', 'b', 'c'], + sortBy: '', + sortDirection: 'none', + type: 'lens_datatable_columns', + }, + }; + + return { data, args }; +} + +function copyData(data: LensMultiTable): LensMultiTable { + return JSON.parse(JSON.stringify(data)); +} + +describe('DatatableComponent', () => { + let onDispatchEvent: jest.Mock; + + beforeEach(() => { + onDispatchEvent = jest.fn(); + }); + + test('it renders the title and value', () => { + const { data, args } = sampleArgs(); + + expect( + shallow( + <DatatableComponent + data={data} + args={args} + formatFactory={(x) => x as IFieldFormat} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="edit" + /> + ) + ).toMatchSnapshot(); + }); + + test('it renders actions column when there are row actions', () => { + const { data, args } = sampleArgs(); + + expect( + shallow( + <DatatableComponent + data={data} + args={args} + formatFactory={(x) => x as IFieldFormat} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + rowHasRowClickTriggerActions={[true, true, true]} + renderMode="edit" + /> + ) + ).toMatchSnapshot(); + }); + + test('it should not render actions on header when it is in read only mode', () => { + const { data, args } = sampleArgs(); + + expect( + shallow( + <DatatableComponent + data={data} + args={args} + formatFactory={(x) => x as IFieldFormat} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + rowHasRowClickTriggerActions={[false, false, false]} + renderMode="display" + /> + ) + ).toMatchSnapshot(); + }); + + test('it invokes executeTriggerActions with correct context on click on top value', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + <DatatableComponent + data={{ + ...data, + dateRange: { + fromDate: new Date('2020-04-20T05:00:00.000Z'), + toDate: new Date('2020-05-03T05:00:00.000Z'), + }, + }} + args={args} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} + renderMode="edit" + /> + ); + + wrapper.find('[data-test-subj="lensDatatableFilterOut"]').first().simulate('click'); + + expect(onDispatchEvent).toHaveBeenCalledWith({ + name: 'filter', + data: { + data: [ + { + column: 0, + row: 0, + table: data.tables.l1, + value: 'shoes', + }, + ], + negate: true, + timeFieldName: 'a', + }, + }); + }); + + test('it invokes executeTriggerActions with correct context on click on timefield', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + <DatatableComponent + data={{ + ...data, + dateRange: { + fromDate: new Date('2020-04-20T05:00:00.000Z'), + toDate: new Date('2020-05-03T05:00:00.000Z'), + }, + }} + args={args} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} + renderMode="edit" + /> + ); + + wrapper.find('[data-test-subj="lensDatatableFilterFor"]').at(3).simulate('click'); + + expect(onDispatchEvent).toHaveBeenCalledWith({ + name: 'filter', + data: { + data: [ + { + column: 1, + row: 0, + table: data.tables.l1, + value: 1588024800000, + }, + ], + negate: false, + timeFieldName: 'b', + }, + }); + }); + + test('it invokes executeTriggerActions with correct context on click on timefield from range', () => { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + l1: { + type: 'datatable', + columns: [ + { + id: 'a', + name: 'a', + meta: { + type: 'date', + source: 'esaggs', + field: 'a', + sourceParams: { type: 'date_range', indexPatternId: 'a' }, + }, + }, + { + id: 'b', + name: 'b', + meta: { + type: 'number', + source: 'esaggs', + sourceParams: { type: 'count', indexPatternId: 'a' }, + }, + }, + ], + rows: [{ a: 1588024800000, b: 3 }], + }, + }, + }; + + const args: DatatableProps['args'] = { + title: '', + columns: { + columnIds: ['a', 'b'], + sortBy: '', + sortDirection: 'none', + type: 'lens_datatable_columns', + }, + }; + + const wrapper = mountWithIntl( + <DatatableComponent + data={{ + ...data, + dateRange: { + fromDate: new Date('2020-04-20T05:00:00.000Z'), + toDate: new Date('2020-05-03T05:00:00.000Z'), + }, + }} + args={args} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} + renderMode="edit" + /> + ); + + wrapper.find('[data-test-subj="lensDatatableFilterFor"]').at(1).simulate('click'); + + expect(onDispatchEvent).toHaveBeenCalledWith({ + name: 'filter', + data: { + data: [ + { + column: 0, + row: 0, + table: data.tables.l1, + value: 1588024800000, + }, + ], + negate: false, + timeFieldName: 'a', + }, + }); + }); + + test('it shows emptyPlaceholder for undefined bucketed data', () => { + const { args, data } = sampleArgs(); + const emptyData: LensMultiTable = { + ...data, + tables: { + l1: { + ...data.tables.l1, + rows: [{ a: undefined, b: undefined, c: 0 }], + }, + }, + }; + + const component = shallow( + <DatatableComponent + data={emptyData} + args={args} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn((type) => + type === 'count' ? ({ type: 'metrics' } as IAggType) : ({ type: 'buckets' } as IAggType) + )} + renderMode="edit" + /> + ); + expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDatatable); + }); + + test('it renders the table with the given sorting', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + <DatatableComponent + data={data} + args={{ + ...args, + columns: { + ...args.columns, + sortBy: 'b', + sortDirection: 'desc', + }, + }} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="edit" + /> + ); + + expect(wrapper.find(EuiDataGrid).prop('sorting')!.columns).toEqual([ + { id: 'b', direction: 'desc' }, + ]); + + wrapper.find(EuiDataGrid).prop('sorting')!.onSort([]); + + expect(onDispatchEvent).toHaveBeenCalledWith({ + name: 'edit', + data: { + action: 'sort', + columnId: undefined, + direction: 'none', + }, + }); + + wrapper + .find(EuiDataGrid) + .prop('sorting')! + .onSort([{ id: 'a', direction: 'asc' }]); + + expect(onDispatchEvent).toHaveBeenCalledWith({ + name: 'edit', + data: { + action: 'sort', + columnId: 'a', + direction: 'asc', + }, + }); + }); + + test('it renders the table with the given sorting in readOnly mode', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + <DatatableComponent + data={data} + args={{ + ...args, + columns: { + ...args.columns, + sortBy: 'b', + sortDirection: 'desc', + }, + }} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + /> + ); + + expect(wrapper.find(EuiDataGrid).prop('sorting')!.columns).toEqual([ + { id: 'b', direction: 'desc' }, + ]); + }); + + test('it should refresh the table header when the datatable data changes', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + <DatatableComponent + data={data} + args={args} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="edit" + /> + ); + // mnake a copy of the data, changing only the name of the first column + const newData = copyData(data); + newData.tables.l1.columns[0].name = 'new a'; + wrapper.setProps({ data: newData }); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="dataGridHeader"]').children().first().text()).toEqual( + 'new a' + ); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx new file mode 100644 index 0000000000000..171074d6e6797 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -0,0 +1,245 @@ +/* + * 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 './table_basic.scss'; + +import React, { useCallback, useMemo, useRef, useState, useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; +import { + EuiButtonIcon, + EuiDataGrid, + EuiDataGridControlColumn, + EuiDataGridColumn, + EuiDataGridSorting, + EuiDataGridStyle, +} from '@elastic/eui'; +import { FormatFactory, LensFilterEvent, LensTableRowContextMenuEvent } from '../../types'; +import { VisualizationContainer } from '../../visualization_container'; +import { EmptyPlaceholder } from '../../shared_components'; +import { LensIconChartDatatable } from '../../assets/chart_datatable'; +import { + DataContextType, + DatatableRenderProps, + LensSortAction, + LensResizeAction, + LensGridDirection, +} from './types'; +import { createGridColumns } from './columns'; +import { createGridCell } from './cell_value'; +import { + createGridFilterHandler, + createGridResizeHandler, + createGridSortingConfig, +} from './table_actions'; + +const DataContext = React.createContext<DataContextType>({}); + +const gridStyle: EuiDataGridStyle = { + border: 'horizontal', + header: 'underline', +}; + +export const DatatableComponent = (props: DatatableRenderProps) => { + const [firstTable] = Object.values(props.data.tables); + + const [columnConfig, setColumnConfig] = useState(props.args.columns); + const [firstLocalTable, updateTable] = useState(firstTable); + + useDeepCompareEffect(() => { + setColumnConfig(props.args.columns); + }, [props.args.columns]); + + useDeepCompareEffect(() => { + updateTable(firstTable); + }, [firstTable]); + + const firstTableRef = useRef(firstLocalTable); + firstTableRef.current = firstLocalTable; + + const hasAtLeastOneRowClickAction = props.rowHasRowClickTriggerActions?.some((x) => x); + + const { getType, dispatchEvent, renderMode, formatFactory } = props; + + const formatters: Record<string, ReturnType<FormatFactory>> = useMemo( + () => + firstLocalTable.columns.reduce( + (map, column) => ({ + ...map, + [column.id]: formatFactory(column.meta?.params), + }), + {} + ), + [firstLocalTable, formatFactory] + ); + + const onClickValue = useCallback( + (data: LensFilterEvent['data']) => { + dispatchEvent({ name: 'filter', data }); + }, + [dispatchEvent] + ); + + const onEditAction = useCallback( + (data: LensSortAction['data'] | LensResizeAction['data']) => { + if (renderMode === 'edit') { + dispatchEvent({ name: 'edit', data }); + } + }, + [dispatchEvent, renderMode] + ); + const onRowContextMenuClick = useCallback( + (data: LensTableRowContextMenuEvent['data']) => { + dispatchEvent({ name: 'tableRowContextMenuClick', data }); + }, + [dispatchEvent] + ); + + const handleFilterClick = useMemo(() => createGridFilterHandler(firstTableRef, onClickValue), [ + firstTableRef, + onClickValue, + ]); + + const bucketColumns = useMemo( + () => + columnConfig.columnIds.filter((_colId, index) => { + const col = firstTableRef.current.columns[index]; + return ( + col?.meta?.sourceParams?.type && + getType(col.meta.sourceParams.type as string)?.type === 'buckets' + ); + }), + [firstTableRef, columnConfig, getType] + ); + + const isEmpty = + firstLocalTable.rows.length === 0 || + (bucketColumns.length && + firstTable.rows.every((row) => bucketColumns.every((col) => row[col] == null))); + + const visibleColumns = useMemo(() => columnConfig.columnIds.filter((field) => !!field), [ + columnConfig, + ]); + + const { sortBy, sortDirection } = columnConfig; + + const isReadOnlySorted = renderMode !== 'edit'; + + const onColumnResize = useMemo( + () => createGridResizeHandler(columnConfig, setColumnConfig, onEditAction), + [onEditAction, setColumnConfig, columnConfig] + ); + + const columns: EuiDataGridColumn[] = useMemo( + () => + createGridColumns( + bucketColumns, + firstLocalTable, + handleFilterClick, + isReadOnlySorted, + columnConfig, + visibleColumns, + formatFactory, + onColumnResize + ), + [ + bucketColumns, + firstLocalTable, + handleFilterClick, + isReadOnlySorted, + columnConfig, + visibleColumns, + formatFactory, + onColumnResize, + ] + ); + + const trailingControlColumns: EuiDataGridControlColumn[] = useMemo(() => { + if (!hasAtLeastOneRowClickAction || !onRowContextMenuClick) { + return []; + } + return [ + { + headerCellRender: () => null, + width: 40, + id: 'trailingControlColumn', + rowCellRender: function RowCellRender({ rowIndex }) { + const { rowHasRowClickTriggerActions } = useContext(DataContext); + return ( + <EuiButtonIcon + aria-label={i18n.translate('xpack.lens.table.actionsLabel', { + defaultMessage: 'Show actions', + })} + iconType={ + !!rowHasRowClickTriggerActions && !rowHasRowClickTriggerActions[rowIndex] + ? 'empty' + : 'boxesVertical' + } + color="text" + onClick={() => { + onRowContextMenuClick({ + rowIndex, + table: firstTableRef.current, + columns: columnConfig.columnIds, + }); + }} + /> + ); + }, + }, + ]; + }, [firstTableRef, onRowContextMenuClick, columnConfig, hasAtLeastOneRowClickAction]); + + const renderCellValue = useMemo(() => createGridCell(formatters, DataContext), [formatters]); + + const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns: () => {} }), [ + visibleColumns, + ]); + + const sorting = useMemo<EuiDataGridSorting>( + () => createGridSortingConfig(sortBy, sortDirection as LensGridDirection, onEditAction), + [onEditAction, sortBy, sortDirection] + ); + + if (isEmpty) { + return <EmptyPlaceholder icon={LensIconChartDatatable} />; + } + + const dataGridAriaLabel = + props.args.title || + i18n.translate('xpack.lens.table.defaultAriaLabel', { + defaultMessage: 'Data table visualization', + }); + + return ( + <VisualizationContainer + className="lnsDataTableContainer" + reportTitle={props.args.title} + reportDescription={props.args.description} + > + <DataContext.Provider + value={{ + table: firstLocalTable, + rowHasRowClickTriggerActions: props.rowHasRowClickTriggerActions, + }} + > + <EuiDataGrid + aria-label={dataGridAriaLabel} + data-test-subj="lnsDataTable" + columns={columns} + columnVisibility={columnVisibility} + trailingControlColumns={trailingControlColumns} + rowCount={firstLocalTable.rows.length} + renderCellValue={renderCellValue} + gridStyle={gridStyle} + sorting={sorting} + onColumnResize={onColumnResize} + toolbarVisibility={false} + /> + </DataContext.Provider> + </VisualizationContainer> + ); +}; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/types.ts b/x-pack/plugins/lens/public/datatable_visualization/components/types.ts new file mode 100644 index 0000000000000..4f1a1141fdaa8 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/types.ts @@ -0,0 +1,67 @@ +/* + * 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 type { Direction } from '@elastic/eui'; +import type { IAggType } from 'src/plugins/data/public'; +import type { Datatable, RenderMode } from 'src/plugins/expressions'; +import type { FormatFactory, ILensInterpreterRenderHandlers, LensEditEvent } from '../../types'; +import type { DatatableProps } from '../expression'; +import { LENS_EDIT_SORT_ACTION, LENS_EDIT_RESIZE_ACTION } from './constants'; + +export type LensGridDirection = 'none' | Direction; + +export interface LensSortActionData { + columnId: string | undefined; + direction: LensGridDirection; +} + +export interface LensResizeActionData { + columnId: string; + width: number | undefined; +} + +export type LensSortAction = LensEditEvent<typeof LENS_EDIT_SORT_ACTION>; +export type LensResizeAction = LensEditEvent<typeof LENS_EDIT_RESIZE_ACTION>; + +export interface DatatableColumns { + columnIds: string[]; + sortBy: string; + sortDirection: string; + columnWidth?: DatatableColumnWidthResult[]; +} + +export interface DatatableColumnWidth { + columnId: string; + width: number; +} + +export type DatatableColumnWidthResult = DatatableColumnWidth & { + type: 'lens_datatable_column_width'; +}; + +export type DatatableRenderProps = DatatableProps & { + formatFactory: FormatFactory; + dispatchEvent: ILensInterpreterRenderHandlers['event']; + getType: (name: string) => IAggType; + renderMode: RenderMode; + + /** + * A boolean for each table row, which is true if the row active + * ROW_CLICK_TRIGGER actions attached to it, otherwise false. + */ + rowHasRowClickTriggerActions?: boolean[]; +}; + +export interface DatatableRender { + type: 'render'; + as: 'lens_datatable_renderer'; + value: DatatableProps; +} + +export interface DataContextType { + table?: Datatable; + rowHasRowClickTriggerActions?: boolean[]; +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.scss b/x-pack/plugins/lens/public/datatable_visualization/expression.scss deleted file mode 100644 index 7d95d73143870..0000000000000 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.scss +++ /dev/null @@ -1,13 +0,0 @@ -.lnsDataTable { - align-self: flex-start; -} - -.lnsDataTable__filter { - opacity: 0; - transition: opacity $euiAnimSpeedNormal ease-in-out; -} - -.lnsDataTable__cell:hover .lnsDataTable__filter, -.lnsDataTable__filter:focus-within { - opacity: 1; -} diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index d0811e0ad05a6..60d9461a5e0d9 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -4,18 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { mountWithIntl } from '@kbn/test/jest'; -import { getDatatable, DatatableComponent } from './expression'; +import { DatatableProps, getDatatable } from './expression'; import { LensMultiTable } from '../types'; -import { DatatableProps } from './expression'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { IFieldFormat } from '../../../../../src/plugins/data/public'; -import { IAggType } from 'src/plugins/data/public'; -import { EmptyPlaceholder } from '../shared_components'; -import { LensIconChartDatatable } from '../assets/chart_datatable'; -import { EuiBasicTable } from '@elastic/eui'; function sampleArgs() { const indexPatternId = 'indexPatternId'; @@ -78,14 +70,6 @@ function sampleArgs() { } describe('datatable_expression', () => { - let onClickValue: jest.Mock; - let onEditAction: jest.Mock; - - beforeEach(() => { - onClickValue = jest.fn(); - onEditAction = jest.fn(); - }); - describe('datatable renders', () => { test('it renders with the specified data and args', () => { const { data, args } = sampleArgs(); @@ -102,296 +86,4 @@ describe('datatable_expression', () => { }); }); }); - - describe('DatatableComponent', () => { - test('it renders the title and value', () => { - const { data, args } = sampleArgs(); - - expect( - shallow( - <DatatableComponent - data={data} - args={args} - formatFactory={(x) => x as IFieldFormat} - onClickValue={onClickValue} - getType={jest.fn()} - renderMode="edit" - /> - ) - ).toMatchSnapshot(); - }); - - test('it renders actions column when there are row actions', () => { - const { data, args } = sampleArgs(); - - expect( - shallow( - <DatatableComponent - data={data} - args={args} - formatFactory={(x) => x as IFieldFormat} - onClickValue={onClickValue} - getType={jest.fn()} - onRowContextMenuClick={() => undefined} - rowHasRowClickTriggerActions={[true, true, true]} - renderMode="edit" - /> - ) - ).toMatchSnapshot(); - }); - - test('it invokes executeTriggerActions with correct context on click on top value', () => { - const { args, data } = sampleArgs(); - - const wrapper = mountWithIntl( - <DatatableComponent - data={{ - ...data, - dateRange: { - fromDate: new Date('2020-04-20T05:00:00.000Z'), - toDate: new Date('2020-05-03T05:00:00.000Z'), - }, - }} - args={args} - formatFactory={(x) => x as IFieldFormat} - onClickValue={onClickValue} - getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} - renderMode="edit" - /> - ); - - wrapper.find('[data-test-subj="lensDatatableFilterOut"]').first().simulate('click'); - - expect(onClickValue).toHaveBeenCalledWith({ - data: [ - { - column: 0, - row: 0, - table: data.tables.l1, - value: 'shoes', - }, - ], - negate: true, - timeFieldName: 'a', - }); - }); - - test('it invokes executeTriggerActions with correct context on click on timefield', () => { - const { args, data } = sampleArgs(); - - const wrapper = mountWithIntl( - <DatatableComponent - data={{ - ...data, - dateRange: { - fromDate: new Date('2020-04-20T05:00:00.000Z'), - toDate: new Date('2020-05-03T05:00:00.000Z'), - }, - }} - args={args} - formatFactory={(x) => x as IFieldFormat} - onClickValue={onClickValue} - getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} - renderMode="edit" - /> - ); - - wrapper.find('[data-test-subj="lensDatatableFilterFor"]').at(3).simulate('click'); - - expect(onClickValue).toHaveBeenCalledWith({ - data: [ - { - column: 1, - row: 0, - table: data.tables.l1, - value: 1588024800000, - }, - ], - negate: false, - timeFieldName: 'b', - }); - }); - - test('it invokes executeTriggerActions with correct context on click on timefield from range', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - l1: { - type: 'datatable', - columns: [ - { - id: 'a', - name: 'a', - meta: { - type: 'date', - source: 'esaggs', - field: 'a', - sourceParams: { type: 'date_range', indexPatternId: 'a' }, - }, - }, - { - id: 'b', - name: 'b', - meta: { - type: 'number', - source: 'esaggs', - sourceParams: { type: 'count', indexPatternId: 'a' }, - }, - }, - ], - rows: [{ a: 1588024800000, b: 3 }], - }, - }, - }; - - const args: DatatableProps['args'] = { - title: '', - columns: { - columnIds: ['a', 'b'], - sortBy: '', - sortDirection: 'none', - type: 'lens_datatable_columns', - }, - }; - - const wrapper = mountWithIntl( - <DatatableComponent - data={{ - ...data, - dateRange: { - fromDate: new Date('2020-04-20T05:00:00.000Z'), - toDate: new Date('2020-05-03T05:00:00.000Z'), - }, - }} - args={args} - formatFactory={(x) => x as IFieldFormat} - onClickValue={onClickValue} - getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} - renderMode="edit" - /> - ); - - wrapper.find('[data-test-subj="lensDatatableFilterFor"]').at(1).simulate('click'); - - expect(onClickValue).toHaveBeenCalledWith({ - data: [ - { - column: 0, - row: 0, - table: data.tables.l1, - value: 1588024800000, - }, - ], - negate: false, - timeFieldName: 'a', - }); - }); - - test('it shows emptyPlaceholder for undefined bucketed data', () => { - const { args, data } = sampleArgs(); - const emptyData: LensMultiTable = { - ...data, - tables: { - l1: { - ...data.tables.l1, - rows: [{ a: undefined, b: undefined, c: 0 }], - }, - }, - }; - - const component = shallow( - <DatatableComponent - data={emptyData} - args={args} - formatFactory={(x) => x as IFieldFormat} - onClickValue={onClickValue} - getType={jest.fn((type) => - type === 'count' ? ({ type: 'metrics' } as IAggType) : ({ type: 'buckets' } as IAggType) - )} - renderMode="edit" - /> - ); - expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDatatable); - }); - - test('it renders the table with the given sorting', () => { - const { data, args } = sampleArgs(); - - const wrapper = mountWithIntl( - <DatatableComponent - data={data} - args={{ - ...args, - columns: { - ...args.columns, - sortBy: 'b', - sortDirection: 'desc', - }, - }} - formatFactory={(x) => x as IFieldFormat} - onClickValue={onClickValue} - onEditAction={onEditAction} - getType={jest.fn()} - renderMode="edit" - /> - ); - - // there's currently no way to detect the sorting column via DOM - expect( - wrapper.exists('[className*="isSorted"][data-test-subj="tableHeaderSortButton"]') - ).toBe(true); - // check that the sorting is passing the right next state for the same column - wrapper - .find('[className*="isSorted"][data-test-subj="tableHeaderSortButton"]') - .first() - .simulate('click'); - - expect(onEditAction).toHaveBeenCalledWith({ - action: 'sort', - columnId: undefined, - direction: 'none', - }); - - // check that the sorting is passing the right next state for another column - wrapper - .find('[data-test-subj="tableHeaderSortButton"]') - .not('[className*="isSorted"]') - .first() - .simulate('click'); - - expect(onEditAction).toHaveBeenCalledWith({ - action: 'sort', - columnId: 'a', - direction: 'asc', - }); - }); - - test('it renders the table with the given sorting in readOnly mode', () => { - const { data, args } = sampleArgs(); - - const wrapper = mountWithIntl( - <DatatableComponent - data={data} - args={{ - ...args, - columns: { - ...args.columns, - sortBy: 'b', - sortDirection: 'desc', - }, - }} - formatFactory={(x) => x as IFieldFormat} - onClickValue={onClickValue} - onEditAction={onEditAction} - getType={jest.fn()} - renderMode="display" - /> - ); - - expect(wrapper.find(EuiBasicTable).prop('sorting')).toMatchObject({ - sort: undefined, - allowNeutralSort: true, - }); - }); - }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 57289fc0ac169..e8a0abb0316db 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -4,62 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import './expression.scss'; - -import React, { useMemo } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; -import { - EuiBasicTable, - EuiFlexGroup, - EuiButtonIcon, - EuiFlexItem, - EuiToolTip, - Direction, - EuiScreenReaderOnly, - EuiIcon, - EuiBasicTableColumn, - EuiTableActionsColumnType, -} from '@elastic/eui'; -import { IAggType } from 'src/plugins/data/public'; -import { Datatable, DatatableColumnMeta, RenderMode } from 'src/plugins/expressions'; -import { - FormatFactory, - ILensInterpreterRenderHandlers, - LensEditEvent, - LensFilterEvent, - LensMultiTable, - LensTableRowContextMenuEvent, -} from '../types'; -import { +import type { IAggType } from 'src/plugins/data/public'; +import type { + DatatableColumnMeta, ExpressionFunctionDefinition, ExpressionRenderDefinition, -} from '../../../../../src/plugins/expressions/public'; -import { VisualizationContainer } from '../visualization_container'; -import { EmptyPlaceholder } from '../shared_components'; -import { desanitizeFilterContext } from '../utils'; -import { LensIconChartDatatable } from '../assets/chart_datatable'; +} from 'src/plugins/expressions'; import { getSortingCriteria } from './sorting'; -export const LENS_EDIT_SORT_ACTION = 'sort'; - -export interface LensSortActionData { - columnId: string | undefined; - direction: 'asc' | 'desc' | 'none'; -} - -type LensSortAction = LensEditEvent<typeof LENS_EDIT_SORT_ACTION>; - -// This is a way to circumvent the explicit "any" forbidden type -type TableRowField = Datatable['rows'][number] & { rowIndex: number }; +import { DatatableComponent } from './components/table_basic'; -export interface DatatableColumns { - columnIds: string[]; - sortBy: string; - sortDirection: string; -} +import type { FormatFactory, ILensInterpreterRenderHandlers, LensMultiTable } from '../types'; +import type { + DatatableRender, + DatatableColumns, + DatatableColumnWidth, + DatatableColumnWidthResult, +} from './components/types'; interface Args { title: string; @@ -72,27 +38,6 @@ export interface DatatableProps { args: Args; } -type DatatableRenderProps = DatatableProps & { - formatFactory: FormatFactory; - onClickValue: (data: LensFilterEvent['data']) => void; - onEditAction?: (data: LensSortAction['data']) => void; - getType: (name: string) => IAggType; - renderMode: RenderMode; - onRowContextMenuClick?: (data: LensTableRowContextMenuEvent['data']) => void; - - /** - * A boolean for each table row, which is true if the row active - * ROW_CLICK_TRIGGER actions attached to it, otherwise false. - */ - rowHasRowClickTriggerActions?: boolean[]; -}; - -export interface DatatableRender { - type: 'render'; - as: 'lens_datatable_renderer'; - value: DatatableProps; -} - function isRange(meta: { params?: { id?: string } } | undefined) { return meta?.params?.id === 'range'; } @@ -191,6 +136,11 @@ export const datatableColumns: ExpressionFunctionDefinition< multi: true, help: '', }, + columnWidth: { + types: ['lens_datatable_column_width'], + multi: true, + help: '', + }, }, fn: function fn(input: unknown, args: DatatableColumns) { return { @@ -200,6 +150,35 @@ export const datatableColumns: ExpressionFunctionDefinition< }, }; +export const datatableColumnWidth: ExpressionFunctionDefinition< + 'lens_datatable_column_width', + null, + DatatableColumnWidth, + DatatableColumnWidthResult +> = { + name: 'lens_datatable_column_width', + aliases: [], + type: 'lens_datatable_column_width', + help: '', + inputTypes: ['null'], + args: { + columnId: { + types: ['string'], + help: '', + }, + width: { + types: ['number'], + help: '', + }, + }, + fn: function fn(input: unknown, args: DatatableColumnWidth) { + return { + type: 'lens_datatable_column_width', + ...args, + }; + }, +}; + export const getDatatableRenderer = (dependencies: { formatFactory: FormatFactory; getType: Promise<(name: string) => IAggType>; @@ -217,18 +196,6 @@ export const getDatatableRenderer = (dependencies: { handlers: ILensInterpreterRenderHandlers ) => { const resolvedGetType = await dependencies.getType; - const onClickValue = (data: LensFilterEvent['data']) => { - handlers.event({ name: 'filter', data }); - }; - - const onEditAction = (data: LensSortAction['data']) => { - if (handlers.getRenderMode() === 'edit') { - handlers.event({ name: 'edit', data }); - } - }; - const onRowContextMenuClick = (data: LensTableRowContextMenuEvent['data']) => { - handlers.event({ name: 'tableRowContextMenuClick', data }); - }; const { hasCompatibleActions } = handlers; // An entry for each table row, whether it has any actions attached to @@ -263,10 +230,8 @@ export const getDatatableRenderer = (dependencies: { <DatatableComponent {...config} formatFactory={dependencies.formatFactory} - onClickValue={onClickValue} - onEditAction={onEditAction} + dispatchEvent={handlers.event} renderMode={handlers.getRenderMode()} - onRowContextMenuClick={onRowContextMenuClick} getType={resolvedGetType} rowHasRowClickTriggerActions={rowHasRowClickTriggerActions} /> @@ -279,281 +244,3 @@ export const getDatatableRenderer = (dependencies: { handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); }, }); - -function getNextOrderValue(currentValue: LensSortAction['data']['direction']) { - const states: Array<LensSortAction['data']['direction']> = ['asc', 'desc', 'none']; - const newStateIndex = (1 + states.findIndex((state) => state === currentValue)) % states.length; - return states[newStateIndex]; -} - -function getDirectionLongLabel(sortDirection: LensSortAction['data']['direction']) { - if (sortDirection === 'none') { - return sortDirection; - } - return sortDirection === 'asc' ? 'ascending' : 'descending'; -} - -function getHeaderSortingCell( - name: string, - columnId: string, - sorting: Omit<LensSortAction['data'], 'action'>, - sortingLabel: string -) { - if (columnId !== sorting.columnId || sorting.direction === 'none') { - return name || ''; - } - // This is a workaround to hijack the title value of the header cell - return ( - <span aria-sort={getDirectionLongLabel(sorting.direction)}> - {name || ''} - <EuiScreenReaderOnly> - <span>{sortingLabel}</span> - </EuiScreenReaderOnly> - <EuiIcon - className="euiTableSortIcon" - type={sorting.direction === 'asc' ? 'sortUp' : 'sortDown'} - size="m" - aria-label={sortingLabel} - /> - </span> - ); -} - -export function DatatableComponent(props: DatatableRenderProps) { - const [firstTable] = Object.values(props.data.tables); - const formatters: Record<string, ReturnType<FormatFactory>> = {}; - - firstTable.columns.forEach((column) => { - formatters[column.id] = props.formatFactory(column.meta?.params); - }); - - const { onClickValue, onEditAction, onRowContextMenuClick } = props; - const handleFilterClick = useMemo( - () => (field: string, value: unknown, colIndex: number, negate: boolean = false) => { - const col = firstTable.columns[colIndex]; - const isDate = col.meta?.type === 'date'; - const timeFieldName = negate && isDate ? undefined : col?.meta?.field; - const rowIndex = firstTable.rows.findIndex((row) => row[field] === value); - - const data: LensFilterEvent['data'] = { - negate, - data: [ - { - row: rowIndex, - column: colIndex, - value, - table: firstTable, - }, - ], - timeFieldName, - }; - onClickValue(desanitizeFilterContext(data)); - }, - [firstTable, onClickValue] - ); - - const bucketColumns = firstTable.columns - .filter((col) => { - return ( - col?.meta?.sourceParams?.type && - props.getType(col.meta.sourceParams.type as string)?.type === 'buckets' - ); - }) - .map((col) => col.id); - - const isEmpty = - firstTable.rows.length === 0 || - (bucketColumns.length && - firstTable.rows.every((row) => - bucketColumns.every((col) => typeof row[col] === 'undefined') - )); - - if (isEmpty) { - return <EmptyPlaceholder icon={LensIconChartDatatable} />; - } - - const visibleColumns = props.args.columns.columnIds.filter((field) => !!field); - const columnsReverseLookup = firstTable.columns.reduce< - Record<string, { name: string; index: number; meta?: DatatableColumnMeta }> - >((memo, { id, name, meta }, i) => { - memo[id] = { name, index: i, meta }; - return memo; - }, {}); - - const { sortBy, sortDirection } = props.args.columns; - - const sortedRows: TableRowField[] = - firstTable?.rows.map((row, rowIndex) => ({ ...row, rowIndex })) || []; - const isReadOnlySorted = props.renderMode !== 'edit'; - - const sortedInLabel = i18n.translate('xpack.lens.datatableSortedInReadOnlyMode', { - defaultMessage: 'Sorted in {sortValue} order', - values: { - sortValue: sortDirection === 'asc' ? 'ascending' : 'descending', - }, - }); - - const tableColumns: Array<EuiBasicTableColumn<TableRowField>> = visibleColumns.map((field) => { - const filterable = bucketColumns.includes(field); - const { name, index: colIndex, meta } = columnsReverseLookup[field]; - const fieldName = meta?.field; - const nameContent = !isReadOnlySorted - ? name - : getHeaderSortingCell( - name, - field, - { - columnId: sortBy, - direction: sortDirection as LensSortAction['data']['direction'], - }, - sortedInLabel - ); - return { - field, - name: nameContent, - sortable: !isReadOnlySorted, - render: (value: unknown) => { - const formattedValue = formatters[field]?.convert(value); - - if (filterable) { - return ( - <EuiFlexGroup - className="lnsDataTable__cell" - data-test-subj="lnsDataTableCellValueFilterable" - gutterSize="xs" - > - <EuiFlexItem grow={false}>{formattedValue}</EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFlexGroup - responsive={false} - gutterSize="none" - alignItems="center" - className="lnsDataTable__filter" - > - <EuiToolTip - position="bottom" - content={i18n.translate('xpack.lens.includeValueButtonTooltip', { - defaultMessage: 'Include value', - })} - > - <EuiButtonIcon - iconType="plusInCircle" - color="text" - aria-label={i18n.translate('xpack.lens.includeValueButtonAriaLabel', { - defaultMessage: `Include {value}`, - values: { - value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`, - }, - })} - data-test-subj="lensDatatableFilterFor" - onClick={() => handleFilterClick(field, value, colIndex)} - /> - </EuiToolTip> - <EuiFlexItem grow={false}> - <EuiToolTip - position="bottom" - content={i18n.translate('xpack.lens.excludeValueButtonTooltip', { - defaultMessage: 'Exclude value', - })} - > - <EuiButtonIcon - iconType="minusInCircle" - color="text" - aria-label={i18n.translate('xpack.lens.excludeValueButtonAriaLabel', { - defaultMessage: `Exclude {value}`, - values: { - value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`, - }, - })} - data-test-subj="lensDatatableFilterOut" - onClick={() => handleFilterClick(field, value, colIndex, true)} - /> - </EuiToolTip> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - ); - } - return <span data-test-subj="lnsDataTableCellValue">{formattedValue}</span>; - }, - }; - }); - - if (!!props.rowHasRowClickTriggerActions && !!onRowContextMenuClick) { - const hasAtLeastOneRowClickAction = props.rowHasRowClickTriggerActions.find((x) => x); - if (hasAtLeastOneRowClickAction) { - const actions: EuiTableActionsColumnType<TableRowField> = { - name: i18n.translate('xpack.lens.datatable.actionsColumnName', { - defaultMessage: 'Actions', - }), - actions: [ - { - name: i18n.translate('xpack.lens.tableRowMore', { - defaultMessage: 'More', - }), - description: i18n.translate('xpack.lens.tableRowMoreDescription', { - defaultMessage: 'Table row context menu', - }), - type: 'icon', - icon: ({ rowIndex }: { rowIndex: number }) => { - if ( - !!props.rowHasRowClickTriggerActions && - !props.rowHasRowClickTriggerActions[rowIndex] - ) - return 'empty'; - return 'boxesVertical'; - }, - onClick: ({ rowIndex }) => { - onRowContextMenuClick({ - rowIndex, - table: firstTable, - columns: props.args.columns.columnIds, - }); - }, - }, - ], - }; - tableColumns.push(actions); - } - } - - return ( - <VisualizationContainer - reportTitle={props.args.title} - reportDescription={props.args.description} - > - <EuiBasicTable - className="lnsDataTable" - data-test-subj="lnsDataTable" - tableLayout="auto" - sorting={{ - sort: - !sortBy || sortDirection === 'none' || isReadOnlySorted - ? undefined - : { - field: sortBy, - direction: sortDirection as Direction, - }, - allowNeutralSort: true, // this flag enables the 3rd Neutral state on the column header - }} - onChange={(event: { sort?: { field: string } }) => { - if (event.sort && onEditAction) { - const isNewColumn = sortBy !== event.sort.field; - // unfortunately the neutral state is not propagated and we need to manually handle it - const nextDirection = getNextOrderValue( - (isNewColumn ? 'none' : sortDirection) as LensSortAction['data']['direction'] - ); - return onEditAction({ - action: 'sort', - columnId: nextDirection !== 'none' || isNewColumn ? event.sort.field : undefined, - direction: nextDirection, - }); - } - }} - columns={tableColumns} - items={sortedRows} - /> - </VisualizationContainer> - ); -} diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index 42d2ff6a220c0..cf23d56adb915 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -29,12 +29,14 @@ export class DatatableVisualization { const { getDatatable, datatableColumns, + datatableColumnWidth, getDatatableRenderer, datatableVisualization, } = await import('../async_services'); const resolvedFormatFactory = await formatFactory; expressions.registerFunction(() => datatableColumns); + expressions.registerFunction(() => datatableColumnWidth); expressions.registerFunction(() => getDatatable({ formatFactory: resolvedFormatFactory })); expressions.registerRenderer(() => getDatatableRenderer({ diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 088246ccf4b9c..f067093891d29 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -408,6 +408,7 @@ describe('Datatable Visualization', () => { columnIds: ['c', 'b'], sortBy: [''], sortDirection: ['none'], + columnWidth: [], }); }); @@ -467,4 +468,80 @@ describe('Datatable Visualization', () => { expect(error).toBeUndefined(); }); }); + + describe('#onEditAction', () => { + it('should add a sort column to the state', () => { + const currentState: DatatableVisualizationState = { + layers: [ + { + layerId: 'foo', + columns: ['saved'], + }, + ], + }; + expect( + datatableVisualization.onEditAction!(currentState, { + name: 'edit', + data: { action: 'sort', columnId: 'saved', direction: 'none' }, + }) + ).toEqual({ + ...currentState, + sorting: { + columnId: 'saved', + direction: 'none', + }, + }); + }); + + it('should add a custom width to a column in the state', () => { + const currentState: DatatableVisualizationState = { + layers: [ + { + layerId: 'foo', + columns: ['saved'], + }, + ], + }; + expect( + datatableVisualization.onEditAction!(currentState, { + name: 'edit', + data: { action: 'resize', columnId: 'saved', width: 500 }, + }) + ).toEqual({ + ...currentState, + columnWidth: [ + { + columnId: 'saved', + width: 500, + }, + ], + }); + }); + + it('should clear custom width value for the column from the state', () => { + const currentState: DatatableVisualizationState = { + layers: [ + { + layerId: 'foo', + columns: ['saved'], + }, + ], + columnWidth: [ + { + columnId: 'saved', + width: 500, + }, + ], + }; + expect( + datatableVisualization.onEditAction!(currentState, { + name: 'edit', + data: { action: 'resize', columnId: 'saved', width: undefined }, + }) + ).toEqual({ + ...currentState, + columnWidth: [], + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index e4f787a265186..3df9e8a5145bc 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -6,13 +6,14 @@ import { Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; -import { +import type { SuggestionRequest, Visualization, VisualizationSuggestion, Operation, DatasourcePublicAPI, } from '../types'; +import type { DatatableColumnWidth } from './components/types'; import { LensIconChartDatatable } from '../assets/chart_datatable'; export interface LayerState { @@ -26,6 +27,7 @@ export interface DatatableVisualizationState { columnId: string | undefined; direction: 'asc' | 'desc' | 'none'; }; + columnWidth?: DatatableColumnWidth[]; } function newLayerState(layerId: string): LayerState { @@ -239,6 +241,19 @@ export const datatableVisualization: Visualization<DatatableVisualizationState> columnIds: operations.map((o) => o.columnId), sortBy: [state.sorting?.columnId || ''], sortDirection: [state.sorting?.direction || 'none'], + columnWidth: (state.columnWidth || []).map((columnWidth) => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_datatable_column_width', + arguments: { + columnId: [columnWidth.columnId], + width: [columnWidth.width], + }, + }, + ], + })), }, }, ], @@ -255,16 +270,28 @@ export const datatableVisualization: Visualization<DatatableVisualizationState> }, onEditAction(state, event) { - if (event.data.action !== 'sort') { - return state; + switch (event.data.action) { + case 'sort': + return { + ...state, + sorting: { + columnId: event.data.columnId, + direction: event.data.direction, + }, + }; + case 'resize': + return { + ...state, + columnWidth: [ + ...(state.columnWidth || []).filter(({ columnId }) => columnId !== event.data.columnId), + ...(event.data.width !== undefined + ? [{ columnId: event.data.columnId, width: event.data.width }] + : []), + ], + }; + default: + return state; } - return { - ...state, - sorting: { - columnId: event.data.columnId, - direction: event.data.direction, - }, - }; }, }; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 9feed918635b3..907ef3a700ce6 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -22,10 +22,14 @@ import { DateRange } from '../common'; import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../src/plugins/ui_actions/public'; import { RangeSelectContext, ValueClickContext } from '../../../../src/plugins/embeddable/public'; +import { + LENS_EDIT_SORT_ACTION, + LENS_EDIT_RESIZE_ACTION, +} from './datatable_visualization/components/constants'; import type { LensSortActionData, - LENS_EDIT_SORT_ACTION, -} from './datatable_visualization/expression'; + LensResizeActionData, +} from './datatable_visualization/components/types'; export type ErrorCallback = (e: { message: string }) => void; @@ -641,6 +645,7 @@ export interface LensBrushEvent { // Use same technique as TriggerContext interface LensEditContextMapping { [LENS_EDIT_SORT_ACTION]: LensSortActionData; + [LENS_EDIT_RESIZE_ACTION]: LensResizeActionData; } type LensEditSupportedActions = keyof LensEditContextMapping; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ca58c43ba3f98..47267dc36673d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11166,7 +11166,6 @@ "xpack.lens.configure.invalidConfigTooltipClick": "詳細はクリックしてください。", "xpack.lens.customBucketContainer.dragToReorder": "ドラッグして並べ替え", "xpack.lens.dataPanelWrapper.switchDatasource": "データソースに切り替える", - "xpack.lens.datatable.actionsColumnName": "アクション", "xpack.lens.datatable.breakdown": "内訳の基準", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "データベースレンダー", @@ -11176,7 +11175,6 @@ "xpack.lens.datatable.titleLabel": "タイトル", "xpack.lens.datatable.visualizationName": "データベース", "xpack.lens.datatable.visualizationOf": "テーブル {operations}", - "xpack.lens.datatableSortedInReadOnlyMode": "{sortValue} 順で並べ替え", "xpack.lens.datatypes.boolean": "ブール", "xpack.lens.datatypes.date": "日付", "xpack.lens.datatypes.ipAddress": "IP", @@ -11212,8 +11210,6 @@ "xpack.lens.editorFrame.suggestionPanelTitle": "提案", "xpack.lens.embeddable.failure": "ビジュアライゼーションを表示できませんでした", "xpack.lens.embeddableDisplayName": "レンズ", - "xpack.lens.excludeValueButtonAriaLabel": "{value}を除外", - "xpack.lens.excludeValueButtonTooltip": "値を除外", "xpack.lens.fieldFormats.longSuffix.d": "日単位", "xpack.lens.fieldFormats.longSuffix.h": "時間単位", "xpack.lens.fieldFormats.longSuffix.m": "分単位", @@ -11244,8 +11240,6 @@ "xpack.lens.functions.renameColumns.idMap.help": "キーが古い列 ID で値が対応する新しい列 ID となるように JSON エンコーディングされたオブジェクトです。他の列 ID はすべてのそのままです。", "xpack.lens.functions.timeScale.dateColumnMissingMessage": "指定した dateColumnId {columnId} は存在しません。", "xpack.lens.functions.timeScale.timeInfoMissingMessage": "日付ヒストグラム情報を取得できませんでした", - "xpack.lens.includeValueButtonAriaLabel": "{value}を含める", - "xpack.lens.includeValueButtonTooltip": "値を含める", "xpack.lens.indexPattern.allFieldsLabel": "すべてのフィールド", "xpack.lens.indexPattern.allFieldsLabelHelp": "使用可能なフィールドには、フィルターと一致する最初の 500 件のドキュメントのデータがあります。すべてのフィールドを表示するには、空のフィールドを展開します。一部のフィールドタイプは、完全なテキストおよびグラフィックフィールドを含む Lens では、ビジュアライゼーションできません。", "xpack.lens.indexPattern.availableFieldsLabel": "利用可能なフィールド", @@ -11449,8 +11443,6 @@ "xpack.lens.sugegstion.refreshSuggestionLabel": "更新", "xpack.lens.suggestion.refreshSuggestionTooltip": "選択したビジュアライゼーションに基づいて、候補を更新します。", "xpack.lens.suggestions.currentVisLabel": "現在のビジュアライゼーション", - "xpack.lens.tableRowMore": "詳細", - "xpack.lens.tableRowMoreDescription": "テーブル行コンテキストメニュー", "xpack.lens.timeScale.removeLabel": "時間単位で正規化を削除", "xpack.lens.visTypeAlias.description": "ドラッグアンドドロップエディターでビジュアライゼーションを作成します。いつでもビジュアライゼーションタイプを切り替えることができます。", "xpack.lens.visTypeAlias.note": "ほとんどのユーザーに推奨されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ae148b9a0c133..3f78abf14ae38 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11195,7 +11195,6 @@ "xpack.lens.configure.invalidConfigTooltipClick": "单击了解更多详情。", "xpack.lens.customBucketContainer.dragToReorder": "拖动以重新排序", "xpack.lens.dataPanelWrapper.switchDatasource": "切换到数据源", - "xpack.lens.datatable.actionsColumnName": "操作", "xpack.lens.datatable.breakdown": "细分方式", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "数据表呈现器", @@ -11205,7 +11204,6 @@ "xpack.lens.datatable.titleLabel": "标题", "xpack.lens.datatable.visualizationName": "数据表", "xpack.lens.datatable.visualizationOf": "表{operations}", - "xpack.lens.datatableSortedInReadOnlyMode": "按 {sortValue} 排序", "xpack.lens.datatypes.boolean": "布尔值", "xpack.lens.datatypes.date": "日期", "xpack.lens.datatypes.ipAddress": "IP", @@ -11241,8 +11239,6 @@ "xpack.lens.editorFrame.suggestionPanelTitle": "建议", "xpack.lens.embeddable.failure": "无法显示可视化", "xpack.lens.embeddableDisplayName": "lens", - "xpack.lens.excludeValueButtonAriaLabel": "排除 {value}", - "xpack.lens.excludeValueButtonTooltip": "排除值", "xpack.lens.fieldFormats.longSuffix.d": "每天", "xpack.lens.fieldFormats.longSuffix.h": "每小时", "xpack.lens.fieldFormats.longSuffix.m": "每分钟", @@ -11273,8 +11269,6 @@ "xpack.lens.functions.renameColumns.idMap.help": "旧列 ID 为键且相应新列 ID 为值的 JSON 编码对象。所有其他列 ID 都将保留。", "xpack.lens.functions.timeScale.dateColumnMissingMessage": "指定的 dateColumnId {columnId} 不存在。", "xpack.lens.functions.timeScale.timeInfoMissingMessage": "无法获取日期直方图信息", - "xpack.lens.includeValueButtonAriaLabel": "包括 {value}", - "xpack.lens.includeValueButtonTooltip": "包括值", "xpack.lens.indexPattern.allFieldsLabel": "所有字段", "xpack.lens.indexPattern.allFieldsLabelHelp": "可用字段在与您的筛选匹配的前 500 个文档中有数据。要查看所有字段,请展开空字段。一些字段类型无法在 Lens 中可视化,包括全文本字段和地理字段。", "xpack.lens.indexPattern.availableFieldsLabel": "可用字段", @@ -11478,8 +11472,6 @@ "xpack.lens.sugegstion.refreshSuggestionLabel": "刷新", "xpack.lens.suggestion.refreshSuggestionTooltip": "基于选定可视化刷新建议。", "xpack.lens.suggestions.currentVisLabel": "当前可视化", - "xpack.lens.tableRowMore": "更多", - "xpack.lens.tableRowMoreDescription": "表格行上下文菜单", "xpack.lens.timeScale.removeLabel": "删除按时间单位标准化", "xpack.lens.visTypeAlias.description": "使用拖放编辑器创建可视化。随时在可视化类型之间切换。", "xpack.lens.visTypeAlias.note": "适合绝大多数用户。", diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 88682d475146f..badcadedd7138 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -561,5 +561,40 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await testSubjects.isEnabled('lnsApp_downloadCSVButton')).to.eql(true); }); + + it('should able to sort a table by a column', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + // Sort by number + await PageObjects.lens.changeTableSortingBy(2, 'asc'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDatatableCellText(0, 2)).to.eql('17,246'); + // Now sort by IP + await PageObjects.lens.changeTableSortingBy(0, 'asc'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('78.83.247.30'); + // Change the sorting + await PageObjects.lens.changeTableSortingBy(0, 'desc'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('169.228.188.120'); + // Remove the sorting + await PageObjects.lens.changeTableSortingBy(0, 'none'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.isDatatableHeaderSorted(0)).to.eql(false); + }); + + it('should able to use filters cell actions in table', async () => { + const firstCellContent = await PageObjects.lens.getDatatableCellText(0, 0); + await PageObjects.lens.clickTableCellAction(0, 0, 'lensDatatableFilterOut'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect( + await find.existsByCssSelector( + `[data-test-subj*="filter-value-${firstCellContent}"][data-test-subj*="filter-negated"]` + ) + ).to.eql(true); + }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 31a4d6e29fc35..dabead6ffbdad 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -506,13 +506,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param index - index of th element in datatable */ async getDatatableHeaderText(index = 0) { - return find - .byCssSelector( - `[data-test-subj="lnsDataTable"] thead th:nth-child(${ - index + 1 - }) .euiTableCellContent__text` - ) - .then((el) => el.getVisibleText()); + const el = await this.getDatatableHeader(index); + return el.getVisibleText(); }, /** @@ -522,13 +517,55 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param colIndex - index of column of the cell */ async getDatatableCellText(rowIndex = 0, colIndex = 0) { - return find - .byCssSelector( - `[data-test-subj="lnsDataTable"] tr:nth-child(${rowIndex + 1}) td:nth-child(${ - colIndex + 1 - })` - ) - .then((el) => el.getVisibleText()); + const el = await this.getDatatableCell(rowIndex, colIndex); + return el.getVisibleText(); + }, + + async getDatatableHeader(index = 0) { + return find.byCssSelector( + `[data-test-subj="lnsDataTable"] [data-test-subj="dataGridHeader"] [role=columnheader]:nth-child(${ + index + 1 + })` + ); + }, + + async getDatatableCell(rowIndex = 0, colIndex = 0) { + return await find.byCssSelector( + `[data-test-subj="lnsDataTable"] [data-test-subj="dataGridRow"]:nth-child(${ + rowIndex + 2 // this is a bit specific for EuiDataGrid: the first row is the Header + }) [data-test-subj="dataGridRowCell"]:nth-child(${colIndex + 1})` + ); + }, + + async isDatatableHeaderSorted(index = 0) { + return find.existsByCssSelector( + `[data-test-subj="lnsDataTable"] [data-test-subj="dataGridHeader"] [role=columnheader]:nth-child(${ + index + 1 + }) [data-test-subj^="dataGridHeaderCellSortingIcon"]` + ); + }, + + async changeTableSortingBy(colIndex = 0, direction: 'none' | 'asc' | 'desc') { + const el = await this.getDatatableHeader(colIndex); + await el.click(); + let buttonEl; + if (direction !== 'none') { + buttonEl = await find.byCssSelector( + `[data-test-subj^="dataGridHeaderCellActionGroup"] [title="Sort ${direction}"]` + ); + } else { + buttonEl = await find.byCssSelector( + `[data-test-subj^="dataGridHeaderCellActionGroup"] li[class$="selected"] [title^="Sort"]` + ); + } + return buttonEl.click(); + }, + + async clickTableCellAction(rowIndex = 0, colIndex = 0, actionTestSub: string) { + const el = await this.getDatatableCell(rowIndex, colIndex); + await el.focus(); + const action = await el.findByTestSubject(actionTestSub); + return action.click(); }, /** From 049135192e397f089adce5c50639fcd2d563d8d2 Mon Sep 17 00:00:00 2001 From: ymao1 <ying.mao@elastic.co> Date: Fri, 29 Jan 2021 07:45:00 -0500 Subject: [PATCH 112/163] [Alerting] Search alert (#88528) * Adding es query alert type to server with commented out executor * Adding skeleton es query alert to client with JSON editor. Pulled out index popoover into component for reuse between index threshold and es query alert types * Implementing alert executor that performs query and matches condition against doc count * Added tests for server side alert type * Updated alert executor to de-duplicate matches and create instance for every document if threshold is not defined * Moving more index popover code out of index threshold and es query expression components * Ability to remove threshold condition from es query alert * Validation tests * Adding ability to test out query. Need to add error handling and it looks ugly * Fixing bug with creating alert with threshold and i18n * wip * Fixing tests * Simplifying executor logic to only handle threshold and store hits in action context * Adding functional test for es query alert * Types * Adding functional test for query testing * Fixing unit test * Adding link to ES docs. Cleaning up logger statements * Adding docs * Updating docs based on feedback * PR fixes * Using ES client typings * Fixing unit test * Fixing copy based on comments * Fixing copy based on comments * Fixing bug in index select popover * Fixing unit tests * Making track_total_hits configurable * Fixing functional test * PR fixes * Added unit test * Removing unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/alerting/alert-types.asciidoc | 43 +- .../alert-types-es-query-conditions.png | Bin 0 -> 97147 bytes .../images/alert-types-es-query-invalid.png | Bin 0 -> 82855 bytes .../images/alert-types-es-query-select.png | Bin 0 -> 57025 bytes .../images/alert-types-es-query-valid.png | Bin 0 -> 79515 bytes x-pack/plugins/alerts/common/index.ts | 1 + .../common/build_sorted_events_query.test.ts | 398 ++++++++++++++++++ .../common/build_sorted_events_query.ts | 93 ++++ x-pack/plugins/stack_alerts/kibana.json | 1 + .../components/index_select_popover.test.tsx | 114 +++++ .../components/index_select_popover.tsx | 239 +++++++++++ .../alert_types/es_query/expression.test.tsx | 235 +++++++++++ .../alert_types/es_query/expression.tsx | 371 ++++++++++++++++ .../public/alert_types/es_query/index.ts | 36 ++ .../public/alert_types/es_query/types.ts | 23 + .../alert_types/es_query/validation.test.ts | 99 +++++ .../public/alert_types/es_query/validation.ts | 96 +++++ .../stack_alerts/public/alert_types/index.ts | 2 + .../alert_types/threshold/expression.tsx | 259 ++---------- .../es_query/action_context.test.ts | 64 +++ .../alert_types/es_query/action_context.ts | 63 +++ .../alert_types/es_query/alert_type.test.ts | 103 +++++ .../server/alert_types/es_query/alert_type.ts | 307 ++++++++++++++ .../es_query/alert_type_params.test.ts | 190 +++++++++ .../alert_types/es_query/alert_type_params.ts | 77 ++++ .../server/alert_types/es_query/index.ts | 19 + .../stack_alerts/server/alert_types/index.ts | 3 +- .../alert_types/index_threshold/README.md | 2 +- .../alert_types/index_threshold/alert_type.ts | 68 +-- .../index_threshold/alert_type_params.ts | 9 +- .../alert_types/lib/comparator_types.ts | 54 +++ .../server/alert_types/lib/index.ts | 7 + .../stack_alerts/server/plugin.test.ts | 21 +- .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../triggers_actions_ui/server/data/index.ts | 1 + .../server/data/lib/index.ts | 1 + .../triggers_actions_ui/server/index.ts | 1 + .../builtin_alert_types/es_query/alert.ts | 251 +++++++++++ .../es_query/create_test_data.ts | 59 +++ .../builtin_alert_types/es_query/index.ts | 14 + .../alerting/builtin_alert_types/index.ts | 1 + .../alert_create_flyout.ts | 26 +- .../typings/elasticsearch/aggregations.d.ts | 2 +- x-pack/typings/elasticsearch/index.d.ts | 1 + 45 files changed, 3072 insertions(+), 294 deletions(-) create mode 100644 docs/user/alerting/images/alert-types-es-query-conditions.png create mode 100644 docs/user/alerting/images/alert-types-es-query-invalid.png create mode 100644 docs/user/alerting/images/alert-types-es-query-select.png create mode 100644 docs/user/alerting/images/alert-types-es-query-valid.png create mode 100644 x-pack/plugins/stack_alerts/common/build_sorted_events_query.test.ts create mode 100644 x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/create_test_data.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 7c5a957d1cf79..279739e95b522 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -8,7 +8,7 @@ This section covers stack alerts. For domain-specific alert types, refer to the Users will need `all` access to the *Stack Alerts* feature to be able to create and edit any of the alerts listed below. See <<kibana-feature-privileges, feature privileges>> for more information on configuring roles that provide access to this feature. -Currently {kib} provides one stack alert: the <<alert-type-index-threshold>> type. +Currently {kib} provides two stack alerts: <<alert-type-index-threshold>> and <<alert-type-es-query>>. [float] [[alert-type-index-threshold]] @@ -112,6 +112,47 @@ You can interactively change the time window and observe the effect it has on th [role="screenshot"] image::images/alert-types-index-threshold-example-comparison.png[Comparing two time windows] +[float] +[[alert-type-es-query]] +=== ES query + +The ES query alert type is designed to run a user-configured {es} query over indices, compare the number of matches to a configured threshold, and schedule +actions to run when the threshold condition is met. + +[float] +==== Creating the alert + +An ES query alert can be created from the *Create* button in the <<alert-management, alert management UI>>. Fill in the <<defining-alerts-general-details, general alert details>>, then select *ES query*. + +[role="screenshot"] +image::images/alert-types-es-query-select.png[Choosing an ES query alert type] + +[float] +==== Defining the conditions +The ES query alert has 4 clauses that define the condition to detect. +[role="screenshot"] +image::images/alert-types-es-query-conditions.png[Four clauses define the condition to detect] + +Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. +ES query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaulated against the threshold +condition. Aggregations are not supported at this time. +Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold. +Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be set to a value higher than the *check every* value in the <<defining-alerts-general-details, general alert details>>, to avoid gaps in detection. + +[float] +==== Testing your query + +Use the *Test query* feature to verify that your query DSL is valid. +When your query is valid:: Valid queries will be executed against the configured *index* using the configured *time window*. The number of documents that +match the query will be displayed. + +[role="screenshot"] +image::images/alert-types-es-query-valid.png[Test ES query returns number of matches when valid] + +When your query is invalid:: An error message is shown if the query is invalid. + +[role="screenshot"] +image::images/alert-types-es-query-invalid.png[Test ES query shows error when invalid] \ No newline at end of file diff --git a/docs/user/alerting/images/alert-types-es-query-conditions.png b/docs/user/alerting/images/alert-types-es-query-conditions.png new file mode 100644 index 0000000000000000000000000000000000000000..ce2bd6a42a4b5c2256121bf513e751988c7cb3e3 GIT binary patch literal 97147 zcmeFZbySq!`ZqkZD5)YTARq`5(hZ|@3)0=)Jv5@y-QCh4-JpVWcZ1XnIdlyJ@9;h6 z>+ksWS<hPU``7zC>&#j+_szZUUDvhuwLe!8{8nB9`##Bi004k3B`K-|0H6X9-=KTw zh$oLkn-Tzk`^y$0B5$QcL@3`n+L>Bdn*ack!SRV0Z{KY_@w@f3<HKZnhOG3)?!7az zBxW|++bEozhcUo6!OzeLNEt$3Hat-l{Za}k4tj4wZz#ez^W?`8p)vm3sUPpLIX(R) zW)``jtst|_;mghS>w;^iTYfMyFnC3pX7&p$fY-2*gOA29Iw@}8>p9>a1WDikpw6s7 zYak{j22k!gUEYA}OpwNFol#fbz;ACE=DhsFFaUAy9ZB@M`d>Gq=>!M_E8_qhiE2{^ z6J^9NxcWHgH7WbNsnb}y^u&2=;#gC1g(^p_;|7r^qZ+j>&;iC06GJ^~CRq+WiZA#> zE~!}i&|bifK4=T|nd`oKPG`1s4>!v?{7MFpHS*T<GyJ(}*YavX(W+A;F$4LImGV7* zs$`nf=7U>-lv^g=*^ooB1PIlG{Or-fDY0Rw&s2*&LFl-tXhe08@V0Q@8!__*o@3+P zb%#wSlnxcoYBSL=r=}N4xgpfx&WLK@lgI2sW1!Ju?JXrPtF*?k;Fge5o_L)XD}}4z z2S$&&coiNC8>N|Ou>O1`WF4<l9HctQf)|J?yN<1Tnn4YdX{U;?tk5PRE#6M&S8N9k zqy*8+C{K0~qN2-PbQWDdKLV&QOS#7N=|w%wsp>y6RQB9}bnsSkOS^nZUiebzfR0az z`U*(agYHO)rj3Us3dBNn{Dyof$-WDU`KYA$>FtnyHn;rOl^_t#JEEb8A$dU9!g8g= zDwFq#?wgaBkLuzl0T*Oy4Docd)&|@Fo|yZ9BA@T3f~0^!T>x|XI(;6<h-;t+_eqo= zdRDd_2DLtVslT8-`u<naivUVIbgI{klE_W~+<UKaG%vlr5HVtiWuca0q`pR*!E_CP zui)|AC;292|3n*A?Hjf|h5&MU7Dfwdz=|NzC(3)cij)j_7{nq^qMoi}u|Cs#aWBL# zC!6L4L7!h|6tX5(*$3$?r9$L6G!+qvthEsh2-^gbpkGs#4+Q&!SX`e?HUQf|#+_l{ zgN8j1*@NLv>?_(l6wDu{zi-$Rc;VUl4S!er$!|}0{gC_(Am`Hv{-aKE11f8phf?BW z`1Dne>Bak^81kN2h|fi7Nz$?Td3|4brHqAQ6nWV7!Ep7JfGo2qy(#u*<Z6m>Ki<g3 zk6Yid4BHIo)lo{HPCe-VMr+9a<+xI8hHY2ZTJQ|{45tnQMjqHP>Y&PltNnr*Gcj1} zgI2(NL`g^O3ZMOA!!rT$iop5~*b4Qbb_<!_hvg9AAmM=aVCXkrnnL_A%%KnZZ`fYT zs=t0gO-7kUyFhdD@KbP#=z|<W)|ehTW}0&9{byW{Djs<Tb<i-y5cCE<mg%EmqKl8U zlp>Ujl(L(gn2?<mm^3X8p142JH;GfCqqL(06P==+kC|P6T=l*sp(S$RjStXACi^WT z{Y{*4NLp2bk{quDNcyr+Ud8P#xbS!~u}H0$yKq_AH~;m-ml1u7fzh7n7vnsi9dwL< zv4s{zy;JL!%#)=P0_7kDC&|#_jXZ*)_}p6Y9lt4)G9jCmx_s9tF1_Zrp7m$Ed$tU3 zICHCt1h%EO$+yugaj_+;a~#Z*YaY%$)8w~#E1}h`LZ@ryFP}d|pDxe$rasRJGJE23 zl6%s8G8~IVS4oRWTb7~BzqOf%+Z5{pcR_3V(d5$P<}AF!dh-6H>Vylzxh*)_4Vm8_ zp2)0d7rA|Z^VN9H7~0#?`y)0|-p3q3Izu8#%E&fjV?U_hGt%>_^Q~dS<U!g`Km5g> z<rm}Tv7;8bK1Rir6_q{iv4_NKgji3}L!$>@n5CIvr4S{3NO>Xanp~5Lm8zNCm0bR+ zjNRK%oXsdDKh=iKij$Osn=8|{ZKkE`IvZ{Dy~VSd8N*T2!*64KY~LK?O>#{#<I4%U z8J3H?wktV*%nkERH}zfBVGn(>h&8ER-2B93y<_^armUvb7Gu6~K6tlo-bMDC{9zhZ zh7#vShTv?5Nv4^e(eeh+Ey!N|=LqD$viNdhy@>R4og91<vYE8k(0v#kTlX$${C<IH z0rybdkN{X3JkR_%Q9sdyS&q3vXHOfgoL?K-tho5Oxy;eZ1-8Bali8)h1$zGKT=H~g z(;-sINSt;06${8v5JeynlyP#szOn!OoNxOyu)ftT;T#FtW}!NcI({a*bH;l5^J@2K z>%`>Z^VJPxe#>mN`?Tc9bJK2f_<VV1xxdQ5qzb1>7mWv95@j1j=u7L2%nXyiq`suS zgT7~!>s#%&Vk6`F<11k+DJ$Rb>Z8P-F=fw;9E@ORpYwboBQHo{C)&iXquKT1;T525 zp}N-iVDo_^2gD~JXnobS6=|kEfH!O&h8msL6CSOQQ<~$LD?iXV7?z^Jdkm_1cd`np zBeEP=NJ3$S8lf0Y{}2kPj(FEu+hx^R(5cq#D|t`$g&agSEv13x&@#p+=0y=baVA-2 z3pdDIb)<B#+CynUDMV?1s4k^d|7?Tz%Bc#RmQuo+$bYWRz{UCO6u3=jOH&`hAJ-){ zpTN~)bX<3!dj!8sy?`TAye`V(UZiW~Xl3c3GcpoyA5l=JCBv%}BgmOf&Q9iM7d93% z-RoB}r8A!1z;je`iU_U!p|N^=9Ja}nH2thH_OTS7Q>!C47)QDz-_s<@x~^18UIZiG z>b}(vw~%sy<(9sd$m2CIg;rKAuw`ZL&HTp_!m~{b@NLb#>Y(~Um&)GNjnL+VFA2Mv z_nG9h=9&hqHGWoQypMVh@0aSYPPnc5?jW`m;`Z{9Di<NilP8Z|eF=|r<f1*IYamMu zP!<06ZZDRrHt*X0?Xr?~g5%JNIG%*KcrT3(EvF5P0*XH0x8-tr^KC=kixH;{r>m!K z+!r=8dc`U!dULG~S6!dv!sLFWF4<%>UpgjR%?>f;F@-RtZ{TmpX${twFJI-qQFka{ z%c`<X{Z=<QFJuk2Yj5oEmDrQ-NPo^ReEZ_+@TNRU2U{gU#iZGx`Pfx>`B8hp9^IzG zr03~*!Y;f?p{2sf_vGeyl6-O!0}H3L#NX;MCmiD)&JxdK_rjv7HdnQ$t>x>lcuIM$ zS0%Tjn<Y9T>&T7BOW?DsV4Ks4t5x(u1A`9z+%J{Y^hHH$?lXr|rqIo+M>uq6=^VTo zAZhSK(|F$a$AWmXqhYLCNlo-2)3iO?^!Y1@_G;ri*et`Y9na_I*pHP^@SM7h8S|w! zA82;x=<wyh%W$>D3NCG@X4Tu!d6v_?{XOhy!E)}!gO<?+*0Qw|x%N~uJG7-H_f#j? zb~>>e-+tF(t1rujF4+49-i9%Yt1CR<$#Y$}!g}U#_-K&ML;*WP6>fQKrBmPuE-L?8 zZZ&L}A-KKi(ta&>HlLXB9UKQXY3*vA^d9o2g9X6sT9Qt4!1?PN2f$INX1krI349wY ze0|%%mBcf1ta+t*X*#IYS`S-lY_obALgH~H1co03u10!LockL3Si;f{8r|G)2(J^5 zD!BAg+dukxmJTQ~JlCdobAAjQK`SUgIWVQ*qo6|qikaOTE3LfrP|m8fd#{Lm8{BPr zpy+^+^+bg_7rz<JuLDxW2mp3a_(fN~n4<W~%v^lR5}n`76ZwNGcl`NTuF~Zc9>9wo zpqDSbap0PNv_oDqg8`^nj~G0qHvHgYG!TO_rm~(rfph(?%k-xG<U#>koWUOLcET4q zAcTAxjUAkgK)fL)>Qbh1asURz{XGCG5(xkeafgKX3L%mHx)(=!0YLfdIWhncXaPX| z_dD{4>)j^`@x80_uPaK-CjbWGuZM`QdlvHFZ=(XUP=4J9A<6)+--$>`A+GO?98FAY zoy_f=jl$g(5f3o!B{iG?fG5xIzDQC^GzW<GXDpP}oz>-J`Hbvrm<^2W3{9BbZS3#b z0SLJBA#QC<oDC@5ZLDpb_}m4l{(6HCaew!ig^Kd8SDdW`snq4(Qi|9)nox2wb1=W6 z61q=GNh#oHY|5u3D*o^4h`$7>%$=R>`B+%o+}xPm*qQAd%~)7@d3jl0v9YkRF(KYy zast^p8@MysI#K_tk-ys!HE}X>w6J%!u(PGSYuCWg&c#`fit4VTU!Q-C)5P84e|oZY z`uDUD6J)t7VPR!{#qz6dL{)*ir+jZM+)b?2MJ;R)G(+?u#QKVdN8qmte-!;sm;b7& z;$-3|VrPS>=`8d=RR4G7|1A7x#lQO0_>VqWxnBKepZ`(x@2UbUcXR&_TKtR9e?3J| zTIjw2%P+18-Cs)L%|I}c%tBN_8F57{v%3$nDB|bEzpi)pwOKqJO;`W`5FjP`TG<_G zcM)Sje&hD*VMUZq$Xhs7Na5~_k{*T_W3jW(&#;TEqpho|xW(u_69|ZjM{|rLtNMD* zgqM%6haH9*p|Iq7@ZrKaxJ9>p-$@6|x8|YOy>rUH3?`8(Cr<tH%nyKkkMg&lXN*xF z0Fw9WnN>spNT_(g|NMF92e`+A{LdQqKC}RkqN3~!8G`<34Hf8)@p~JHCMnm^?)iQ2 zdi3t~KiWsdqXc~C|7|#bjqVu>5J0n@nslG)k1`~@rGJhR74IGh37PWQa;GxhA7%Ia z4sd>-Fro)w&jUOF<|Xat_x~WqJxU<pgWG?S;9nztqw5Ed?BQl55dEVJnX(1tk5=%G zkWqne+JcKH{~#V7;OEodh<w+Be-t4l;Ng*+EE@hl%MdJp{z2ydvfw}Y@V{k&NX7d= zT#D%6Sla%O_>zsrzW6+!Rz64AUVWghSP}m3`~i-jKI8y6?#*$|R+*GA>eOjZRV=@d zh#-q!ql=Jfa^9kG`Yap6t*2Y0o}CuQsHh!7r$AmK4Qn15%kMWijLARCmhFPiw;z8_ zHwACv7cOT7dmZoHhxu!A*fHqv`RW%uvNY?P*s5~b;ews<ItjgC(&DHd-~Go5rAY(6 zM+5r6Z3>)sCsd}&U_yO!;GSrmtCM~DA0dMHCbKh7Ag78H#9Ur^-X2|H#K!Xp;jUMv z3!{J-Cz%+UV0Pjdm@frCE!zDjE|W|)a~wzWEb}@b%xL%4z0{*0iF&j3%u&Lb1DS4u zHWd08zwa8#CA5b$fGVF3aZshl9+8@B=fSfhuXwPbm#(wCWEfHu_mY={>CpA}@}<~( z>An+-*j?ur`{pCyt*YUkA)V&kZ9$l<7%{uYhjjVEr^u9;&xgH#Th{)CXsd#NoMUbQ z)f)E+g(q2^RnH&LC}b=kMRW=AKSrhm9SbppJ{brWd_%?Sv^VE6)@X%$xsj;1ahM~P zr#8GF!+$P<d2@maeAt5N4){%|P-X#JFo1k+Tg*n?a`u8zINVzY`}|5*MRa243TZsg zmMfMi;a9jZi_N#r@nuT+RbgZ|!7o{eFp(+0y$xHoA`|v`!)DyT?qf&NyLit}^nJ&( z-`n@U8+%N<K0dGOZi9|Zdb>gI>SYX*l`8-69IwlEoN|%oBHg_zyJ2Rj6pkVl#uaG3 zOd^d`j24YXRbhYEbHKw`7^;{0+SkbvwT-^`m!la6ll-Wg;wJ!J-<y^^*<`k=?ugOK z2ZYR$A&=<|orl6-kWGURzz-7vl3$jxNezB*{!YxdctO*Eo@92jjcfssj_stL*XvJM z<STtnw9Y2sZu|{u7c(^mWeeVJPs~`IZrg8fVjGV)`%76&`|Y}DN>sDN`h^bN6+xp7 z)9(}#jz|T9r2sU{=MF+OLuQq(d*^JRZH`NadDq!yyG)Qe3h~GLPoj{~j-(o^Og4u2 zp<fjEJx&TatQQt!Z!f`_IiH8*hHLAd1;Av=(|F5_Ykb^xFQh)pranvMg6re3R?3g< z2pozkTD-kAX%~q);kBG7HX=yOt6!kXl?cbvkpuV7R+}8fZB>DepBy)w9nqUlRM@7^ zDP?={!qOo7Emw1-M=*-!z17K*R?5LtZq{+`ZC?3IUg~O7Z~9DcA(=Y6WrHP|0M|%T zsLD7k_Qt3LMRXD>&=7{>0v(l^@}R}Zcx4-B+`9^c8caYcKf$N#V)wCN#!&(XKpZu4 z%-^|9hPuiLaJ)LP=!M(+B$f<;vyVMc=a_X{%yEJp$j{F!3n-?;Q|rD2kPjUVdFx1- zLZXm_D8fdu{W?#Drm7*9w1ZkT3E`x^U#hNeEDuck@($0)2r+QT;<`iQkF=&zd13v- z`R38xEweu3X2Th=)4FzG&HFQUuc@oIk5vY;;;YRD5j=YdBBx*B6H@~w3;SkcQ5+V% zTuv@jY)v`YndhHt)je4-V-U2%c*dbqZR$eIkyWwZ7w#Uh?7AP5xWoB4y`6T1%WFno zqwRX$ybSqKqMP~n=Ru`fP3&o-7VY!rTgMJ8YD>}75_$Ic=i2;ydVPe|rbMkdZ+vSh z3DJM2$k)5o78J6BTDXKOm42J+Qu1V!q<FPrvhC9K<AfdFD9YDm2fK_j;Dza|;q*2B zD5f9kn$Q&^1}=Gw^sq`eoBEp9zSN^GX@r1X7e@s;^GKYMu=GyZ2=mzyZ~3GrT1qVo zQOr_|-yq|j>XuW_1<)0j$$)Y5#Ew5J5>!Waa{wMDf@geJ{Ni5TmMSQa!VJjxo3yK) zhDdz-SJ2m<9^rV8NvOA3N3Z+5NQ86PwZh8>F04PNPo<M``hVG>rTGn>_@^pUE}>A` zwU594JbbYbDB}6PMY;Ji>-w5S<L!;LP&bMR<W=Isd})|h)^>N{%@0@=Ye82E=kg`0 zP6qQkO<DyKGd-?i#Ma!;J@#r*u5a@?U*v-=O-10a_qzJ3-W?;$D?L+8qyst78sFjT z(}eL$<(k0IS_}Q&OGWDqPhW{hVmh5>Z^&T6n~(QHd^#&&KCM<fVJZhZv(@XB-S?BF ze39@<(rzqAq)?cap7E<D+h}p6n3Osjx3gmV@l+6ugGaumpfk0kYVPotyc|U>j^zeC zEOmoawBN+M|5#C9EtHDh%^~@DepvXHYU8CU*Lm5kxt|n@ob_+G1=w)E(-W}X862zh z*&3~#;zE@gB4K&yXd<TOMX%W6o+l`>bemNzo5t~jZ}NI?;!x0HsWez{I&i3dq{xXo z^J^$GvDIKQz6yXw{XCi5&cDVQUc3XUZF>|RsRq}dk_&`G*eYV_6kOsx!F#Sp;`7tH z0(J_1GqUeJE{{w=G<L72C0cwISoE4CZ_e92_dExa`4u$lL5uP`^7ZBEc^czgrn*^x zAaq4%_Yh9#2Xvpgma8TOn3kZnHut@#AUOaH*Kj4`WG&LpPZ@q^K1{fWlq({|k(CeC z>z3E??gODve)9-C5x;q_S}l}mivD$b!iKU$wW2apV!48|q%GQcYit(ukHh#UEglpJ z4{b_{Myt+aGUJ4u%^B`qqEqs@#$xf!Sb+kRq1gX@%jJV+p(U4?p-7KY`}pw><yDH= zt4>u;)6B4><BHX2G~jzsrnp>6KLRG~nT@3XcmPg`HRoe@85`^ldF(VPmx!6cXYyW$ zkq8M7?l(=2;(5LwE9iNi($1$*Do?VraE&INCi4*Xz|Wl0WG^MYJX_pDE}bw>tVAs> z(GBgLHpVrLAqgOO&&g|{AQS7-xM2sAqhYGpq?4v*2n7WlC*cMKoxgg(FJUMkN9)z{ zv(}6{iR3B*?@tRYv*l$R=IiA0dB?Gk7N&o!37!Wuk^s?fwQ5~oerA&R_LA>h_25nr zHLZTa(WvxzXx_+aH6^{rcdA`bs@1Giwd9uha#%Wym|wSvNbqIp;3eTz58_~Lij?O_ zd3k=SqIts0p!$Mkt;@$!FlNWCcg&=mK)7nzx1Gb<zP0V!J--H9A}Pb+jMmNd)Tur` z%^Jz*??;n~_{Xvr5BxZ6CxAk`qm$s`tHTT?*y$xhjo;bK)?9wBf`%_KVlQ-_@iGXX zzaMUS(SFl1Xs2-BW%PMQdg?L!{uKXuOWWD!qSLYdA;wJarw=)gtBcOGnS{`Ke#hJ2 zMJc}_QzlgSU{!E!LeyJ~lNq(@7R<_V5F2n??}$#0%xq{c+ai5kk<QcEc20(B<ZON@ zy?n<C`Y_$|P5ob!9j53SD^o~tWr5AXa&hy0TATdIdB?6v@d2Zkj6OFTgtMlyK}}ZE z&??_sB=$WlFLhi36+j+1a4}D@<4V<Gy26c38+-R;dv;NU_AS+Cq^KF8Ky;3@Q`e#D zpU~N2<uY5A(~|CVq$vGXktJB2<#EbDI{$L2O+mXxn>u+PL3MuAUhM4a|Afe>ZzL(d zp-}4fx5CbXhhkY>wz1kpjBDt6isSnSL9fwqi5a}Fa1L~-#J5^~_t`24H12lsY=cGY zM4?9asn=YI7@cwW;FAGBPp0pFw5_1W71nU@Zez}UKP<8l3Cl@mmRU=k3isf#^0U@x z#E1m~aj=_M<^!hIQrZ;-bGeCrV)>!&JauFiEX8$!j2O@#xnoW{X$EtZY4u@1n^8VY zvHJXuYwRC2Y#~#sqChYTT@UT1a^i>ai1)_yTHIOLFS02T9A;f_Z$k4Vqp8NoDM^em zyS%xqXN=1MYhFvl<FqnGN;ks#=^NL37%u^A2CK%GSw@7u^+6mgaVV6DEPAY;wi-O; zIc&xxo7{HDMnviACs)#IdH*FKbX;t2ua6@id*|d0x8pO79|quT9*qqh?Na}yj{ao8 zt3blrTXa}RdiiZmDPP)z`MG629KJ0;6rNgR|8#r0zP|B(U_0*lw}7i|^R@uOVpUeP z(BM)G#A#9O1ZJjJ%Z@2wjy{#QCpd|{Ck_y$6K2w_s>v(~ohZ~D)V_c2PsY2Rz0jB^ zlSqDan%3w#`xeK~{5eHm>@<OTkagP=rdz|)xhfx#&L{>s{=dc^Ln~LDSjVm7ZF>Dd z%5|=}+M+|V5w_2`WIgt~u%E%~v;yS8o1y&^7VO$Rp02mFy|Hv|8@!MBU9V{fGF>Y4 z4d5o8H}A#9a!g3g94-ykG<##7Z&G;O2{RW8-$y1>+^S_;duEd)WpP8qN8S62u5hJ% z*^O<chhc!A_6=p1RVAHGPLsa4vsRbx=J#@i7{EROozYs$x8g=aMA$DbJoQ}O&%cQj z+`pg1^VjIpu&egE_mqkpRm>8GZ=8lzViY7g%M#;;#d?3=rVq1$E$F~&IaFd5+<U$v z7zn^iIT@a>8A--V^)d9ZTV?yLt|us8?xk8r{LB}JSl-Hl8!-Wy&4Eq5E@;CV&au5Q zV)rabc=}x*^;b+$C%0?)T<Efy^sV=eKO-{fQ?+Mu+D`v4P73IaXD{%A4H{0n<VZX3 zkLh$pr0Ns)$I{g^YB_on1-bw;3+$umv}50Y@&M#@z|xJ6){CC_U-DZ+^hb3?mgM>^ zK;TWp(m#GW=4tLJaPGzFBxSLaL6S8Jl5Xig>@vw9-kH1p-OdrLL#c~G*-k71%wX0U z?Uq%$EmWy)f<zHn@q1s@TpBFZF9Top-z+wn1T4g}<|a>>0A0%Qz+C4$ECbo3Lju@* z?tz&+!YkAd{2(sxT{7{hC3*t|J^3l*oQuXPb0vEA>X&W77$j`j9ZIEx%NR}LT?~Y? zP>EJp&xir_9u*1MmCv4~lc75Q_{^m-<lYwCLC;Jzu75@R$wJdC-RsE^pJv1SiIU|R z6*HY+NHy`h!JkZ1NKenkvJ4)nL2(5U6E^2lheql4>wOhN2^dx@VpK@<>>}V94-QCV z(Slr^=TGbZZf^Q6bvI|e$}Jg}9T+O(V{Ku^WAHFt@_v&{1Q}cgp#v}3@yD^~snUj# z+E9$I1p26Z)T4Ke#L#8Dur#gEYgcT$aI71~*oG)l=Z#h4BPAF9>)_EWlKIJ(s5Tim z@ihQPO){1*d?1;B_!P-%a<5=7^||-eiK<dpc;<_AKDX-errd~p>4dm`HRk>X*33Sp z`h``#eK|-Pxs9&HPM@lgPU~~KC5iQ!<!<A#b&I3R0gZCD6npfj8~C|i@3AD3T5=~j zpwW3NM&Z@B;;cHAAq<K*Jzu!o6Xv7koi0)U<`n<Qd8Lt3l``=(OuO*y>r0O_+3Hrc zikl&`szvVk+W7h<=lW_D!hg!bzfkx43;~c*pIRv+!k`gcS%HrEggMFR{N@YuNSUVs zF{ejqeApCpO)Ru$V7MUpQPwu5A{wwp_SJsw^x<?h@R)l3WKBibFkMU830SIe8g4x7 z(|-Io)7vJnJESQP|MKzuDEw_Cyt2Bp{gGa|+-eh!Pgy)DT1$W+U1}m|lp7>g*X?Fu zRw)bUv&dPGa2dc*O;yX(X>kRjIvBpQ{ANkPQ$;=`2PiJ4H@iwTyFz;q_+Fe;OSlep z4xfQyEX2Q5GhWG32)o4&o;uB7R8i`RAf_*RS5Vh4K?isk22<z0-kc{;YJdIcCGi%t zyVX;=HF4*HX}sb#%kLD2L+|7Hn;TllNx~>O`o{-^aih82vw`2A0dnHu+dGX%eI@%V zvEc`|{NbdWuhb~Uba|&XSpW~G-5{`2(mapw#8R!Kc9zL<dN|B`w{M&v(K#vF>7`;$ zk{6e^beO7?W5%%1Z?lO?y0eJn=uemH#1gzz>s@&!VzSmnflRV3;o8itn^=&+Nbn`h z1xUx6{>q@rsz`Mw>1JEv`lXGgGy*^GWl&5VLQsLFRSu1jF4*wJdf)cTmUjhmZrEmD z`pj7f$~=pdo|iIa>cO)z`0e~z4>u??4cMnFd4=dMdq?<AuOLS4Hi`(mbEH@i)T@;K z$QofrSWU-LrGrj1>gQAozS{y#uV-?GUmBrf2PI|Fd=_!Yfsv7vlCl{mG3r)LoCXoI zn?|`U`HW+|-Lj`K19Wb!@%2h}o;@vAs%^4fYWd;9>l+KP^hZL$fXN2&*=%>3!?he2 z{ZM}+10FIm<`aO!+?K>OtC4O%00wSv<y!Z1qfc0l(AINFaig5^JgHc|P;o20L>fWo zizqT)uZgq=YZ;#V(Vk&H*|)~>L#HlBnI4zIv|HR2ah~!q28OHHloOSB=E)^am9}$^ zS~+KePm<X;U1Eq$n;zSza7=qPh@O-1wRhfl!^A4$nQb(CEoHNcC@!t>k@*7qZ{QfB zC``CQ7k<Orf~+nGp!awjrbPamLF*p3e<~H=p)F*B@VDo8N6M-i!mxYo;l%x$+JA?f zBmoGUGN#`U({HHct^rCPggLvjg4^wP33g}3y)$UX03Ztw|J%`jN%#MY#c-$2s8#63 z_N$eL+~_vDGWl2yF8*c$`D;m+q9O#G<KY-VqamvB%pc_d{|mgJb3o6J(8P_9(VYP7 zKh`&15R1P%w%^Dxbz4e2>>u`QN<Y7MluO8zeAC@%x#t+9ze$z9Fg4yX^5GLe_PK-? zH`Dna`wB_st~QuO+q9NYCSC83y&9zE@BYB=+U-WEMKvt-f8pdx03hMXy#KS}J&ilZ z(R__E)*lmMM3f|;w*Q8j@BA=$Rd=qPI*THrKW3qhC}G6>{pkLSy2f|@FY9q7);~u6 z1@)ev`on+VKSZCvp8E(7)$Hed!#@U~kBJAUq`46LO})DlK2Zb+A5xW^%#T0V_w^wq zz)<QS6o4G8q`y-~v4OBC9>aE}?4%gPLWMTZiIRgAR!+9q*<dBSRS3mow%V*hyU|gM zZ2?qpXBMS@w<$&cmhx6kt?0sCxI(v;ft33Ov8;6B=k1#1$bEQ*<-lK2e$kyf0O21@ z%_eaFWR7{=d`#GAdQAa`*jiLAvGghxREB2qI|G^Fu*hWjRHkMcx%fGu&4tDjC_S$m zBr7?d&FP7hg9JM4VAk7n{iw0pRnsk|pjl37(Yu1CapQ@aPu2&a03jLYdK#(NJ{w-A z`5(hiXXp}#(zwRy*_NKuvzt~$g4~#IoTP8~$rKZ{V(1c`ao3NYU`k(&<;xs(Q#700 z`Uf~3bxJkDGMb-j)s-H#dqShNa<(4MV-zV?mtVurEBy*8v@2;=SaE2`k>FpU3;BU* z{BA=~5pHXBg%l3kmGh+dON2SCCxvroy$yClcX=deX|g=Bj3Cc&I!qy>OnI_YVhLe- z&KAK~8sT$r_?z>Tl?cX4l`;{U4tt-DiD@X6ONLqMG@QR}EK#S2r7=C7?;V|7PTC@K zNtG;5u-P*jO7B=^Ot4v;OVwyhX=mo$=B&l7@U6{GTWFh<=XcxhQIoVCh6gR^RcGW4 z#QgyE)cWe*SkI0Yl)Pjq?qq*iKP%=#%KP>vP3W6!LF+9NJ*(|!^Qg|{y35w-jf&cr zgCcGOo=W%6Z#wlV)xleOD}*e%v};`oC!`;G;&)zyPUELygngIPm#K>%t*xQ37n`kI zgcI<9j`AkF8!Mk)OQ!SH&Q?o!`lh{#uvu)9o2#*XM!*mkdh&Y7qdCBOu4a~z-SA?? zeTyES0!oJzC7GHY@b{YbV*&y?KkI`{pWJf=i0)`^hMMFEzG#{Mxe;otb?!E}i!j`m z?wD7^2F|y#oGZHz^*O@3H~~4W!m3lH=I5fl^V!V{FzZe|-*j%iN|#t`b5B9{ca}%f zIjx58Yjv7k<ve^B6rM0?l59NJs<mWx(Jdx6X`Fe?qkqEJX#ey5&V(Dka}5exa3w}h z%=7;B=Gx^{moZ)Jxm4P;dV>z(cm#{T)4WhF^mB$zX&_e54%MgurQ#`<<x);KtJkL6 zaZ83^vvlvnA3D<w_`wL~WBM%wC9I|VdxCf&ce0{VMWj-`tNZMbiS!2%iqZ7`#ply1 z&p96U^n7QuM(E}C8S7#rlNq^`&-|WiMm0u|si|jW#Ja`&HzC_SQEtG|1>M@l)QMYP zV+V$4AUJKN<#hz<ZFL>2SUFK*Y2W(!o}L+C{YLZjdH=iU+eytZ5aXEe?*3~B`=8TQ zt!|yQcCBmjAj_^dQYmct-ndU0YRXM?jvG1HtH@c-q*xb{4`*eGSq*EvRjWUT=XqUL z#Rz$O@VXyl4WwP>Gip0~U-bIjwC3CYeB*ja+W4-*=h1Atuwjy-A(kJH@C63$w&%Mc z8wcw7Vb47^JsY}tw`*flg$x1tDiRTO!|E+BvTCzoRut(|T^}ji*3+pOZQ3C)Dhy5o zcnGHb`L__oqeYPFdtSCkWy(-C11^X8Gv3s;hZdZzNO(-w6JcnZ*H4EY<&pyEk0-gy zFJGUKz`z4BnOEmuSk#QazUX~>;lN+UKLqHZhB_y}GMq4MwLhZJaC|-V-tjmZuE3}G zh+At(H@m}8CBwgyg%m?qFcfQSQ9c}KNEp@;aZJ76L9UahT=Y;#jr4MhC|L;Z#I6_E z5k}6rP%seQV=qLC6h-=stbN+6ZtTJ40pZiDD*JD0hn~lz_VxPhnW#WpAhm<kzxVh( zsk=jEw)CfBkv=N$#>`7y@XD5(aOUU6gYajs@C8MvNRiiA06BzXAaRbF9IX^sl~nMd zUXVP{(gtAyh1AN!uXFKHi|lk&0~ZBOXOVNeWjq=^4&Rp+Yl#irdQS)>Km#(60@sqz z3=cCOb%_>C>XzDMfV!V<ZIsO7zs7;)JDW>?3{lzLMLTYf;jp+?5GwiT93%ZSCo8si z6X8c@GaEv9Bxx##BlqRCg<iRs4<npE8<C66&sB<*kAM+m;uQ%yg26e*SbW?eINlfI z>P0m0y?UvsQjI&G-#z#3%VWazfn@fx!^$<<zV~RM1J2+fsLYMmJ~<F!B?>)cpCyd^ z7ExaAbTDGTYd*0t^|)R-VIX;f-01!MV|t~|6$VxokKGvu7>x~Xx`ar5^M2n8YKe;> zqgXB*5~(c+QohM>h9IH63#DK=)MgCe1co^Hrq+!^+i#zBM?7Wsj5Z$h&<m@`o(}Ja z6+wIL+V?m<dF#Z%pu@Co%jrJ~!64e#r_=iDzYZmG1XIZIVuB?o5jOTRo1)$>3DDH8 z>knL}bQWQsi?-wlIe=&wZk<b+Zk?yuW5|?3RUEUOp0GL+UTYRk!069cO%}L#dze*b zn~yJD%=Oq=;2oFihX#*wTi=9(lsImjP9Ta!D*RLzIll3k!q9%dV-_r8wgsZLFC8u0 z7|?brpBj+e;Z8-nRAw8>V7zf!_sLv%*rcH4WHEUzP|{lXRU)GfGYW=}p+cR7V5pCK z`}EJf7l8*5wz2?p+<24zwo3V0Unj7S<dQk8c)j;HD&1^vAca#)h?wuYwD={d#t401 zY_1{@1uvFXF1`Xha7<rjGnVC}Q>0o}jPHEUhjM+{S3!ng*q$ph@W6PYL`@=v+iJIO zZ`wC%Aieo(JXp5V_~FD_w?dDfzepH?@f076!M-p|7R_S*jDTHz)9DpZ+<HmDCKgwC zmq{xOQ!hQWEfrelE3@PS7GzIV(NE+19}}r9g5I{cO2Lwp%t%qzlRG_2ET2<r2bDhO zJci75mxIz!D7)g*T7l%&3!3EC6MbzyuTUrnBPe3+^cb!5nlB2ss(yx>o}m`~5K1>K zmo*~1w;JK(5{e^k4-`f<s+P-SeJ38#B9s`^ZUs3=VHQ^iT93%!xardXvBJMjkke6| zU8}2>s+;J1huZZ&k;}JT49Z|xLT<vc@0MR&j~X?is@e-UpNEyII<lhyIT|Jl9b_^o zqw3`q1XZM)T(`_o+YFaPx;>f9_*9YbjD#XL2UB@isNq+{NKqgMT)*ps4%1wnW{K_W z{_V3JShR6(%<L(a|3g-9VK?Z_yCtt39bV9pa+b{8B`VPRzUwbgMu8xV5REa-6aNNO zAjjJ;WfB;?&At^Hi>pRL?6h)&Hv1Hsh9`h6;y`lBE|sCD3wBr{!Rlz@bmmMWOgR#t zmMD`DOL8e=n6AH~<7%ti3Z}PVd1MYNFK^MO6^|rldG}cqHTtCKO6@|EQ!j4bF7n16 zKRvF>X1;!}dkhfZqrNlpfubI2f7<0<y=bLWyM6kT<4nrO)$q!ncNDVsYKX`+C-tn{ z9bT(Oqt_;}ev({WWjRR#OCqP>cFnuPgNwD?PmjjFC^~trs=)6}0;S5edBd&JqIva( zV_D23P9{cUTONV7?Ze*{l>08oC-I0mKPOt`XozEh=5Dr~QanY@?Fg{#+)T5taX+NX zvlthT86?`yZq-t)&^0>w&>)1m0Y6`g4k4&Gx4l1gOeWyo(>6h1)RubHl~>nfVHfx< zz-#$*-nMzrOeHDP_eSh&f8I_{!tDv8I-~GS!{R`wJTBhTm{X<nPp$_d0bZQpZrXHL zi`8b4SfoOI`?bnVC$VE?Rua>2<mZN0K78|{*eP6`9-hR|1U{x)(~n-TJ!zQg|AuYS zVX5?BoZVQa^kue^)S`=bN|4-Vn?A1)x>h(jnoOUdsjdPTiJsFWVsIPFZz17jWM1ns zoYjVq!*xtBNvmqwc4gpbcKZ4Ib}Q`;oK172^hbFUHH|{CHCJh5fmm?J9pW7R0)XQ& z!kjJW5q_{d5gKQoDDiNu89~|;7Km8LV^_h^HaNy^q#m~HK#~yen3X17_d1FbbjcfG zhGjoie!9d88dL90VhwPiKXyA@%^%yz4Wdjq6Cah*OP~M#F*N+SKP*)P?ZJBDQmgO@ zXbAHWZNK}PZxbiSr&GvLoqyd2O|;GR-q@ozwL#>}b0r4X4T|CwsmA5q6S({Pi=N3K z61)+>VE970ZY!-xM!EUYn@o$4%KNg`+)pD;e<b3^7K+mWjufG7XKcfKy9Ed~muu~$ zQ4Xfm*P-YciQ^`w@lrU)A0`I%!Y(^DnT|G7p(%-@z9^XyWZU<Bw#exDtoupIMFE|e zJfOcJBk%)4m0S8C`Vwy%AUU^-+c#%gyKzFnFBd>3F3XjWI!JeSgrc>Bvct3mK;N;% z<Fwqp_W@=A8iwm;au)iJFs#txBr=^^?K)?Vab|FObEgpA`crJffmfftgpmsOg8N8h zN7@snOAm8~^}w|Ub1pj@t>=8nV3oqR^3LW#?KTH12CmTH*C;UQZ|^VR1Wp@25M*EP z8O`aiciuVJ*E-s*#;2Gcday0wzDCOw)_x^%LZsPboMDAIxydw_6kET|+wln}MuNh7 z2GrE(dvgscx)?a#yfKO-h`E7#z~U;|9x?!OC{9#ygZbGP=y5$fnjptporS{!T9aPN z=^va{KlB8$Xt;#z*}Xwnc)R<ZRCGBgv^;fWGr4-{i1IMhO0}7E!hl(}BokvRs>)ij zOT$9m7(LgQ_>We%$=D`~#acv$Jkta{RT+o{+nlKZt8oSte`~<@h}cL6&nRh8WCv`& zJWtBR>sFue>yaCR>M7#;1o2cT5(qE^>cUIH9T2diG=P_2Z>*n{ZB_zHzN&xd&tS40 z`jOqrii`$y4xXEEyB+5H0X~no{x}dU=Z)je<{C~)>nlrU0ygz2PJ6sdK1%&<sKuWj zVbNy2MTXb1POL37I+gQp>6JCR?9p5vgIzWhlKDi$BO@-X+Rmt6Xyi2PG{md7Uh!Wx zD2VDfS9MFrd!am5)!5;trAV@CU3|@DGiRN_=hAoKC*uAIo0R?mArosqZ{Qam;gM4j z(A0p%l+net?ebx)HphE$i}8H+z*iTKFK)v=-1l?O1Q)Yjiw62bP}6QRgqZlgRoL3K zO@6zcBry1_5K6>nY`Wlg%j3O<0O@t{ID(^PjvHoqh!7YbE4S57%w`NlV}Ai~FDpnm zy+d!)OK=<t>t$<@*E##!hUotlfpU@O?bm4ugWds*uNkEhsS7kj%--hq_$fNx%;;6M zs&%U0%%B6yuifMmgx(FbwBb0>3weAh+uB7s>fuk{Xag6|cN<o(rP+0Ye4>#ur9XXF z;HkE~xIu)OnN*LO=mmyB-eQ6`pE_oByl62=+ko%ENI$JqXv+${%H&LVbB1nntuvks zCM!n(7QL<z9(><Z7R?jZt$8{__n>Mo+4DzyfE+;`j--vW`cA#4Z+HPYFMA-;Y^<{g zML!baNj~``_57WuGLtqQt5>(DH!`Iz{x9dEpD*HcyV2>9Lq&GL^ovV7_z1xxT9mh5 zq#>Q-;yib`H{d-y{VFkWG7K@tq3g@$Qj5p<GYtjbonjD}cJuU9_#2L&wGY(g(#ocj zCg|$Az~^SPC2>el$?%%cQHDcJzLMgVBtI++^TqOQR;Zxxn{sVjl4C-wYBg+&*=^3t z)9GbH=lX4yHp+_jCE7lx9GHJmY@}-O^_#fq<0p`%vRGb?lFglM7P%Dh(xZ3f3*%!& zMxxMrP+nFkI%9z7yy$#a<5ssDx{;)8u`>POZaTj&x335<fX#A(Cz)^0V`Xd$-(0v! zP&o(Y!{^y9YKjOuc^euP`FD(N1H#tRASn5q%3mL`H~UhTET4CCm9bk@9NR)hh!zb= z0lcrEGP9%gTAiGlZO?eOxwy^|v&QnfZndqw9{H(qJja)|Sw#H6q#wg>#-$g&&F|<t z7_@|WhJtD!0S;+!+^7QOD;Fuvzs82<BSISKCv{uetV^9BJ^XviiP)rE$ptmuZ~}yJ zgK=rxd(6$Omx>Q6s;uQK{K|!Dx}aakO0V1v8q`R(8wTKSr06p#ec}`dQVtAMu2Ae= zU_mImSY-CzQX7B*iRARRh<KE!sU~$nLj+7^W+8eBUGRhomYew$JEO5bot0?Z`M^Vy z_Sz4EPK3P3OdFdepLx?5cI_MnDw2-Rh^EVA#D`S5`j8kkXtjCm6L{_hM5II7GClep zOj^$NFPCfMv!eDp&^sjpg48~6{H+YG==lMHd<h*N`2!b_DfLJMf#hGTK-=KGBX6r^ z;A?AS$|mH2V!rr|>t%_-ot-Z!SRWC9hM*MCGZ{)~EZ0G}v5<|jS$pg~BC}ugRzv=_ z?OmId=6IW4GE1UN2o~|4^D9?A*KJeA;Hz{{Q6Vj@_ugs!ZZIdzYvCQaZ%YtpsUWwj zuf`%X8AjnVqaq!DB8(BFLHX%6xEJHI6Y_XPdx{040}0&3>$K9DAY#mfYV-N%rF`4< zYPBCyHbj*=8<{ed+b-VKPFz-8>+}*FHYuTd4(s`e2heEu$*u9_He{yMS-Ol@qw=`C zKnpf7l|{g6TpPm0wj|C)vyW7Rbby@uzd~_<69}o1!<Ex1$ADJ==;@pl>L0e{@{qr- z-PBUEF?s~JG&(Y$<uok>rH6@y_8WOiG?Wt~h_PR^W(+P2x<q$=GmQY3u{8AyhD%o6 z-8$=EZ-)u(D=G~omT(qzhViC5OEs!DVzcp2i<R<YTHsTpNGwGiBF;%cwrgDxaTqtg zCea)H?eY79LTtw_8zIj`&{46-Aom&Q--%<jzfuTV6=uEsIsqbgZo^n~MvLr8D8N1; z8wzHq#Rxh>J+w^>`#N*1VwKYRGH089RhzBe34A_i^09Tg8Gnm7b1)8{no89LvxW)* z@Z;X@Bdw;*iv<GYQu)3?PGgr?nuold`kAC?EXMMDB0brSju88<tj2Oyc6*{|qcx3J zv8+w2sq%gpE-vxkDB<6RCAvyXRDM8C|4*Okb&p}p(;z9pddOk)P5(;d?$>|}J<B7t z7+NdW#oBs=qCRx%BC0yk(nb<euA5sjqn>&uq@hS{nDl;AL*`nud+n;(^E|Vw>I>6e zKkAjF8#?a^wPPDA2PEMZcMNh($}(+b>;|=)s)@c+tYT=S4E5C9?t)RTpSR^I7rs^1 z<-EC{DL~?s(z%XuT%X>YCl(qM?|0&Za&?~Y{o?}!kWUr#jKCrhP+kHRqzkBBUVR~E zszCtntqESLo;;^?EXZ=V*_NJ;rCQ>`WSL^_)!591Ue@5&*)BVQs9JAuq#Rwv-l?MA zv1-0_d=##yv%{ilqSoGAS%IK4eX)w2HeEMc^ji`A6NhxcbZHX7I6j|)a#mbv1~s{e zr~I_g)@2Q)g_Ji<u6vOBz)w4<)cCRI<GkHrH>4!CvQ;UItXQ(%=vYLIPGB+A!+U=2 zwNk(0VWts6PrF_pO8cb2l(8GJ-h-*sDrOWehlljmA$=yMR}n2}>*aLPv)#^gp{$J0 zPFv4sL0WuuvAHGftmDt=hy{UF8($(d_~$)Z=mHbPyd=$a5fK^!Hp{W=7UR7L!`FJI zo@-C9jc`)`lIFC>dYSy@hho*aD;IPiTcU^NKI1`>%ZnB0ZCJcv44((kdgrE`Ot-Cv z(O0dKF5e!i;57vIUa-IPUM(<fD;IsYJ8t2Mi=X+uvL{IZl!Ay?`zG{G7%$VOsb(Qs zs5Ol<;k!<yVLYSOlb>ZDQ@CtsnnFUIw9erkYin(>>G>T-41!vU2qOxkL?k)qr=i^_ zmbU9Yp_|}QnNI%h@6%43y<5KF&o!UUD;t@Ib4H3Cn}$D?5M?tSpd?YYx8<7-7`oKA z;hQZMLZlQ}YzzyZhX@sLu1g5N5x%t!dHL00kO+Oe_;lwiubfXoYEP3hY>9bx1A*At ze80WXs4*`BfdqXAl72cu1@}RmrJg&`rB>K(z6xiV$SdgtMgvsenBJcVgU<Q#EuME* zn7GdS#-}{?Blo0r&KY#CG4FC0h~viM4NQGGyK&St&apXIKDr2$w^w5OLGTiEntj|B zeKv|G4dqsfW#*Th&KIX6N&UHP*g5OCux*tq9N_7b<SbV>X8j2&LutG(g>SDzy-rWH z=S7z}u~&KP^;<I2n@^$;KK|6i$@7Bp(Ff~41D*PP#Ya@sb}Bc=MiJ61?dlIp0e8{Y zNf|;Dsm7p2>Ks_mM^D`N<`5m+5JYIujfW}3Wq5y{x@1SK)(&3-(8;_$JNolQ({||1 z)qENfo^LmNBmTy)C~qLCS72ScR-9e>ig+mZGBLb;s-jSF38znBuC3&{!jV((ah#m+ zRUkG@*>vcNt#7AQy{x#=BedQvFats+|7iB(^=Zq<5vS!;eEY4?li8wjqd5_ylfs|n z0_hVj2T=x9`maUb_PUHjk37R`^>9fHJYIKQ7vH@>;0{xks6y2`Vu6Ot$32<DU@yC_ zm2VwXI-lyyhRc<zL=QIQD))tZ?^%btpw^XM1KZg@E5tNIhaN)g(~gZu0bA3<L~|#% ztyDQMyyH!ut^3wnAYr0@zVla~EYk%*3}gM~K(WC08iHx1-@myFyW}jMFF}nl>`JY) z+DRT)BKshmPXzzte9}Vzu{PTEg%8Mlm5>-m#q&6Op(`WrK$)(b=ewn4I`jR}c7&;0 zKqNfDhFk?Tr~D_2zJ-&dF|9+V?Lig3WtC~y4%OsU^~PO&xJ}okXH}Ak?DfXXDqZRx zXR5FUH}J~$iait8)^6nd^he3%RvKzIm#1{q=2L`W?WpxI$`Oky-+ji*B0b#W1|hTA z#u6H-svX~-IPQj6IOc@#!$-99f;4FbmMkGnE>;qk-`eeNk&O0(6%na1k!OKvF6s9- z`uk*9CTqYrl}BaEd@iJY*&=~u#&PY9PQu2F-8=I};tCVGD3566S#PVLEm1!Ds%2Mc zi0Ez8yx6>L0SZ3f?miW(-3{Ne9SDYFY!`O!qLoMh+xTZ++DGBwL)#{jzInTL{QQ#h z7DyiFm(#)emxE^q=n2lr%b(>)LgdGqOb0cO9T&r%`^yNs2#ye?dv@l9u_Lgt6|H;_ zk@(|DPIR2>&fRo;6<+Wm+Hv?ZD+?t=jIDS2x$(fvp(zWN9c7-r|FzOMB>P>CC~M|1 z2empB?0XA08)`n@t1r>M6+hbOf61pOGta%mh2o!bV=U?0KKzFEZy1EPe`gbiV`tsm zLzMywE8pJ0?Cw%b^5jxg?_$+R<nhwyA>H+7d3WxZ!^8_`(=U_=%fKivXQ7ERNc$;H zPKTOpJDls7H;4aQ{wXD&-Y0ZZ4)+{EnifHnQb$F&Zj1Y9%MkAx3J<8t_|uL~7yA$E z{FK6de8@%7yW1q2;@F+-hn+X?QT&InC$AAhSBoIK?Dwj7!FlXGz2-FTYleYC9``bl zCAI(2vWL}*zLl2{;rW}tU6T**9D$u9(gM_fPw7_xWv3J(@FMMkhxl)`^H+f1e=Gf) zB>x`?0c=474(T;8B7&7<uxS4mr2mj~_X@La6O~%IHa4qJalqfY%P+Tm@asE=`(50? z140%$^hX)^f3WF|?A<Hd;w({ko}Ps`|738m=v_RYrQ;|B&G82W`}?Oq`ncDbc$cbG zV|aoZT(P+!^fz|<^<ow>=0m_JDZ*lu7%ruFnmB2o{Nc~Q1KYT}Tsd3ij}6TK9K$`z z6@*KCmEc+UuQvZ)f6wTEhz!$|xck5S1Alk#hlmLJPhI5i8uvb=-DSO6DKUKbqcvTG zto;uq_*XfeBVq%;DVHk3`(tSC2*l(rCix3N{oOsV2jSkwY^2S9^Y@cqt^Id0|K-eo zm)Za2dqxL$;@AHIKtuukR~Y;sDy9$f?o4x}T()sG^FDFQKJ(A(KR2k{B}m1+P?XK^ zsEWHjU-mtk1y>ty4zT1Qf-A-v6j6FJ;6B4DlE0?|APxfHo4ee+ylDw<M1I~Qq@6TI zkYnRZBXNYUz4|V*7qohq6T|1S?YCjK?!PzhCfSM}zA;m|CR7h<lC?+{ut*Cd;*}QK zot+oTpi{8bs5ACY$2m<$<k<EglHzwQg{~mfV}%CZ{NC*QUP4X@)-zw|RU^+<0&pcu z5(eQJp8qd0;z^;r{|N8{USr;67FOo`*f{t}&01^#zhaRI@Nn7q8F<_YX=!-8WO;H? zc)jodiDfkZ7-K94^p5`6S!}k^DzRkYDF4B8xBWnEP%u+A#qkgmB5q+QueAcH%%DSJ zZmR`E5ij;&b7m{mq2S-iEQY7%vsI??waaA(yyw%)uF{CyKa<5KsCNX2-EqB#+1ukh z&hSUZ0Xfw_V1a!A>O(O=XR+rr^V57pvf)FB(E<Zu*mC)zP~50d7h222n>A|T%)?43 zQz9Zt;nHD;Rw|lG?5mUA8OoeU&SL7GPUIzQD3b;eL{TmvhX=sdXgA1hQE#n_lv5_A z{)8J1E)gJ9VU765p^y5S@-9%njk8!S7U5M|JfIA!H<LhQ_2+rk4;{IWQaK{>rFs)t zGHGNIlyS&-UO?S;8T%7`U(WGo8Fil{vK<HlMmR9tFny7t^nB(R>K9#8hebYm0z^I8 zEh;|G*z##h2g+6c1L-WN`=jFeL89(5u###g3<$d;Ghcm*yJqGOe};CI=;Esd)fE&C z{IO8Dx9ocxFA(|IUZ>3@)O4(I99Ax1wu*2J-u)AW{<|EB0r@!CPa?qOc7O7y<;!$b z?snG1z*?7or^`Ga)Wc42aX)`b51)w5f$@O{&v&>b+iwDl&-aCOOiGlC7^wq&SNo{_ ze?gsp#e{m@jZolR`}w7F=i7G9@kY+MT2BdQAslF(=_L24uFYNkyyNNiq`88?<@B<z z%(pj@&9=OV)EiK?C4rjmmxqMR+Cj%r$%ssn)aiR&2U+?UKxVx*LnVvI<QZF5n~t9S zwR9BGKqmNE|5=YbraPnWH>z?e$5Tz2C&S#HC?EyIp874L|77#ap^%gI_%~~A?*w+U zA(B|Cg8RWhBW)YrUk<c8O~#Pc{v86_GF%;AyTWib*{X+%w@~x=@Qd2z0-lykd(qvg zq4T~A&pglf*h9DU)+mNzC5lx%mxw+el|s6UoyLEXAu>~=AAngdWUY%|ju~^P7F2Rs z<tU`nt%tI-S5-ZiIQxItd-HfG`}b{p2!*mmiY$d}p{zw1YgD%Edqws^*6drNWJ?Ii zntjQhv84!E$1)hklHFLcuQT(!r|;)`f2#ZW^Y_>9_55?c?$>?SHP`iC&gD3d^Eg+8 zK~uL#F`K=Z-B4LHmzZ}LAS}rXU*He+z3EeZ@4c|8xYNWV)?sUYgO7oO#oESvZ-lJV z&f44#5f!llT)~LM_Vs|{hyLg73{xLeIE)J4Z$MFIHrPKZ7qc1E!5w0pW<qdpdljp^ z(W17O>j7Eh@{swfx9sA5H0Q<Gq`lu=6@K5Fysa1JeQVd?q1Vig{wrPoa>YZNGT2uY zp+o%15C#Zh*N>-nXM)|^%l(4zQ|RZmx5Do5`mtv0gd^%B(qw~YO4XkV6!=xHs3%_r zrMl`9e5eVHTUt71J|7}2+nsYo?I(=DAFxS7e_j;(u%76Msw=oV%({EsG~TNJ*>#sT z7S9ncWY2qPpAWhz!$sK#QY&$Co1ahIH1SFfzgy>pZ&=*fmRWMV%Z-hk5Ha_YZu9R= z^UKLR_Bk{^*qa<M>Kb?3;W16(?GwjDj0#(=JRP3)_miPBtquPp*^oD%%ApRlIeWY_ zY<~aaN1xWyXU-pg-0*Q_rLq>W9nJX`md0S<%<8&k54D*6nepwU*CnaCZhqJt21r=A zSxTMUTNkJHIRQB|8LM^>jXq$$BN8y7FC2iAzaaBtATV**OZ{+XmE~jGOEQlMTfEul zt>P1EDWb3UChdiIwR0-VFJmLuD5L@1#@H;NIMPeN3A@8#BAeZ*U$nA(VY<GI>^&LP zBBOK3n@^P)TQWhrVDWE)cClIi8%-E-ErAf<jUP&BkJGa#$Z8!QaLx-Ea0(bH@B2jC z8cjA6Iwm9ZN=KS$y8~8-kn?>P(Ze_GMW^P*8;!BW;{gHU)(N$E0(WV2vlqT8n|59I z&KsWj?-BNO2S<Ctuw}&yx1Q$pI^V$V88@03CW(3#U@qwS)+xHk?5Bf{;6d-D#|LY5 z?t1;lXGCiv3u*ok6*xs6q(}?VS_0iCckx8D()?Esgsi$_xLJklxVp(z`SranO@-B% z_LXJ1$^!D(NY{&Khze*33g0CTx2s4P1wHj%oMco?Kf0;j`V;(}4GCwah=eMjW1zGZ zeb^kv_Z2={ms9Oy4$|YkfBSF-P(OB=iy+|>U(=F$@CdjrW!jX9WiLJ2A|ml>{1r80 zWkZuOxf2wCX;#xY(GF3$hUzv4?U^jQ5{!LgP7M!99=yC;#b`geGr*FXdwT)1@vC9< z*0GJ8zW7$@ryDB&WYT~zzDfMz3Z&e2z$)LU_<5lm+4ed|mA%re?eJ0JGIH;LvJ@v) zuhT*ITHI7}v~uYrLFmMExNnBP$&_ztAU6>pWgXvj3zZh^e)@mLnD62Vz45Nxpbc)# z#|OH&a~VVQ$o%sNqFmB5%mceuv*cd+s;by{ui<)jPDy-azz)X-@Uw5FA6I$?dSWb# z?hO<?c6F-OI9&CVYzO^2kJsP($tVQ6$@k9IT#@MYDl^jcInZ;$HZTswKPkGTGGT!l zc>FUR=izY;)z@giDFtekd86AG(vHT}CKD6whTph6PTL(T{Ic-O#&YrYaomT_+F&N6 zEsEoE#r4YXlfF0gl9(AF<a%`nlz|4lYuKR)KEsL$ZlyFHTk5iYVE|a23pk7F`A@UI z`vt6oNu#gfUs4>WOoJ59LN;o~7wc9hg`YpFe^lL5??svY7^810pt~BnFvnrh`qoYP zr-Fp(V`U$6x2L69g(m-z4e)s1q^jv|7G2P*U)E_LH35OT5wnL<FuajFafou|v8g#0 zFKt}gdMIW4#lTgDx*<`f&i`0apB9n9$q`9z+4;u*yZ1#z?IpLT&$bi{I$@K$A>-bd zVvpK(j~~M11@36-i2!}K5}H(F`7l45()dehr)#KEnhs5RI&O!IJ}+VRq61xYtVrsG zMTa=<$V4~W0hAIUJ_MbnKW(Mz0?V@2pHU6>v7Y35$r$b48>4YMGImYc`=$`A$3nz! znG@l8N8(JLE%x13zXA7n9*~<r31~tTC*xSrbt)IKB(w|O<j>}&1&AkRA-#8&q@ZNf zPc$;?-8;z;2hew8g{2LbDw>iltXsDypZQ&u`W;$`u_2`c%tvm8?gZ&vO8Ap3vdxFl zC01rxSGpfB_0R2YnA~*z{zJ(X>%ZN!=g&PnPBh`g><B95vh~OkLtCn|Wo*b1gqNIK z4;g55?Q%GJQm0d3GUR2Te46diUW+qpA1h<)-8UXq9s5tRGR0~O-qQo-&e*Vd{O)@F z61?fkvV_g$N#D9J@%XJMf%(nm<O^NmZs?Y{pNx<Vdh)l&wntVKH8B;RcUPsd9~bJM z*mZl7F!VqLltsEfq*V=T-K+lI+b{*tav*EtYpebqQy;b4o~Kn2)!P^QKj)5=BJiql zIdpq6uc>?Tm=jf~;=^c|LN-+{J0;WfFvPpR(Au-`)E})LG-QNJn+bce?zg7Py<D%; z`R?lW%BbsW{u-awv!*Y;8s)c)MIAOD-utj%I@*NW%)%yOnJ++|kip(jI|Dfjj?ZzO zNso8a7g1c9H7|PtGPlwVQmDN8Zi*7aBsHG(Fg}EpHum3{!1PVrQ9fh_3O@5{7*5yu zE}6YP-5aRJjRgLHjW4zHd>5M?6^7Mry#S%h_9h;?mv)*Nm;Z`YM0_gd%7VpWAG>K; z%j6xt9v38TNyM7d^Sv)HvO^gf=KdQ=!1C^=%x(jjmEciF9O%D!l^4(yU2J!N6#((| zaEyTIC9AGQT948Hs5q=tu65xyU3raWum64hV1(<3$RP9Dste<-N9>Au5i~qNGe!6G zrJkIA>aLa1bx*6&BkSJOourwswZz{dPX}%ve@qdzGnn*0w-DtiueM;GFXo(1M6!^x z<wAlI$Nmh#DAZeZFVBBWfzG8K0J@O@|Ll#@{3A@Gxzp}F=Xp%Q4bsg8N^fS#^=WkU zT}aCXo-H%|0jBnTuD6UrhF3Zh1U$PnrUgnHg028%2I_^Mte!dUyePU4-)~e9tMBKB zin4D#5Zv1=kpK;VT>(B|4Nzb2hCGjj;hvLW8@ISs<N4yfjD{WAn1b>#oilbnoxkqc zXNp~Sw%nWhqrrz$@R$7=wpUo#vjO5o2mV;He6yJkH#(&BLu!HILC}dqmEvUDvo+pq z24TfFxKD%9q}!%;Dv4P2*necQGTpsRh}nB?YSv3^NyHWdo9Mypy}o+S=L9=bxIxvU zq>AuKzf^^}D5<rdK36q!s&ji{2}%w6V@7jgnl*NFn>-7o8`gXI*YTa8Ilf$QZtF<% zeSHc0NaU$Nk_z-ODfIHu-XmRug$6{2W-8!tA{b)a*4`0^zJ$-)lo99tC1C(vq~uHn zt@_a}_GXl(wI9|x&&&)OrS-nGyIN@2RsXH~JS6{r`a`*p?t`i_*y5<^^<Z34z=3Vy zxW^bwrj;Xw1Zg}ndAt6fZvhe|H@Gd_6V4zTdpa}X(Bk%p?+kDHFulhP1@uC!8*8NS zG(plMpj%M#RkBMV%!dT^g9Gf1M+6LXfRu{&%(;kND<JA8usT7j^U7tTUs68p(&Jsa zf3~AO%Z6rxsx#b8^*x{I#DdTw@^)=g^u^S05%?CrO=p5=FdMR-LBax(ZMcOjcSbxq z;_8a!s9Q8I&#FT|x$V6r^aH`0Mgw#JY93ndoYn*BlIKNPkMYKMs@hYchPk>nK%S)k zlPV>@Ep8VxX6z=D(q3)agctnKspHImh}TLM-DTU~lGw1;k%m@+s#kzWp?-k1;Put| zT83+<t?F92Aenb*F|S*YInuG!#~;m}Q!!s++goyaoU)n~S5W?YLI&L@=X?oifA%77 zE+op-q&UkI?|v%KF#x-hNPqSC8Rls^!+n!bVot|z5S82>wB`8zi`}!Bz84_N{W#D( zt{M(p!q_$8XuvtDA4d4Bjbc-3x1G|D)<-ttbbOHw?_AobB~>tbYw6*8z5%XNH}Gek zb{=71Gp7R=?QX&OqFs+14(WLf%Rh;9j<d=9&|Vttl%~xG+xqJ}T$TrUfk4lt;R^fp z%ch$~VZDaN!#yk?yj(pSiY@+VUxV6Ejt>&iq-wgfv?KbDW*TLRA4G1{P`&XkC|a4o z=El>fvb!z6A@@MlR^UGqA#{Za-3SDhl3}@}xK99ROlZ04Ap$NB>x+b9b9X(3#`n^9 zS$(>&2y2FY(BADbx_C2g4v+ENLwnu@5^Kd@V{wO+fevHkTHB?|69NW+;Q()TP*``B z9GLhV+CxN_a_O3gObRbS3TmsLZs;8YkUEKJR=7;;iF^@TY+xCWFkioTJ$Vlc*L9i1 z<8tdtia{!2xi)?P5?#n_qPc&(3&Z$Un2i(Ns_u_*L6c7psDwu0tGBJIL3cp+X}w=t z-8;R?_2KTcH#ZeBKF*C!NBn<Vz6jN@Q?i5}WZ{)KOlRG)`5XMPYcy@wh=6u(!2BVm zc<9#QLy@O_JC3W#va@jyOG?&~7o5Ta1-TE8w}wnRt?Uf35G3_OG}!(2TGs{61%<T_ z{;qRfyr6_!9=7(D+UdB>95PcZG4?%W6=kG!89Ox9!CwmRR8PL9_dUVAR6diD+zP_^ z7Ixl{6yya5umwRcc+vXBiw)i$5?A2Gj^I!95@gOUNI3>?uS)bSAgUcl**zb3Eyk^P z_d%z)6-tHH3);5k{d98fF|AXmM-nuC+*YThO$p`q&f#Tpz+;I;RoT5=U=?}3SHJ5m zWmx4M*<1AvT(Bx*lN0P)j8|Wu6BygSRT!V)dc5db@yGdpO2PRQ!kM#jI6UT*D3kz{ zm}=FrM<dsHZLgm7p6@c$Z!96v%I_C(SxEJ4M;4D+P8^4pyuWM&6l#?F8;q@w>Qk;m zGN~mVOg&hVo$~&uvL$Lm2y|T?LqJ2rTmIHAoLv;F3ryC*#D16T$zw8=ede4DC3mhQ zdR}dnT<VF6g_o!PXNl77tg0m-nMpYBn*BQE<U!k&jNAVsOpy|o;@xRiH!G2_*X=fJ z>N2}%PNe^n*SxXl-gya>4}7w+>4S4sx{MF#`tP0uRJ__NC)~~2FF+8-!W#P9<Ro^D zW*Xe}?z?&YzQ0sK3ei89AP2p@{Y^dAQ3ak+6<3dbkIiVjiC^$R?iY=m74}_Z?|8!- zH<<u5iSk8lQT^*>(4@n%8k=Sl{hIw0&=jW-T+pbgQ2&*dc&PjP@Ow7Y2~PMJAkcCF zOhcxb6XR<w6!d@$x_#Jer+_iUV!@r+=V#<upTePcGk`2ta^b6sGQKT65o=LoTR>Uj zZ1DseQVhFO{V_^u2XiKHKV^JyQp~M{mL+&@9Bg_wwQ^l6_}$)&vl+|J@^Iqz{gwf3 z?Y}qg2K6zj&;tgcy>|*yU@l&~n9Hy{JGS~|$(u}9B1LPU;?a}!1dkmxX>~$E!ewRw zlZ2zR%5UJ^*Or|=BD&Hhr1As|?-7_4RKa09Q904+K07lZ-5nRg0zpKOWt}-Oe2<gR z4ABczC$mlKJRi}&<-4E#aeMu!3>|l+e8HWy&SmbU8)jnQqWVnN-WoNV_()N1#qBp> zdl#N^5AT@WPIKMPqrIR0Kx_Dt6GV~vTNOH(7GhXoRXkXt-MdqiocAAzNDY~P$YlhS zibjT5;xR-~|3>Y?giluo3qK$P-AVKp?vEF%@2>J(mr1%|`|@6@INC{w4BL5BSX!Bo zELL!odR`~wG_|ZzpZjQ;h4Ob126ShPT~IqQ<T^HeK$X|s<&M4mNc#IlU%Cl3+294H zfY>z4!6(?~krQ97;6M*B`*Ek$Ygu!%rnaoOeEt{POT&}a;bBz#{i-(Z5k<HOpMB#b zMg3)-2d5b}9G}1x)_2}55xPvyFF1V}v>z_%0BCO)xw?eoakH1ya3dK#s$m0uYsZpT z-~ZlO6o*C3^`wF=KZN~a{WU+CH7@V8u+3Tb20RFxyrXs}_CH!XrCkytsA{B%d2L$0 zUWg?&8k_)O$Mypglt2#NY$fc;3P|3RC>Xuu-EedZ;{M}2`I?RIL?ZnDu!bT*PpJc2 za@V;m7~Uv^Q8Bw-y9ji4de19ZuL(bgj|+glDPH7Dp^bxE5<kq~x0XSBT9;(by;%9R z+67kE8yXZAvIb+V5sY(F%?d8f`$IPc++^bSQFIo}c7e-puX9Hxvq<@y2wSzY?=cZI zyaFw9s&UE_A-q>gnA{MmA$#kxHt%;e*GygmJ&7Ya1PDC27ZvN6KC4)ec&%N>r2?e2 zOOk$O@|lB#6T4Q3o~$F9v-wzp)`CX;AD#ke=m{cWJhcQ-3C3(fPI0C~@10;L-_Mh) ztJ)3sF7t*B?=TGp7|JXUeT~0O{CoZUi$)+sWxwjC7}ePzXYxSbCjXG^Caed7+;$SQ z%&wqodWXJH&IhpMW;`f~i2dUS75PEk8X?X?9R-~w26R${s#(?hpz89+o$cA=7AIJ) z{DDG#Q2{@6UQAr~j8KsCDUby1MgpbIE~l?nU2o;)eRo1FI}?W1p9udiJ)Q89?De1} z{h7Xx_SiMLQ(=&HpLNgIs&To8m3C1VN9P7;6$q&P{zZ-xe|!;8idK1AEmcKzBIvqB zTPGlojED*Bw#?iI#1|<R3Mu%r^)E_yUk$ydQYEl91NJ8PNh#0Kn372vmQNLbI!mE9 zNc7(a200Vtkk_0bJGTkW^S2dXKTUW$ISy^5KmIfLQ(sKMD|~7q8vJ7g2%B^bh;q=8 zMc!lh{WE_#WlvDIpT?1C2K?(`P{P8A3hVx@_sl*H>h{l$n4#EzJ?sQ{*u%bdoxiCP z|L<e|-_HDhdCUkh8$~8nMnH*-28LWSmFmRqS-Zx|`s}{zN|iA80D8OC4x@Zc_>C}7 zqcNtSZx`}n^V4}>dfyPJRj)~g{VD20D78GRA;_u89Qh{HALt#~L<pJP&}a4ArmZ_B zi2VUP^gBV;`PO<5L0l>~jVi8A*lPP<^)v9KhkP+M#O!1K<APOo!||X#qCBcYkRFPk z6pY_q8n|tk@Gl|^JIx_6ITHlI$_k2^lzBy8&F|;jvt<paHRCWWbBQ;t2JJQXs~s+Q z@#qVV!eJ?dj@QQbBEvE=mOml+xF3`r$t-_gUz$_qK_q7aSFo;$v=CLM4>-U@<-}lb zuDSr<MPNo(4ZvNezmWc_G6G%WBs`!{gIJfgL^zfUaG?b!i%t2D_SoLsX6@imilC1b zwY^V}?(DY5)GDzFT_&iKm6+CDA)v*biGnGKS3%Ta+4FAH{Q&eK#Q6Deyq}u@ni9dJ zl%JyPe;gldl>;u}XoY7f0omLHx@HGkbMb@u+DC7Ore$TZ+Z9P^yslSf4p@Qc=WeoG z2I!c-12m!LKrfWb%DRPBlGJ*=!b?V>b4%{q50?5hR8f_-zTad2DwqXAc?el#IF*kV zkA~S;gV)xT{wmkoQn1yPL#*d$!;ywalDL`?uhtg{$b<MQe+?j;td>&sX1&qa`f*Yd zp=$bMMp<>#rFY~&Bu7o0_zYJURG}w~wQ4K(9;?vfSI}Trdt`mpRg=n*t^(bP=Ro?g z&1>Y=u5w)smTJkXcI_^J(x~;F^E4)jgrBx+ZZR?NzexnLw%>0zl$vXAt-<-T?j=d0 zuV%?brd2y_EIk}p>)s{EsKq8ck>>fMC_pg!@=$gy66oMT8Qub%y%MH9d~^VlsBPwD zT3)4)H_k3R7ozer<>ZA2VzcFW=yPMKRE5vY#pFG^!QCS#ET$5tqPUeUYRl?BF}z-< zcQmnw+MC!LG4d?MM6Wdr!;z?Ae=~{bHt)${=Wphqn5ZjrQ1SJ-!IqU;F*Jx6SJu@y z(%=(~cI$-gHsSr~p61KbI4Qp1o7I7A4(XeZfePc<{iKQJ1OdT{W)0i`wcMi<G@;BJ z_8Im2Un)EdMulzHq;|W9SGz=*y=#tL-*m7R`ngEsQN~UOA}Y)AGAf%wQ>1@ihy%r` zKr;4TS*bw^HoAJUcJiTuQF3mbt)#G9*jHzKSJH4k4L6qaUJ8=j#PsH~diM>#iSK&D zeUiFN4u5gQ+-0-FedRf3tWN7J(Eoj|20~ETT*jNUfi`dM#JLJ>uNTH%M8iF*UFN=z zTBrHFx6--2ASH$JL$5z^w>Uww{zzaKRX#UY?T_~MIWEJMpmX0N`$SW3fA|Wk8Tm@R z^Rr-VSUcAc!(XyFsp0GCJ2iN*iCE}&7ftZ2?cBsCMIjySAN`Iz#M38bkNKDy`aWRL zySvXku@)ObqC>Or-QnzG&B;o`2w6tW)EUPt1S^~0eh^#c3aY9z*?a;ukE=}x(|5TH zL5Q9Uy}-xRas6m)2`67Fx%=&H&B~h3+Q41UiIi`2Y=PrG8u}Qw+qVOrh1X5>1|04L zbiKc@`_88@(B@mEP=huS=p{1mu57I}G_EtI1-ve%6~#q*{p<3ylU_a>_%zDsjay46 z$7%&{non3ib_H+k?1D}sx}&T_bUVI)`%pI+$?$^B{q<R*c6W*K6^&X1{TrXPwdEQ) zA&<3jTkf>}&$7E6ZE;1)JIjKi8%T(jvv9z~Rel{FEO9x=pRE~9`^TGbfJ+;l>h|c& z9<J0{Y$JDj+|`p2gZfZm9?kfB21u~Zv(`Z;m;!m+cns1Xq_5`(tR?psjeZDPaB~Kw z@7<x{0f)_5UR>t;viPdSJmqJce4t(*V(L7NJBs!_;HsMn-s;y$yKa$VI^lwsE@uv0 zLDvo2A!S<4LpZv_xSnny-wuyE3YKv|MRgZs**ICr2NvILVOt;g?0)K%oS0}ncl@cZ zM+$!#uPt-5-K1A+q>GpS_m1}xc3jn+dEAmEom7sy5g#P$uDH`HRr8`j(oXTqUD*S- za?>%KGX5AZb6At=){mpTBv}URO`&dXqd9jSQ9ICfaCBc@V?$3*-UwmjcrNRNn&uLR z<nJwb)UIp1i}}f?7KG3_&rp1Qb3$rnWpu&pBj{@VWYJOL8NdJ63<F4zFDK0lh~j`` zeU&6IoQ}oDRi{>h6Q4IZG<dI@OIS_^d+B8={```6H&LMCxwZK2dX|!6X)U}MZzkk_ zFsCn7w}I2T^n#dX^|Q+o*U@eLMbyZS(xaMY=?}Oi+~p6+&s~f{UY&@IvwLsD@IA3t z2a=g;FN$vEpY-3VLLV<w*Na&DW2kmkx5ABAhNOBm{`OIubx=;?n6jCP>N@!N>RxNt z1@qC^+pJ4XduBF48Uou(3<B2*Qtt<UZ}dA_$u@GwF7W`l`>KWK(y2Mi5kgTPC!%*Z zqP>X1&sj>Z?JwgtMgkR1;`c2Ihi>GnPaQOQ)(-#|<mb88xaMiRAhrGz<0T||Kigrn z-1Zz#ch@NzbgojNxVYb;#9D&UwVe$)4ey#7Cu{X84nCcqlUsizEQBcfSP-n|>_Qr& z;mj4reZLG_mgsHozGbBJC=6SW&zGi->yn&!l4zc5H#7tLK8rtC4>2?G>vp$2ryYqt zx7!v<S$y!g_&88;*oP?GtWA)oU@yz;g5$!wfstxQ?ANu$!B!dEtTu}WmpJ<iYTX~h zKb}6GEktfLo2^N%4hs8^GwYQ-YP}Z;LA2t%VU=jl01eNyk$~4~XI>U}+W%{HBR&(I zfmWudD$f;5+%`tbG9C}wL^3-yc2}Ob=bLZ1#W@h1v+<!<)_W{c!h|v4M)wgOw`Wm@ z`)1sKQ5%}xdWRveC2<7E>abFe7M3<9<h*Q*MsmCmDkExlYAUWsC@$S*<kdA@HOLaM zxAvFf@AkZfDH2)jmcZeyEj>uu;qI7X$A*C)DUW(j`+YYuX$(kw>&9^77qR8He?VpE zZ=@5c2x~Z<q#qiq_o^`UIAF+$y#Ok{u(6T!wv*XO@~;<@M_T1(Fg$_#KO#zxzk*jt zuCgyiq+voxNLh_6E0rUeaBHGvMB$Frk6Q246)D*p*Sf?9tnV`>!?$|IEQ;^M&y44H zk+H)UpQkvgoy}Vu30_|?DxXr_;rW-b;SOpiVV9q*vQLa;mTi81+Ur|opSABn=Y`xc zG#dUkIHHSQP0DKz{X*Ow<%SoIWL+9RpehVlHb1riZ$Je`BebjqOvwc>MaoA+2*LZ{ z<L<e(+zE;17v@SDs?kH@&QNo_j5iQMiGY2!2sz$?KGLxBnHrNCPqs>pD<WZEZ<Ihd zu8)j1Rh^4w*&IBJ^D+Can*M9;oIjqLwS~Nn%NwhPxpMGU;@W6$Uz|pE@R)qktd^1A zLjhD!SF)f#%|3!2%m7p`aDN%lyANrRDzj*#GOpW|kUCtv!y75NQ8Lh5Y+M76Wd}l9 zl|bLAu8*T`@@pOb4X^o#?r`Pa1RNG5)*2QJtnlye@VWU4%_xLZNb=5r<9%}IW~M+A z_B5%YeobA^B)M{yc4HDQ-dI$;VlVSg9Z~S|S|GFSNI#i%KJebV{UEd-s4p6~mS_k+ z+TZd(gn|(UV=febro7<lfu#Sn+@$h#$P%i!u4%tuZwSPHu#mK;<364LGAvY*M8q+t z;U)trOt`8t^O?D6p^^Mt<lbc1hSGqQfCF;nW{GX`t_!TrzG!BZWu;Hy?z6JmO7g#d z^%gC7)3fvSSQ<4ygnU8kONP!d>iL1cUHz$i2$714)N`xf;oM*A1??fH37mb$<@rxP z$p8M`pco38z-N^fHs}6aSAw$xEPIt6Ll^G9Z#2#SAAbn?o|L^%zm$qst}@wuHum-v zho2IYz81#S4rM@3e6*~M)jo{P{}aPigTA53I?vK#^QMV}tA>w2b8+=+uDeDdWM}9m zBF_GCI0=i$4URzgj#Vba5@@bg+aNA8`h%|2UTT>iO<?9DfH!MtvOz<)UaiaAK41O6 zK$c0caf(HE&yuoFEJmZM?2QjL=R}V6ny|YmexT~|inv4bMciLMdH@=FI3G16@j#M< zAHTG9yPc&RNf(9t*;ufg-<Z>uCqw%80Xr6iLg>m^)#(H8y}vc;7w6h@llv$0>R3%L z(3!FTrSLMZt@+WF(F)Us<)Ol{$BCv^?#qK^#H17?W}R|>pN~xNwAb>~Yan_QU@f4@ zP<%nBmw*2QeaZJ!K0|Wo?#ifzx&Ofj@PrOh_A&vfAK@GhU2m@=KRubr=D7W+bVls& zv#3SK4pHql?o1T&jELYn*5uVIdYk&>+XdB<?+pXsk(E$8&|?D||MX;uNg47w>e=6O zzz#IQlE~#cj#YQ7F(7(J=&Hadg>HRwzsKMsJ>}F>#9>9C$Hr0e6%wK<`uNJO)E3j< zi<C!66G*Z>g)BCP0Y)e;2_#cV!6X8#OyF5C+)71n5p1klaQnQdO^o}>2qH%<WxkP( zD15TJ{x{U@S4iGQOu|l-S_dMo8kM|rq(ceiVs?YqU?O#XN4xE$?1$bJR^8=*;x$o> zM-&@N_$+3G9V=;)Ly7%~Zv55q60=Tn3Q7!j1N<o9aVQE+YRee}O-6?L!e8W(u^Y9` zmej4e*awQjP=n8@SnMJgg*+xBAe>W-feL@08g;Nlt%8v*1cKBR%y4PinTt32z|nkM z+IU#7^j7&3O<?vuB~<Ls?G91_%aT3fr$q%N1l?c5s6Fii-tg&u{}#>WWHkItNmF~p zx8N^}qzHz%f#2$Kglu*SHDzzBwy_2HKHn)XKU0#kc583k)sf!><^#qV*+?9QKsXib z)c@jP&vJlkMK2`+V%2b3j{Aq%DUNX7uii;#ew{9Wc2xGHN&#j|0F2SpEmnG~O9TKh z!h1LG1$D%v{Oi6b65b|?@HXIe>XdXO97d-6zxfwcBL#pa<=DgdZ_G}wq~q7z`G~?j z$}UKG6j91lPP73TDzu*{_VHkzR!*p9)A8nA&sPvmmg|3y-P_CnhV*4>G8&>-R|4Am zzrkedz`Ba28I?U!lA3#?eQ!V;5Dh+wF8r9xrVA`GuIYh$t~+jIsRPjzD~WOE5C$Q$ z@hckdR~*$KbV6M|f9=3A54dlSO06Uyh|DZt!#s$>ztBXIXd&9ELs==H^o};)?<jLg z5vdFn=&pjvP&HT7l1CWLeWzufLpV8LJ%4S^EF)oUeT3_X!mHn*%Phji{CB}hRuOEK zMu~pud)@r|t#=XyVM-C_s=$eTJd&wH95cd9$QDCcJkjon-AN*C$r4VE3eQhp552}m z!d_-nuS_`k|Gfz9rQi<m?mBpYMAxEn>!osV3mH3w*zOt{qucS*+Uf)i+5v<V;%<wY z5(3Ht4&cVk1fTN&+n4;W?E}y{XKPPaK17iX$Qh^)$baAiqYlb0E4}<QoPQh_4^d3Z z-GPGcOOHKTpvP9OKy|a_6!boA?q98PP=5eVlP@Z%DQMZy7>hnq&^Dmg8!R=K-F=e+ ze?1R<Es;!>^;1rXL-w_r&x;0AJ>nR(6!FnOq7y{{c${9A@|l62YsNK>VpSo$*UI4k z*H0#rE<{`am=e{Ev%ZT$0S9+;6k?{NxMY*#A(p_j=ZHbSMKXiL6dc;4m5OT14!XUE zB<vmC`G4ieG=5-{0%Zon$(<sj0tc3;#2iMhX%86mquI}%ET;<uV@QT!0aC8>_b}tN z6+3I_F@f-cXf8+xiHz=Fc==@z)6kWb+y%xzNw`%l*LE;3R<=ReTy4PK4M1xigqy>B z!#N%Oq>V+~;@fF`jUC><Y{#itfZqc*=bQ<dO>t^01J}l26;)O2d$5VCc}WdL)i^nU zC%KPp0GHX%Bx*A?Joj(7MHlSjBk2?#h#}l9U%;rc{Z2gJV~|+i6``=)_|Yo3{S>g= z2mFkkjK{$u<<?#?Rr=RJNt%a*7jZYsgeaPSWhTv}nH{l+uKG$XJ08kZ=eb@@EqmMt zq%ZZ1!PQH4x(9jvJVfHJk2%mGya!8)76b^jiKoa0G^S9T#aC6L*MRSXH3yRll4#wy z|H=7bjZZfqh=N3U@X6SZZ{}@(-Af{pP0)0!(&BwR3(|7LmrMn$I@1{3z0ml)ZX4yf zt2SJB-du~^&jTh~KR)fRmpFBi9$XMw%sjYS;eK4nKrr}^KLITPW_MQdNsm7vNb`32 zOEo<xM1k-T*n41Ssj_PS_^S}&48l2AlE|JoMI19&ocH3dX9sdaAwN_VLFl5QDEZRr zFVp+%4tTN-I=22gIG2%mf1S%fD40m9Qm@iJ8Fmvg`sqoQEu~pJsEMJjw_UUV7rFwl ziAGtZw)+%T_h+I#OAol+gVOvDJAgdLnqZz;1Drv?Xz*8DB@T0GE6^)muD+p;jY=x4 zCFBbq9|U!SgS`U=uKOHUSK1)H0Fi~HTIT!Ae;Ah=5U_USeV*$RFn;}#$}0O|c-Ve} z&mIaS@I}tkU)COKQ*Qzjq3Z0u#zN%^*+kiwj#dquVH$k_j}3Obzp)E87G-FiIv2IH zu#gt;-si@_b`OBAs~>kJ)H+FkYgK68)M)Cv(JF4+&*{066)jUvDfo$9zYz=4O^2UT z&B$DhG`GF=|6D#Qsg;jz3xDOD&(y$5rr$=EnmcXHcT?Vo+wCLge)k}rPxp?`wZzHL zn~;O;0gcsh*IswxR7v;p+%$g_P|&pmX56){q<-@bvxwC_LCU$NF$u@<k(_%;KN?3~ zGVrhDWkr>PTR5ux>il%nl4q62D(WV#m4O(*>By4C!{yIDX9G#fo&T87D$fUuK;LzH zWKkKL|9*4iZk}dF@DZgaA<@^q1;hYhAQFC}&%mSkxku*6VW793?;#a{sT8zDbq;qh z*3`Ft1_I6DA<-SBxwX3WYPk@y+Tr6k_~$bc;}1A)N4YT)+ymB328cE8Mh6^!Wf|a! z1lRJ`4s(uywzk7y^of&HZu<3L2ux0_^vOrF+Aq~|nEp8PF@zW{P#Ghz*es1u_<)q( z$L>v{<H7Y)H4vIYj>^!_gRx^PeP7F2$8`z*i4dA|r}_4SVWB^wHZ+!r%|@QI(BA$8 zQP-CN#uZs+aoh8(TJQHr8ug}`<R8M8?KD!@?Ey8vWM&Sv`-$oQbpQX#bYt9sWw|<H zOY|L#5g9V|>R07XV!gLj@a!feHi*%~#_uJiP<>z7Vuly)_+WQF#mNW+m76d5CVhLr z|9HS6>E?W?rP6j_d@uRh!(isf3{uSoB`eoMO8&n4Hanod3r-5F`gRw@>MmRk9ZS`& zvcLOFC_5o!GUE*016E|ybE=R2EhuT^IP3Q4ko!RT)8$SK$fp9oQVotDT*z<fLn5jP zk9Den6x6WWU5*5!IZ&X;felopbV%UW@1bDev-BPr#_Ecq=YjNs`!2}6sp`=nD0pZj zGP9T!UEPG+DGhQLO|}L>bxyCq{p|2m50ovk(pI|=<U<eq1eW2x2QGyMWto&GbZmip zUSp-WEQ(%eIji9&7mbujy5&`FdnX<>S)g0^*~j}NxDIQLH6FUPv#;;gf8E6$ts8?c zxi_h8?~><>-5K_nPqIc$;&vsDetxCK!sUc2?K>9{S*C0}f&D!K`Sis^nW2P;08<dn zB=)@ozRV{z8>4W)_MPM(#toVW;$h)YmgIBkVO9*%E8f(zN%d+8g<;Z=3zBYmgvg|r zd3u#9vsH5EGp?N@`V;A`kj7&+(0o%f?h?_k@r_w77hPuT)G#E<B(Id_3P?`yVN@!2 z9*?Nz)4qByS09je4d^10IW@raO!y2C_z{*~IY~-Naec!72v)b48DVlGJ@x|raJZ=- zt<VN?Hfs<hxpVO7T=LMlcT7sbTv-w0;bS}5*l^@q+9T9-{0MurCn4~Bd9^&N%7)c@ z@siZcE0K38`Vxz|jOkAo=%-R$zVl9uJ(Ha~1jUZpU7PF1J850@Vy1Lv?SB?(*q;Hz zm$V2P9jj}bb}`MG_JTaeJX)EGSJq;%1rMpA<(p@!o`-rcA?PnKkkWu}drx^+v_O8W zzF^i8l9|tfhP%7&!A3r2Fwdp)vYjA1?Tm?Ee5M>~lbbg3gOuGaO<De_F~Y9p`Lf*t zGlLLB>%n|6R~$n8bJ?X(<q}S&GP<z@9drIqeMYQiUK_29anG-qnO?AIyRa(n3Y>S{ zs&m8o%+NIV9~Ya36x=L&AJsZ18!V|Bw`RUp*7Fpy*dKE_O+Rw)i&C&>y=FT>vuc$Y zDx)iUC`al0ElRl0hBT6|BOhY<qveI8PPmK)^<J;^-n%hFrS&0=WV@_PC6lV3)(#uQ zH0y7X0SIiRO5V$w^ZAT{iv~c=812aCjooNtJ2EE4H0akO_>#r5mlSRq0@cz7Muo~s z)2|z|au`0`Z^Yw#>Xvd+#!s=rV8`HEz)fTC*CzaR<lcfKcYpDWnns@_H9)W2s&)c> zyRNroJI~?xlF44*L437R=4v8kH`<dQxS>O*rrWUuX)%sdoDNlfYttP$lId<LN}<5z z9~M6XQGECXzz#D`*uq0OqVSwTn{FD=1(Q4i?AQ-8AAHMmK|+QV6yhnE>u*PCjis_j z$LQP^u-$%{a}f<H86_9&QgWtjis>o-Tw*b>#GKMOv~1qa;~=Cj%x}FOExkL!ssBN? zIF<xu=K1ycnKf&N$VaU&4Z);LHCd<njc+5`OJ)l-0J_%{Wl>U+Y8@;x6hV9Tbbz-^ zs0m*X8#b{3lV{ZTyimJU8fh|XpUzz?25!TgTO5KQ&HzE+cJERsk_%y-$JX%RcAG_0 zZcZGJ<;F|oX5Rv@reUqi<79-8S!(@$e!i*E=iP?A30G}1QKMFVrZZWwl1XQ9^_YV{ zgOiIk;5$xxL#dw~QZ^x<Yj)V}XV?Ro{1uNAi0$XPg?dXd=Ax2N>I0@&=kX_P2W=r{ zv)}`ya*jc?p;xFUa0HxFaASEzM?UX0cyy7r00qhNaOt}K9x+WZb0(?0w!qy)!JUwb zkyK;)BHjn100%aH;MSKemp>ughOw=TK#C$xC5>e6sK57`zYxHrwsO<#&n8kW0$7)x zORs0M9C*j$3$<w}OjC?OeoyCoW-L1&(?Ld`0`(k)M7@~uct>EsrZMD)+69CPMP%M7 zzMD6REx$Y?$K^%p5a)uhL*^O;A$Sdc&>Ik(obvPSIocs_4n7JE`pj1sM83fU3N2)h ze};_7>z7h1s?rDM2@pbT?T41if6g1sB&3Jw!-Sx!33pg1RoK*bQmtFX*>>uo^dhDd z3*3~;-jH549|*Nwr|$rms@MjRnWM#hA^w;?(B_UNh`rInLyxkB8n*AlOG{*ao_ii? zO5#59@u3H3`TKOZfpJ?*e+M{itJU$5rpAh68+I(G8DK$$Cy%=5ok&zbo<s=o%!Pow zaE{}K4h&Yvd)=_s+zOQ%52siW-Bi#v*Qpe92G-s11SkJnlRs2YPMQ#+B+AN}2-Y_Y z$~LA{t3RhBbx!5I&sPAvB{z;=rCuiQaE%YMxu6e6zd1T5fP@s1><!X#jvB$x<!=B7 z(QY-qrYHCE3rA`D=1cyPk2RnToaN;iD2Tn**8}^3?h!M{>OBqb$X1*UTs#3l6G@+) zA$_#9t(F$4EZ=(TH9t)VL`+9jQBJ4vsH90GQi|qvz<f_C#^WnOCmnzdb*paFAjpfU zjI#P2pN0_U6JR3)+rc2^z&y*xZHa-#IXAP>mi75!AOh_eG#}hDhGd4ySDk(J9cU1N zIoNj^<Ihc8yy=pSml20}VTx;3zVny5E&RxhvXRpgxVlWR5^eP~3Z>i{?;V&zX?3b$ zWAo9P8$CY5WTfI|sjvKdQY3oSWe#RVbt^ySc?9#Cd~!~nxp_eF^i`m?(&RDvIPtMv zayQVrGgZmb`qid9)*~?_;O1J61-;+D2OjMuAf69Q$1N8NKy`sHPnZtLucjYbV@cv= zQu}qHi(?#6cUKy~XKWJusTS#B6w_)(EDO^CqQY9EhWUkqn7ksW9wLnRl4Y_p183s( z&7KVJtO9t(`fKWO(-zaiX@<}<Mga;^^@}(r-b|X7F&1TQT~U_X0}h0Ecdj#WPjKgJ zToEXn#46{$VwUJ>TKF7vq8N;3#T~u#r<Gd0y{Gp%(V@(AXkdK~#5u*_CiF}<pJa1e z?OIu{TT}Y;Mo?9PZ-P=Umd;Hmcz#|I4coqUzV5C{a$N1tD+D`TsKCKa=N0g?`K04U z_rB?a%Aq0;{sUnCiQ~<E0dyan3ksx-lq!H^V4BX$a)wD%#kDv`(pFT}!5CR$nlb~B z6J4aWmr@<^oK=(e*ot>rZm5Bj+u~G%+P?NTqqlk(|2&>|_s{<zRU#Lf{gVYmBM(em zpgQ94qW3k84>o5gojB$Bb}EdyCD`6nj8zwzH9cr1mMDJ&qD-7Xk0j=*1aX!l8Xhz@ zRgj6?>U$F<)VTkpNnJak(s`kk(R`%`m{lJS(8^eMt)+B3Ku8q(!J;kd+)=F=i>WIJ zyy|>xPO_ESpdxc7O<X#-a-6U5D@kE(Sk90NB<p3STDerTL_-j#-ty@daAH-|h?qba z#k`Y&Hm|gZRQ~=nEAjP2AjO|CSU7Nq@`D%hMY&|j*bgU|VCn4g#v}k=MXO6Lai($D zFq`0K(}aK~{KHA#tw>{8#fLf$4`_{r7OQf+7G1>z6h8n`gyV41_`CV*00gX?PqrUl zu!TqF7|wYh@~)7|P&lgJ+Gu$rqi!r_fAailg6ndAF5Fgm1CyBT#=-X|pmm9m?aFam z=8zwV0U`vuA>=ip>Y-GPBU)185fCVN?C8m$#!qhl;tZ7jl<hE6aZP4w?4Q**=pgu_ z%Sa0nb|J;0wp|H*d%t%9xC6(Q7sR9jv);!WZR@2p<@zY9!)}SmO8e3V@sE!kBADC1 z$o+nm{!=!&qE&GUnt3+RCM+#k%pT6mO(9#q>OWLtl#&e~wioc5qHRNtY9WZI8yle@ z<bSsJ`{L4AfzxqqPaQh;KhG!|0GBv-Non=z7RVKiBhCr*U!>kj0lxRG$Fx>p7Vj-1 z2_@s3_QeSa0>*@~p<l;+QGY5x-2hJv$6mhp3V@rlh|Bp#KA8Zqk6pS>N_v)CT{JS2 z$E^Mn<>|`z<n}g-Ih3|Mzeg6ofY@;N`R4PWOc7!8m+%wx6e&0fBfJ;HUI3(#<!_{s zo#raYI#=jWStRTkt4vIPz85>qBk*Q4p6Z$dr=Y<)H~#v+fzWRt*u8M{Z0#zM$|C$3 z*`K?9N)Zr8If+y0#}tWU49@)Jq))NOBm)~@r=F3o1bJP}?|-<&J=W+~CuIPX1o@3B z^8^gbTPod4?tomIqsqO+-c1~j*sTNV1rpd>yCW9Gg!$C<7Hg)BZcF#aK0UF3Q{MoY zEx~#(Mfsfc$74N#%5Lo*5}z;dT;s+x3EnT0@$jBsHDTEL>RXGbG7zGVf|)ieQ7qnz z86MyS)eRrarLjnPjDWn(VQst?&}v32+Wf?@o&%XmlyOUMuUj|(n4nP(KmkP{r3V$G z$3RWsfq%x7&woA6#cZ%BL@Z4J2i<e70!Mt$xxsVYj9)W7&<DZ;vN#_D$>qE(;*u_` z0tvfq<jw;EyyxjW-<71-<nLDp>e{gWY}K_9pownDXyPn~ir74;1N{>t55rjI8|Oe( z!V#2B+-;i5SnDUf7E;|-2?4Uh{?BQ`hrjn;=EZ>7g5blwK&m|g5HsxF3$yxNl)cAl z)i8Vj9L&m%nYJ;|$zyAs>YgB#lLinWugp#kPZPWl^R1^Jca{dYO~*hS?Eny)Y7o+m zf;NArfm{s@?b|to7$0;<c2V7&jS*dV`dgp?eFLs2v9~=CqNoNCPoT&Vbv%#O7j>!C zO6<waMFt`jqeXxkj=ka49&cvg*LxeB{|`n2*c`oBQmIwllWVE0qC1Iupmt*f0%F$> zm$WFLgZkhaY_ot$*o+xBC3LU}U>#66=KdL|40c;v3REl5JO5XBMuO=JKJZi%V6N3f zvPg+Pq)E66LFntVZ-1c_GAK5xx_nh`;J;Wbl?TD@PeMr9lZ`SmHh|NUI1{Fipn0NJ z+BZlHK%s(9yku)?tXKue>vM<CnH@g@Dn2}<&SSM4n1C^Wr8waC+hoVDW9N(qc)#2i zeXXKIJ?{OpCz9?1gcEm^2I^&wB=Y6p-{Am0Givh3ULArc;{EKG!(NtXzXfWKdibWD zy7k5}vNKHd`0KI0rArSEqX@XnY5?#bYFTFALEp>IBb&joAFUCqFSy@-1Uis#@0o9r z6`+hW4nT!BKPA=wdl=b&S*ySag!y6vuh2$rmB(jLLqt!5zdg2;o5pI+r9XK#Fnez2 zOVr58Nx*?aN_wm;@_7#=_M5p+P0$8fI;80nsW^;eK%sKUFOhc<Oy@OZ4lJkWne}-e z;dwp{wP#&Yc>W1*zI0?~8f~e>al7(LxP-1sDHt&|C;!cI*yXqRlR?)&soOF`zfwGq zwP+_%)2Tb~>iCtriJ*M-c;MqPn1T7}WF3f5Yvaydx#t`(jU>*S$V{*;m2_SQkV18D zAw;qG=NGDCM%U?3M)-5y^L@nSPK^gSRiIop^G$iS=nE^UB$$7hZrr4TNfWgjY!9u! z;72MzQFu)&>mmKMC{}4yvZ(EsM|l@Ao>2W0WU&KDY4)TQR;;d01XqDgMn{~6%*{B@ zb~AV4m`Z;_pke&3%pHStOg)i`J;am(4r6a+6L!^UcRGyikkQ66`dpok?L>X8afHHv zwN-cWW22LDQf7Je!0qoMjh$+p??%YmS;y{Y`q;9N7c*q>YIEI9aMOm>v*`DLHlNS= z5~JkM?Zjg|c0FJh%p*ntHKcV9Uc`DDD+=I&OQ>`YGC=pDV)qWzeVRM@P~dc-yGniQ z{h20nkIBMFCf*Oc7Rd?Owkn5LS2^$3IGRdrw46O5ds*m23^+({&-Lg^4PA-9E7UNE z1HT}ZJ5C`nBnwPmJ>>!p1fyg5x$l{5Vzo~WO&&Hlja3)d&E;ujtrQfN+<~X(=S15+ z8~XV0>v#YqNIzZ!vYP76p{r}ZU;OtVz_tuFu@3-KklH4zD*y-Q7e8tI(vdDXw4VtF zih9VVfPjHVc@vs`tHhfi0ZU@daRL+U@WH?y8GzdKI8YbgZ!W2wXWE0T==N=2QKJq= zn}T_AYZXK%$e!ws_Y$J2O6AsQS=`(Crz<I-Ci-2#rC^6~J7$%^X}TMP*67MUE8l!j zA2as3g8LNwZg+JorAEc(*sEa=ne&1g+H-T^;vQpwz~%+x?W4l=Om|0@0-H-lYTSr5 zsTVJinTrz*K2uBer=gYAp*SBspL%wch(bQ74<I9q2g8FRyXKpZ)|)2HqZOK>L9*pO zCSko*O)fON9_r7wbCfkn<R=li{1YjD$1OuyW2uRa)Q^IAt_2@JXZz&he@`0)!D(*y z1Hfw<XH)`vBK8UtY39}%1QF(kJ0FvWLr=yOMYNHzd!P3uBtU)sLOOlH(F9zGE&Zf+ zF=<9D(L}P4WJZ`gtg6tJ55=I6r6&4CUM?(0Bkw6uxUz{2T&_Tg;}#fBRWCXOY$*Xs zlg&Z!7i4MiFp+02G1+NoLNeF01@18*7p0brR$pbE3}A>P>1&U9$CE6a;lT^lL{yTi zCG{;VZ#AI3zt9EFws`h@mnPuOjJLdo3#eUvEJKy~Y};6Ng1$(=!crWbJcxXC|7XTz zyjg?ysPx|WOtkPOs^@n;P9`UTxZ|{RNlz-Gg%MV@yvj}+AXo#;kEQ7ikeis&2is7* zQdW6Nz2I%2A9EelxoTiGD?bXDx-&k0rM&rL_(h+4vR(!N!MvZoVhI)YJ9P6j+ml}V zG&7DF38<V8_M~RP15bt4VQkPF6Me+tkBa}o$g&q4H$uw}S66Bd3#Rug$x-Bavk^?u z(EB1)T6FX4SnTbri<M`ibDEyErr=yZo@6%)`N~|J9k>XLz(%;sYOegbNTd5{_Inw? z9}lwayk1n;sVl@QZ*Kucq1b9=9HQs?i?y%+%ru06?wiYmFV$Fl=zg@jiZe%A6xy}d zBd-JBud*_25}*co?fzpQqG<1FLlH;(a{6mn9W#GmAN(|hqG+*lMo2JqJSatfEe=Z` zUI&>jCBrs~A0SblJx5ESkbK9GLU(5R$~=-ts*N?~BM})0g6Zs`45(!NEVMmC;inP5 zgBj-bJHIP*@29+d&Qon~I@l$Q-(H9`gf=5sK#kE3pavyEd<9h&l?ET>c^R-J{DG>w zctJI;0kP8h&6l?DHy*JF`V!0GTIy-G-=b%E0*WD@YhxGmd=I}n=2Q&=FNqy!qAm0I zfVz~xk|PDLui|%IfW+n8(Z4PZ_*qqjw%*|{&p~zBGd?O%`z;THsn+l?Z6OGPCjz*t zPx)7vq&EQb`a@lSBMJMmlNth{lLEe35N%Dy5`G3c=#M3B0wqY>f^TH!3iXP=@1m%> zlf^^?R0?|szWViTQxM09n#R~WZR7c|f_B%<NzK8nD#IHxeaMQv*>=(ORlrpQ<*M6< z*n0#WFxh2Qcd>Y7N;3YR1z=`GX6MbQ|JL20iy(Rt9_8}p>1(HvrYW>39MWOEB7AV2 zyzp!wXSS;MWpT(0Q_p5f=vw`j`kuZcng=X>kL*MVyU{J}EV&<d)xp`hsI9H1Q|TDu zF{APUl+%sy3S%T~j826QScXyJqh2vX^vQ(Ux1S=Ya$!?Fe#piVN(!D67`zq=r7wkQ zIj0uu4RCnjs)_JJJ-IvJqaQiVAw3Xp3IJc>!_6M%-!|k6h}~a@P9TEgQ1jYK+7EvW zTeg1@&~AJN;e1@+#sC=XuwEk&qw2R&LLcQ?wVY%p#0mT*pS}#G-FT8YCNGXV;UWo1 zlJYERlC}rY$V$*j)^L3L=RLrx1YzbHQusybJ}@H&AgLLjULj*=lFM{|q7&@V{e&BL z90qBLYe%O6TCiV%vW%@!1Y^<TLAppz1b%^j*9+77bf?OLoG3iE`M*{mDDM>T6lJ;f zyfg&rLeue_kYWAZc_9u+M}3l~k_R9$q#9ICEt=yG_1CL$y@VQ8>t*-P#w0_s2LVF$ zG*Aq$O^d3slD2t04mn5)L!{j4z(5p@DTa#-2gKBj>3r<fTB_XvBM=wBT_&gPz|zpu z33Hy2m1=4nt`r2Ldj=WfwX&3xkO`{Q31K<M(jaHZaepHEeHA#t^1FhyEo!u#rFy<X z|EzP+F&NDGIutkb(OK<<CNH3);Pnr$j`Iy9KBaC+YK)=0Dg|I-A`<}VsB2-YpJvJ3 z(j$F@Qv1f=K^z0hokr2D()G(hvO9^skwrhNmxfN*0;U-;<{N7lc=L!n@6~=P4kHb& z>O~@xF>;?tz_!ujb~3XUjg=RTyx$+53!D<S>As$Q&-l}sNXPHyiGrpk=QC-Y=>K^| zB9cr{JbWGc{s?eO&~S3Nj&Twh%sM%AEXR$UNafDGp!jl-?OsA?tKV!z*MLw)++)Q6 zF9H(M`E$HOgFK}+)2gd)Y0X7fm3K;k3m)*u>-erfEp6#{xcy|P(mn$B+Wg~#=F>u* z;FTkJP2rWs?0VY#=Y-5002R^|HYnZ?z|zL1?vGS50D@Y3h|&!|T6l1tsNA}jl~7KC zAu^RbhII80rnN3AMKzj!a?UWW*NMoq)6!PU-;^&*r{2tI0-yx!XuMEgsBYoCPZ5|Q zCpUESx0DmgNysnYAkgyC%hSBX=+NTGQc&RibSNa^C97gp<m2ftq3EOf{V)h8<~s<e zDi>ElF?Z_FTws4e7QZBaS}km8q%4aWolg3sVe*WCur~;)bM1>*dGI@>0kt!ziE2P5 zS*ucJu!Y5fHL5+SyDkrF_Gu<%{M7UX^LE)@fM%5<Yf0ZIz+!W@!!ef{z^E?l#4#Wh zOhPhiX?@xPOov*QxXSrw$ml+L3T#alUEPUjDVnWlMu0WY3W#GENcm6?uV^ieR^<BR z=|i&c{tcP<s1T3rjUKk}G!WW+a$lc7`&If|)2nk*R+SH^hh8umQ1;87>^;fm+vn%6 z{QmG3X#?u=;edv06@j28>Jo;>Tjp|qI`>b6F$E<6p4*=RM(uz>7O%bfaz@(Mc9o1} zz}%@Jla>geo*NVM9U83A@}768fSYp&*i|HegzlRA-n+mLfNmgqQ);LpRBlCK?2R^@ z9l%JhD&Jjv$2uYwtb(zzA?Z+)KwE-Bh=%JZ2$0w0Y1GWy^A_dw=b$9)-XGN~Il@^h z8T#EY$ZqL-5>6B26M!rExfJ;B4eqMlXTE1a<%(Q#y_O_Zil(jS)>fBj#7n-9`2aL5 z5mTB*?QhJW;cSOA#avm}TE=R*5hv}x$0J=4>si(Q{-CI}3TmA9bW&%2!)2Zks<Y`4 zl}ZKG>}onx(%YIfix$U=F^W5`n>UD5kYf1#8R~#jP0Ab>{k9QnR!p(ip8k9=U_LL_ zWv*n*f_Z2{z0MRm%^*!;M<M5c2x-X4rw5#`Pzc2d(SC&pHL`esb$@~>iB+*z^PKkj z@q&B&tL@5@2ISX80PooY&w1yVR{|~{Q62?K1F70R4G$c*7ZHxhO2<lN_Q^#iwf&?L zs0|BXf;V(Ue1$mHn@$u*WWk&ps$@X8*lg20Y!NMugv}Mgymq9k1XVO&P@k-q1Sis+ z!?J#FL$=Bwx2E0txb`C{yO%VvC4lN3MI)6{+s&G|Bbg=CC6_r1hg*-feowbP0&B~; zNRA!Sp>|=~$#6d{N~gFrxdZiOUuXtSZSmu+TSc_zm?ATj?uc!26lz};MJr{5<P6)u zVu02orytLdO$>0T2*7!^IlJ1Jxg+6{$;spwi)awb<nmy)@k<25vFjjOSxms6zDCTQ zoV)3Ch6dJJ)aoW;6v4qAxP4UMtTLVKZ_bd}x@(x(+lSv1U0#A?N?dj6-+^&1d>XmO zTz1TD|INW^+Ft@xNN<p8N{od4B#mrZz%~H%98dzBB1+WlMs2}#W<dabCo0LQv|)N( zqY2AQupf-A_V_>Sy=PEV-_kxhARs|9h=2$Rf`EWZ7|AG#AUO*Nh)8CJ3^L?w1Po-! zh~%6z5+r9B(twEMoWl_An%_C^IXb={{`LQGt8QJtSX6Ci@4a^S>ec=9)7?X(%v|8p zI<O*&a)^In*ZN!oA?Lj;BnCV*5}@-rb3NhRMP1uHz&9baep$+sbADj(>6hpeT0;T* zXN@l?;iAUbLHB+Et65$Fcb#%c5W{J@(0QK)50@A!Kxzt{s0G~*4<Va<@_glB!*-GW z`p~q09y<3w4-F+Wr(??=Nus=@>m~EL!BXG>Nc5n76a$K<3rPy%^@(N1Yv)~Iz<fZR z?{O_hxyZJd6Z{9`laiojfxLVaL<QE-@CUwk4H(6{y`<?-UkIBMw<Q);h~+zAFF&WE zJdlG>y$eo?`enmlK!QZT>Lt>2VY4}hBWurU`3r9F58;A>4}gPwRGWKvESJ|<qJ#eH zEn)tmcnIJU6=(S$GL+tIB~$1A^TPlBZ!f`{QMLMSgWdPPP~!b{!G0Y;eW<ZBB9Q`t zni+;N3S##<=e3CyhR2C_@kI}Qe#pkaY&Gr&+lIcZSnp2wGaqOQdtE7P%oIaQ*4D^+ z)Qce_z07n-wH(F`#n6pX<UT9|2`TR3GYVOocZ+Sk8O$}DB&N9*x+;cE5~XP21`DG3 zEZAoR%JNG9e&=!@!`B2@q1?ver_A%8L2~P`WDH&?;c*x#6QJ@7nerc6<!u$9Dx7Vn zM_b(|^W^$>Aynr|t&qH6l2OUdd~XA$uW+VbY0le=!hHahqjSf3@QdF6bB7jCHy=t9 z3Jj+8B%m@xM+1QHc3;&<k;Mnn3peTx_ttEREeGv!WC7PblHh~POKj$xD<|*+?&+x; z0+JE8mydF;2+j+RcPDdz1F-@KAElJ6GLHbi1p^jpRU>w`X@#-NIixNDjbZaK07S2B zgX+;iwfTHNnLib@^2VEPT?S}IF=)V9CMboW908<{B&S357eC~mPqHZmM0*Cz*F8mH z@^hB+I5oP(mS7a2z+N0BOTFzV?QCJUL?K9;jRN?684&vmm10G7K$6ps@B44qHbx;` zS-_GYu7ESJ3xc~C&+}XwEk&O*&+oHA<RH6VK{GoT4;7qqsCOV{d;~mZ48e@vutf*^ zz3f%Nd*Q6QOnR7s|L$Oh0kS~Cu)!LHX@IR(Rn<T&#jf<p91tIEgA6<mOhAku%Vghj z*?`s)dtZgl0a@ex#8d%v#!F7drh}~eAn-w}*&#%A#J5u{qydQl4Ca+Ufssjz&VJx% zD*=@YHxh(`u+^poLnOUDjjD91Oy|>WzuPY)u+!vb%K>@W@NnH3(8@6wk`8#sV+P$D z7#GDElWOU`3^1ha?u|cc)5gyOYe5~yZx#$#B=t-H5PucMsotEM2$Jxh(k{CGe1|#R z_jLP3LD$kLgSlPF!|`h$0SYq=Tnd;tVq3O<9As1Vz#Jvi$0PxcUm@OqcD58Rux^c0 zpfE5ZRh1=*6L^4y{9U(M`6Hf{!)b6Sc+O`!NnvoM%Gf7QYe8xtiy#+E(Kj&+MkU0* zwpmwMGJ0|qR62O6CrIJ!9RhI3NnP}ax_ti%F-{AB!fXI@D-UShBS0OtuJgtj0$eL+ z-`1u{5;#`Z>QIbHXqK?(Dj<+$2(E*U;?GK+9)w`<>R2z}H=uM`K-@u^)%{ym2($Gz z?}9g?ZUcnMd;t%0KmmZ)s}Ek{H+=*4+pAvY)&*>uW$0_508w>UA#%Z=Q4WC3#%~3X zGriO4U)1@7p#te2N{A^9q0Yl&NjJhM4!q{Tv^3ThNO`6A0dp}(x_x%OS^S5JVE8!- z39G~T4MnmUZGbmp1E`Q)jf2AKg2b;Az|7fEz^ag71SxAhyq(Vx24F^U#>_d0%P$8R zSR`m@LIU1eM@lp_;@)Kl#;gFapqqf~qB+K_+<D>bv*Fw+9O~&V^t&;ys|R+4D&Ldb z4YYyu1&yT5fwq>qOt^kq9Liq84MprM-dBj8#o8qQQ+YECbOIp)SeljFee!3njsYsN zE@3g?x%kWpzxe%3@s=_GKFiMldooX%kGP=>f|kP$-Qs4z!6^a79y1BJrP%dTEDjhs zY>9j?ZaD94-qsEv$Q8g!ECsHlaD3AJ*Sa7vuw2xyR0s&7(Z!Nv`!=Mc|Jd69{sZ>Q z@R}4jF<xx+KK@-^K7RkAA}jyjv3Ntxa9ThzsjB;@nGhbAcFw1_ndLP9@zej22*k$^ z^os#RVWt1ONBo}=g+VBaA<6@c6b9HePEhX0WqE^XstrI*OhAIC7%=tOPAUFy07APl zA(x2b-YTjD$k>~Ifjo@QaN^Z@p6HW%yi9WEy|X|O7GihV0vsw2k-p$y90onmp8`tW zLzmz)f1KE6K#jd8j;BK;mqENpcKgFg52maOP<j=;u0#Ys<AA8HCqDTJ4$xAJxYbC? zd45+!2<{X$a9QR*3`0^tTLQpYuJP7AJyik8nJkbHB<h~qe)$YX_Gk{UDZ2nA(ilMS zxuSz0rd%3G{{YfaS8BM{u#6E!ODV>d>oYQX|J)%A@t#c){x|};H`NK6bgZPtzzaaB zR1;!*-Uw4z5q4eYR;P#SFX;EbY@#Qyx<vCRK-U5Jm2a4%-U?Mwj7|mYtP&8W#Wks% zIgFLY`XC3|K=KJTTk4Pkz^!(h3DX9ZCvf;iKhnUhvrByMF($iM2c7$4V$K0ZWfBZA zKQw9}SQ;zM1^K}a;8d9LRjvCD=ID!l*K;7mRy0iMBvnr-s7yV7XVCm5$XQidu<EWC zJubJ$(Vpo{?8w0Zc7v-DjMiGoE}bX<%#UFpDmeJKZ86Q8f0P{fW2$x%Sf8E=_3r1T zNC!kzpd1qOjyadwts$Vv2ZP&ppR>zvp%Zx+UT&QGrd)rQ;IF8~KW2`M{U4>607bUh zkMh65yfTrXOhAZ-9aP{<2owvxzmh8*3hH&imvQPh0J^p?dNQD{tOEid6oSQ~=A6wR z))G_Q6*WH&ti~v4iVvIXiapT;I#3F-Ug$Lk^nFm<Qv6_SQeywRpODd4yjs@?14Q~i z<XS+l%LEJ?iwyE#-1&dLqt6irVrh*-mD<GfJj$yhzf*Ln1(@vE*2-tq2rXQSVplN# z55s7V5wr?FoucK^$OAyr2#B0Jb8gfSV>b!dOa@2Qd4y*FLrmt|#RSm2vq0WNr%Zl` zxAh^U08o3E`oDybh(47J1Bj3x5?gn4z{e-4&McM!MNo4&1FJWp@4nZ*T8PsE{3`38 zE$^HGOSY1IcWrVqr+Qac6yRK28LjSE1ve1ff5=@V`gtIQhQ@&BVPN?6Ca@DD0RH(Y z<0W9x*BE$?{wZ*T+rO61QvGfo0q_KL>V0cXKZ1dTj<*Pu@>Oz&-2Oq>A719Fc}o=c z-s_SH;BN`R$8THpCIyI}>`$kntbV6<fDLp}0kZ?Es{x`+XM1h@8UUDTrC&LF^#*f^ zIxpL5V`@Gvhq5d{n`+`R`PGYUK)-%h{JD}w4CQE5Ah;6i6k?%J!zfs=PSx{#i*~)- zYx63-qs#`qH(@?{Mdqq&Dse|0){vxkT$t<@Bfc6Kbj&D@D2fFYn9GOnIjWn2u)-q$ z`xv$70KFeD4x*r<3lSHwcu{cRd8y8&_al&Jz*EJPBv|#QzO`EHNtDLCmsX1GyDGRy zJXQ$hU55kAnVeMax-kHRQ+aFzckZ)FV1gLahYG&(4m-9$RXsugY(9z^Es?qg5QT=- ziY4AXef#3X@&6{3Apg=)3gEl@r}3*<JOsak`DNm(U?@-U$zSYWSV;08I&gq-ex9Nt zuVW_iR_`p2^uyd3w+APlPm@*xvN&1Gc5K3~?Ky0FQUz!|tt2xSkKQcfvFKMg<^vKG zpp?$o=iMLwqi%a0U{e0IA$>(Rbp9!CWR&!*!caW)wSm(o3P8hb0QO?|q1<2&Uo?7y z0Q+fvcQ7wm{pmV5t&xe?1vP{}UxNJ<cVlim=zp!VzWp4WWLJiem{|S>b}bJDv5y#u z6ezJ^dc}8fN5)*7{CDmyCK!H(Db92-I9nxz(}J^x%kWQt{NH~l?f@lw;lu!{jYQsH zJ3{`L?@NOUMX-Y8NNLjVRSn=CaS#xtQND`@FSl?l`ggu93k4Urvx?J$&1MjnL6iQA z3;f%2+5lT;<H}_(;G%4jCAngmi9{N3E}4|F!D(=0pE7n~sWM9_rgHC{f@}!&|5k!7 zm4+b?p~M#$A3)wu`2P`svtg8%ou*(5-~&q((O8Ef1rd00S;zhp87lAP((km`CwV#u zBT~3%-y^`U7YL)V%9toPF~4?-p#S?t|1D^MxcvR1f3!*XtD}E+{r{NZzjpMm9sSq& z_xEJ_d)EJB75;ipfBn&ayTpIJr$3ua0q^OA83KWvo=VSLwS+*VAej29Wz^X+n!)<! z4P$9lJQ^CoS2Xp$SMH~fyn5j)&fvCPxOd)dt@^$(Uw+Iz#p?+PEQD&-8u<5U8iNxw z@)O+WV%wwKoSk=9S50(G*Ck!Qt^$^UFj|x(WNVc(^Ll;^X9I@h8a~CveR>siqR89y zl9jlO9UWXf|0n>G(wNkK;E2c4P9NF-dyOAq)<+Qca9V-NwvU1r2Jb_D3m0YmK`?kF zI&u9Wg*OfSz8p4c0s3ma_#oTL3b-*0uds8bY?$PPYUjJ!TX-x7Z?9p+CEijPM40RS zItV3+Y*;d|BKlBS5VqVCl3#sD>HV{0iV%BQ<sb~TZ)J0o#F4$r7-)%AF~NpOiuQJX z&=<xHwWi+vBU%H!(%Irwz6OKjebyc9Ipb1X;NEJz3SnVvko2PO^w^IM0Pz+LS6HY$ zcySBD46Jp9V!})f!3fY(g460o23EI=P(sYI1f-|JSRkC^uij%tnS`K%g@WVV`v*W8 z0yKkIjlyL=0%;^gvwE2jN^LT#{*kdt1D7?}rgDj-s3xQ|$bXZh6w5mx=a<&u<6&1I zt*{14Y*E?+$m8dGxp`0AP=BFwgIMVSSxlW-QVd5WSo8?{@*nAtcyL)F(VvL}BKOEo z1*><mj|Rv^q$EXgamzu}Eb95OA59Dfs&i^^U7i7X?mh~Q-<rUKREs8f@pMaGV<42F zCmM8E%c8_A%RKNa_Z^5@8SPbUOxhM)R=RRE1W3c;S_T#*5PA(G4Z#N|ZD8%tq~EL8 zz_F(aE~m+UH3Vu><&$>lcSVr80)l#~k?ZB$pzdZY@Q<MWClx?-u72n_25JJkkP+~o z=<}cRG!#?*dk?DY3>ICWIE)qYdrL`x%kFoHM3o@#^9T}uFVhB<m;f=7yU(ww^&XPa z=WB+oMKl0nRZG*j+Xe^)DVNi*pbn`FjD8P31~^DcR(%?Fq0<#`m8$n#fVM9r#g_Xa zHVXJ1T>f<L;Uyv|wLTRMYAk7}Vx(aid=z#W(rR<>_mX}HKsmu>)p!22>=3!<XH><1 zR|KDXU}Lcr=-6w&g0Pj6KKKKUz6u~xdG<s!(C^V38ow8JO0o1}9u1FM28(_cllN!q zR&e>ipUU|Ygw2Tbt`pYH)h>g3zcS6pV#AUBjNgrQmEsz(RVgaz{vTN(a<bAYSjk8q z4X}4oGRogvhREsosp?}d3&S2tq9@M9!;ziR`iQlurRu<sZ@>H~M&+H7l9+~tX-Oqv zq%nB$fE(1xOunGPmh?TKgGpD2FkI+$f+Pgi<(B}ol6zM8GY5cHaK_+2%%D6lgAodM z_keyE8Kq(MO_WN)=r>AiBn+@hg^b95*M11@0eG`bqN~e*mhzRse-Z2EDHy1Zwo^k# z4mb26NgS4aW>LY|szfy+P5_971|~3Iy?8p<J93&1^kC5i1j!XxS6^bZ{~kY$0TFKK z*X(Opo7&qOplNngV6+rOPEJ}?5nCGIbF*a#Z)$^%VYQT3u_gTi=-|EkR4368wkgtk zJ6M;gFtm^s(^h>>+|Y5Mi+`BG2(T*NDF0Pp2F)8(c(5N0pj);I*8*;^XxP_Fzh4EZ zbBqP1emHCgEU+NqAlA)g1HiqxqSFiB<FSOD7yET04rUJ~r~rd;_cN7RFof+g=^a6= z7gxjVos8LAV9_R|GEP`lk_w~!FHEmhQQ@%!=&4fuzNsNN93VN$rnV^ydH+5zDH>ZE z7+GmJ{LBU1Cr{RgSbD%h0gU_0S8PXuKus85EMh4Fn;KA)cb)Gm)$mx#VYmMv(eQ#* zJ-$VRQii<m2~5<*el$oH$~kU5(*}zk^B2duO865fLMz9|ylG&8ttc|EY-}hoxVMIY zy2(`{DK4JpzYt{qvD8wDz+ePCyqa<mco+=6*eTr<jB}&eX#y7A#_(nl%Sy6gw4bt5 z<@r@YDdN&3Htf9vFz`mXl%MY)Y*$I|3t~$HBdfI87DIa6&?A|T|IM%e>n|2y+^J~g z_N{=LKq&^X6hSrxsL7R>t1h2`e>7%`hb2m%$6!_E&!{TmA#91H(%1wEDhx$?ezr9S zEV_iq4NHkY2X=5-`I^x;V1b8NG_Y(e3oW>Jx_N5bLn?0#-St1x9Cv{|Ar5;e3w$zv z0lY!1b;*W-7te@pzwp-){U1I4>xlk!ME_+u{yL)n|8YcBN`T|Upz+5cjHmSw99Urm zItMEMV<7$$QhW!V#Xf@=cF$#qPZ&5~pMAF0lAyspf(ZA4wXPxP#yWf`q@H{RUZW0Y zOc*&<$O7V)8W8X2);nN(E)<VAfo<1nGo6vfI^S8my#e?6ht)cEiXG?wE6b5`!VkZG zbGF}RHl`E$_*+)ZXvUy8>DsTyu8%*_M?uKJ%HGx4H|R;;7YR+XPMvbqrEOfI4qeKr zr)a}Sa)}t@X7^iTM~7o;sZJku6km5D`74Vj8U@Dlw1(V>Q8%q7lxGj7;-lVuFpFN8 z<h>zzdKhCd{zSh}v7utNF6V7WxL1bye7aVJeQwQOz{1`told;f+d-|Hq(hk&Lb^7v zevitx3dsZq$1@oY9_u6Irbk6<oe_mz0z8cAq)X9VV*)o|rD+jPv7Clo{4O;Dy;&uT zT(cFdW6@4Sy5gA%=`KfZ^8)<K4K|%*or_V)&M>XTeuo-=;>SnB=-C+;324la;kPRH zgPeL7-bBsMzd2(n{!%(;LYsuL%pDHp@imnbn~lB3IFpy>(+fK}_ksmTymUY1PkPyX z+gR!39kW?p<C;Hkuu+6+i}RPX6`&o>&3VOB!qW2(nb)+Z(1yn?<0u!kFbR>e>2Su# zOfnmokdD{NaKYqJUdyNB@1l41DfQ}BHP)TJUeb%!gbh!X>`BC#1lLSwf1gQMa)vSv z=(IEG!b6a!W>v1UoywhYqVV(nyzMJ@Ki$^)p3FBFU1|*}hIfLRl?tI?9`)GRgRU|3 zCbE;R@I1j6UZhL5NPP0Znyc|RQbwqEMR|8^vuk%!qxcNtFWboC{39bU9>IM3wO0=8 z*a<0sDnVTn6PxWHnQhSv!p~(K$hHU`$&RvsyFGK4JT>EAOkV4#%QwXD*8I-Q;dDdP zb&XW{{dB9}vE(h6>c_4QUb3a^sV37q9nqP&rV_`_HLi%3FH_}CeqN{Pu_v~*#FdbH z;(T4APbyXHcdOz%2Xf~|;x0=1L*H_3eYc41EO|Rb(!S8@d%re1F5(>Fnd<=;fy9|V zx|+4Qn9iS_ys)#~v+E@u6IHkiqZC6j1HM2L^K9$uWYb9b_zO(ijn8M=;;y2{54zAf zq+@YRLMX>^#F;|FZ=LeasFZl~J$XZZLyuEqYr#y$>)+zu#QtK3vRHg*&wG#CD~J~A z#YoZ~<)d;p6rl!4F*_xwI`=KSyHMn}BagDE?SqSjR+)+&ux~4mddfKzPU|l^Z^hiv z3E)+ZNAhIuea(IC1^t48w!MiI;J@ZFE5XyzE98B^3gd;0jFa8$%k!S!t&F`rm0lko z9TOi<aZycn=j&(jTY(u>60_V}r9l%!!ICb$yh3#uv33lx(~D-!BSy0QetXqU4)Hh4 z0rr(}C|%V;r=dY-<QiLAv7=+K7Gra&ShY}2BqJ`mEmi?j1AFN-1C><SfQ%567_Hjg zee=B-VG=vQ;U@d^Q6$GNt4@((p#>`N#-%*ZtA}JdqhT!4LsaO^XQpST(x;Z9Z%jx< z?Tk({T}W)=j7j)VuKPST1l4rgo0D{oP(7RTlic@l+9^xP1moSsC7;jrFuKK56kFnT z*T$gLZ!o_!XbcdE*05{w&wcAlv%`{Jsnmq_h4LL6cljDxD~CtbyI)w!DzC7p>a9o( zV<IS;Wym~e@}1+a(KBZsOE6;<-IHl=USh1iWHQGS1#@dCveKFy$gK`vk-G`Ka5w43 zH8{Fy$fRXI*H2n+q#HEncKY!u0KJLNl9O;ZiTd>BmFKQW)fOTnl@|66+{-3GA0J!I zZM+y0_||R+zi{zl#oW)}`Xg>czSx;idVadd-gtMLn%KgYYj{?ZRh~B@-jJX&7;uxz zpEi%UDr`@PSRrpHT1$NqR?yP_Y`?EGKIga=V=?)Ku{T>N+iET*J?!T4gEa?OHe%(; zm{7$+v-8isCtatjZ;!X#k{vQ(&_V|tbG7chFI~hZN<~&RcVyhU(HqDoyy2~e9i%nt zi(!Quhl0rw#D+^~G3Qb1t^Ad2ewQge(?U{9{1uu#CVMaFP+mi`GrT;%cMtyKjj+s) zx^kjv%m7)<+*Iodwa3}!(MyJF3Q;B}{MNcEt8ERsXYY@@Oz`E-5#QROA0%VQNIXF3 z3hGNHGZBm1NI0Bsb?3($HsqHSJ5VeXHo`phT4WpQwhw9|caklhooOm@4vyNf%%<NP zG8z4H5Kb^wJ8eH{*}r`7db_$mucoXsfZ46o=7K*99*f<><^P^2vN2L3xD=wUmYMwq z=d9%qn14jOJA||wkaW2PRM(Hzst=-2lBHX?+xc5(%(wg^RX@~%b`gSN!;&Yf9)1r_ zL*hLTI+vdhwb$o-=~^)@d3}!5)pDI<Rt7B(bL?k(6LGPsdZB11kUmI&E(n(h6`A>^ zsUcU9D8yylvf;^Bt<J*46|acx8;gR}D1q^x18xuSD^DHjyLM%S(FCnt%858HCLA`! zS^dfxHA;~=3nq`x+jcdqMI&Xr=oNGsV<($xwZ^w*D81)pb_310XAfQ&Y`KeeJb->J zpM~`L*D%EOB9+4FQ3LX`6Y3%a($J)D8Ho5meU!7cTcHuE?LhIdo{L3q-8G6=@bVfr z-r>rQE}i6;kDXYTZM;^;TONCwb8k&j#z%3-F-$WP$5|!mc+@7v;F5aS4D@qVSj-Qn zyTM{_YWru#e+~ZDHPL#}b*#sh@S|HTyeC&8>1}%TE<=5e=b9&+*L~Z=<@WSV1{qF; z^4^lE3yf*^pv8WHnF}(8dWmz{ZI9zmQz{?$OBWwmEjo-lZj<RGezqbxX5{Z0+VWzR z+?qU(Cw6#L{<ZDR*ZWoGu5BbTIL(4yF!jdDHyaJ))!K8MGqsiXH3Pr?Vl+OgV2p;u z+Pjjy0Jyy)>u1LLA;l#gem_tBnpwkX->vQHB(vt+ALU9DTR(8>b2}<V4$wp^eot4~ zmH9xYzr)(o&%$f(T>Q4?3!U;a6U^PLew0qVeJUVe2Q}x)B{JH%7BcCn_mpgP=r*B3 zh2grTtpaMuX^RTQTfnV9KdV3W;_iEQy_+~Mn6<>bHh8D_ruTMZtXDlu>}@$wkxT63 z0jvi(Yw?8|)aqQf57Dow8|3#2qzeY5?`*wm(X8eb_@=O(s%O*VUmks1JLafzgw|G~ zh@0BO)a`?Lf9D?c-15i4u?Le-kF_ETLY3EnOCu8;g$;WPkNNaVtZuhvS}iSt4*7`# z+;_!egbJ(4X{5>(#bvg2O+;)8jcnC(!(7Kx4Kz-kmAtkNbs2y)d|j8G8xWINgrx{S zDY95)>a3p@I$aCg`nXjAnk3(Zj--A4Kyuo5>v;^qVZKs$C(~7C%g(&t4t5x~Hc|3) z{*xALbg9i}YLXg1a0N!5Nz`9Ueuj7joqJ4{qiI~65FvYcuG1uE&7b{;(^N=TcF)6j z=}BtXp*8eml8j#5^)V!A^RUWtPfKx;K7iBn2JH@+QrS5#4UV!Gfy@=|PF;oP24}N> zk??h>&j22aF7M8|USJ?ah3gpV#8CR-IN!IadffQ=S>#UK1$2nVt%~+Jk*4wzx(0|_ ze_G6iPUFPR+E#9M$U)Bu!lUJ_w6BA?+VJaT(geMOdr%jz!s^>Jhu2b6_((}vduHK1 z+mE52kfVkcOZNf?AEVdoWZKIXQ#0d9U5*l_zQxnUkeTfbj`<NOl24XftXE}7z9Nzm zpfNLJtTR`C^f|@_#=x83J?Qr}<~}LoaQALSZ)d4jG80Ykk?JC(kvYSmN9k>6xz_~m zW7Gq2KY=&j0_b}oKVL#6h~`98W0=gxl-jo}`?(m1i5v^F!rojeuD<l?xOVx~E#yd; zNs+#L&#ny8DdzM*sj&K5@R&#Y_t1aDF48)JhFL{ypsw7W#MR4XUC!f!ew3e_{<dN% zZ#8D5Jt~nm)sQGZ*Vt<&`rKkY!FW+Kr8SVYFAW+ru~ZVV@|cotOqjJOovv{e1|^L{ zdqKDL-M<$p8FgRa3Cc3KW23aseogc!T!Oy(!Di>+RFE8b+=MWpq%ZY|q`<}1tg~F< z<3n@(wzo=WL~VE-I?w!ET#Vmx8DpHMtyCzwdmk1(zV}hQ)H$N?6s}2Ds7@Yb>?TNl znyp&r(9t*e7Wi}J!p8BiuX{X8AD;TXE?bv&y0$CUu4^+9@}FqTyKa*TxQ4Pd<SDJP zPJYWa(bQ5~vh#E>ul~2D%8Y^L@R#Nk-y>d2ASnn?=Q!`VwoFLT794NwgmkX(xnCH^ z3$AkN{?2ovXmAs!CHwTaPE5!=qc@M1G|F}=C^KmG<uE$Kr@oy7W`o<gm)MC)SSV*F zeoNwgR5u^BRXG+%5atjwukD3;ZpmE)y$|+zV7Rr{B1!$hVDV(UnN+Q};Zsy`N`CFa z?4#P{%yv&!Q92pNtuwxNmoAD2osOc!xE~)z?7ZwMzSX?jcDs*&el!Vib&=JS+c#7Q zzqGn1>z(e^=2asmNSx8)ugB5j>mTOPG|knPHYqXI8lr@hj^o8nPG=}~`g+ENPG<Mq z`zSK!sS4qQAAE0?{s@_^7+$E-_U%8~%F3x1<2VuC89(;eTv|Lul`S=r-BG4L^@uJt zHe`Hd@tUlujZVWHo{;andVXhgA#af2#NurrgOhNnA;rD>exKau-tr?aii^e?ZX6^} zg;nOEmD0bohcfQkRyKf9HX9uWsITh{v}NCW_d2-OJSjT{rx0P~#_gMr6l~vdANA}M zi@ZU+Kky2P_-s)1OJ>oB_G;)bgiU0*0cSybx_-Krh<(<ry$kl;=Fmh}{)>#w{L!rw z2bbx@DUu3d?q_a8WNtn8GAo7(2w32cLfG*%pHr8X$C%c>sl21U{=tfuG=Rb*=ehX2 zJ+r==R_&W5y3KjpfU>1Zk0wy%ul%ichZHU$*zFJBKURe15KB3-W?aHw<e9Rultev8 zJ{q0A|Lq}@_eIBs&?ncVw`YfQg95eF=w2{;?^m1Ae6pU#mp^e6#FJ~dGH&_c1YIPe zL@nA1yBuR>B?+eP&7D2xxj&0wGB3W8t^3o4Vb*?L(T1Rs;4zBZ>Z>}m>RY_z4_M{E zww((Zd0=~$CyejZacT>?Qg!Rg$ibZ>NL}t^Pt0;-{kyn_i{ewWdoKyt4t!s&e|^Vs z2>rn-nz%$%LKjk__h4nTW7jubpv7h4w!^8z&qbb8VdLE*ueqXw%du9kYj=mgekwdI zc{j?iHD4Ff2|GpKNY>WTO0-<QCICt2I!bFcaV7aBx_U{AiLNSUT=J!v?O}_Q33g~r zSVj84+YN|lNOSwdvyXPQmfvFM&X1j$wi`RsP;2y>dv2}2;JxywoV;eOcFr>W#Z8;w zqZpOFnld>Ry3^fC3lN*$p$y<G*YdG-U`rc2T5!{L3<;^c<AB^`W@fn2G=9+6_<gI| z4z+rRoH~0y(XhF=JjUTvygZJM+It^aw&hQj>el|&K2~V|vw?$zdAbr$`NFJP55+6S zlU&%ol`AJ0_QFysTKH6LbiRkEHVNLRB?;+$b;+xEtJj>-X)JE4hw{_|k@NDdU~d*l z{L$Xqa;tdT7KLq(UgW{32-2k+<%n)D94|jk)V7%@dobkEz4L7SOy~mx^zit`kDz+a zZB)pZ=c9Fp3#ObG2lhm?kB{~$^Ol3B$71wLCU1*<<V#bzv$L&on}573ivM7%e3p7F zxzQtN{AF-aeZyqc_sf&6(;X=Ho6%d-a+T*?#%F_sqtD*QNiJzCF{kvu*}GS9NjRYE z8?}d54l)3b#h<x+d-%VWE$<d_RvsiJGG^`WcR`zmgHw0tr#JbbF&z)iDizmOc~X3u zIBhD)ShA?9X<k7$m~NteHsP}vdeydUS2#*(Mzq~dHJ~JRE^0z0n9vv;-kp+L`<yu> zW*Nj&l;2)hQO@A#AhLfbz8R>nS_QU-Zq)Sm&kiwSFzxCQp@PM?qGsEwKH<3R*$PRW zhT&x&abAR9My<RjcI%w}(QPt?Y$aV-KSKenYFYyo#oxqTF8eaFtIwmy(J14ZlCB+8 zX4OK@r_+YmLQu`UMEj%b-I{9Vsd?+@<DIQZLom~5CCz{no^$^66N3bFW$fS%i#o_e zkH(!8AI%-2Q)^vLZbW5_-d)pI#UY#unza7~Z`)@j%o4zP!5}YAM5ZU=W9%s5Q`WrA z;l9nWq@jcI&(!v1&#PLm3$Is;UK~m2xr<s9)JbIL83<ovSI!jQE=%)L86sWT@ZzaC zNh?WJ>_4pEc<f~!M~-r|YIjjzj-O5&3&L+u9Oq(;9#^OKKJ<BID1qF1ic8^An0823 zo$qqn)hfpUjXs{Br+c&~T?r*3i?{{prTC=vMeBN|`<R{R>|@V?&VZ@2Eb29%%ml8> z#YpI(bOn9qdL<vLq_*A=<V;0y9L)sQ-^nkvnQR~E=&x*z<ulYnZ<X&?hh$VN<S0Rs zX1sGtsmr*oS1dIK=OFS!GDpi1#RYFl+`XV0sTDXc(D0>;hjD4&?iVUr#vqE3=MZG& zi3T7OHI7%Jxzo2^TN(0cq-nV)>&Kd-CM>Zc_XVVJ$9Kc+AdUAV)$0_F@bq2q8ib#~ z<5>S8N&Jf`q3ff8$8zSIyGh)|&6hfzsg$K-@J+i9o*DQ>1x=nv4TZG%+`iCECjsR^ z1g^Q7rqdF#F%uq)8^E;KlwjzvW3`!V<{}s&E-Vt6r82iQXSW^%HQ$sbP2Yf`WIvTV z-X{jV^!JHe-riv4=<e7r9`-WKnLB{LI^HX1WD20vT^-tSzt;Fo5)=1K5_q;G9Z>%K zva(%WLWD@(pL6M^J?CT~;`*8SRv7r=#lgkmXD9NdIh2az@#3vHp1*-tL^;V5p>bYF zI&vb{qYPJ4K1JV9zY|3=RFu0yPb(c~U#*PP;EU8;K7E|`BhcVy#YWA@M1rLz1FW4N zVmE2$5hAdWqqoDpc;2JB3X}l1j-6z^GEyiHtXi1F^<b8#^xpqKJjZbObI+Z@NV_Oq z)8VZ*>r0##?UHZfjMKG3n%R(<@a)}M?cUv)U+2+TP|4nh{D9Zsxz)s8+{(MyUQ!_} z?)X8dL#eFn(2};jS!<Ammqmr1l(PJEKxyg~%SrC;;f!AGiB-Q^_%}ZNRE=D-u7lQP zgx!7e5P5P76SCz#*-Ae}XpPzMdV}Kd;#roxZ4g(unb=cAny2dyj!im#uus&#oTS;0 zsK1v)3Qdc2ON@j(J4rHBoO9rsJ(>qW>76z50P)_>cE?N2;(TlnRav{K<gm=`?OH^s zt*hQCawAp`Zf&3Kd~<l>W31ij*d5v<IT?8-Z*vKfPOHT1_}F%n!j`J@-Q@A`9Cw7Z z?KrtqQG=pE=9>Q~e}(UxS^eH4?oom<sjP`UsSXt3l3VH}+{_HO-abX6PvJU)RhNm! zX__sTZ01V;iLvIo4XfmP8ZJLKI#uaTI+M$MvCDO8GOfZY%#w`4i%PrJEI9M^C8q|l z5haO?T~w)Y!*>-<T#DFhA}NYnZyiNzpQf7xB#Bp*OKY4==gvk+8tV0`b7(`TTntuT zqtP?u##?^kVaU{S+2E4u>>}%(S;nDTe1<hvO*m*Xut)A{?m!O43G%!$N}Zcsu8P^k zaRhYWs10%wN@es-UHH#|DaC-V^$3!rNslNRW&09^aI=1T+5oydI8CB6?%c6CS3T5r zo}n+_C^mXl+fAXpNuuYJHF*j$ngq?N@4D!<4~^qWk|Pr=x(`jPkgP`KbbVLO%ZNb+ zb<e-wASpxeEI$n@D{H0WE4=*TYapMs_eNedr4}D~=>g7*SsdY?sh8`~j;MAr3#M~7 zdk#`Z4RiPq%hfj=T^9=U8)l5A;XUIA)0a?|(A8lJp*rC%65H{Uj7$2EqzNA7;j6C2 ztxDBqsUt5r1dn8m-jBgW5N9j1R#I0@)}J@28-G0zFWT&)cUD6-I}6eXTanT6;AeEF zDk5E{5u$cWeW7(g#^;s-osh(BO~?5b)lR++%}fqx)8RpVvI24U0eNXyw%jef5lyG? zVRFGb&k&lQWju!-n>LGXrDFpZ2a}^l?E6JbhpOT`mP$(dNV|i{ClHZL;dV7l&C^1q z?_U`#1?n3`Nqq4=htEsgSfuatJm!Yfdc<8V+s~R-MZO)4-E)zQ@v))#Y}=dXq%`>U zs{0-Axq}?7F8WJ4tWcF7TaT4@uaIZ1XCECdT!gm^;x&`A&{mj7Jqw7=IUA{WS+2c( z0%2X5Ko{Zr!cNHl_RPe)J7bFsy%4rZSJd2_oT}u!zT$W6Y+7SB1t+6-hLbMe?H-{` zDndDDF=-7}Y)+P7LTX%E!dYDjXxSrLFA8rBpN7l-N!0jArqmIWNNrkVLjCnjwM?8? zpQn+MoF8T!$|zagZ9Y6qc5YczR2_8NUdmyw((rz|wvah)PiolX*6p#68s3Y47W8Ny zsR8qZBq0|y?FV4e4c#*cpZVs;F^7boRy(0d9_8>J$LsL^Pt4Ki2AFO&JM!VB9EI&( ze3sth6RSbZo!XVM-P8sx#{D4QIeUr45~KP~x1l>WK?vh<w}%h<drsc33Q0oj(Dveo z;t%Y@dOh)}zKtc`$m!H{nXIl{X-#14F^44WS>Do7yh6Wcz@%Mgmsw3|;G~(Z0gu*` zxf41@<6ZV$0WC82T|u1Th&EJUFA$!W9Mk3gMxUu=M`+_rU<SM*lmG0Coa{Pzr_;%n z?yni<IphIP;Ob-I_F56|J8+W9b$2VCCaCfemaZV}92{n*vi%|os``e$75mt5#_WWT z%A5&xpX=P&<6hlDPtt2XY==Gw3ey-ok5GP0PH`-XY~{VW0+!)~-V_JjKB<4){?87r zV^(AVCF5{Yf%w4Mg^0}u1~E_L7~XsEry0r>`=^G#=5h4IJsq#$L>Mn4oh1U&??9bT zBM~0%!b{)2%P1pVk8Jb{1I5#-JdcrF?5lw);v|Q|hA+Wju4|ui>kLO4^`*B+#&^F4 z)j#_TkG~|i)*$B`Oq|X3!v;tO6RsR{<$Shra+vH=>e$a?@>E?8=Z5r>J~<w>!mn0G zj36?!l{Kl|x$s=OcAb07Y>oUfU*C;lia|onR>r;#=x{jL_ddl9HS1b~>k4EoDe285 z&qlnf)`LF^5cHp#O&*K7GhK7*a2%7&$&pA>9C96)o?+%wCLSB3Rp}1l-?ey0`LxU! zG4Qq$_vW5>uY|{>lSA&5X75^VpsEFL=(pwfom~plo|mfBrxPADZn%&lIf>)WG_K)L zmY&N&*o(nVgI6Xk92}aAXpN+8anL|>(&_wh>9y%x6r`n<ma9$HJ#5iMPv5iq56+$y zSS`EcumkDMAKCIE`zi<Zzb;aBPud@x2?{d$&s^Wa_;xbrpUq9GhVXWBOxtM2mS)r% z9v+?MO`^QWW5b;~saKwro*j#`=;bkl{J5JxFUFVi?IYp?%I)Eos_N(@<W)dxTL158 z)$bv^DnpPIjp2&K(;;4;gW}^;5vv?{^l(B#^I~TB;<gNQLHDUT(qbS%%T{_Z_rxx= zdSl>LM0ts@v#j@GA?SH>GiFwhFb6$G)nCVZIpc}Z0%7P=r4O1z@jdF$&5A_B-WX3s zWGLlaRRByHYVoXmDb@Mp5Y61_d;hgxqF{v&uVcUMRgb_m*w|oJKH`R<-=rHW(skVB z(OSPsPDbE%nL~?p*Vk7|H1?!>H8UIguSrd|%jZVc4n+jDp^Uku&Run1XpqRs*0trm z`(7MBa7%Kn(F>FQq_B{WZ;`q5WU*~7D;VwfJdW+7D8Y9QW>S1qna@N_O`2n0T^f13 zNTk*t_-1f~#(Tfv-7E2+!0*@U`uuaOe}Trb&f%t?lhUH8LOd@!5bG+cU9T)B)Y9-E zuFjF{&8!u<pSbqrA}(QyvJ_4rLaDg0Iz!-r;>{TLCquQ2?NkXg4&rUUaII9xKuX;H zTCTU%e8jYHiHi@S)f`UM$^Xu(6-lci&I_o4?K)E2QWkLxJ3myE!=6F|nCF<wVs%PQ zo9t_vYT`G4c{Mi75IYMVwWLEa_}uHBf;xEr`|R#dNK}^U*TlQ4<6^%&_+Or>BY1jB zsfyr|OYq80Vwi<Md;t5?!GijxoFlbP`7NRUnBRZ1u&6NnsWNs>{Q!Y;G9MblpI#a4 z8+^r&oikVQ1oz#Hak!8Ddx~p?fFu=FUk<(y7SHaZAz-K;%|52YB7EWCwSn%YYNocl zSOj~pf*CM2oU>L~8=H{|@wo-~1Rv=?wPU}Tx6}bh^tah`(f&ajNaz530uzEcd+ax3 z`MVs#ztZ`uY=8F-;a^jL`~Rvb$S{(y>wC6^aw*K%=xQs?Drn;}4Jv_kj9MYH##{fW z!=ZS@3vBq{6Ay=Zz1bk<S&iw~D4T`R&mYpYZH_x)tFUT<q#!=;N!UQT3becF^e1PJ z&aFLZ&dP6QNVVv1PPO>ZeD$>M^X;j&i{4eoU2b<6TPzAYg#=+^MO^swb6D(OsV4!% z;8yhFh^(lM13LN(pr0E57!3HG>FT1!r>{J3x~z(AfyR1GTGLB5H8=aebQ1<=S1e|v z9tVpyhWArMf<B5dn%eqTclTe7%0UW}ewJeuw$yFX$SOek*1}F^k%n|Jv6XAjJdZXW zj4}R{MweaNU)F;Sj$0R-Y|SjF6gViy>_*W;J@48|;Ip``4OTvz<Nfv$oP3EL*v$Gi zp3q3h&j30!LA=ML$L_gHf^4RnKRIVVD1X7v1^uUB{i!><CCGc#_KdVn)oS=G@_<fO zdwNo-11lbURpD8b*OfTIe&}^suN*hD=`~#p7>rpR$`w}JVN-it(lMKnH@XJK<qBEl zl$-uQ9DJnx`1<!#nTS%>8uG&(jLU$rEA01l#6tOoKbPMbV7#~G&t|6=4tWOpDb4zk zbI^<P8XW1ZZw$P?H`P5$3KayuQ0;jTcgrN>z<E}IzdJfz`>jwcm}N6`bF5@|6jT%% zHM@dwI#z)p7wDJ!ot(CE6cbw)GqnkBFpaJ~T9MPEz3zUr!C^YOy)X;L;E?7dZXq*@ zgAe-IV=nA$Mv0@{_!n1Pe64Kq>IqE0Im7(sRzo~<&*dpc;KhHQ%c<0$Pbe!u<hBig z4r;ON(;HAm3dLB<B@?&RBhb3csCxUjuO}0fICc#bz{`bPn8GHr&B(VpOZhvWkDY1R zG3Y}N>E-5Fz{lX*k@-^=&MWy26v6(YPvg!p35|EeOG@7E%QKP|y=Wa1skV%zz1Qxs zYyiZPVv+RLK{98(Jjyx)+5-AF|0Ls>&9A^|aa#)!0wdBi!{Uvzi-(A3X?jVW#&^MR zm`Ed=w|US`k{@J2XrX+#D8k{xwdXzPO&F8NvboSJe~yER<mrl+%$u`%GM8lmGi(RR z-&taAIdJBF%-whtV)ec1_zqQHm$>tqj)}L5tPVbC9H}`dkF@M7dp82FJfB}}ezfGc zGTM5MF?^*Ao~@gzSIR46^pYfk<_G6?YcXb?__^mWmFH!k!&YkVENKH$469h=ni7j3 zpcsVD5`Z1`DN4}Id7YEidtTcPb)CZ`Qweg*z402xEEGui2EH&)dr_Ot@olx*xpT2M z!T$Jc)ur8wc5ABLXLo))Dw_YYKCAbQMyM#S`c4t&4ON>nJ2|%EGDWpC;!@{%k9yBt z6u3P-Dze95l?C?;n)EZ0KxD&Kimd|0Il^5aNhWW0qV!o*RnJhL^Xkn@oIhb#WQYnM zgDt%Z2mxS1Nv|7<T{8B8k$yv93_cQlf@bz@ILunS`Q8d``y(ac+neAeF}IcS2R&O? zwOx0AfrT+1xCT#X2BgvBqq0PlshgiW&3INO^o6IKa@+gzCIS|IQ+Iff%o_*hPBL|! zF*5a$W@~dd-K>)BK*W#6R?|g}_&^f@@`n*!Obtjx(k*p~IEK}+0$2Y2GCS77Azdp1 zwCBkI-Bvgv=z7{bQ&P;LM75g<DJ$pBMHZWLp8aG^=@*(4UzuB=R`lSE2HJ^3E$`O+ zcZj*HP9MeLytw;P8(j+z`f>Dw*DM-3d4-U3+%}@zVJ@1Ib+V-4DOdY)fdf>%a<^j2 zZM%_NYFqcRto)eXvwp=>8uw-}<d`7-{)C~&PO<A8YESd}@Ib>=?dQnJp*0Rswg)kR z*PV*0ZHBSxGcx!>+aULtwLhd}KwpH%4yLNG5XsM;XY{Tb-y5_glmR0jI&;J;zA1e@ zIG8hx+*?trhcAtn`;FA`CNBx?nA%p82u+x?_SYPft*Y#&3G)hM-No^S{}lZ4pflPk z;M-2hN1PUyLer?Jh6e$2AtZ1|l^s`bRyePIC^s6Jq4zfDrP|4})w2?<dbSlgbrlT! zX$14K>TbN~Q3=0P;&IeTE=6O%J(?Xv8hcAo-_=8@xOC}i!To-A)3HcbaT+x+ez38+ z^WK<<SRM86v5${0z8a%<nezS)a+1xmbsPW@QT(+e2>SWL`PuTlOO=|!<x2_BK>sbb zdp2>C7sLc2w>9SaE?!1CFD8A_(Q9gB6MQ;Xs4}HSz_L~Y9d$o2X{usp2rP8VjWg;F zF>gwO9s{)9)#Lk<<T(0Kp;Bpe-LcDD<wA;I(L?L6h@KA0pqEh`$!W~q*3~=VGU7HF zqZ5VfhSvpk96A}Cc|V{EUWG89`W!fpZa=9Fyt6W5wwHBl8S9h-+j0kNOSog}W744B zlrZk6fbFqi69{@L8auB}6t{7C3CtMtTl77+6GbYNW+URVND&^Ql$2q_4tnqrhtM4Y z3A|uG))+Il8c0jDDE4UQ%A5+i-41}9$DAJM9rC4Kl#-HeDf}p<Ag4#3ua;_eRE3;; zXTUCo*_+0mH+wyq$OtuL904~)Iq?S#?!%+Ki$BBUu_9F;6Y56*#D0)4!BXK9Qt9e! z624~)xCo)ppnwJmfrwTaM!~cK1j%v_q2ffkB6O;P-X$h5WZwNG_28P&xb2xrdwIX_ z<8TRzT7_+p6b+>_T%=QCkG6_@>2-Y5nfGJ&@~3N~KSq1V88a%0-^}h_5KHN^;1|^n zS|OX(W?d>r<d+$7>xw_foG;6c6e-`U+FZ1nI%4g8a*YAhRx7^{We&j#GY{~!`XDKb zv_hMO8nyQT=V5lb$NtOU$d5o{^Qb30bf2vH$7x7c*irzAZ|@uIGqIF_SF=3d9tj^M zvj4JJ*>)=!r`Fjh_rXsad`-K{I@_ZGHNm`wda;nk_&lx5sqF=Q;l}%0RpvExu%)cR z;Dz#~t^lpK*x{}wVHPO1yn)v|ab)G>W3oK_KfM4hz%1x-w=7SQx3uWflVS_VKw^|j zz-@OAb2JBv-!GUjcQGd#imh-a(tfeg{*en1%1bAGi6l|wPOQ0%hZ#a%FS<90r6f*| z<VB;FBQr8oXY%LAxd#EOhx~Q;b?2B_tKKS}(ZgLg!N+Zn<i()H)SnA}cNGY)S4u6O z34JA}!ok4vC4=XX<hX#E)+O*M0;G_lEz{L`nsn#28pxtvb}>!WYCGt=Z>=b7Wgnk{ zUYyy=rpg}%we&idi{~z#==sipPw05di?D+Xrgv7p!BoFz>?V91j~{kuRv%|VB;|=d zcG)V3Ep33E*wTBcsWYvO7JuN@>M@QTE#_yp&j#s4l7sd$Ru`1?mZp)4INIa4n(#H> z-^G%v4sJTBl&^{H$|p)<siMJLvxxd)>i8kx6)s7KJ3IhNxlWnf;WF7tobd&cVjRR2 zp{F_58DrKw>RrzhmPukkrKy;@X!H4;)X~H_#hB5T7NyyUr=#EuCq(3>NgUvY>H}`c z^_e9kN<4(jVdD%1{+--vi5A-8%)D&@jMKGWG0ZVYDwV1nZ!~7?ZYxYDQ@yo{p3Tgy zA=@3yEUf_!a+z)FaE1CW69WJUv(HMD0`h?>be%4V6!#piSr^ZQJEuua-zx&lyOwY2 zk~f(>Z*!)VhQC~?e!!RfQr-bZ%bk%$<vv?>i@`aG@yH38P3v+yGIV>UCO4280k0|7 z$1TyErONY!6o5H}kxZi{FU7Sliw2P~ULxS_mD5XJI|XLl*XT0vFo`hgl~E~?T#~`A z>2Z2|b9Rfb=3LuCh<VFY{Ff#ny%^vx2AWLArMbLYq%PHjLR9&}YIe8Ex-)NB{!DZE zTC&hnqdy^MH@(;(fn|!H`)J^^R8TU(+&s+Fyi49fM_YCehuP;kEsh=-)VRGSlA<v; zZ&fe7uZ?Gi-brKhQj9(XeiNR<b8|Rhr&E$*wWv(>G5mIi$=s^w%reomauoxe?S3_d z<=E7v%H=9`+qa8HV4M+uM`S*2^~G(^L7gq#M%W4Q4UtQ9-c=Ioq=mLdO)4CU^A&En z`h`lLH8lds7%2&exF|@aj%I=c2ky<a4w5t$fALKRp-FeqSfZlL2*{6QAY0zq#Mqd5 ze)+QdN$~5zlgA;U-yvSuNZLJ&@1zA)%_WB|?)0WQaTn&7UbK)CcxX%I(vYRTkm|Zt zjnH--0+W}!-H#QUZd{@rci##$MTd`#?B!6^C|BbNh&3|TB!xXLv7WeoE%`u#CZ%iw zZxNO_qn^xNeNX$P#fZILe~Ml5x(H%(;Y)w#J`pn>diw+}>Ldha7b^5bspfgpEzYbj zW}f*fC%zL-<{a5A<6IU}s~EYSc<{6T6_GXSqzv+tsj_iHTRuoE{S)p>vK1ErLCY*k zyi10|2F%2dKlzDj3qW+hgc$nU=CY4+KvUFRu@&E~^1XRg>b4p|_h*lk>iuVDR4OZA zI^Z}JI=Wya9XekUrJw!<7NgaRs*-{H1|~it@ByQb7FTXg`H^>ymv8xoq#KZ9qNYF= ztLqKl3_NXkgdnQV6KU_9=gYY;ni#zxqvsH1UK}ng*&#>*>!v$R_1qaOcC^fW<1H;N zRRg9tulsRUrD<9*72Y{MtXH|bBcQTIS*m%id_<hpy!R&Q{oX}^lT`2TE{vwz!=EeD zbsTt3RVL<o4{mP<f=>8RJBvg4$itDN8)k47$rbHbkSQaPI?r>+8Cly9*6UW(Djf`0 z_c-omAeGYKWgxIH<f(k9xH7s#Fn4$oBuqayuu-O|6MhxaD}3y#wympoS)M@q<In$& zS^8)BK6mk{7`@G(zksWu`bvn;3@jyk9wP!gOK8<{zyyu3fxKX7P)(R!e_ZC~;&IYt z-d<r&fqSOw-xE!JR#2ff>g4ZYmlrt@Z^n`w%*$?Nm)KYcD0~@)Ee<Fz_f^q4#QsF& z;?k48!%SuNyJCm)f~*mr?nHP{)EIe5lun>`q4DL1&W|Bq4sE&*9&h=Z*8mx1;j}y& zGd8JU;`&+gSlVse?h+Oro52C1(v(T=W0<mcQ+;Rk>|x~k9sBSSp1=~oed)@*bgtqu zvNI2%cMpmhh88p!3v6H2UZV?Oq2};-24;p{awFUMIBcFwP{00aem!!2N8nCoAk)Gp zxL$Z7J;Ulv&!e;XIBya9j;jMVAK{pc?-SL}*yFQ^q&ZZZafs}=xtiB@B`2~vEe_oD zywUq*Z&jEhc(7dJ({0XWqm-@vm~e)PlBOY<%x$o*)oSg;6@N@juj%DUHP{gZRSs0j z$qN@^9bF%_Sb*J}?=|jx(_P!L?<l7;DwM3&eh?7o(h&odBqYYn*R~vOl_<`T&(>Ws zCDm@#q>VK`Il?zR@x{MGk#za;&ayi_vD7ORXcRp6<o#grkZ%8wG3t#Gv?)%0h8Avd zyZ<R;*8HcPAw$Ve>J8)}8N^cKPPAPe`O@YzWUXl;@!hfV4R*7grK?+mex@(eO}e$m zJSQH1NUi{E(CIzNqbGr6Oqb$hxN$6$sIgtDJ`BFm8%n1bm`-$Oup!Zcy0T5~E|3V1 zuvz<66>G@*B{7T9wQiIevg>tUwH{JHGrr>Yj6*sl05IM&+~lel$2&2{d4bqB*Lhl6 z48}iLmn_7O%VR6SLojGg?XH4Su{ExjKjhi&-lUbk;tj9X`J2Xmt~M#D2Fki4L8QO! zaj7Q<pj*XtX6-IrQXfe$d82eWJ8k}0^)25yo8M#1SD*<4C+_Q{cI_u7<WjR^a$0Ar zH%!kbX?gT6^8K3e!I;-}&~WG3!HE6r75t(rZ37{aL=&1|+-n#wO(YnaM$hkD#k9Mb zsjq0JS=pw*gQhP%7rw^x{?!jGXW%|)&Ok#X^XrVje}zD@nEo?|qi-0n0z?*XDNNg& z`@X7w)*tq9fMiUKP;xWYLjX#NsiA-Ogyzq3%5Bi1Atkpn?dBi(7aGtRLXGW06V^Ti z?Q1;13oKaiTfX@(KltB&{awyq>HJkTT*AM52luZj_-hLOnu7l$Qy_H(FZ{Zyps!>H zS7z4x7b2>^cNt@ez*M4NrFBs-(kCvr;M#FJ+M=~T&MOepvA{c8^Om5Q^!pL*JX*9c zd)#Z>QD!&YX~u6F2|@|Mw+w=}D)$yGN+uAQKa^w^xbTnGxF-9#(r^DVT}R5mrvRTN zcbEAI7zzGKHSrAD@T8-8t#Px&Y15l-NhlcRQ(D`;TstX|s{SRI9(_ay#-4<ggEZB# zzG)ECXtW=>NYC~P`W5}=Y|r{|$ymva2TZQ__M<ttYwPwVPoLyeo;1H9NBlG-hmOHY zVa*Z-jU-~|<*nI{!tS@?&s&@5B0ngJN$g$_eNER>YkauZ0YfiWM}R58g>J2^zd9x~ zoS>J)vpTEXnB)-Q%gKlb7wsa;rvL?R_kb_%`KE<KeL%#1Wrrb|Slzhim?Gv~PhEF~ z9fn5c4k|kqzQkI0Nv>UVn|Aw9j*dfrHLh4#Ic<}lvWm+(!Ud_3&lZ#UR?TNU_7l<8 zYXPWRyBk;ZzHe~E-J&yawAl6e>~^^0QC~D#W70KkA-N6)ew(@bgAASEv{Sm_@`RfW zztPvTQx?j?2lpI$1ENk#0?xYr*8j;BzwNv?@a?;;QAwII0YWyzJ#Q{Zuo=gn{_y{x zM2djzqZg66X3fE#9CLa20nEgAj#kRe;%_pE7I8o}R8JDRiKH?n*G1R*+7%A=r{7SQ z8POM8W*bXfkZjJXIkcqzrkzSRRtm>VnrHG%A>q5-{Rz?ba%xFU@-l?8_}bimD{}Xf zNj~yGD7SIEga_VS6y|Dh-*#hF1jFNQ?JhL^XsV4mKURE;<oxvx5us=6R;eTB^`5OD zgXa}r?JA<56{M>s6X3kKaq^uJM|P(MQn7)Y<naO{FR~BDbj+f`(+TRm13zYW=xtAi z&!AZSAkR!rgf>S0i|%Xv(_O)B&8_aS!wM;V{FV=TRmkIT`CYy;U-MYbq}wp>ce|Lt zPV;>Q$No>3nM9r1hpRzCr$d^2f^pfHP=TP6c@rNzJX(Ki?`S8@_}0YHJS%y0oVeSw zWxXgv3h%6r>~^zX*2_wW>1grYk6bZVe7DV3{O~kbK-<vCwOOLN?9?ZyLsQ!@qS#RM zaC-tJW_v2G^X&v(OV?@fB|a?wbT%dj8g$8CB`+5wXR=EY;y6YhYkMM_B!{Es$X$ZL zK)(Nnz4r=hs@>W~Ls3KpeL+Q}D@CPAuK^V4O+iFDNbe;SAwW<>R1lCVUApw%4H0S5 zrI!Ga-V<s9gd}^q%C~rz`)FVLKlrcfJA@~hnNJ(-zQ-7S_RYa!W6y5AP-%wL;~IwX zOEVQ2Fydr!!*cSOw1G69<JJBptYD9Kp>(1Q0@`aJKJHQ1yAI&Xj!%tU#ImIrPNY=^ z5HEI~H#x<B-kR?&2xtbijoKJ~osoKi(SK@K^9vIL&A^+WMeGsHIzTtrANF6~Au-)o zfd`jCy42xW)%xCYxb&d1eF>h_7wR3kv#e6RCu=$>uknS_QM`-&D`t4ipb&usz?ucS zP3Zo|Y2u!k(E}LJW9BzD)KIwS<&JR}we5`HxW|)`BPDg0=k<ZS#*3j`ooOlSG8o(W zqq@vOR(5ni<xqpyR$(byNt9U=<O&}AA-yL}Wh)aCR}=<~NLV?|$mdo6Tuk<sxPb6R zVi_g=>Prz52|-<tJ~+B0bMhtPs__YVBlU^<_lfO}Ar=|ZtMI;99q@wL*{EA8Umb3^ z)*5ch8i0feRRh4j6NdNMwv&&JH{4!T8ujZHb*5VKJemO-AB*%e24*rPTe{Im44iDp z+kQX1^eXqhzVcqYbJf9^Eh-Qv4988h7B{(;tQv=K$WfgvMZ1$6vD~B<Dzq4rYkj64 z5kacSSTVP)HuZdx(m&ql%?tU_0uyRFm=^mwcn)RKPEW&ichoy;Wyq$i{v;-q_jr@E z64DzLel4^{X02@@;%W6yELH}S+*4LBEn5r3ewN>LvI+f0yB<enCYqs{g3L#_jcQaf z%wN8(EdWBLQ73ysds7<}t+m68wY!7fKo1+nbYmSL;sE#AVC-cPvG}GS643Vcv*{un zo;IRa)aHe$)qBwwLGLg48|2^fL<QB(nB8Lw6y++$0Vwa4iA_6$d+W7&26(vX4Ah`~ z_={qRQB5x*J?Oh;s^s8wK+o3lhZe<Gua41d(B8r2X^3!0b1}WY%szox=y{yqdJ-&Y zXMy`QN++$zZ*SRlKn~tV2Adri0V2Y<()al{YHKZ8$nl1@q0ObyM09DxQ>EMEjicD* z<Gb9VJ{YNkYLUJX9XW5oJ}J8l2cH>lhP~Y$LNCQ7FkR5NzJ!)0bgF<Sg}>JRMjYv2 zynQ*8!R(@3XA`EGZ4(0H(XLwpT643cOB_FxSUYc#;lGSp2D;};0FC`<!&f0*tL52J z^V^CcHUR`C9MBIYCB+R6zP+KNx<0%4^pftmouLZ@HiWbhgkL3Wcg;?C^PpKAsBr=` zqqp<f8oV_oX8_8=bQ5s$SAul(6McLnhqO(bI)YYAO=lJ=19WDj_HRdxQ#D&6vSWKE zJQluVRARY5^ob*#T_Pnn9}PD^I{D~H%%}3B6h5bt+t4WNxAaTlUa{ZamR~%qz}QTK zRukK}jh(j`y8MaifB?br*Y!-G3Lv=cWo@c`w)xgs<|_&BE-0|#S3+#-W^fnXp0<A1 z$XHe0tTlF{^-p^kwRQl6VVxqu#Kr9n{?Z8S8u#IB6Xv|@lM`W&*j7ie56yw*HDuoi zL4y%1jh67GWv1`uF(R}bUvDpY&-drX`&B>CnXX+LL)!E%?bcNd+_TH@R(|wE;?rQ} z)mWZL2{>-LGvIiwx_)@_YG8j2FFw`U3>FwIwZ6LWyvII&ii8@Fbr7GlUMSZz0eO<( zq<a*o>(13}PNiD#NOV2nS`~K6sq>m2i7>dlYNhkz2<+*$K2Z_Uuz{(w4R!ROZAIO1 z_MgPqda<Sk@NzUbtvyERXmM|nQ;?qRAE7YyV&@*55C&`aqK*cxUIrg5W)n*%Rt8gm zh<ek+*^rI1B!M>geX`Ea<M@;s=V<R30<*xm_q@=-(3KQD?S2yn^`%AMV@5lsl85nb zdiQw3#KyjyE136>Uy|;OSYxw$Z7qVp0|_ACyZp8e+*;|Qz;v$#i4ECLlOZ|Ky3*xu zA(dgI2HcuV8OXbcKc&f(F{<YnJlZt^U6o_{W&%@0kJe8s9R$J_=kQuC$d1#IpJMx$ zl~nA3_-4B4=p!~0(#vA=Uo+wWBcwOulH>&ZVk2QA@esh0-{1w{iw_|4)bSDsRKVT` z2V|+<3lkk}N#ZleWG+J}J`14DU)wM8<&X%&G=pUql#O@@Cwqdu=}=4}E>EI3IB*xS zLbCFi_6Tz~F!|(uXc=aGU`>ts1#Z|J3g5Dm?Lfxj9qyFT`<4RuFLY~^XZ5dW{P1-M zP9A+PKh@t>U+I+<6g`Q&{mb1L$o5a?3W@?5h#!ARIpN7Tv>Owg3UPk1JAUhe6II&q z%*e)|3lOGtWzCK~TXa%I3hMS+9|2l9uVgQ`O*&x+;m1MgzG$iCh+!StfrhU@kF+SF z))z-40Cxb)DDLI^NJUN*_Bc4`K8C_TyVYdhtqZtAGo@dn)-7`~ApnUQNJT_d$w9Y_ z;|NxyfZMAy3wN&rm&Yw<BW<y}4P5A}hDszRjw+U3LFt(ax_Xz^`_~5^v&!#{Se*&0 zK?j~@DnD}ky$L`M*^o(Bd@W<f!r?-s&@tD9OC8E1zhP-iX1d3s{`G^9aKVC9UhDi) z(_RrBc~;=kJr#ZZ*=fArGRYo9$6BuARh4byC!Zp8i<t?-W)O9OAiOE=?IM5p^Oo=f zMi!(+$Z?)iVt-x&p}i`rh`<Sz1EgRIN`eaE3beLl+KjkBZ{jejCehXSZn$hh<Ak1{ zU(~g?={_Sa-6SgatM+t*I%9DCz-JD{b(YVTY91Lgg83^A5hp40^`N%2&Oj>1@d6ev zOjOfyN!_$Pt=y^t?%Hn#i)Lb)Pyc#uCKI4gYDL1gwqxryw{O#|0l9r6PG6#s^FWHE z=7DF5|AmMfLZaFEFJ+eH9VdG3hcSP=`gzqvI_&A=8`7I4&&C;uc;^Ydwdp+p(JI>k zTG{FCd%kG`rqZF{=b|G;(Uww!DgM3!cJg@qq%9CscJ&?0E`k9yBjndAW{ZC=zUM9L zl8oxloD(kD+>ZU2Jxl$)%-$Grh*;UYTrb{Io@DGsf^HMX&6qxUuu3&@+>_qk4#Az- z8(BQAyFWhap3=u8CTi6YLDMY5$WtxY5$V@yfBjQ!VeyDaNg+Ue-+&&^LbLni@GO@{ z1_+r0xflr0CbjQ3XdNyHPd$L^mc`9N_dVU_PBFk=UcF^OYDH`!fs2B$j@4ocBVh(1 zQ4!J;q;IrQ1=Cdn1mAaQ8yr>{SV5!e+W{FgDwUDQ3iO-@xB=V9RMovI>x+V*=wy;C z9*xl=3_V*vNZ7?YHILRZu2e#<S;trGG4WN*DLm_+J;(2N@-Dx!dorc6ymz_*QWaUG z4c*#X`6bfy$_*~|ybD|6^W(0J`!A+a)%bsIIFXQH2Qf4EIo3pz>SldZo;^MA#S=QA zXgKWMmLpqS733lxAPTJFm!9~v{dlZ#yxHLPw1EWQ)_nm+I`M4OmpU07J`)Z)d0jpY z^k7V!FmJ5tE@X8ys55;rm}_XWIn8A`qpyu>MWjK3o=TDY8<>4&bH`&WTi*yKNx+D7 zGO0H2(tKM?8@oFh%mVIg_@hM{j|Y-O?7HF&OjGm)j&Y9}){&UeVo&jrf=XHssx8hR zhAWfab+dE6rm#y80yw@sKSTS1Hh!x&Nn@X72O;8LiuFiIn})pGYN=e*NfD#&S*vNL zAlkZ3v@W0nh^WT~wKj{GjCIDkb^?+eFJ#pj+J1oPJz_A6@it{5zOk7vegcz;<2j-2 zZlUGqkseUv$zV-a(NRADk0Rpxe}JcE#TdtjaOxQboi=48Hs2F9Mw=I>O3;6MfB81_ z(&ctp7-ElYIWNVnM>T*u=w1iFqwFLe%b=WV`ml_h$_R-eQTw0YHp{K@-Lt?~U!di6 zn7qrz#%heCBBXBl?@vt15?5PWgm5Y*m!d_q<wsL}(UYaBhcDX9qxzHGzKuQ(6~(<? z{_LI-G|BRlwB&9>Q)eE9U2dIV=bX;vV4yHBTE()Br#wKQBVTOvMfg45Uk)OeiFK+x zov+`0ud$W$iNugDGajpJ#)CG=-6W%v=xi?{P<^tBlDV_6pak0*Z@I$;bKd;msoVe| z`s7J%^bUsvHULBg_`~Uxnwvj^U__0v69HEuW68J7Sxq7{3#_I`q*Lo`VZ6A{MvdJg zdEX$!%JAn;4Ua}{t$E|YhIVO(ek#*`^Q>ZJ@B7!>);+S!*9lsCQWFZot8;T0Hqmle zhb*1Z8Rb30LnIUvan3*X5J=;S+8F)J`q(u~4~{x3+VWL)&{B=UEO64f$t65OBQ>C# z@qQEqY$ach=S<O$RoUdvdsPFr*R{N4tvXhe=?8u65!vRlpN6yAeM&^89=DdGn^sCA zJh6FlR;uC4i}kDD_k_DwmG)6*x5A6?0WuBFGI+OS)IxiP23P;OgG}5*`nBT}V&4ej z0}=O=B#{?~(qsCRM8(>B;_JPS&(@@nai~H7j0C99?)b+R6A31l5%yKn@h^kRL4Tc2 zqkiP`@+w{M7kr&-<B67K=z|Mdo?o_GT-Kk@lHwCzB_^5%#rqw8RGG=NxT&S}rJ(Dq zah(GjdOX&ttU>shP#cPoA-LYX9T3U`V3ItfBMH_svE};wHv@3OjE1pORCaj)k4dQ* zfd6!)hVD;x!Y{J&=-hA{E79+13#BIcj+pm}8JK^3Y$oVc)d<ue=%R^|!LC}INhtpQ zk>%eCplxQ+L3lu1#=0LmC+M*$SE}__Umi_LV`9sKN2g(Sj{tRg=Nb0%XHMpB6H^vk zPsGY1mJ=)Bo}2e#TM6^FZQC4kg^G++n->Co30K{WCjDGQ=SphpL7c9-L!aH#<-Kkd zH^`i5?Wx$8rqydfer-0vmdaV<8@6BV^%mr(UcVLvxeeq*GreZ0(h+Dp6yBRy(0S8z z<$RE+!-K;q@ZfocL^GdUD5I=0kAA;o{eEaOk86q?q3YM(K(ZKe>--y#!|iAUu+D7+ z<fC}_Q^DO8KZlVokIUDiOPOdCvSOw2-c{g;UIa1C5G5#I_RJ+e4mGU82EA!@J8S*( zWg}F)K$^@M#VS$GH>ZHPmz?K|qd^Z5#dQ;<`W=D^=<yM!QaHym;)ta~&E-($sjRuT z9Xp7V`91DZ-)}!TC_>)R&-m?M2evGQ5|Nr7#e7gJ_3N9Wu+N6AzJznegpI84db3l9 z5Wxsl?~xr5h;IMR#$c;eEgdjnk!}8P;rr|Q?T$3wsSC;))n>k1`e^t5&VSz2iwg5o z0_fx@x!uoelT|8Xc9FARmM<t2t}A7muhmU8+H{C%OYgjZ0AVUhiPXlOeF?&=3@5yO zcU%Ok=E2s9Kys@RU}DXa*O{#J)X1apenh{OiFk-G)V&_pIaAGM39X#|#qZPvWV!>; zE)iTM@oD`kp=ljw?|v|5W%39(hqt#M-4pc~;uPr@I3o){%ocQ3aBRpWU$oEGRPs7d zObshx0@EsQE0yuhi~>?<OFLs_##vwp-e&@n@Ud)somPu#L393kyj2+AQ6hB)Z_fHu zu15J2Uzw)Y^$D9kMQg7^Uz`d>eLH{U83(}ID@(`?AR?|Jf;MXKT+dn=L?c>UT!kON zBby$l?9V-eq@;-+v{3Q##EmSzKK7PwRB|gk0g_fH>$`|1AUe}r?&H}9W=-$77hE@s z&eKbK_nWNy0?dnbv-}C^B8gWhuslk*UVwR*Wuert5VZKOAkUn+z=B^j(e=S}eWnSc z8{e;XyJ0!nryFlCD9kgTm$Y$yk?2O(ZC{>eq1NnbY)1CuB^7aU+5_2#7M`=($E;UU zvS$QI_nCP2c{3`GeZR2u0<bSR1tdQ(`Nu6(GB#xcCF^<NUHyH3y;detn}Bq?F$Mi) zOKzCv6=Lx_ZGzKA1Nld*D5;KAzm7Y_Ao}*X|AZ{Qo&W21m6q?9LfMIV=0rmJ)#FR6 zmv|!jp7rmPw?+vo&NAO<)>5=9MeHm`*l0!(5x4qA4NO@RJan=o^Z{1YEVFbkv;V|L zboHE-kST&h=#9;TtVC9T6$+U&UtfLaN5{ba1Sj2|&U=HMlOHrF++Y+bHh!&yBnQRV zpd0X*7FqGehW)zd%@qD99yXne%SE?sK+k!!XrA8Q-H69<%Swl7d&TEgpJgWn?0Z1E zat#iz%<aAZc;jlT*oa~qs*owTZBoS`d$&w)bPr(XC%Y9qLFpNwNV53U!RUu)QXb0~ zbbuY-_QHKpjK$GQ*qu|cU_jXd(^s8Nr9HMWHJzHNMKwcHVAI}Vza@m5{gaN;-H(qu z`gme0hMRBegR<UTg<{uZJ^Z!u3UhTX%rs7HE7CVzW8&tlq58oufwG{LIutZyGpD~N z?%Jr|LCiVBe&az-x~K;d=)c~3t<Tt+*>kV7tuO4epUive``8(~GK<;QfJfuedgaQw zyVoO-^UDeEv>&E)fF8zX_^c0s7j98`glWm`yM1_LFX}SX=G>qUfgCLv*lFe&0f(T! zx$v`4w&zC%fXk<T1U@MGk{OY``RM+c?-)2KNU~Q@;Iaoi?*zAZ3O#@6c>yQ^^N{U{ zRaZ7I_z;xL#4cU&#gum33)zrw&bQIPt?>l&arJ2-$Qno;$n!?NAGMnnL&I0Jt8iw} z#>6x7c|xFfDGaR{MeU^RY?UY>A2oBB!M*(OZYSRT4*C9qCyEwC1YCx!uE3E(k{I-+ zms5mMU47__{m^G&(PUm>>jbI0cav=TW$8b1?D55`-nnxp`N})tFuJ6`4bx6J>?AH# zrhdf?myS0d*qf;es~Mlg*F~kfA?!+gL%S6-$MdQ07L6C@h7;!(htFNRa_CPm>PnF) zL%2FKJm0vSf)&jVUB2&;xZ)_!8;A!?>Rmyq9^=fT2m5kqbem?aA1TE~%4so((Qrm` ztGvqi(iwm0ItzKRhVrxPL-{&v<ufvnp7yf}uO^{;1@$_`BrvkEFR}PZot<jNV{{XS z)tJkKEZSlK{Tz{xcESlbvAcw*xr6<7p9l@#quQh$OA-7`&U=cL!1?MM7ftZR>ypoA zg^y$&#JlzGzi$8!^E#TkHIw6ZW=rwtz}dvYa2E1x7yqz}_4#F98G4Dno6`>F`+JLB z{71b#71|dH1iPm6*eAjRy%5)VLRo>jQ?r&F$FCZlyT0F6yL52oiTT4Bt^y!q+Q}G! z7U{~6&%g_BRcgbF)O=>z8HdX423YTScg~<qAKU79U`%jJ{zprR=365bep(o|K7C00 zRfCDUa~Ff{LuO5+8@Gmao}SejL1Nnzt;dR`jM~jpp<d#Y(0AJFWS23;k27E0o{G&J zVevi7srW$))V4;wcwH)^g}kZ7Hf3l}?(or3^ssLjFNm%bvj$k?iwyMTQstyz=EIHP zO(gVNZvrG`OKoAgX^+v_|2bx)TiaCb6!k)t@1@2K(2*y>KsVQM#FE~B5549Fzb?I% zs|{l;UiHeC-cTv0>j(Y#X?ZKh4e^pJe{FV)MyB^0m~uYjmdav5ejyd7&uB=h+1)NN zP!?Re+Bd9e^<GSWnQtYcbmQko4n>vU#M5d0O;6p6!K)DuVo|I-lT~&&bIgy&R(<6K z%|vh0pXQYku)3Cvk&0!Jnx(1%xl3QF+=<SE*i_6*TI!$a+Ow8==)_HTDh1^zMjo(# zGo!nyf3u-Tep0Hm!)DEzHyklf;V`lK-K(x2><__kc7on=Z!pU}uSO|QXlKB%Y6{WX zG;e6wT!{Qs+|Rw`nexfpwja|=2;Liz2J|A)uEbmA11qm|N_>+&Ys8L~ih6C2AOp*r z?3K~XJF_R8=z^%$tDwP+bDg;Tws-WN2V&z*ZarsrM1A!-A{YtCgIVb$hlqSlzj1^X zL-S<0w&zOs)$$KlPZ2`t^nS&hRnSh8=29wc7FZ>`xuKgGFMqshRbY&;{J9`^Y>{+D zko9Yy`&k7nn<TpphL=Yr@`TBqWr;OF;jVt6MbPAc0Fy)zk4A#Pg~Lz-UJ2(%=AzHC zxEjtXH2O$@WlTgc6DdM4QH4k47A^K+;#}>Zw`MSH5q_8U^GjPLt8q0O<1m-R8JJOx zvjB^^Ul&3xgzeaKjo-UZw9=;g@J(g_An<HEvi-<J=IjG;y?iar!wfPPB5DaZ6bza9 z9N$-VfX*7oSC8+;D(%OLIlY>-Do?%LB?PLpn7cWceh!^gg&g@})<^lwtsKmlr<o>@ z{Bzj&)ft5wqRx&K%IL-$Pa0-4kBoBMIY_vhaqzLglL{5Ba`sq0qpy<PFox<GjD$;c zk5=?U4M__2x#RTalkTEjbTuRPI91+89mSy|yYngT>)ctHRF&8#fFs^E=^|g&e0<li z>Nq>trT7#-HF|<PQ52LVfvon^0Q#^y#NmB(U>h@x&eA3&*@ERv5&?|cPQ~~MV704F zSH>ZPc9B{xgVI!=FK?SKI;fSBUGDRC#{^zdrUv=3sK?ctMJ6ZR3_?}3B6thVD0n7e z`hp<xLRVRCW+Qy;enfM8cC16gtvBnb$&k;D8mcrM{M?qCYH`71G`^sR$dSc3X(wHi z=8Q{n?uf4<ey?)W<JD8P6Bn<8CgISi>Br0Ik~0WMYYRF1jqTxY=H*$Wts`v80nwDL z%!uE6UHt7qgSYYxnq%n(kLt|d?+kYj&;cM8yOgFo0;oG%)Cbtt6<IAjr@M$J>i&wM zQn2k!<=T?@N@wvIp$l#LMfKz!@9dhpH_WT+tT(Klf_*A)Im~`~G4#5**ZvtEyZ%&R z7@F^nZcaI?qlPC<@M`;p;3mIOEsY<>u+lo=k^x_=vQ}~rKzt08a3*rsfPPF2+Dht? zUS@SED8}Cmz0lJH_;<20`K8OsFPygTG0Iv$Q@xO1+I-P5J$YwA`OHNo)wiIx>5!$) zB2B@!V?t*Ofr4lLC^7;ng6`L#PmC3)tpT0aXY>$nq8{JachRts>$8epIq2>3=8IY% zQ^O{4_aCw8Dm{tj0e8=wU&NJsLO7b&nYG#vl*ErV52S#V=tmnhfQii=rmr732&6cU zX1v>Jt}Z<rpz?v4+jquL+2GD8m0_a$QiBzAv=Ak~)Zc`;pzyQ6C5$GJ*CNGN2@-4{ zqv*?M*D+yG_^btf$HI_;{g%py9XE)7#Gyp}H2@h6c~WZBTBBsWljWDSK!hvhBU4ZM zpCpL~D#uyU8W}*L)h{xWw)`{h42M?iHtJBYbE(f3h7*PDEzAcUR70-z-I`06QrUCe zU!UZEHX7_g?VX+xG<!2SR>@AogMm<kTcQ<w&z)YpKcPoP7R`XH!IFkb1c?XfZi{jw z+#6n0@4W2FH!_H58Qt1qw~mXx6m`>2rP(uW7L*4x7-E@}HOeu_Vq9L=2QG~%j9&TN zWkFH5yM4*xwXMn1D?0*C+DvI=iWG`1@0ZW`W%|+kEzX6VfaO#DE4<y;JD2ogx3!Z+ zxfXGHdwL^f1}dy#igo)ovd^e}0;d)@RA|lV#w|K2k;?>uRla*}G@A^$g*a5<l!NP4 zw?T(0WNp(u;}2+@S)v?69~GUmYV6rSf807ZJ-2queK%9pAH`(RevsL{^nN0KjD(F~ z7L)Eq95g^|)z@TPFG+H9Ls{g8YPE_!qZTHN)~Hn?m|f69_oBMkWNOcvgjdmJU<!uc za6cU-Ew5ECl9E}?W<6aho_nsO<>o5leT0ZjJFGp@r(k|+F83}9c4}{{#4OW3hf1e$ z*U2j1vJ)rIkj4JA9!mM$$;7rtf(eQ9()9DG#~X*(Eu_ovF>k%^C)Ee3A{<jA_z5gh zT%@%<h^fADx0^ghz_d{^O+CzhYYMJ`-JIpkA4nCi>RC)Y;XaqZrI8?6Lyzv}(MhjH z4a_Vr9c(65#qb+Hg}Nk{adTbiLsmgLwFezitE~I^LeF2VY+V+>&U#;;AQJFg+0>5f zo7#z{K88Q?rm=RtzI;_{EV(3Y&m=R;#KPAL&_a?KQCQ^rs(UtqC~a@2I~2g?L6CqH zT$B|2p9w2;)3F0x!O|ss|EslPu)=$waYf8cHW`Tz|3$U@-|qlK*#BL(|9jV<-kL1B z%5I=*O~g(~ejrUI@#ap0!znB5Uo2?mml|{+DZllIRH2qjr<LZvG(+@t@^`Gj2X?Dd zh2V0xPw9()*%%7XsW}}%uhZGy9XylzqvPMX`ZIL=psWTt?N768SN`~U1@?;H;}0-V zZ9&zk|J8mJNF`x@`S3z8v%E+0q#3jKX>0voea#;K{`M|vNAI1#RqB6t4+B{0?v&?( zCFuhG&WQ)+pwM>yGU(leO3#P?N*Mr}&)iY~wSD~cUtKDJce(jA7^K=JXXN_l?=X{* zvqw^9{INKfE66DoZej;Lt^OGLb?!3AgY)puIy!HG=67@zQ3*`{q1OC6B~&FC^!8Q@ z&;Q7^e~YY##UQfiw}I8u|EcZw?;%Ox1(>P-Usmq<nRCGpH?t3BCd^5!9|K7zEE`R| zp9Ipb^nc{FCO~Wlnh}?JuoU>k0Y#YF)QCflYX7vZq@6%lGKbZV1v<yh1Td^SscL2M zy=D@ZPHj&%xUV9XXAwt%4;kx9mV+;L`#0oh#VQjSs+C7RIPR>NQ(5E99X%d-TKC@! z^<0cAf~>@f7U(L0zd29V!pc%5y}Qv(+&Xd#iPH99yM;2SPgg;e-PXGI(RtNE(ow1F zz?`Dz>cr!GqlgT{VY_H@gT=QMW|DVPMeeCYGJh1Y>sQ5Y4$9Nxj)+GqZ=GjqwQ60E zuIY+KcZ{mvnt1E~)%8gTiz+&-_2@S_Arhz-$4iWDIx#X!@0q1?R!5a`9%a6Y5qB}d zEmi)FcHgB5Y`ZYj!UI0t<s=tVX8^mt(OTMPtk5Cu$XHFJrQiRwJ9+oWVp!rnZoHu` zRW@)uFlLhSF<myZ<Zjdxw%t6i{yXwe>&3mkZ17g1aovxeVN_&o<SeYOsk@sno)}kM z@PLX3OMpC6hE<z94I;V3Vc#$J^SbcrCoo;O7Fk&~+4k4&aH;$zmRNJ+2a5qixNhg9 z`vj%r-An6h#ofCGjT*;$f^+a|Um13#4llX$#|ikC>_f4B;|be>=~9rV%>=BBVQoNY z@w1&wQKzwI>4AC55j@g1<PQJ?Bw=kBb-}34CVW}9#K=Z1{;rq(U1k?%*%zv_o5SYK zC%Z+r$20GLN(VQp7U<R~Pj;R?eT!9DfddxP329s{8Ug*_)B!z&3P`r&)eZR`f)>Z& zO*w=0mi?AwaL(zirS4>{4pC<ZsP!Q)&(H7QY0SGn=kA@947d)fdD?cZo8tstP_Uz+ zzNipZgCR=6<8jB^jIU1C5hzNt<OnwIut?&DeCTjOQ(6%Bx5rcTzdjL<c!Za;ZLsz0 zxtNa$OKiBhIn%=*lm`C2DV7o_^RGExMP^9zb|#QVKc?)LVg0R_<m^*c&oQ%syK`Z@ zHpEv-wYb-i^$ALaNH%#67HO|ItrSFE7y%P-iNTy{<ss0U!0*d6#ZUu;hkDx0S50zi zqQWY_+I85BmPOi`$vZ;@Gk9lfsy3!OKEwU#(doPV(wb8N#K{OQ4sUqEayL^^e>!)s zxFD<be)WE#;W(;DVO%67lYM-tNtc1JltGGAgAckLnIDQt{`^H$(S(zGPM`Rbwjxp1 zhxqWtUq`{;_(RJQUe++FuU~FeIHBo;WVTkn+a0YnKPmBgQCuh+w8293#=MdN?Yegv z=j?9z9v6PX8MEB2_UIH(LJ%PyX1;9oMCCQ3lBWtHc6~kWJ!;uYVe-BcTW2cGIkZ>{ zISp32t&pGKXptkYo=Ze4+gg7v3;TXd9>WJC$3ct1;xQJ2H~a#4Wgza7gj;ordD%(z zs+deW3&`)FAUOtQqT;0##&h+(t-YRC=bNcIzr9Pl{c*lFGmQuLi`uwB!gjOdjMCPx z!DeAeU$N^wy2TS>+}j@7%>>ujeIc+gc1y+rhg%y~L^oDx#z8`}Ka9)x0-m*8l}jQ& zGz#z@3KsiPRJ2niE&ZzkI4P)Tb~1d4Uz2R@b$m$D7f#F9Xjwq&b4IOP87nFl%R+1l zOYv6o!E#fmgH5iv;EQ0>e5T7^oi?rvz;Z7ZARi7TG1H*4{FK*I7@$1mL%7Lf-Mm$A z^j%9+EbXPi4yzl>bF{&-z5^jct1Y{aOV=1;A=wpcGjnlDEVI#k+^{!#W5nI;uhM%g zO-4T#@z-ZQ=URKx91Swy?)NPB+#Hrs(ukKq$B17|ukPv3kj_5atsN`#uqP+aCHF0L z`iq`BvGNNAFTp427m&WRKPu*FgEd{p`WG#_wp?zujCV$)u@C3<Rj&+2G9B%$B+B}E z_Pn8K{;Z`*7QN6Jr4e5^@T0=2)0rv!T7N>ZAq8~Xd8Sbd2s7B?%%NLi!alukgU#6I z>=#z}5vsA={F_p)jcdw`!Y>r!=EN^t6G*rp@@7g}x>yDC;S_uUBJTnk{i^Qzn^`tY z)t^;(lRV1;8853@30iu$t_##Jzh7oGH8m>iv*g(%_#~KP^(8}v$5EZRMut*VHuD<I zvwB`&&$AAw_>y?S5j$t@Rr6>R6qW8s4EuCTP9EaLo8;=Xtp;ZWX0z^pUJpU-Qwb(~ z9ciaj8^RZ9nGjxnt+LHEh0T5J%&|>^67Z>^ZZMM8lqRE-fty6D{n<}*qUe|><m}0H z3p<^6)k5D#sdh1`|5(?Nix)8UpV0Vl2UH1^yd0X#)vk5zyP=<}biw>{YhXs>$<&7F z(y&e-3gLj!jv+s@i!XYe{ehaEJ1L!S*f|9Z^@*><&2G7&41c~Zjgo)dch*U?V&lZG zWiMXx;O`ea2gn8ETm#2aBC>!5nkIY6pyfHhqoKs|<}(A_1;x7N6x2TMsW(@Y6YAHX z^Cb&@uUg{GHI`#xi!PI<>4t8R-JdiPNE=MX#94)o(g5?k5>uN?u)d3eev8WH^F0gs z+Uk4`MmInp$;)aF?>}j8Hr@1L@!UmUU?KOvHNmn%x@cUXx*V_ub--cY403)+zNp`$ z75e_i&ql2QHt#t_3bh-LhUj`_?@%g;;#9_p4LKFxTmV}?Eit3t0R6~P(y;mVtp&^4 z<h{XnY1({D07?Sf&ZoBh`+;R=Y1VmjU7MociZZ)+VP=ixj2gPrebA|lF}GAk<{l>h zsxjP4sI=)RtDVL7FsL2xzl+@*-)K4QVV*Nd1#FUVh{l*oI}i=6YF@oicP(wsy4=rl z;JIh0N#Sl8qx8;Uuq$@om!Zi_8mzCFe*Nc6DOC@0&cx1{oV`;gBgS?i4CxSbP+!5F zxlzMC{w@LI<D58#iKytgK{@p8;g`zg6^s(3#J<1)%i%{Ph7t6|qEu^I-~=iCR|iHO z^4U1e^yq1B^S4c$p`f>T|J0>=N`;QdwA&c%6w6C2LILG*e1_j0I)c*W0*&&t(*)df zlIKyx?w+lsK^5M^@zz1*VcU{Lpp;UrOQ|TeqaHMmd#u14k7ZiLgl1jM*G#${dj9Nk z_m26MzY8Evz_{Y<>WnF8SPE*0)S|ttb=k;nQGpa4d6OIUu;;waL?#FKfnpXzntv_+ zX!5aH(AQ*H|DLVg`H`JUOZ-zPKYH!?48sadu;~6GXwrK-_#(;TT>!C12YYWTO(;j3 z9wr#2FYg*t3JFl;ntBqTTR0t**mM-Uw1vm16NH*)X`*I4!@lmn7!4+%S#KuW>r+T* zAGMlvLOP7YmrWRFz9nXSpDfFm1*vA*dj@kZi@etW<rrXsd>T#Q8EUZ|UHVGrH?sJp zxm?O-1DA@*#3fTU?Lt;I)ZzXEM|FU7?$&plnl$$+MA~Sq;E`jY02jOcV#^xcy`~SY zY{K=3X-|#qbmhj3zX=sJAg5Dx2+B<zq~rla)Xum}Irz(KvMywBDAhU8{Zvt(y^Aw9 z*WEuOTVYl$b}qB!9Dn~v-Cpyt$s0a%`?`am#!~x%%WfW23YB)<o0!SW1D~WM&(yY@ z#!|4U*9|0dQq^i9|Al7agIS~6ZvqI*H&byJghrHkneXW>Jq1j$lA`gf@kN0f#e<(e zB)pfH;bzGwo<o5LdLN1`2BP4iVj{h=(D6WGU_YQrb(fPj$mw#Vy;BFmG-gvjQ#+*X zt=gH8vxEJy6~im*9%#^yJgq!jFVl>P-q3rP^z_Qc_{ZaAm^QTvQ#@;lGXx4m-|}K3 z`OGRc&tBZ_o5)r(^G?y5y)mNW+O&1eF8*(6<Em$Wn^G-`=W$$SnxKbebDgF#HrqpI zcK6cdvrS^SDM}!2Qp3DC_Hlu8)4%##A7?3WFdhX!kS7^bq?oB&R1-_@xz(IhVv9$O zrvbo34s#5OJ=ym$*wvfC&}!~~*-4Fqqt9Y2iQZ|_hiW3>;n!#$?FZ&HoB9w_<)F&x z%)e5WxE5^I9)`6@4xeqx$I*zjhRcBFs~Oj;lBly}={euh6$S;=cJhF9Cp1H3?E1+W z<(|jEDQTodU8W{z#rzIx1bmDE#T4$)$^<&XAQptJf4Kj)Ji&H(nn^jv<Ks&kWfDL3 z8LKwM$=MTL%AZo(oV<V&)wS*sy7F959F%1z4@DQR1y&d)d~z_WVF=k!@ql_rGv~VG z<}X%`IDCgxq(7BI?uLBDrVH}C#<kBTnLVXAe?i780dOj)n73$y?dgU$c1?fGZD%Ak z*z};~mwpYx2%VyL3?4hq1R`MIRI<x1PwXlNC!MP=mZ3D&%?zJSA8A#G^~Ei3Z3SW2 zfN*sfXM<hR&REIQW3)ZA=*32-9pO0zdute+t7@e;R=-HDA|EmTesid%XTx)^PojHj zU|cO<J8cWQO3u!Jd?N!A_TRtfx9|a78HmSV10+5rxio&=DNLL7-Cy;fRG8me`t|IT zH7!s9$V`nBkU;y|0zJsX9D{BRTa3*RPXP+W#{w?e7AhBftI?orYane7_1BxwS#HLS z^RaRIJvp{pIpw^#b|lbPjS4)j>dH$u!33CQ)Z$uY+&qNCFicM8(aBB!NDl9mAAO;v zLrR9$ujD8a`>T4;la`xj2Qmm9*&RRSe2V<-qyBO`a_))Z{<If4v&pGWSj@*PeVA-s zESSp9o1=(&aj9wp4O0DLcJuxv&0ilF0OQ(x%%yJjW}1vI9nCBIcP^mC=SX*QcJzXG zgYrRP9+mnnwUjZ2-dmvgtIt{flR5r!kqc0ue&Dil6PJ2&_Wt%%X^66IiSaGzz+$}z zOLOj6^WGIT5nk0Jx#o0`|4Azs1?1j|ZQk6BBQakI3a-9OdJUZ1wksfyb^e*d_<dv9 z&eB<^3-y}ea3W-WI0F_vZg=FLAqqQu?04K>+0XG<t`0rJPt!?LgH-IhzQEd_Ev3RO z>R>fcv@$MS*mA)$kod}H63;_N&K`Jtar=_UAbyxbu*ONAH+gjgOy6Ud5h&1q`BTS- z^u(2P=-9wtk>aVGU$nDHA`HL@aN7Mw@v^}{IfD5X2EKwfyT~T4>iV@J2XgLjae?TI zzm3z7uq$Yd2o1nnQeu|zm^OQmEb71+m|V>vXjh%|z^h}Wnd4&A^ot^U!FTT0d_lMJ ze8jreQftf9?*<mhnNN38ZII26$T*^L6~viMM*$JSCZ7mRNdiCAM=f@zb<<N5w~}6v zr`B-Z_I4iyRuYpTP=$&g1#I~Q2Y#~{Wo~=V-iw{oJ_NMYEe?<{03#<I)|OF4aI*(4 ze9-nBPqUe>b4X0&GZY{kU#jdWgJa*v!4^4SY3|j7h2GF3w2Zm#%uQSFWL9t^W!ikL zL0NO<d`Q%0gI)ipn?P#Ajycue72^JHg-Ecwg5?D!-8K_@KR=YksLtWX#*r;v+(E)S zbq3}#OD<gdxF_0M&^C-#e$eu^(fOp4>jk95yXqp)zMu3o?Djj@&u@X_by%RMTY+J! zrJ;G)>x4@-m4jf5WraRBxtQ-o@Y$0Zb7($pw1J=4J%i_YTD9D)rNLwXE=yecp50X6 zjVDLHPPWqveW&?L`ku6As&i5*JS{%_sp*Xzn0=0CK>mo;k-N6<GjpHTZ6F+&ZXJr< zu+gglTn81ufoZR&;NsOPADGc-H%|0Eb#pJP`u`T*vm$ESynrTcBMg->h{Kn9VlbZO zu4gf!L4fKWa#*%*IwH<<<)(B|(3Q?N(_)J6pds~TEy$i*wPcrlT_dS9ra4Uu&8~Z9 zww$LivR|{#D=o3eD?dme*q)!hv6BC7)G3WLzu}p*2Bt5rw+r{cusuO8-g{@OoThp% z*m>#%UhO>DKkJkGeks?glh&1-<Qej&&w69b#V4?T(;-I0PMg&1UM+Mm!zk=J{ScXF z7Gf4&$1Lrode^{j%C#Q=@HI1*@#)ZwipYOQkbZ~x)C_*>?LcvA9~%$q;8i8B6qy-r zs!^M*Nue*=x4KdV;HAes2p;Vp;d&iOmN%R4cp{$$sNY2e<-=eB4C2IQt*K;ejjV^U zg9FJv{<Bu&p5v;fpmcHD9icM{UdkyMJ_L@f{74+`=FV)VpzU?dg}qh_(fZ8c?Q31$ zmbcqr`P%PkodzKegPeKEy`qit>DZ?mNB)Uk;&2Uey9$rGzo>y=XP$~yc8B0$2$NN3 zMD#`}?{c%&(f)AFE@1O-)j~0FWB-GFek9GZ4&cWlNWo9a9`}eR@fv6+?PDdoQl(-c z=Y_JrS&yt%2t$^gn*b8xvM$_z8<Rm|zxJQJI-oj37xiV3nr9T}SB7f86-=c^7-k%j zzvPes^h^1*v3bj3*yP1vf09(uldlbNdZR(C_rNzqaiJng#m%3``Hi(ph2P2O<*Rcc zwots7zzSurWqN*tPnNf=m}+8D4J)S(+)o&5pMOkpu`O{a91uBJ?00$}=8W|g(L(3j zHk$CqCf6QN-3Bx~OTXUOzOG02;6mrpZ(CueOkO5slTVo72xha9o*~m%?wWTCQ1qy- zT*lo_<*j3u^LcX3?HP{@N!kMW6V&_Z4L_I0@kOF|2tVJ6f7(SOaTKB`YfXP>a9`kE zDPXz`OnYZ6zHN~<eiNZPd$B@ol`hfHV&7=2l<+_rzRu*D<EL;#!jWBK)+;1cl&r>0 zJZJ@zl<D0W#dN3ry=ith85grl1hY5Cw@k2XvbX~$P@*snLCYF<N&eZI&R8-Sza(sR zd_^ro&h(pD`QHsipftJa5KMWe@%#S--NYX(p(ku@4c4~Nl(5U3PePJ3-K}3I!E@d^ z`A<FawrV15HdN`lY^Qjxt|G9}OJ!b(`wogcR;Jjk@gd)jm~nq!a$R2#XCs6(u*nDM z^WH5~n@=94r2VR72lpGjcwmUwV!#NH8l@v#Cmx(%{;`C*=GX$ZDIHrg%#NIfn!bB9 zy5$2rVCwyfuRpZ4ZTVAp0sT1Whtx3qc`;*<aNw~fa`@(ilvtEY__e;x>gpD3SoVio zD#UnJFtn%9+x(17=uE;LJ#Hk$(io77WEjSB%Kl-G_WCU(XYA+`cvE)Q)RXGhL!Q;? z%aY=`{p`a(A9&xG;$j-_*{>-gz`?2SlliKR)}Rmb13naVhCno(<-Hw|%2r_$>$vut zb#JlHJUrd)b)q;{=<9+A$TIP_^y^>GXgwdDx1E~obmQmuH}O(}#PA*2!ZnW<6=4<~ z1*F$P_U@!xo_-w{T5qQYX3C}LFk6P9R*AWSzOfe*O|KOe&dl1)@%fROXa>hTFe6ZC zz8@5qM`>tg^`_VhJ>6GJc(BK<TV-4GENx{^aIEWaC-$V(GBxBq1EQZ&UXyMzMnW>> zK~P5>4U4vjRR<qHsLxz2zn*VDaD~Rd+GSYR>+Que|1l8AC}Z*gduZ`!THr{Z<hz@* zDIM@=-ww*M9>Y&7h)+D3Zw(-u5>bMBSPT0--c6>gkmh^q=|RrZ`!mtB$p;xy_H|cX zEk*pr?!>CmB^9QrTh;^V;4HY@?s6Y*fvy7rXnt~P{W<M^+n+~k9$TmW`A>}F@^90$ z;&~R`>CgXT=zqWTuO<2axDfw>cZ~W0d@8l1-siMR{Rirynn(Wb+CT9rDTsKIu*c*d zyAz%I8wB#iZc0gc?0R|D&QG0Ub^Qw`N;=PJ1bP^_dL~8_!DS-;-}~5Q!v2_o-O+n! zpIpkyKjhJAaStFrz@IPM=v4PXZ)dNA@6&DUc(VTSqK|&#XoK`l+R?+@cM5}qWg_1H z`55+0N&!O<XQu00pFiFaL{<hOyIgi|<Bthm_5f^)o9K_8ihm6K0>GmAtCpwC>VMsz zo7#W?Z3FANo;!V^{xPqwz|+3oEdI0XQ5&Gw-qovg)BK5v0s8-~!Rx<SJpWkPw~{Y4 zfbD@|Q&Z(2PxtqLES|rpdj5QX!q8v!oMh3l`Tai_=}$iY3pzY)V!Y#sUIjkDqtnAU zyb<y@W$@2gb0#QcomE&p`uHp=e(#S51RJvt0`b6&h1Y9@`+u)G^^Zx@{bPKesJf}r zbM(jde)~(c8uVj_K0j%z{qf45&>x-t@28Vk<m10cbaegkbr9MA0c38e<B#9@_7||* zs@p(^lK(mgfAIpY)N?j|i8p@?MFNLP<<;K;4gcJ^XMjxm3vb^b^v6;D{}(i6ze&MP zN|U{VytElUtk`tdfK@F++~ucLf2s_xF%F%;0%M6r5WzQp;}A-oiwWdXp53HZtaHf2 zjpiB8_5GyVV*9dwRW^zHfDYQ)c}yFz@CJp&!~+4+_MO}22U!h!-8K$G6Y=~|Vw{NG zfLiTEMot)(nFEPmL0IjsWQO=4#v%U%4+o<={e7*qGVY%@`RmwZ_PD~7<UJrD<&7=Q z(MVA)?T?qJ4O244lsA;kEEB7*^rYwv<*2i`1mgN3wl*kK*cR^KPdGHYsnAr;s+5oj zZ(2-cz1|rku79_I+l?s6dNuGsZyA5EQD&T7<`smAH!!;cP{$(mXEaRLQpt!|q}FaH zTT_nnWD}=xt#jgWjHLYOoo&<my>=UQgqwm|Dc5<DKMqJF(w#Z4B(;o}_x%xVb3rKt zG`sulKtjDLjCa!Fjc-P&{8U*Hvy9)(34tblgE42_C2Z4gFi5?lXDGDTw?fA8!>V9Y zKNaWvR7bsXNpH_rjLp%@^t8=6oSE?h|FL5F%7ReyvRDAPKoS(!ML(uoFkdP|I<-J| zTk_maHiZ5SEgpR;Z1+Pz1Sdso68yHz!uNJsXs_=QA0*WgAx%yeTW@sN3!Pr+hgZ6U zQ4eVyIqRz?O5+k@^D&J&!ud_xGm`F$Yz1<QeYaiOeOe301&I?DD~H}2;tmt#hUKP@ zYvEICTm_HjKHf&g&cEqPfkj{AM^0(*>Mhf*jn^6Ie(k+prbZ!UM8qLhAJhnmE1o%Z zh;Ke%SE5vSz6T?yP*Mj6?EQ8yZBj}1n~)ws!Q%{TgEYqKtn|IxfM4AfSUd)a9Jiqf zx>z#)q_itm$DsA8D5z3K6&P~GcuVoHrF<icezfA<4^t7Q4z`bq;5qAsqiQUi_DHNQ z*v^44amd5O;F32!FfF4nSa*3gnwSqls50JOoM!l01;^Ltg6pFobu04k3=buH$)XQ7 z5DBz`N95M%JxXJ3ZL;VA#4{ci8K2s^$2qE&NwRC-VN|bXjCH5QZ8s?=Yn<w6g1my@ z**V%Q_;fY3ouGQ|Qxl!%y}Uzqc`NrZycaAaZ5qo1`*!(y;9#+gV>GE9+wW8wo9vqq zKfN5f@4MGS_!N~e$ssgVPRlSDeBV)|?VD*T=<Qia0Fj+T<tlR5qGxUmkCny%VJ4Tl zjc{}uw+m-D<MCgmS*<=q8E;{ANUH%~0^q1w);S4=N0hRj)wbCpd=&WLrW!r$bkhW^ zU=~=I$EXeog%p0ykUq(N6lroF*KU^$5u95ms{|aSA<tcV5|=|0WXdPz3F*R#z#;Fa zGc0$Xxa}MNw%C0jU9UoOTL@7<0K|WSLj#3A)Ov$JsM4mRU>b8pO3;K=oCbw}JbG%) zwI2vC$6%yKo_>HUg)RRE@_Zp*#}fI?Ie@@Txr^-KFM{r8g;fr50NHr@{%W`>=F<z) zC)a7^fW%MD6mdm>zN%NKU&1Fb^F;V!3AZafjpjgVNnTiF0^pk5b?7S>JZ&1@F=U?y zoS{z~lzps)S61v~*IRRi_rk@n^}7<YcYz49rJ1APNoi?=n9*0TZY>&Vwc%CrR6}%P zUO*efInslDH=Li-Cu2ouITuJ>YFwMz^p4UnhycH{yOzM_jMOk&znk=mQem3M=hfM{ z^Z!Ku>VIs>DP{4?;ON$q+72E}qv%&A9<>&v68;Te8i=tA<+#$r%DW1(UsfD*tIl7{ zT*o^H-qG8aw)6d|voc%VH%KEuIsNJ>?Qll{ra#(aA)vLdBa($<+|-y)lYdw<0Pr{3 z45AnQV~X4a+F7Ya^Y~BiCx4l!3x^G4(YjG?CGCWCUE2MQ%ytI%v1JK1qv`ZO4>yR< zV?4!KF-+$@d@D9`al_s-TAV+G$u56%2LLC;gHZs60ZXNZ>2IL%xoQpX`yJy3GV^>p zrw^RAum<U01max<uI5I%eya7Ui7;)LMnlo*RJns8OCe(rakKXT%|7#%O}~lxY@$?E zzD9rQoQ}!HbOuh3zMNHVbK`zI;5>Q`amtTPvjAMfgpF_phMBxO_pqCjx8w=cN&*Pk zA`|FVP0SrV3s1wqNGjTsC6`%5dOzj4D%-w!^Gmhc1M(`10D(QGgOhe_&ru4p9HDl( zNy~@0lfT%Nrg8piqwth<A=M?;%FcS^cDe>Mk6FsI%@yE7^icSFulmej;>KL2kR10K z<pQUy^~T4udnPkl_A9$nrG9>&TnQ-b1M=tjjcZTtt_L2B8mOhqHgLt~*QMymPpni} zaZ6P)zORCgv#O<W*DMYs8tf*Ka&<D~o_W`vGQOfM08{PNPuGcNeX~^QS+w7L?a&Z$ z@a}!bBk@s2u}411la|$d=$}f$O+=kQ05p@%naiAQwoa9PV!PI3>S9cN6b4d#LC!9Z zAu1+oU$<n_C*WBVuBM1j<Wxyh+8Bi=K9KwV&p1JIHQv1ViD5dQKDYghOP=pzRA|*! zbBgAJ$!k%v*Q(R2c62Ef%rMhhW5q|kX5ZCC`1pkOGy}Hh)PbD7D6lWQvBqXsrrAs^ zQe|AA?6<f3o_5&d{aK#lgDn?w4u&`Ln=?&1j&g)lJ+5xMwV^r<5+K-|eckLw8{q-C z8SXpJw$)<=tzT&dKx2B-niTezP^Rfcga{xD8Ts|ZT-AAFvuxkq#@M{fgZLGf+DDcF z7>i;q2&+2yWK+|0ATEtaD1*=H%MnHxp6$6s*k(R&SIN=q{D~T7gPo`yU%I}@ro12; zFJz@$V2+b2cwFUJ{W4lHjJBsYsml5|!mvM+EQ2P!jCZt9U+DfPO){k6DLWFd+|LD5 zeoWh4>lC(($K<YkxI3WQz~oA++f%2u=g><W!jZ40k#UNXT~4d4)0H=Oi*?^uKj)WO z%0j8|ogS1_zD|(@c=fzRQf6NE;n=C4!YzxC!RdP~@B80mSIyfGN{4KSi-9BNNlu5k z=YmCD#=7@Q4=1typTJJZYc3TAqMttt9vYbHKNS8{5KHfPHs&f@04Lo4KwsH7^)JV% zjaJiu75SA6>G~Ht8`iz{=4HGkb-oxgdJr|Z=`jFWP0hAmN{qdv<*pnK!Sp-xHEvaO z-tF!Ygnt3zG^J$Gv+%|)Fm8IuO_72<BM{pFn+eojkv@^>dk}j5QW%h<^Yi)s>V<da zX$vzhK%hCw%Wq-yHdtey8OY7x?K8LMm+?IYXb<Th+|Fe-(F9&=eM1}9(;N5eH0h7d z2#_zA=URFgew>swmqWQlUHO%=lhSPF-U!<K9P+D0J8|zU-U1L?z323xBa1!dH8EHo zm@_jk$h)-jC_)F6qar%|OW^&IXnit8&LdGN1p#X#k5b8i4DD!k<`!<dWJ?z(ZKS~x zUOp)CX>8|2xXYn}UjYOYa$oSI3>KxD_K|{c1b%-#JmG43`S41|%4`}`qnDXIzG&nM zA=g}^)I`dL6lCspd!<7ou739GW?fawWjo_~xfx(%3jj8?xX(FN01_pP=Lk;t2hnuz z--9cqvNDD<^u_ZT3L`ZJEpGvt3M}r1D!tJOf87ck5C$?RTvn5v-cKoX0ttK8VKu*L zSYIWaN=eKMogX4%E{ivl{XU0Dc>Dm2HZ4uX?&Y9xSMPI3lHEB?05<C_|NN!OAh2r3 z*a0~<F7l9qLF<M@9A1mucxz^FCa+KU#kRYY|Na*j9FQ*#q#>o317H_WXDt5A1qP(n zxH&A%Wg*$ARJ`@I_WOQj(SSpHDaV-mP6fvK2}$Y=&4yX0oT=z?^1s+X4Tu+N1=src z?yU`*agQf6>P+Wuq&iftcWU_z13VxlJ9}#V>h!Vkp~{m5UHE<|qg~d8;MJ%RbJv-h zWtuP~YZ0u`0H%%R4GMvQGp`8%aGv0Cow+IBqK%74RllT~W!Q+rO^FmsChbAiOH+II zH5f&*Bl~yh3S~`pm#@<Yy^$8r+X8m983=Nw4_RzphDzJ58{^{Yr>Yd8FJIqH@o`NY z<9;@eE`D@RMcXfDvBZ;fz~vHryR_x<$}Al(C`(G9`;6MXV!^&yv+HhHwCx)wlhu;T z-CP358M0{C8R_tT;cT1zPdmbajb&k48FJvoQkg)-2!L6_9DYr(#(OqhvSYcbocBJ_ zUM}*kenu^H{sw4pPAmj<<1*LyTl`6h(f`xlm4`#s_x(xbNtU=(D59DyS+XPyV+mzT zLKIo<JKG3}vCUXpERmhDi;^w-Fc?dVLX62y24&xe5o7oKru%-L$MbsMf8XnUuj`qA z&$)irIp_QRo%w!0%lTfx%yb_N>iHxC8L52{s`)t2OJ_!DxPe!e>+9}~s?l)$12@Rx zwXm5kge7j;IhCt3TaBw@lqIa{$qijqrGy{Sgk)SrzIv#+D7!$IyZh3fwodsmgPJ!4 z$}i_LozhX`qg>ur_U21cdOdDnq8v@&Z)M8)ZbBpJDBsX^66*wBU&FL32IH_#0GqFF zTO5g1ZQP0D-6c(@Z>!^GnIz5f6lckj<gOdR#877chH(=Ru7<Agwr{V>s8F>>j}I*Y zGX5(E@B(J}70|mhuD{a*Iv#7F=NXvqG(n1YZch}qyx*BZRy@IoMW$1?xU6xyKBkjH zE^mxA+66bSIyx_imH@{NG{<C~i+xVkwhnhLLSZYCCprUu*mvD6zcmd`95zt4h?Qo` zW(i?11`IxGW#%Gv$Q9>^8mf6F>DlCrSFmb=w3~C9kfeITPRmR39Sbh_zQLXjX=>#% zfIb{jDD3eL9rnJDGQuveyv;Eg7eaKtQCCiV%?i1@xelA|$}Oucq>S^%_nQ02eLs~N zcUJXU6UmElZst2~qon{hG&ff|9CV#Ge}wE<hHTyve9u<^tl-FG*((KHJDIi0d&SLA ztnzapkfmyG#DyG(ge<7o7e(0|GmZx2@^xcThs_aoNY(pRclJC0N`6&3uZ0AeN{JFO zn@Zv6)*9w<9E#JMj3BrRXs`}Lf^~1<x8EtEzntJfH_=+v@teLI=0geaZZS0a&|!RC z*<@SN(6wai5H78(W{*MmRqW0y97)Vk7=8hhwOY>mmf1Gh^<fCG-MwoJ=aIibArhjM z{LF=J&s>?*pPO75M8Cd0%d!3Ok0j{_B1A=l^DlM1jiT)#R?Q<-{3Y2BJzs3@>}<JW zy7J71ovNur_$*^$p?m*&%yB@|^0f;WA;nd)zSp_~k&rMT{IHmRNMK}fzboP0XD{&j z)qE3s5CI0J&GhkUUY&T#Pzf>G15X?|2Re~RAd)yoyw;h_m&(QAH-19ypj|aF`K)mL zZjV!PJH@&g?X?QjC?^WCSITL<)0Fd(>`Ju%adD|9vg5^EN`61b81N=#t+kY^Ol4@< z#%Ht3fNcNOm|lxHX!Sd{i~sR({=aEdE=b*x2QLT31CdHBXCfzcSdNJ%@!`reV`pPu z352(dZsYsAqYpE-ETRZ0W)i*&iryNE>xi;e2~9WZ_A;*Cw|~}2{NpS8#y<I`DqO=+ z|FuivNzWpT3ZoYW!D!_}=;?@wubdtiaFC`tpG`umQ2D&i^qeRxH1wB_NKx3w=8@2Q z7f)_kXJr%IG~cP8k1(gThxoEuNN0g;O1-eY&J$bKR2gxf>tEnofb$WsrPYG;ry`$M z#<KB>MBSZLEDs7p?HsPEQ+^*AMBJQl<tp{g7$m1kd}i?Xw5agl+0eFs!D$)Mp8HTb zz=pn)S)t_UfygxiofPOkBxh7D#28X36;HE`TZ8yZ3JfNaY|-iyB5n3>6>lbzPL=f( zM3o2+Q+vtVNV6(@-^$L4C8MSUB$MHqu%Y({bEloH^*Qkb%U)lJ0Z`kt(Tz&iByjV! zy;amTg<*x>=$C6*BL+8~33dZPHE$x^wyx%Wwm_XYV}!vibRe*X>p9iK#T$dm()XfE zA4CBZ6`Z5{Ais0K-fMcvo|3>7$eIVpB@682llUJi$(qf=jGOs^NB!-dDKgqNQ^;5P zs)tBtcj$k!3xH^B(eV-Z!;dr#|2B9Z)$i?x<P0@vOZxqMqs}EZQABHT;!i{e44>&8 zzGIizy0*oRH?!sFxEq)$OuMt**`E$y$Ga!9F+JmJC+YZp9P_XFNy(=G({cax&Npt1 z&9;G8J{eNR3BoYOn>j8sT|(<Wg3`xknTPu-+`d^+4J&~JFshVlu5pGZkEdb}M#^dd z@G_M{xVasJBppsCs+!E4>avXC>8&?P<WHMCcx_lOqa%0=5O_1Zpjf+0kyZ2NCsL#M zB|yYNkNMASZx$=S-xm4kY>+5V6V6WIme*AlE*jk%@~%$XEpd{T8o$gf(^WWZm=nxY zFwj*`#4i$79Xp(6Wby0YY7SdDyow*Xcwjb!2XeRAGHR+gyhVqcg<45ttUPJ^32wjy zSI*?6GpM~L>Q@)52Rw)|*<TuPKAIRb^{5}js<n?n((#+l<{D~s&Yq6FuA#JTIcz?g z{rj3lq!<*YV6(NRS?aKUbRb1HT>;&L?YnXVz6WU#`&GBWQroZ_k&FQ^axY;cq6O5B zxe_6-KC&Yd9^=(GAnmAyX?N2E1wLsV#mEN1L&{=`kdZT#t~RKEPhMB53h!N2DfL3B z3yUKxW>8m@MTarfunP4Z8FL?$d)!j#yObDYl3ag9QKy`Tl$Ua*Fu2v}frpVIX4NF~ z4*8fy?H?a*v5z?oZ9T!-_4o8J#8b~zZ-yW&Uavq9!x^u<SK^9{`#u~lqhV>cj*twd zKOcu6`b$$CzRe{Qv<@<zvJ1}j{@LExQQMeYku)*O2}4FUcd~hNxBBgFb0Cd)eZ(H7 zAM3F5jns;xV}ETwYD-Lu`9|!f-o(j!v@tbDjb2TAolqFFy$5`~7X}zAR{Q0iWYQV( zrVH9GJgAPSc<F9ieZ$OIJRwZrQVsme@aBDuL4Ft4jC?wiP@qh;CW;{>eG}lHdl|>1 zI~k!rBZK`vkCt48i~*RTV3_Ot@O+=sifsi-onnPon2PbVB>*;X;~<{QELHoc>bJVw zJ8r7Ds?PCI-nz0)tZm~<*b8Wxtx<~K&6G%v?64To$?PUa(<Px?Z}g&NsAJQX<{YFc z?{>NdrVu_qEnV4OcHLMr6&B|zm#NANOA@-BirF|RnxqMovfG=Zcvi%LNT3S-^7vV( zsCIIXXqJ7qz_yyrSnS(-Ro@N?pw@ApjzhS_or^_G##(MKYc;Y^>^@O;?yHJ&!%?mD z{4{ZV{$=|+7WTcq{92_Q<hlW95pqssyhRBrN^rd~yFmtnaGWF=^n46LIYd$AG^vcF zO)ICf7IT_{X?~jQ<oee!GZ!42UgBQg_y?M#{?OQ?WlB#g)00PUU<B(xm?3`DDfz2@ zOhO&?Q?|?8mX274Z)$`PBs06mVr>QED_W9xrQ0ZMGF_Vo(%yiTE46yDDpqRUBshL! zDAbulK;z|nh2XAd7=eS3YV0Je-_;|(MrO-}RpQLT5gO0sD_34J_#!1#ShHz%4rXs2 zg^i-Y*YAs}ty`LJPu$Bj$chGNwDfS1bm{Jvj<~Va7l*)lY;=l5f*dvePT9LDZHG~Q zQLD)|r8yrGs)eAxJZ&E>b)7{%_Ws6C>K2@})VBVfjZXTx>CS9d&XH}dLclULa>u;w zG~1DulAd09(4NK~mrl9h;x<EaR%MVc$S)fL8XPH}MSlD#`%|O@n}GGD#`LVa%SO5} znVs~Q%DT#}rAD!S@Rgpa?P_fx$NQumuerN38PuN$X;>!q#+}wWj)|G5cl6sSF?A;o zT>yjPoMjLgrQJVLk(b{yXU9sERT$>oQK5hjL7=G%F*lN1b<|xid4lv)bE?jLZD6LG zrhus95VhW1op(>BSF!I#U3-2)_#)UyxUI$Q2%9&F5!Nm77V|sKM3%97*1h_PMPPhd z@~<?O9#(aYS?ut-<wkamF9Hx=&FyT>uW6@C82lY}Mm-R62&|*7ZD{*hHikjQG5x<O zbK|EJ-Q44Vfa!VR7xj79$A?PbYas(#+MCz&`)0JS_+HrNAeOYeI3se2IJj)tWOKP6 zA<Zr#hg57T*@{~@Ky(|ZgLnKGWN7!rVV89HWhFD;GwMT6cGAm(NF89Ut>fWeYX>Ee zoIM?Vh89h*c%9m&pV#G>k$10a)%mdU>cZdyIvKYSBr{Rb)F4UPaZHJy8+3Of)d)N3 z)X!!fgF+8_eSasOHh_%%obK$%Sz@FNJRES`w)DHoT%HI=r!nzL!1(UwQauUe_6${1 zudNcTAv1ToP!=DU=b~P$&jK@Wa%0#&4oA=RtKO7eC?rbmVFZ@gf}0>sf%B$W+g(dN z<NbIthJXB`Ikr7&bhr7qRLc9rlf4KHzw#oF-H$Zr9|{HrwN3aF8#mm_Yt^vSCUk*3 z{hVtNR!Wrzh;j%{Giz4Nxy}Zj7RLuCJH}~EMz+$X%Rv#JD$_87Ma90d7{`%1PKut$ zj!iAoJ;~l49wTCFut9OLS&tHImasW#Tf0O<S}em;D7vl|G8=Y)7Qig){DJ;%VAB@@ zWLzEAt9VND&E$E?8o#3&I!MhJd8FH0DpsrI<>aZB0`)?c9N&(bmmpis-xQh6ljnaJ zezm+)-a&1O$@-FzVa%06X!ix5v<q^0;aTT}b^BPg{53<BvVfN&#AYwkC3XLP3^(W= zI^}S#q*;m0*;&L0h}E@FDMaI?iR8iYLiv`yS4hrg(}(MU@yt>mFI~nwJ2~H;p{ZCN z#;c@NC;?~3+Ud8=NM{m@{t&L-=_*`7Dw9Lp=gs4Rn&rU*5i`kQruE>BRu=BHmXz%H zf>*U?P^(iT_U*VF&HKaBni>MSDcr?3$tvgv+;De8m>cTjg6puWvUTtCp@BuV`)WbL zUM)9d1_d7?7oY%STwExfJw-YNw}0z!S=9s-^C$Htqk>wRrHEau^Mh=8-14jGtx0g> z38KXm@61S7H~DktO=HZ(xb(=kcVCg2C%8ZE_5UrM02!g-ed{q#kyPEB^dK%`%cGU$ zNeDvrYVno6KxM&z-PYm&GybBm_2xkqx8`^WgPc4~_szR83ZA<=+b(O1!!GON{ZNku z(r*qYBcbSnno}PxgLe|q{IsXwonp(HAQr?i9!K)iX3?0ls`Sm6OGSuIOoGgtfgUb1 z<XSj=b*WYJ9|!rTJ3cuIc1E4w{W9dUIhL{>KGvGp>8rHn)Bq*={<D$@aAstMFyI8| z_kf(Vp{&4=8O;ui*kflHIG*mKFn^o`j!36l-}V_I1E4eyMl<|3vHdLfuR4^Ji4EGf zoF<JLk48i9vF_PgfBvUGH3f$%)e-xRsL?b#1C**!$@kBj`A2Pk1P2Z|hjyIVw;WUs z)Qp-7QO+uRN`^nt7FH*4=*P*dy;atq4V8uq0DYrL=YKrj(_sB2V<3S;A4Ij#`<9Cw zqp3=(u75bZ|H=Mx;JR!zAmlt2?o}40ukKqH62SROLP6=`0x9Yc#-X;CPy6e1KGH8G z294^M^@5RlZ|=Vef8{XRIqkw392#N7Cr9^RCWI~oz$mjy2vh*D6RYg)xc@?-Xm(;0 zMD1Z%8ehnY=YL};&UtqMz#Jjc0!@gR<F}O(d9JDNUnWwI4kH4I){{}g@4E<p|NmLd z0D{6s)u=kVFUX6)FTC`;_}^5KzgA|WrQpaJA({PoYYtX3XY2VFz4DJIIRb*EMmj$p zxGxg#0FmDy{Ndl6G`}8<8ZG~V(XUDSlhzZ^IG*Zv_D`M)W(WJ`QAF(N|1!oJB-8&( qsmXtW1GfMFRUOcOg7d!>oGo5uq>1=737$3x_-U%^suf<feEfHchWbAM literal 0 HcmV?d00001 diff --git a/docs/user/alerting/images/alert-types-es-query-invalid.png b/docs/user/alerting/images/alert-types-es-query-invalid.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8b8e92181a9791176759e161510d1d6dc7e03b GIT binary patch literal 82855 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 F{y)ye4x9i0 literal 0 HcmV?d00001 diff --git a/docs/user/alerting/images/alert-types-es-query-select.png b/docs/user/alerting/images/alert-types-es-query-select.png new file mode 100644 index 0000000000000000000000000000000000000000..61fe724ea1412521b8b1bc691a408653fc956e30 GIT binary patch literal 57025 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�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 literal 0 HcmV?d00001 diff --git a/docs/user/alerting/images/alert-types-es-query-valid.png b/docs/user/alerting/images/alert-types-es-query-valid.png new file mode 100644 index 0000000000000000000000000000000000000000..1894ad2b445f8d0ac37d0b41c5e23273f12903ff GIT binary patch literal 79515 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 S{q_r_&vRwXr<G4EL;nvaSK8MA literal 0 HcmV?d00001 diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts index cbdfec642fa74..a17b46d7d7abe 100644 --- a/x-pack/plugins/alerts/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -15,6 +15,7 @@ export * from './alert_instance_summary'; export * from './builtin_action_groups'; export * from './disabled_action_groups'; export * from './alert_notify_when_type'; +export * from './parse_duration'; export interface AlertingFrameworkHealth { isSufficientlySecure: boolean; diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.test.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.test.ts new file mode 100644 index 0000000000000..d74edef896c65 --- /dev/null +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.test.ts @@ -0,0 +1,398 @@ +/* + * 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 { buildSortedEventsQuery, BuildSortedEventsQuery } from './build_sorted_events_query'; +import type { Writable } from '@kbn/utility-types'; + +const DefaultQuery: Writable<Partial<BuildSortedEventsQuery>> = { + index: ['index-name'], + from: '2021-01-01T00:00:10.123Z', + to: '2021-01-23T12:00:50.321Z', + filter: {}, + size: 100, + timeField: 'timefield', +}; + +describe('buildSortedEventsQuery', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let query: any; + beforeEach(() => { + query = { ...DefaultQuery }; + }); + + test('it builds a filter with given date range', () => { + expect(buildSortedEventsQuery(query)).toEqual({ + allowNoIndices: true, + index: ['index-name'], + size: 100, + ignoreUnavailable: true, + track_total_hits: false, + body: { + docvalue_fields: [ + { + field: 'timefield', + format: 'strict_date_optional_time', + }, + ], + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + range: { + timefield: { + gte: '2021-01-01T00:00:10.123Z', + lte: '2021-01-23T12:00:50.321Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + sort: [ + { + timefield: { + order: 'asc', + }, + }, + ], + }, + }); + }); + + test('it does not include searchAfterSortId if it is an empty string', () => { + query.searchAfterSortId = ''; + expect(buildSortedEventsQuery(query)).toEqual({ + allowNoIndices: true, + index: ['index-name'], + size: 100, + ignoreUnavailable: true, + track_total_hits: false, + body: { + docvalue_fields: [ + { + field: 'timefield', + format: 'strict_date_optional_time', + }, + ], + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + range: { + timefield: { + gte: '2021-01-01T00:00:10.123Z', + lte: '2021-01-23T12:00:50.321Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + sort: [ + { + timefield: { + order: 'asc', + }, + }, + ], + }, + }); + }); + + test('it includes searchAfterSortId if it is a valid string', () => { + const sortId = '123456789012'; + query.searchAfterSortId = sortId; + expect(buildSortedEventsQuery(query)).toEqual({ + allowNoIndices: true, + index: ['index-name'], + size: 100, + ignoreUnavailable: true, + track_total_hits: false, + body: { + docvalue_fields: [ + { + field: 'timefield', + format: 'strict_date_optional_time', + }, + ], + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + range: { + timefield: { + gte: '2021-01-01T00:00:10.123Z', + lte: '2021-01-23T12:00:50.321Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + sort: [ + { + timefield: { + order: 'asc', + }, + }, + ], + search_after: [sortId], + }, + }); + }); + + test('it includes searchAfterSortId if it is a valid number', () => { + const sortId = 123456789012; + query.searchAfterSortId = sortId; + expect(buildSortedEventsQuery(query)).toEqual({ + allowNoIndices: true, + index: ['index-name'], + size: 100, + ignoreUnavailable: true, + track_total_hits: false, + body: { + docvalue_fields: [ + { + field: 'timefield', + format: 'strict_date_optional_time', + }, + ], + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + range: { + timefield: { + gte: '2021-01-01T00:00:10.123Z', + lte: '2021-01-23T12:00:50.321Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + sort: [ + { + timefield: { + order: 'asc', + }, + }, + ], + search_after: [sortId], + }, + }); + }); + + test('it includes aggregations if provided', () => { + query.aggs = { + tags: { + terms: { + field: 'tag', + }, + }, + }; + expect(buildSortedEventsQuery(query)).toEqual({ + allowNoIndices: true, + index: ['index-name'], + size: 100, + ignoreUnavailable: true, + track_total_hits: false, + body: { + docvalue_fields: [ + { + field: 'timefield', + format: 'strict_date_optional_time', + }, + ], + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + range: { + timefield: { + gte: '2021-01-01T00:00:10.123Z', + lte: '2021-01-23T12:00:50.321Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + aggs: { + tags: { + terms: { + field: 'tag', + }, + }, + }, + sort: [ + { + timefield: { + order: 'asc', + }, + }, + ], + }, + }); + }); + + test('it uses sortOrder if specified', () => { + query.sortOrder = 'desc'; + expect(buildSortedEventsQuery(query)).toEqual({ + allowNoIndices: true, + index: ['index-name'], + size: 100, + ignoreUnavailable: true, + track_total_hits: false, + body: { + docvalue_fields: [ + { + field: 'timefield', + format: 'strict_date_optional_time', + }, + ], + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + range: { + timefield: { + gte: '2021-01-01T00:00:10.123Z', + lte: '2021-01-23T12:00:50.321Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + sort: [ + { + timefield: { + order: 'desc', + }, + }, + ], + }, + }); + }); + + test('it uses track_total_hits if specified', () => { + query.track_total_hits = true; + expect(buildSortedEventsQuery(query)).toEqual({ + allowNoIndices: true, + index: ['index-name'], + size: 100, + ignoreUnavailable: true, + track_total_hits: true, + body: { + docvalue_fields: [ + { + field: 'timefield', + format: 'strict_date_optional_time', + }, + ], + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + range: { + timefield: { + gte: '2021-01-01T00:00:10.123Z', + lte: '2021-01-23T12:00:50.321Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + sort: [ + { + timefield: { + order: 'asc', + }, + }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts new file mode 100644 index 0000000000000..92425433bf814 --- /dev/null +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts @@ -0,0 +1,93 @@ +/* + * 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 { ESSearchBody, ESSearchRequest } from '../../../typings/elasticsearch'; +import { SortOrder } from '../../../typings/elasticsearch/aggregations'; + +type BuildSortedEventsQueryOpts = Pick<ESSearchBody, 'aggs' | 'track_total_hits'> & + Pick<Required<ESSearchRequest>, 'index' | 'size'>; + +export interface BuildSortedEventsQuery extends BuildSortedEventsQueryOpts { + filter: unknown; + from: string; + to: string; + sortOrder?: SortOrder | undefined; + searchAfterSortId: string | number | undefined; + timeField: string; +} + +export const buildSortedEventsQuery = ({ + aggs, + index, + from, + to, + filter, + size, + searchAfterSortId, + sortOrder, + timeField, + // eslint-disable-next-line @typescript-eslint/naming-convention + track_total_hits, +}: BuildSortedEventsQuery): ESSearchRequest => { + const sortField = timeField; + const docFields = [timeField].map((tstamp) => ({ + field: tstamp, + format: 'strict_date_optional_time', + })); + + const rangeFilter: unknown[] = [ + { + range: { + [timeField]: { + lte: to, + gte: from, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + const filterWithTime = [filter, { bool: { filter: rangeFilter } }]; + + const searchQuery = { + allowNoIndices: true, + index, + size, + ignoreUnavailable: true, + track_total_hits: track_total_hits ?? false, + body: { + docvalue_fields: docFields, + query: { + bool: { + filter: [ + ...filterWithTime, + { + match_all: {}, + }, + ], + }, + }, + ...(aggs ? { aggs } : {}), + sort: [ + { + [sortField]: { + order: sortOrder ?? 'asc', + }, + }, + ], + }, + }; + + if (searchAfterSortId) { + return { + ...searchQuery, + body: { + ...searchQuery.body, + search_after: [searchAfterSortId], + }, + }; + } + return searchQuery; +}; diff --git a/x-pack/plugins/stack_alerts/kibana.json b/x-pack/plugins/stack_alerts/kibana.json index 884d33ef669e5..80eb177f92024 100644 --- a/x-pack/plugins/stack_alerts/kibana.json +++ b/x-pack/plugins/stack_alerts/kibana.json @@ -5,5 +5,6 @@ "kibanaVersion": "kibana", "requiredPlugins": ["alerts", "features", "triggersActionsUi", "kibanaReact", "savedObjects", "data"], "configPath": ["xpack", "stack_alerts"], + "requiredBundles": ["esUiShared"], "ui": true } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx new file mode 100644 index 0000000000000..5dc7c9248135c --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { IndexSelectPopover } from './index_select_popover'; + +jest.mock('../../../../triggers_actions_ui/public', () => ({ + getIndexPatterns: () => { + return ['index1', 'index2']; + }, + firstFieldOption: () => { + return { text: 'Select a field', value: '' }; + }, + getTimeFieldOptions: () => { + return [ + { + text: '@timestamp', + value: '@timestamp', + }, + ]; + }, + getFields: () => { + return Promise.resolve([ + { + name: '@timestamp', + type: 'date', + }, + { + name: 'field', + type: 'text', + }, + ]); + }, + getIndexOptions: () => { + return Promise.resolve([ + { + label: 'indexOption', + options: [ + { + label: 'index1', + value: 'index1', + }, + { + label: 'index2', + value: 'index2', + }, + ], + }, + ]); + }, +})); + +describe('IndexSelectPopover', () => { + const props = { + index: [], + esFields: [], + timeField: undefined, + errors: { + index: [], + timeField: [], + }, + onIndexChange: jest.fn(), + onTimeFieldChange: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('renders closed popover initially and opens on click', async () => { + const wrapper = mountWithIntl(<IndexSelectPopover {...props} />); + + expect(wrapper.find('[data-test-subj="selectIndexExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdIndexesComboBox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdAlertTimeFieldSelect"]').exists()).toBeFalsy(); + + wrapper.find('[data-test-subj="selectIndexExpression"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="thresholdIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdAlertTimeFieldSelect"]').exists()).toBeTruthy(); + }); + + test('renders search input', async () => { + const wrapper = mountWithIntl(<IndexSelectPopover {...props} />); + + expect(wrapper.find('[data-test-subj="selectIndexExpression"]').exists()).toBeTruthy(); + wrapper.find('[data-test-subj="selectIndexExpression"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="thresholdIndexesComboBox"]').exists()).toBeTruthy(); + const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); + expect(indexSearchBoxValue.first().props().value).toEqual(''); + + const indexComboBox = wrapper.find('#indexSelectSearchBox'); + indexComboBox.first().simulate('click'); + const event = { target: { value: 'indexPattern1' } }; + indexComboBox.find('input').first().simulate('change', event); + + const updatedIndexSearchValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); + expect(updatedIndexSearchValue.first().props().value).toEqual('indexPattern1'); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx new file mode 100644 index 0000000000000..6fe61be024042 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx @@ -0,0 +1,239 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isString } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonIcon, + EuiComboBox, + EuiComboBoxOptionOption, + EuiExpression, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPopover, + EuiPopoverTitle, + EuiSelect, +} from '@elastic/eui'; +import { HttpSetup } from 'kibana/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { + firstFieldOption, + getFields, + getIndexOptions, + getIndexPatterns, + getTimeFieldOptions, + IErrorObject, +} from '../../../../triggers_actions_ui/public'; + +interface KibanaDeps { + http: HttpSetup; +} +interface Props { + index: string[]; + esFields: Array<{ + name: string; + type: string; + normalizedType: string; + searchable: boolean; + aggregatable: boolean; + }>; + timeField: string | undefined; + errors: IErrorObject; + onIndexChange: (indices: string[]) => void; + onTimeFieldChange: (timeField: string) => void; +} + +export const IndexSelectPopover: React.FunctionComponent<Props> = ({ + index, + esFields, + timeField, + errors, + onIndexChange, + onTimeFieldChange, +}) => { + const { http } = useKibana<KibanaDeps>().services; + + const [indexPopoverOpen, setIndexPopoverOpen] = useState(false); + const [indexOptions, setIndexOptions] = useState<EuiComboBoxOptionOption[]>([]); + const [indexPatterns, setIndexPatterns] = useState([]); + const [areIndicesLoading, setAreIndicesLoading] = useState<boolean>(false); + const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); + + useEffect(() => { + const indexPatternsFunction = async () => { + setIndexPatterns(await getIndexPatterns()); + }; + indexPatternsFunction(); + }, []); + + useEffect(() => { + const timeFields = getTimeFieldOptions(esFields); + setTimeFieldOptions([firstFieldOption, ...timeFields]); + }, [esFields]); + + const renderIndices = (indices: string[]) => { + const rows = indices.map((indexName: string, idx: number) => { + return ( + <p key={idx}> + {indexName} + {idx < indices.length - 1 ? ',' : null} + </p> + ); + }); + return <div>{rows}</div>; + }; + + const closeIndexPopover = () => { + setIndexPopoverOpen(false); + if (timeField === undefined) { + onTimeFieldChange(''); + } + }; + + return ( + <EuiPopover + id="indexPopover" + button={ + <EuiExpression + display="columns" + data-test-subj="selectIndexExpression" + description={i18n.translate('xpack.stackAlerts.components.ui.alertParams.indexLabel', { + defaultMessage: 'index', + })} + value={index && index.length > 0 ? renderIndices(index) : firstFieldOption.text} + isActive={indexPopoverOpen} + onClick={() => { + setIndexPopoverOpen(true); + }} + isInvalid={!(index && index.length > 0 && timeField !== '')} + /> + } + isOpen={indexPopoverOpen} + closePopover={closeIndexPopover} + ownFocus + anchorPosition="downLeft" + zIndex={8000} + display="block" + > + <div style={{ width: '450px' }}> + <EuiPopoverTitle> + <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexItem> + {i18n.translate('xpack.stackAlerts.components.ui.alertParams.indexButtonLabel', { + defaultMessage: 'index', + })} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonIcon + data-test-subj="closePopover" + iconType="cross" + color="danger" + aria-label={i18n.translate( + 'xpack.stackAlerts.components.ui.alertParams.closeIndexPopoverLabel', + { + defaultMessage: 'Close', + } + )} + onClick={closeIndexPopover} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPopoverTitle> + <EuiFormRow + id="indexSelectSearchBox" + fullWidth + label={ + <FormattedMessage + id="xpack.stackAlerts.components.ui.alertParams.indicesToQueryLabel" + defaultMessage="Indices to query" + /> + } + isInvalid={errors.index.length > 0 && index != null && index.length > 0} + error={errors.index} + helpText={ + <FormattedMessage + id="xpack.stackAlerts.components.ui.alertParams.howToBroadenSearchQueryDescription" + defaultMessage="Use * to broaden your query." + /> + } + > + <EuiComboBox + fullWidth + async + isLoading={areIndicesLoading} + isInvalid={errors.index.length > 0 && index != null && index.length > 0} + noSuggestions={!indexOptions.length} + options={indexOptions} + data-test-subj="thresholdIndexesComboBox" + selectedOptions={(index || []).map((anIndex: string) => { + return { + label: anIndex, + value: anIndex, + }; + })} + onChange={async (selected: EuiComboBoxOptionOption[]) => { + const selectedIndices = selected + .map((aSelected) => aSelected.value) + .filter<string>(isString); + onIndexChange(selectedIndices); + + // reset time field if indices have been reset + if (selectedIndices.length === 0) { + setTimeFieldOptions([firstFieldOption]); + } else { + const currentEsFields = await getFields(http!, selectedIndices); + const timeFields = getTimeFieldOptions(currentEsFields); + setTimeFieldOptions([firstFieldOption, ...timeFields]); + } + }} + onSearchChange={async (search) => { + setAreIndicesLoading(true); + setIndexOptions(await getIndexOptions(http!, search, indexPatterns)); + setAreIndicesLoading(false); + }} + onBlur={() => { + if (!index) { + onIndexChange([]); + } + }} + /> + </EuiFormRow> + <EuiFormRow + id="thresholdTimeField" + fullWidth + label={ + <FormattedMessage + id="xpack.stackAlerts.components.ui.alertParams.timeFieldLabel" + defaultMessage="Time field" + /> + } + isInvalid={errors.timeField.length > 0 && timeField !== undefined} + error={errors.timeField} + > + <EuiSelect + options={timeFieldOptions} + isInvalid={errors.timeField.length > 0 && timeField !== undefined} + fullWidth + name="thresholdTimeField" + data-test-subj="thresholdAlertTimeFieldSelect" + value={timeField || ''} + onChange={(e) => { + onTimeFieldChange(e.target.value); + }} + onBlur={() => { + if (timeField === undefined) { + onTimeFieldChange(''); + } + }} + /> + </EuiFormRow> + </div> + </EuiPopover> + ); +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx new file mode 100644 index 0000000000000..96a45da3d0808 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx @@ -0,0 +1,235 @@ +/* + * 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 'brace'; +import { of } from 'rxjs'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import EsQueryAlertTypeExpression from './expression'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { + DataPublicPluginStart, + IKibanaSearchResponse, + ISearchStart, +} from 'src/plugins/data/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { EsQueryAlertParams } from './types'; + +jest.mock('../../../../../../src/plugins/kibana_react/public'); +jest.mock('../../../../../../src/plugins/es_ui_shared/public'); +jest.mock('../../../../../../src/plugins/es_ui_shared/public', () => ({ + XJson: { + useXJsonMode: jest.fn().mockReturnValue({ + convertToJson: jest.fn(), + setXJson: jest.fn(), + xJson: jest.fn(), + }), + }, +})); +jest.mock(''); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiCodeEditor, which uses React Ace under the hood + // eslint-disable-next-line @typescript-eslint/no-explicit-any + EuiCodeEditor: (props: any) => ( + <input + data-test-subj="mockCodeEditor" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onChange={(syntheticEvent: any) => { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); +jest.mock('../../../../triggers_actions_ui/public', () => { + const original = jest.requireActual('../../../../triggers_actions_ui/public'); + return { + ...original, + getIndexPatterns: () => { + return ['index1', 'index2']; + }, + firstFieldOption: () => { + return { text: 'Select a field', value: '' }; + }, + getTimeFieldOptions: () => { + return [ + { + text: '@timestamp', + value: '@timestamp', + }, + ]; + }, + getFields: () => { + return Promise.resolve([ + { + name: '@timestamp', + type: 'date', + }, + { + name: 'field', + type: 'text', + }, + ]); + }, + getIndexOptions: () => { + return Promise.resolve([ + { + label: 'indexOption', + options: [ + { + label: 'index1', + value: 'index1', + }, + { + label: 'index2', + value: 'index2', + }, + ], + }, + ]); + }, + }; +}); + +const createDataPluginMock = () => { + const dataMock = dataPluginMock.createStartContract() as DataPublicPluginStart & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + search: ISearchStart & { search: jest.MockedFunction<any> }; + }; + return dataMock; +}; + +const dataMock = createDataPluginMock(); +const chartsStartMock = chartPluginMock.createStartContract(); + +describe('EsQueryAlertTypeExpression', () => { + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + docLinks: { + ELASTIC_WEBSITE_URL: '', + DOC_LINK_VERSION: '', + }, + }, + }); + }); + + function getAlertParams(overrides = {}) { + return { + index: ['test-index'], + timeField: '@timestamp', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + ...overrides, + }; + } + async function setup(alertParams: EsQueryAlertParams) { + const errors = { + index: [], + esQuery: [], + timeField: [], + timeWindowSize: [], + }; + + const wrapper = mountWithIntl( + <EsQueryAlertTypeExpression + alertInterval="1m" + alertThrottle="1m" + alertParams={alertParams} + setAlertParams={() => {}} + setAlertProperty={() => {}} + errors={errors} + data={dataMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + /> + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + return wrapper; + } + + test('should render EsQueryAlertTypeExpression with expected components', async () => { + const wrapper = await setup(getAlertParams()); + expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="queryJsonEditor"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy(); + + const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); + expect(testQueryButton.exists()).toBeTruthy(); + expect(testQueryButton.prop('disabled')).toBe(false); + }); + + test('should render Test Query button disabled if alert params are invalid', async () => { + const wrapper = await setup(getAlertParams({ timeField: null })); + const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); + expect(testQueryButton.exists()).toBeTruthy(); + expect(testQueryButton.prop('disabled')).toBe(true); + }); + + test('should show success message if Test Query is successful', async () => { + const searchResponseMock$ = of<IKibanaSearchResponse>({ + rawResponse: { + hits: { + total: 1234, + }, + }, + }); + dataMock.search.search.mockImplementation(() => searchResponseMock$); + const wrapper = await setup(getAlertParams()); + const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); + + testQueryButton.simulate('click'); + expect(dataMock.search.search).toHaveBeenCalled(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy(); + expect(wrapper.find('EuiText[data-test-subj="testQuerySuccess"]').text()).toEqual( + `Query matched 1234 documents in the last 15s.` + ); + }); + + test('should show error message if Test Query is throws error', async () => { + dataMock.search.search.mockImplementation(() => { + throw new Error('What is this query'); + }); + const wrapper = await setup(getAlertParams()); + const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); + + testQueryButton.simulate('click'); + expect(dataMock.search.search).toHaveBeenCalled(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx new file mode 100644 index 0000000000000..bba0e30978305 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -0,0 +1,371 @@ +/* + * 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, Fragment, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import 'brace/theme/github'; +import { XJsonMode } from '@kbn/ace'; + +import { + EuiButtonEmpty, + EuiCodeEditor, + EuiSpacer, + EuiFormRow, + EuiCallOut, + EuiText, + EuiTitle, + EuiLink, +} from '@elastic/eui'; +import { DocLinksStart, HttpSetup } from 'kibana/public'; +import { XJson } from '../../../../../../src/plugins/es_ui_shared/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { + getFields, + COMPARATORS, + ThresholdExpression, + ForLastExpression, + AlertTypeParamsExpressionProps, +} from '../../../../triggers_actions_ui/public'; +import { validateExpression } from './validation'; +import { parseDuration } from '../../../../alerts/common'; +import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; +import { EsQueryAlertParams } from './types'; +import { IndexSelectPopover } from '../components/index_select_popover'; + +const DEFAULT_VALUES = { + THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, + QUERY: `{ + "query":{ + "match_all" : {} + } +}`, + TIME_WINDOW_SIZE: 5, + TIME_WINDOW_UNIT: 'm', + THRESHOLD: [1000], +}; + +const expressionFieldsWithValidation = [ + 'index', + 'esQuery', + 'timeField', + 'threshold0', + 'threshold1', + 'timeWindowSize', +]; + +const { useXJsonMode } = XJson; +const xJsonMode = new XJsonMode(); + +interface KibanaDeps { + http: HttpSetup; + docLinks: DocLinksStart; +} + +export const EsQueryAlertTypeExpression: React.FunctionComponent< + AlertTypeParamsExpressionProps<EsQueryAlertParams> +> = ({ alertParams, setAlertParams, setAlertProperty, errors, data }) => { + const { + index, + timeField, + esQuery, + thresholdComparator, + threshold, + timeWindowSize, + timeWindowUnit, + } = alertParams; + + const getDefaultParams = () => ({ + ...alertParams, + esQuery: esQuery ?? DEFAULT_VALUES.QUERY, + timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, + threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, + thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, + }); + + const { http, docLinks } = useKibana<KibanaDeps>().services; + + const [esFields, setEsFields] = useState< + Array<{ + name: string; + type: string; + normalizedType: string; + searchable: boolean; + aggregatable: boolean; + }> + >([]); + const { convertToJson, setXJson, xJson } = useXJsonMode(DEFAULT_VALUES.QUERY); + const [currentAlertParams, setCurrentAlertParams] = useState<EsQueryAlertParams>( + getDefaultParams() + ); + const [testQueryResult, setTestQueryResult] = useState<string | null>(null); + const [testQueryError, setTestQueryError] = useState<string | null>(null); + + const hasExpressionErrors = !!Object.keys(errors).find( + (errorKey) => + expressionFieldsWithValidation.includes(errorKey) && + errors[errorKey].length >= 1 && + alertParams[errorKey as keyof EsQueryAlertParams] !== undefined + ); + + const expressionErrorMessage = i18n.translate( + 'xpack.stackAlerts.esQuery.ui.alertParams.fixErrorInExpressionBelowValidationMessage', + { + defaultMessage: 'Expression contains errors.', + } + ); + + const setDefaultExpressionValues = async () => { + setAlertProperty('params', getDefaultParams()); + + setXJson(esQuery ?? DEFAULT_VALUES.QUERY); + + if (index && index.length > 0) { + await refreshEsFields(); + } + }; + + const setParam = (paramField: string, paramValue: unknown) => { + setCurrentAlertParams({ + ...currentAlertParams, + [paramField]: paramValue, + }); + setAlertParams(paramField, paramValue); + }; + + useEffect(() => { + setDefaultExpressionValues(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const refreshEsFields = async () => { + if (index) { + const currentEsFields = await getFields(http, index); + setEsFields(currentEsFields); + } + }; + + const hasValidationErrors = () => { + const { errors: validationErrors } = validateExpression(currentAlertParams); + return Object.keys(validationErrors).some( + (key) => validationErrors[key] && validationErrors[key].length + ); + }; + + const onTestQuery = async () => { + if (!hasValidationErrors()) { + setTestQueryError(null); + setTestQueryResult(null); + try { + const window = `${timeWindowSize}${timeWindowUnit}`; + const timeWindow = parseDuration(window); + const parsedQuery = JSON.parse(esQuery); + const now = Date.now(); + const { rawResponse } = await data.search + .search({ + params: buildSortedEventsQuery({ + index, + from: new Date(now - timeWindow).toISOString(), + to: new Date(now).toISOString(), + filter: parsedQuery.query, + size: 0, + searchAfterSortId: undefined, + timeField: timeField ? timeField : '', + track_total_hits: true, + }), + }) + .toPromise(); + + const hits = rawResponse.hits; + setTestQueryResult( + i18n.translate('xpack.stackAlerts.esQuery.ui.numQueryMatchesText', { + defaultMessage: 'Query matched {count} documents in the last {window}.', + values: { count: hits.total, window }, + }) + ); + } catch (err) { + const message = err?.body?.attributes?.error?.root_cause[0]?.reason || err?.body?.message; + setTestQueryError( + i18n.translate('xpack.stackAlerts.esQuery.ui.queryError', { + defaultMessage: 'Error testing query: {message}', + values: { message: message ? `${err.message}: ${message}` : err.message }, + }) + ); + } + } + }; + + return ( + <Fragment> + {hasExpressionErrors ? ( + <Fragment> + <EuiSpacer /> + <EuiCallOut color="danger" size="s" title={expressionErrorMessage} /> + <EuiSpacer /> + </Fragment> + ) : null} + <EuiTitle size="xs"> + <h5> + <FormattedMessage + id="xpack.stackAlerts.esQuery.ui.selectIndex" + defaultMessage="Select an index" + /> + </h5> + </EuiTitle> + <EuiSpacer size="s" /> + <IndexSelectPopover + index={index} + data-test-subj="indexSelectPopover" + esFields={esFields} + timeField={timeField} + errors={errors} + onIndexChange={async (indices: string[]) => { + setParam('index', indices); + + // reset expression fields if indices are deleted + if (indices.length === 0) { + setAlertProperty('params', { + ...alertParams, + index: indices, + esQuery: DEFAULT_VALUES.QUERY, + thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR, + timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT, + threshold: DEFAULT_VALUES.THRESHOLD, + timeField: '', + }); + } else { + await refreshEsFields(); + } + }} + onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)} + /> + <EuiSpacer /> + <EuiTitle size="xs"> + <h5> + <FormattedMessage + id="xpack.stackAlerts.esQuery.ui.queryPrompt" + defaultMessage="Define the ES query" + /> + </h5> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiFormRow + id="queryEditor" + fullWidth + label={ + <FormattedMessage + id="xpack.stackAlerts.esQuery.ui.queryPrompt.label" + defaultMessage="ES query" + /> + } + isInvalid={errors.esQuery.length > 0} + error={errors.esQuery} + helpText={ + <EuiLink + href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/query-dsl.html`} + target="_blank" + > + <FormattedMessage + id="xpack.stackAlerts.esQuery.ui.queryPrompt.help" + defaultMessage="ES Query DSL documentation" + /> + </EuiLink> + } + > + <EuiCodeEditor + mode={xJsonMode} + width="100%" + height="200px" + theme="github" + data-test-subj="queryJsonEditor" + aria-label={i18n.translate('xpack.stackAlerts.esQuery.ui.queryEditor', { + defaultMessage: 'ES query editor', + })} + value={xJson} + onChange={(xjson: string) => { + setXJson(xjson); + setParam('esQuery', convertToJson(xjson)); + }} + /> + </EuiFormRow> + <EuiFormRow> + <EuiButtonEmpty + data-test-subj="testQuery" + color={'primary'} + iconSide={'left'} + flush={'left'} + iconType={'play'} + disabled={hasValidationErrors()} + onClick={onTestQuery} + > + <FormattedMessage + id="xpack.stackAlerts.esQuery.ui.testQuery" + defaultMessage="Test query" + /> + </EuiButtonEmpty> + </EuiFormRow> + {testQueryResult && ( + <EuiFormRow> + <EuiText data-test-subj="testQuerySuccess" color="subdued" size="s"> + <p>{testQueryResult}</p> + </EuiText> + </EuiFormRow> + )} + {testQueryError && ( + <EuiFormRow> + <EuiText data-test-subj="testQueryError" color="danger" size="s"> + <p>{testQueryError}</p> + </EuiText> + </EuiFormRow> + )} + <EuiSpacer /> + <EuiTitle size="xs"> + <h5> + <FormattedMessage + id="xpack.stackAlerts.esQuery.ui.conditionPrompt" + defaultMessage="When number of matches" + /> + </h5> + </EuiTitle> + <EuiSpacer size="s" /> + <ThresholdExpression + data-test-subj="thresholdExpression" + thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR} + threshold={threshold ?? DEFAULT_VALUES.THRESHOLD} + errors={errors} + display="fullWidth" + popupPosition={'upLeft'} + onChangeSelectedThreshold={(selectedThresholds) => + setParam('threshold', selectedThresholds) + } + onChangeSelectedThresholdComparator={(selectedThresholdComparator) => + setParam('thresholdComparator', selectedThresholdComparator) + } + /> + <ForLastExpression + data-test-subj="forLastExpression" + popupPosition={'upLeft'} + timeWindowSize={timeWindowSize} + timeWindowUnit={timeWindowUnit} + display="fullWidth" + errors={errors} + onChangeWindowSize={(selectedWindowSize: number | undefined) => + setParam('timeWindowSize', selectedWindowSize) + } + onChangeWindowUnit={(selectedWindowUnit: string) => + setParam('timeWindowUnit', selectedWindowUnit) + } + /> + <EuiSpacer /> + </Fragment> + ); +}; + +// eslint-disable-next-line import/no-default-export +export { EsQueryAlertTypeExpression as default }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts new file mode 100644 index 0000000000000..62b343ffd6d2f --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { validateExpression } from './validation'; +import { EsQueryAlertParams } from './types'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; + +export function getAlertType(): AlertTypeModel<EsQueryAlertParams> { + return { + id: '.es-query', + description: i18n.translate('xpack.stackAlerts.esQuery.ui.alertType.descriptionText', { + defaultMessage: 'Alert on matches against an ES query.', + }), + iconClass: 'logoElastic', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/alert-types.html#alert-type-es-query`; + }, + alertParamsExpression: lazy(() => import('./expression')), + validate: validateExpression, + defaultActionMessage: i18n.translate( + 'xpack.stackAlerts.esQuery.ui.alertType.defaultActionMessage', + { + defaultMessage: `ES query alert '\\{\\{alertName\\}\\}' is active: + +- Value: \\{\\{context.value\\}\\} +- Conditions Met: \\{\\{context.conditions\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\} +- Timestamp: \\{\\{context.date\\}\\}`, + } + ), + requiresAppContext: false, + }; +} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts new file mode 100644 index 0000000000000..803c4bde873b4 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.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 { AlertTypeParams } from '../../../../alerts/common'; + +export interface Comparator { + text: string; + value: string; + requiredValues: number; +} + +export interface EsQueryAlertParams extends AlertTypeParams { + index: string[]; + timeField?: string; + esQuery: string; + thresholdComparator?: string; + threshold: number[]; + timeWindowSize: number; + timeWindowUnit: string; +} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts new file mode 100644 index 0000000000000..15aff9c9a6495 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { EsQueryAlertParams } from './types'; +import { validateExpression } from './validation'; + +describe('expression params validation', () => { + test('if index property is invalid should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: [], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + }; + expect(validateExpression(initialParams).errors.index.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.index[0]).toBe('Index is required.'); + }); + + test('if timeField property is not defined should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + }; + expect(validateExpression(initialParams).errors.timeField.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.timeField[0]).toBe('Time field is required.'); + }); + + test('if esQuery property is invalid JSON should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + }; + expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.esQuery[0]).toBe('Query must be valid JSON.'); + }); + + test('if esQuery property is invalid should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"aggs\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + }; + expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.esQuery[0]).toBe(`Query field is required.`); + }); + + test('if threshold0 property is not set should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + threshold: [], + timeWindowSize: 1, + timeWindowUnit: 's', + thresholdComparator: '<', + }; + expect(validateExpression(initialParams).errors.threshold0.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.threshold0[0]).toBe('Threshold 0 is required.'); + }); + + test('if threshold1 property is needed by thresholdComparator but not set should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + threshold: [1], + timeWindowSize: 1, + timeWindowUnit: 's', + thresholdComparator: 'between', + }; + expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.threshold1[0]).toBe('Threshold 1 is required.'); + }); + + test('if threshold0 property greater than threshold1 property should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + threshold: [10, 1], + timeWindowSize: 1, + timeWindowUnit: 's', + thresholdComparator: 'between', + }; + expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.threshold1[0]).toBe( + 'Threshold 1 must be > Threshold 0.' + ); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts new file mode 100644 index 0000000000000..d54e24e21d61e --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.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 { i18n } from '@kbn/i18n'; +import { EsQueryAlertParams } from './types'; +import { ValidationResult, builtInComparators } from '../../../../triggers_actions_ui/public'; + +export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => { + const { index, timeField, esQuery, threshold, timeWindowSize, thresholdComparator } = alertParams; + const validationResult = { errors: {} }; + const errors = { + index: new Array<string>(), + timeField: new Array<string>(), + esQuery: new Array<string>(), + threshold0: new Array<string>(), + threshold1: new Array<string>(), + thresholdComparator: new Array<string>(), + timeWindowSize: new Array<string>(), + }; + validationResult.errors = errors; + if (!index || index.length === 0) { + errors.index.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredIndexText', { + defaultMessage: 'Index is required.', + }) + ); + } + if (!timeField) { + errors.timeField.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredTimeFieldText', { + defaultMessage: 'Time field is required.', + }) + ); + } + if (!esQuery) { + errors.esQuery.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredQueryText', { + defaultMessage: 'ES query is required.', + }) + ); + } else { + try { + const parsedQuery = JSON.parse(esQuery); + if (!parsedQuery.query) { + errors.esQuery.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredEsQueryText', { + defaultMessage: `Query field is required.`, + }) + ); + } + } catch (err) { + errors.esQuery.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.jsonQueryText', { + defaultMessage: 'Query must be valid JSON.', + }) + ); + } + } + if (!threshold || threshold.length === 0 || threshold[0] === undefined) { + errors.threshold0.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredThreshold0Text', { + defaultMessage: 'Threshold 0 is required.', + }) + ); + } + if ( + thresholdComparator && + builtInComparators[thresholdComparator].requiredValues > 1 && + (!threshold || + threshold[1] === undefined || + (threshold && threshold.length < builtInComparators[thresholdComparator!].requiredValues)) + ) { + errors.threshold1.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredThreshold1Text', { + defaultMessage: 'Threshold 1 is required.', + }) + ); + } + if (threshold && threshold.length === 2 && threshold[0] > threshold[1]) { + errors.threshold1.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.greaterThenThreshold0Text', { + defaultMessage: 'Threshold 1 must be > Threshold 0.', + }) + ); + } + if (!timeWindowSize) { + errors.timeWindowSize.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredTimeWindowSizeText', { + defaultMessage: 'Time window size is required.', + }) + ); + } + return validationResult; +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/index.ts index 1a9710eb08eb0..654bf0a424f09 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/index.ts @@ -7,6 +7,7 @@ import { getAlertType as getGeoThresholdAlertType } from './geo_threshold'; import { getAlertType as getGeoContainmentAlertType } from './geo_containment'; import { getAlertType as getThresholdAlertType } from './threshold'; +import { getAlertType as getEsQueryAlertType } from './es_query'; import { Config } from '../../common'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; @@ -22,4 +23,5 @@ export function registerAlertTypes({ alertTypeRegistry.register(getGeoContainmentAlertType()); } alertTypeRegistry.register(getThresholdAlertType()); + alertTypeRegistry.register(getEsQueryAlertType()); } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index 8348a797972ae..00c170e291504 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -7,33 +7,13 @@ import React, { useState, Fragment, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiExpression, - EuiPopover, - EuiPopoverTitle, - EuiSelect, - EuiSpacer, - EuiComboBox, - EuiComboBoxOptionOption, - EuiFormRow, - EuiCallOut, - EuiEmptyPrompt, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { EuiButtonIcon } from '@elastic/eui'; +import { EuiSpacer, EuiCallOut, EuiEmptyPrompt, EuiText, EuiTitle } from '@elastic/eui'; import { HttpSetup } from 'kibana/public'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { - firstFieldOption, - getIndexPatterns, - getIndexOptions, getFields, COMPARATORS, builtInComparators, - getTimeFieldOptions, OfExpression, ThresholdExpression, ForLastExpression, @@ -45,6 +25,7 @@ import { import { ThresholdVisualization } from './visualization'; import { IndexThresholdAlertParams } from './types'; import './expression.scss'; +import { IndexSelectPopover } from '../components/index_select_popover'; const DEFAULT_VALUES = { AGGREGATION_TYPE: 'count', @@ -101,12 +82,15 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< const indexArray = indexParamToArray(index); const { http } = useKibana<KibanaDeps>().services; - const [indexPopoverOpen, setIndexPopoverOpen] = useState(false); - const [indexPatterns, setIndexPatterns] = useState([]); - const [esFields, setEsFields] = useState<unknown[]>([]); - const [indexOptions, setIndexOptions] = useState<EuiComboBoxOptionOption[]>([]); - const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); - const [isIndiciesLoading, setIsIndiciesLoading] = useState<boolean>(false); + const [esFields, setEsFields] = useState< + Array<{ + name: string; + type: string; + normalizedType: string; + searchable: boolean; + aggregatable: boolean; + }> + >([]); const hasExpressionErrors = !!Object.keys(errors).find( (errorKey) => @@ -139,153 +123,22 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< }); if (indexArray.length > 0) { - const currentEsFields = await getFields(http, indexArray); - const timeFields = getTimeFieldOptions(currentEsFields); - - setEsFields(currentEsFields); - setTimeFieldOptions([firstFieldOption, ...timeFields]); + await refreshEsFields(); } }; - const closeIndexPopover = () => { - setIndexPopoverOpen(false); - if (timeField === undefined) { - setAlertParams('timeField', ''); + const refreshEsFields = async () => { + if (indexArray.length > 0) { + const currentEsFields = await getFields(http, indexArray); + setEsFields(currentEsFields); } }; - useEffect(() => { - const indexPatternsFunction = async () => { - setIndexPatterns(await getIndexPatterns()); - }; - indexPatternsFunction(); - }, []); - useEffect(() => { setDefaultExpressionValues(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const indexPopover = ( - <Fragment> - <EuiFormRow - id="indexSelectSearchBox" - fullWidth - label={ - <FormattedMessage - id="xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel" - defaultMessage="Indices to query" - /> - } - isInvalid={errors.index.length > 0 && indexArray.length > 0} - error={errors.index} - helpText={ - <FormattedMessage - id="xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription" - defaultMessage="Use * to broaden your query." - /> - } - > - <EuiComboBox - fullWidth - async - isLoading={isIndiciesLoading} - isInvalid={errors.index.length > 0 && indexArray.length > 0} - noSuggestions={!indexOptions.length} - options={indexOptions} - data-test-subj="thresholdIndexesComboBox" - selectedOptions={indexArray.map((anIndex: string) => { - return { - label: anIndex, - value: anIndex, - }; - })} - onChange={async (selected: EuiComboBoxOptionOption[]) => { - const indicies: string[] = selected - .map((aSelected) => aSelected.value) - .filter<string>(isString); - setAlertParams('index', indicies); - const indices = selected.map((s) => s.value as string); - - // reset time field and expression fields if indices are deleted - if (indices.length === 0) { - setTimeFieldOptions([firstFieldOption]); - setAlertProperty('params', { - ...alertParams, - index: indices, - aggType: DEFAULT_VALUES.AGGREGATION_TYPE, - termSize: DEFAULT_VALUES.TERM_SIZE, - thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR, - timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE, - timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT, - groupBy: DEFAULT_VALUES.GROUP_BY, - threshold: DEFAULT_VALUES.THRESHOLD, - timeField: '', - }); - return; - } - const currentEsFields = await getFields(http!, indices); - const timeFields = getTimeFieldOptions(currentEsFields); - - setEsFields(currentEsFields); - setTimeFieldOptions([firstFieldOption, ...timeFields]); - }} - onSearchChange={async (search) => { - setIsIndiciesLoading(true); - setIndexOptions(await getIndexOptions(http!, search, indexPatterns)); - setIsIndiciesLoading(false); - }} - onBlur={() => { - if (!index) { - setAlertParams('index', []); - } - }} - /> - </EuiFormRow> - <EuiFormRow - id="thresholdTimeField" - fullWidth - label={ - <FormattedMessage - id="xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel" - defaultMessage="Time field" - /> - } - isInvalid={errors.timeField.length > 0 && timeField !== undefined} - error={errors.timeField} - > - <EuiSelect - options={timeFieldOptions} - isInvalid={errors.timeField.length > 0 && timeField !== undefined} - fullWidth - name="thresholdTimeField" - data-test-subj="thresholdAlertTimeFieldSelect" - value={timeField || ''} - onChange={(e) => { - setAlertParams('timeField', e.target.value); - }} - onBlur={() => { - if (timeField === undefined) { - setAlertParams('timeField', ''); - } - }} - /> - </EuiFormRow> - </Fragment> - ); - - const renderIndices = (indices: string[]) => { - const rows = indices.map((s: string, i: number) => { - return ( - <p key={i}> - {s} - {i < indices.length - 1 ? ',' : null} - </p> - ); - }); - return <div>{rows}</div>; - }; - return ( <Fragment> {hasExpressionErrors ? ( @@ -304,58 +157,36 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< </h5> </EuiTitle> <EuiSpacer size="s" /> - <EuiPopover - id="indexPopover" - button={ - <EuiExpression - display="columns" - data-test-subj="selectIndexExpression" - description={i18n.translate('xpack.stackAlerts.threshold.ui.alertParams.indexLabel', { - defaultMessage: 'index', - })} - value={indexArray.length > 0 ? renderIndices(indexArray) : firstFieldOption.text} - isActive={indexPopoverOpen} - onClick={() => { - setIndexPopoverOpen(true); - }} - isInvalid={!(indexArray.length > 0 && timeField !== '')} - /> - } - isOpen={indexPopoverOpen} - closePopover={closeIndexPopover} - ownFocus - anchorPosition="downLeft" - zIndex={8000} - display="block" - > - <div style={{ width: '450px' }}> - <EuiPopoverTitle> - <EuiFlexGroup alignItems="center" gutterSize="s"> - <EuiFlexItem> - {i18n.translate('xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel', { - defaultMessage: 'index', - })} - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonIcon - data-test-subj="closePopover" - iconType="cross" - color="danger" - aria-label={i18n.translate( - 'xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel', - { - defaultMessage: 'Close', - } - )} - onClick={closeIndexPopover} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPopoverTitle> + <IndexSelectPopover + index={indexArray} + esFields={esFields} + timeField={timeField} + errors={errors} + onIndexChange={async (indices: string[]) => { + setAlertParams('index', indices); - {indexPopover} - </div> - </EuiPopover> + // reset expression fields if indices are deleted + if (indices.length === 0) { + setAlertProperty('params', { + ...alertParams, + index: indices, + aggType: DEFAULT_VALUES.AGGREGATION_TYPE, + termSize: DEFAULT_VALUES.TERM_SIZE, + thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR, + timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT, + groupBy: DEFAULT_VALUES.GROUP_BY, + threshold: DEFAULT_VALUES.THRESHOLD, + timeField: '', + }); + } else { + await refreshEsFields(); + } + }} + onTimeFieldChange={(updatedTimeField: string) => + setAlertParams('timeField', updatedTimeField) + } + /> <WhenExpression display="fullWidth" aggType={aggType ?? DEFAULT_VALUES.AGGREGATION_TYPE} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts new file mode 100644 index 0000000000000..882580a00e951 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EsQueryAlertActionContext, addMessages } from './action_context'; +import { EsQueryAlertParamsSchema } from './alert_type_params'; + +describe('ActionContext', () => { + it('generates expected properties', async () => { + const params = EsQueryAlertParamsSchema.validate({ + index: ['[index]'], + timeField: '[timeField]', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [4], + }); + const base: EsQueryAlertActionContext = { + date: '2020-01-01T00:00:00.000Z', + value: 42, + conditions: 'count greater than 4', + hits: [], + }; + const context = addMessages({ name: '[alert-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"alert '[alert-name]' matched query"`); + expect(context.message).toEqual( + `alert '[alert-name]' is active: + +- Value: 42 +- Conditions Met: count greater than 4 over 5m +- Timestamp: 2020-01-01T00:00:00.000Z` + ); + }); + + it('generates expected properties if comparator is between', async () => { + const params = EsQueryAlertParamsSchema.validate({ + index: ['[index]'], + timeField: '[timeField]', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: 'between', + threshold: [4, 5], + }); + const base: EsQueryAlertActionContext = { + date: '2020-01-01T00:00:00.000Z', + value: 4, + conditions: 'count between 4 and 5', + hits: [], + }; + const context = addMessages({ name: '[alert-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"alert '[alert-name]' matched query"`); + expect(context.message).toEqual( + `alert '[alert-name]' is active: + +- Value: 4 +- Conditions Met: count between 4 and 5 over 5m +- Timestamp: 2020-01-01T00:00:00.000Z` + ); + }); +}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts new file mode 100644 index 0000000000000..67d0ac0df8ffe --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { AlertExecutorOptions, AlertInstanceContext } from '../../../../alerts/server'; +import { EsQueryAlertParams } from './alert_type_params'; +import { ESSearchHit } from '../../../../../typings/elasticsearch'; + +// alert type context provided to actions + +type AlertInfo = Pick<AlertExecutorOptions, 'name'>; + +export interface ActionContext extends EsQueryAlertActionContext { + // a short pre-constructed message which may be used in an action field + title: string; + // a longer pre-constructed message which may be used in an action field + message: string; +} + +export interface EsQueryAlertActionContext extends AlertInstanceContext { + // the date the alert was run as an ISO date + date: string; + // the value that met the threshold + value: number; + // threshold conditions + conditions: string; + // query matches + hits: ESSearchHit[]; +} + +export function addMessages( + alertInfo: AlertInfo, + baseContext: EsQueryAlertActionContext, + params: EsQueryAlertParams +): ActionContext { + const title = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle', { + defaultMessage: `alert '{name}' matched query`, + values: { + name: alertInfo.name, + }, + }); + + const window = `${params.timeWindowSize}${params.timeWindowUnit}`; + const message = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextMessageDescription', { + defaultMessage: `alert '{name}' is active: + +- Value: {value} +- Conditions Met: {conditions} over {window} +- Timestamp: {date}`, + values: { + name: alertInfo.name, + value: baseContext.value, + conditions: baseContext.conditions, + window, + date: baseContext.date, + }, + }); + + return { ...baseContext, title, message }; +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts new file mode 100644 index 0000000000000..c5f57a056b002 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -0,0 +1,103 @@ +/* + * 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 type { Writable } from '@kbn/utility-types'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { getAlertType } from './alert_type'; +import { EsQueryAlertParams } from './alert_type_params'; + +describe('alertType', () => { + const logger = loggingSystemMock.create().get(); + + const alertType = getAlertType(logger); + + it('alert type creation structure is the expected value', async () => { + expect(alertType.id).toBe('.es-query'); + expect(alertType.name).toBe('ES query'); + expect(alertType.actionGroups).toEqual([{ id: 'query matched', name: 'Query matched' }]); + + expect(alertType.actionVariables).toMatchInlineSnapshot(` + Object { + "context": Array [ + Object { + "description": "A message for the alert.", + "name": "message", + }, + Object { + "description": "A title for the alert.", + "name": "title", + }, + Object { + "description": "The date that the alert met the threshold condition.", + "name": "date", + }, + Object { + "description": "The value that met the threshold condition.", + "name": "value", + }, + Object { + "description": "The documents that met the threshold condition.", + "name": "hits", + }, + Object { + "description": "A string that describes the threshold condition.", + "name": "conditions", + }, + ], + "params": Array [ + Object { + "description": "The index the query was run against.", + "name": "index", + }, + Object { + "description": "The string representation of the ES query.", + "name": "esQuery", + }, + Object { + "description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", + "name": "threshold", + }, + Object { + "description": "A function to determine if the threshold has been met.", + "name": "thresholdComparator", + }, + ], + } + `); + }); + + it('validator succeeds with valid params', async () => { + const params: Partial<Writable<EsQueryAlertParams>> = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '<', + threshold: [0], + }; + + expect(alertType.validate?.params?.validate(params)).toBeTruthy(); + }); + + it('validator fails with invalid params - threshold', async () => { + const paramsSchema = alertType.validate?.params; + if (!paramsSchema) throw new Error('params validator not set'); + + const params: Partial<Writable<EsQueryAlertParams>> = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: 'between', + threshold: [0], + }; + + expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( + `"[threshold]: must have two elements for the \\"between\\" comparator"` + ); + }); +}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts new file mode 100644 index 0000000000000..b8190340c4d68 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -0,0 +1,307 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Logger } from 'src/core/server'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { AlertType, AlertExecutorOptions } from '../../types'; +import { ActionContext, EsQueryAlertActionContext, addMessages } from './action_context'; +import { + EsQueryAlertParams, + EsQueryAlertParamsSchema, + EsQueryAlertState, +} from './alert_type_params'; +import { STACK_ALERTS_FEATURE_ID } from '../../../common'; +import { ComparatorFns, getHumanReadableComparator } from '../lib'; +import { parseDuration } from '../../../../alerts/server'; +import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; +import { ESSearchHit } from '../../../../../typings/elasticsearch'; + +export const ES_QUERY_ID = '.es-query'; + +const DEFAULT_MAX_HITS_PER_EXECUTION = 1000; + +const ActionGroupId = 'query matched'; +const ConditionMetAlertInstanceId = 'query matched'; + +export function getAlertType( + logger: Logger +): AlertType<EsQueryAlertParams, EsQueryAlertState, {}, ActionContext, typeof ActionGroupId> { + const alertTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', { + defaultMessage: 'ES query', + }); + + const actionGroupName = i18n.translate('xpack.stackAlerts.esQuery.actionGroupThresholdMetTitle', { + defaultMessage: 'Query matched', + }); + + const actionVariableContextDateLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextDateLabel', + { + defaultMessage: 'The date that the alert met the threshold condition.', + } + ); + + const actionVariableContextValueLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextValueLabel', + { + defaultMessage: 'The value that met the threshold condition.', + } + ); + + const actionVariableContextHitsLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextHitsLabel', + { + defaultMessage: 'The documents that met the threshold condition.', + } + ); + + const actionVariableContextMessageLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextMessageLabel', + { + defaultMessage: 'A message for the alert.', + } + ); + + const actionVariableContextTitleLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextTitleLabel', + { + defaultMessage: 'A title for the alert.', + } + ); + + const actionVariableContextIndexLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextIndexLabel', + { + defaultMessage: 'The index the query was run against.', + } + ); + + const actionVariableContextQueryLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextQueryLabel', + { + defaultMessage: 'The string representation of the ES query.', + } + ); + + const actionVariableContextThresholdLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel', + { + defaultMessage: + "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", + } + ); + + const actionVariableContextThresholdComparatorLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextThresholdComparatorLabel', + { + defaultMessage: 'A function to determine if the threshold has been met.', + } + ); + + const actionVariableContextConditionsLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextConditionsLabel', + { + defaultMessage: 'A string that describes the threshold condition.', + } + ); + + return { + id: ES_QUERY_ID, + name: alertTypeName, + actionGroups: [{ id: ActionGroupId, name: actionGroupName }], + defaultActionGroupId: ActionGroupId, + validate: { + params: EsQueryAlertParamsSchema, + }, + actionVariables: { + context: [ + { name: 'message', description: actionVariableContextMessageLabel }, + { name: 'title', description: actionVariableContextTitleLabel }, + { name: 'date', description: actionVariableContextDateLabel }, + { name: 'value', description: actionVariableContextValueLabel }, + { name: 'hits', description: actionVariableContextHitsLabel }, + { name: 'conditions', description: actionVariableContextConditionsLabel }, + ], + params: [ + { name: 'index', description: actionVariableContextIndexLabel }, + { name: 'esQuery', description: actionVariableContextQueryLabel }, + { name: 'threshold', description: actionVariableContextThresholdLabel }, + { name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel }, + ], + }, + minimumLicenseRequired: 'basic', + executor, + producer: STACK_ALERTS_FEATURE_ID, + }; + + async function executor( + options: AlertExecutorOptions< + EsQueryAlertParams, + EsQueryAlertState, + {}, + ActionContext, + typeof ActionGroupId + > + ) { + const { alertId, name, services, params, state } = options; + const previousTimestamp = state.latestTimestamp; + + const callCluster = services.callCluster; + const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); + + const compareFn = ComparatorFns.get(params.thresholdComparator); + if (compareFn == null) { + throw new Error(getInvalidComparatorError(params.thresholdComparator)); + } + + // During each alert execution, we run the configured query, get a hit count + // (hits.total) and retrieve up to DEFAULT_MAX_HITS_PER_EXECUTION hits. We + // evaluate the threshold condition using the value of hits.total. If the threshold + // condition is met, the hits are counted toward the query match and we update + // the alert state with the timestamp of the latest hit. In the next execution + // of the alert, the latestTimestamp will be used to gate the query in order to + // avoid counting a document multiple times. + + let timestamp: string | undefined = previousTimestamp; + const filter = timestamp + ? { + bool: { + filter: [ + parsedQuery.query, + { + bool: { + must_not: [ + { bool: { filter: [{ range: { [params.timeField]: { lte: timestamp } } }] } }, + ], + }, + }, + ], + }, + } + : parsedQuery.query; + + const query = buildSortedEventsQuery({ + index: params.index, + from: dateStart, + to: dateEnd, + filter, + size: DEFAULT_MAX_HITS_PER_EXECUTION, + sortOrder: 'desc', + searchAfterSortId: undefined, + timeField: params.timeField, + track_total_hits: true, + }); + + logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}`); + + const searchResult: ESSearchResponse<unknown, {}> = await callCluster('search', query); + + if (searchResult.hits.hits.length > 0) { + const numMatches = searchResult.hits.total.value; + logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query has ${numMatches} matches`); + + // apply the alert condition + const conditionMet = compareFn(numMatches, params.threshold); + + if (conditionMet) { + const humanFn = i18n.translate( + 'xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', + { + defaultMessage: `Number of matching documents is {thresholdComparator} {threshold}`, + values: { + thresholdComparator: getHumanReadableComparator(params.thresholdComparator), + threshold: params.threshold.join(' and '), + }, + } + ); + + const baseContext: EsQueryAlertActionContext = { + date: new Date().toISOString(), + value: numMatches, + conditions: humanFn, + hits: searchResult.hits.hits, + }; + + const actionContext = addMessages(options, baseContext, params); + const alertInstance = options.services.alertInstanceFactory(ConditionMetAlertInstanceId); + alertInstance + // store the params we would need to recreate the query that led to this alert instance + .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) + .scheduleActions(ActionGroupId, actionContext); + + // update the timestamp based on the current search results + const firstHitWithSort = searchResult.hits.hits.find( + (hit: ESSearchHit) => hit.sort != null + ); + const lastTimestamp = firstHitWithSort?.sort; + if (lastTimestamp != null && lastTimestamp.length > 0) { + timestamp = lastTimestamp[0]; + } + } + } + + return { + latestTimestamp: timestamp, + }; + } +} + +function getInvalidComparatorError(comparator: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); +} + +function getInvalidWindowSizeError(windowValue: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', { + defaultMessage: 'invalid format for windowSize: "{windowValue}"', + values: { + windowValue, + }, + }); +} + +function getInvalidQueryError(query: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', { + defaultMessage: 'invalid query specified: "{query}" - query must be JSON', + values: { + query, + }, + }); +} + +function getSearchParams(queryParams: EsQueryAlertParams) { + const date = Date.now(); + const { esQuery, timeWindowSize, timeWindowUnit } = queryParams; + + let parsedQuery; + try { + parsedQuery = JSON.parse(esQuery); + } catch (err) { + throw new Error(getInvalidQueryError(esQuery)); + } + + if (parsedQuery && !parsedQuery.query) { + throw new Error(getInvalidQueryError(esQuery)); + } + + const window = `${timeWindowSize}${timeWindowUnit}`; + let timeWindow: number; + try { + timeWindow = parseDuration(window); + } catch (err) { + throw new Error(getInvalidWindowSizeError(window)); + } + + const dateStart = new Date(date - timeWindow).toISOString(); + const dateEnd = new Date(date).toISOString(); + + return { parsedQuery, dateStart, dateEnd }; +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts new file mode 100644 index 0000000000000..09ad66f248fee --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts @@ -0,0 +1,190 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import type { Writable } from '@kbn/utility-types'; +import { EsQueryAlertParamsSchema, EsQueryAlertParams } from './alert_type_params'; + +const DefaultParams: Writable<Partial<EsQueryAlertParams>> = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], +}; + +describe('alertType Params validate()', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let params: any; + beforeEach(() => { + params = { ...DefaultParams }; + }); + + it('passes for valid input', async () => { + expect(validate()).toBeTruthy(); + }); + + it('fails for invalid index', async () => { + delete params.index; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[index]: expected value of type [array] but got [undefined]"` + ); + + params.index = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[index]: expected value of type [array] but got [number]"` + ); + + params.index = 'index-name'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[index]: could not parse array value from json input"` + ); + + params.index = []; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[index]: array size is [0], but cannot be smaller than [1]"` + ); + + params.index = ['', 'a']; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[index.0]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + it('fails for invalid timeField', async () => { + delete params.timeField; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: expected value of type [string] but got [undefined]"` + ); + + params.timeField = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: expected value of type [string] but got [number]"` + ); + + params.timeField = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + it('fails for invalid esQuery', async () => { + delete params.esQuery; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[esQuery]: expected value of type [string] but got [undefined]"` + ); + + params.esQuery = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[esQuery]: expected value of type [string] but got [number]"` + ); + + params.esQuery = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[esQuery]: value has length [0] but it must have a minimum length of [1]."` + ); + + params.esQuery = '{\n "query":{\n "match_all" : {}\n }\n'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot(`"[esQuery]: must be valid JSON"`); + + params.esQuery = '{\n "aggs":{\n "match_all" : {}\n }\n}'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[esQuery]: must contain \\"query\\""` + ); + }); + + it('fails for invalid timeWindowSize', async () => { + delete params.timeWindowSize; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowSize]: expected value of type [number] but got [undefined]"` + ); + + params.timeWindowSize = 'foo'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowSize]: expected value of type [number] but got [string]"` + ); + + params.timeWindowSize = 0; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowSize]: Value must be equal to or greater than [1]."` + ); + }); + + it('fails for invalid timeWindowUnit', async () => { + delete params.timeWindowUnit; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowUnit]: expected value of type [string] but got [undefined]"` + ); + + params.timeWindowUnit = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowUnit]: expected value of type [string] but got [number]"` + ); + + params.timeWindowUnit = 'x'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowUnit]: invalid timeWindowUnit: \\"x\\""` + ); + }); + + it('fails for invalid threshold', async () => { + params.threshold = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[threshold]: expected value of type [array] but got [number]"` + ); + + params.threshold = 'x'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[threshold]: could not parse array value from json input"` + ); + + params.threshold = []; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[threshold]: array size is [0], but cannot be smaller than [1]"` + ); + + params.threshold = [1, 2, 3]; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[threshold]: array size is [3], but cannot be greater than [2]"` + ); + + params.threshold = ['foo']; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[threshold.0]: expected value of type [number] but got [string]"` + ); + }); + + it('fails for invalid thresholdComparator', async () => { + params.thresholdComparator = '[invalid-comparator]'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[thresholdComparator]: invalid thresholdComparator specified: [invalid-comparator]"` + ); + }); + + it('fails for invalid threshold length', async () => { + params.thresholdComparator = '<'; + params.threshold = [0, 1, 2]; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[threshold]: array size is [3], but cannot be greater than [2]"` + ); + + params.thresholdComparator = 'between'; + params.threshold = [0]; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[threshold]: must have two elements for the \\"between\\" comparator"` + ); + }); + + function onValidate(): () => void { + return () => validate(); + } + + function validate(): TypeOf<typeof EsQueryAlertParamsSchema> { + return EsQueryAlertParamsSchema.validate(params); + } +}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts new file mode 100644 index 0000000000000..2e7cd15d323e7 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -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 { i18n } from '@kbn/i18n'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { ComparatorFnNames } from '../lib'; +import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server'; +import { AlertTypeState } from '../../../../alerts/server'; + +// alert type parameters +export type EsQueryAlertParams = TypeOf<typeof EsQueryAlertParamsSchema>; +export interface EsQueryAlertState extends AlertTypeState { + latestTimestamp: string | undefined; +} + +export const EsQueryAlertParamsSchemaProperties = { + index: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), + timeField: schema.string({ minLength: 1 }), + esQuery: schema.string({ minLength: 1 }), + timeWindowSize: schema.number({ min: 1 }), + timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), + threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), + thresholdComparator: schema.string({ validate: validateComparator }), +}; + +export const EsQueryAlertParamsSchema = schema.object(EsQueryAlertParamsSchemaProperties, { + validate: validateParams, +}); + +const betweenComparators = new Set(['between', 'notBetween']); + +// using direct type not allowed, circular reference, so body is typed to any +function validateParams(anyParams: unknown): string | undefined { + const { + esQuery, + thresholdComparator, + threshold, + }: EsQueryAlertParams = anyParams as EsQueryAlertParams; + + if (betweenComparators.has(thresholdComparator) && threshold.length === 1) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage', { + defaultMessage: + '[threshold]: must have two elements for the "{thresholdComparator}" comparator', + values: { + thresholdComparator, + }, + }); + } + + try { + const parsedQuery = JSON.parse(esQuery); + + if (parsedQuery && !parsedQuery.query) { + return i18n.translate('xpack.stackAlerts.esQuery.missingEsQueryErrorMessage', { + defaultMessage: '[esQuery]: must contain "query"', + }); + } + } catch (err) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidEsQueryErrorMessage', { + defaultMessage: '[esQuery]: must be valid JSON', + }); + } +} + +export function validateComparator(comparator: string): string | undefined { + if (ComparatorFnNames.has(comparator)) return; + + return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts new file mode 100644 index 0000000000000..2fa2bed9d8419 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.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 { Logger } from 'src/core/server'; +import { AlertingSetup } from '../../types'; +import { getAlertType } from './alert_type'; + +interface RegisterParams { + logger: Logger; + alerts: AlertingSetup; +} + +export function register(params: RegisterParams) { + const { logger, alerts } = params; + alerts.registerType(getAlertType(logger)); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index.ts index 21a7ffc481323..2a343cb49a91b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index.ts @@ -9,7 +9,7 @@ import { AlertingSetup, StackAlertsStartDeps } from '../types'; import { register as registerIndexThreshold } from './index_threshold'; import { register as registerGeoThreshold } from './geo_threshold'; import { register as registerGeoContainment } from './geo_containment'; - +import { register as registerEsQuery } from './es_query'; interface RegisterAlertTypesParams { logger: Logger; data: Promise<StackAlertsStartDeps['triggersActionsUi']['data']>; @@ -20,4 +20,5 @@ export function registerBuiltInAlertTypes(params: RegisterAlertTypesParams) { registerIndexThreshold(params); registerGeoThreshold(params); registerGeoContainment(params); + registerEsQuery(params); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md index 9b0eb23950cc3..de5b57dfbffc6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md @@ -6,7 +6,7 @@ The index threshold alert type is designed to run an ES query over indices, aggregating field values from documents, comparing them to threshold values, and scheduling actions to run when the thresholds are met. -And example would be checking a monitoring index for percent cpu usage field +An example would be checking a monitoring index for percent cpu usage field values that are greater than some threshold, which could then be used to invoke an action (email, slack, etc) to notify interested parties when the threshold is exceeded. diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index 2366a872b855b..10dfabffddfcf 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -14,30 +14,10 @@ import { CoreQueryParamsSchemaProperties, TimeSeriesQuery, } from '../../../../triggers_actions_ui/server'; +import { ComparatorFns, getHumanReadableComparator } from '../lib'; export const ID = '.index-threshold'; - -enum Comparator { - GT = '>', - LT = '<', - GT_OR_EQ = '>=', - LT_OR_EQ = '<=', - BETWEEN = 'between', - NOT_BETWEEN = 'notBetween', -} - -const humanReadableComparators = new Map<string, string>([ - [Comparator.LT, 'less than'], - [Comparator.LT_OR_EQ, 'less than or equal to'], - [Comparator.GT_OR_EQ, 'greater than or equal to'], - [Comparator.GT, 'greater than'], - [Comparator.BETWEEN, 'between'], - [Comparator.NOT_BETWEEN, 'not between'], -]); - const ActionGroupId = 'threshold met'; -const ComparatorFns = getComparatorFns(); -export const ComparatorFnNames = new Set(ComparatorFns.keys()); export function getAlertType( logger: Logger, @@ -155,7 +135,14 @@ export function getAlertType( const compareFn = ComparatorFns.get(params.thresholdComparator); if (compareFn == null) { - throw new Error(getInvalidComparatorMessage(params.thresholdComparator)); + throw new Error( + i18n.translate('xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator: params.thresholdComparator, + }, + }) + ); } const callCluster = services.callCluster; @@ -210,40 +197,3 @@ export function getAlertType( } } } - -export function getInvalidComparatorMessage(comparator: string) { - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage', { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator, - }, - }); -} - -type ComparatorFn = (value: number, threshold: number[]) => boolean; - -function getComparatorFns(): Map<string, ComparatorFn> { - const fns: Record<string, ComparatorFn> = { - [Comparator.LT]: (value: number, threshold: number[]) => value < threshold[0], - [Comparator.LT_OR_EQ]: (value: number, threshold: number[]) => value <= threshold[0], - [Comparator.GT_OR_EQ]: (value: number, threshold: number[]) => value >= threshold[0], - [Comparator.GT]: (value: number, threshold: number[]) => value > threshold[0], - [Comparator.BETWEEN]: (value: number, threshold: number[]) => - value >= threshold[0] && value <= threshold[1], - [Comparator.NOT_BETWEEN]: (value: number, threshold: number[]) => - value < threshold[0] || value > threshold[1], - }; - - const result = new Map<string, ComparatorFn>(); - for (const key of Object.keys(fns)) { - result.set(key, fns[key]); - } - - return result; -} - -function getHumanReadableComparator(comparator: string) { - return humanReadableComparators.has(comparator) - ? humanReadableComparators.get(comparator) - : comparator; -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts index b51545770dd7b..2c83d5edc255a 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { ComparatorFnNames, getInvalidComparatorMessage } from './alert_type'; +import { ComparatorFnNames } from '../lib'; import { CoreQueryParamsSchemaProperties, validateCoreQueryBody, @@ -54,5 +54,10 @@ function validateParams(anyParams: unknown): string | undefined { export function validateComparator(comparator: string): string | undefined { if (ComparatorFnNames.has(comparator)) return; - return getInvalidComparatorMessage(comparator); + return i18n.translate('xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts new file mode 100644 index 0000000000000..cfa824d159686 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +enum Comparator { + GT = '>', + LT = '<', + GT_OR_EQ = '>=', + LT_OR_EQ = '<=', + BETWEEN = 'between', + NOT_BETWEEN = 'notBetween', +} + +const humanReadableComparators = new Map<string, string>([ + [Comparator.LT, 'less than'], + [Comparator.LT_OR_EQ, 'less than or equal to'], + [Comparator.GT_OR_EQ, 'greater than or equal to'], + [Comparator.GT, 'greater than'], + [Comparator.BETWEEN, 'between'], + [Comparator.NOT_BETWEEN, 'not between'], +]); + +export const ComparatorFns = getComparatorFns(); +export const ComparatorFnNames = new Set(ComparatorFns.keys()); + +type ComparatorFn = (value: number, threshold: number[]) => boolean; + +function getComparatorFns(): Map<string, ComparatorFn> { + const fns: Record<string, ComparatorFn> = { + [Comparator.LT]: (value: number, threshold: number[]) => value < threshold[0], + [Comparator.LT_OR_EQ]: (value: number, threshold: number[]) => value <= threshold[0], + [Comparator.GT_OR_EQ]: (value: number, threshold: number[]) => value >= threshold[0], + [Comparator.GT]: (value: number, threshold: number[]) => value > threshold[0], + [Comparator.BETWEEN]: (value: number, threshold: number[]) => + value >= threshold[0] && value <= threshold[1], + [Comparator.NOT_BETWEEN]: (value: number, threshold: number[]) => + value < threshold[0] || value > threshold[1], + }; + + const result = new Map<string, ComparatorFn>(); + for (const key of Object.keys(fns)) { + result.set(key, fns[key]); + } + + return result; +} + +export function getHumanReadableComparator(comparator: string) { + return humanReadableComparators.has(comparator) + ? humanReadableComparators.get(comparator) + : comparator; +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts new file mode 100644 index 0000000000000..7e40a7247a4c9 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts @@ -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 { ComparatorFns, ComparatorFnNames, getHumanReadableComparator } from './comparator_types'; diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index 7226c2175a769..8d69fad4afa46 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -27,7 +27,7 @@ describe('AlertingBuiltins Plugin', () => { const featuresSetup = featuresPluginMock.createSetup(); await plugin.setup(coreSetup, { alerts: alertingSetup, features: featuresSetup }); - expect(alertingSetup.registerType).toHaveBeenCalledTimes(3); + expect(alertingSetup.registerType).toHaveBeenCalledTimes(4); const indexThresholdArgs = alertingSetup.registerType.mock.calls[0][0]; const testedIndexThresholdArgs = { @@ -67,6 +67,25 @@ describe('AlertingBuiltins Plugin', () => { } `); + const esQueryArgs = alertingSetup.registerType.mock.calls[3][0]; + const testedEsQueryArgs = { + id: esQueryArgs.id, + name: esQueryArgs.name, + actionGroups: esQueryArgs.actionGroups, + }; + expect(testedEsQueryArgs).toMatchInlineSnapshot(` + Object { + "actionGroups": Array [ + Object { + "id": "query matched", + "name": "Query matched", + }, + ], + "id": ".es-query", + "name": "ES query", + } + `); + expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE); }); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 47267dc36673d..1c058245f04cd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20872,13 +20872,7 @@ "xpack.stackAlerts.indexThreshold.alertTypeTitle": "インデックスしきい値", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効な thresholdComparator が指定されました:{comparator}", "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]: 「{thresholdComparator}」比較子の場合には2つの要素が必要です", - "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "閉じる", "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", - "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", - "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "インデックス", - "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "インデックス", - "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "クエリを実行するインデックス", - "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "時間フィールド", "xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage": "アラート '\\{\\{alertName\\}\\}' はグループ '\\{\\{context.group\\}\\}' でアクティブです:\n\n- 値:\\{\\{context.value\\}\\}\n- 満たされた条件:\\{\\{context.conditions\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- タイムスタンプ:\\{\\{context.date\\}\\}", "xpack.stackAlerts.threshold.ui.alertType.descriptionText": "アグリゲーションされたクエリがしきい値に達したときにアラートを発行します。", "xpack.stackAlerts.threshold.ui.conditionPrompt": "条件を定義してください", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3f78abf14ae38..e7dbc0c161a37 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20920,13 +20920,7 @@ "xpack.stackAlerts.indexThreshold.alertTypeTitle": "索引阈值", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素", - "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "关闭", "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", - "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", - "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "索引", - "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "索引", - "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "要查询的索引", - "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "时间字段", "xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage": "组“\\{\\{context.group\\}\\}”的告警“\\{\\{alertName\\}\\}”处于活动状态:\n\n- 值:\\{\\{context.value\\}\\}\n- 满足的条件:\\{\\{context.conditions\\}\\} 超过 \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- 时间戳:\\{\\{context.date\\}\\}", "xpack.stackAlerts.threshold.ui.alertType.descriptionText": "聚合查询达到阈值时告警。", "xpack.stackAlerts.threshold.ui.conditionPrompt": "定义条件", diff --git a/x-pack/plugins/triggers_actions_ui/server/data/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/index.ts index 6ee2b4bb8a5fe..cc76af90bcde6 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/index.ts @@ -13,6 +13,7 @@ export { CoreQueryParams, CoreQueryParamsSchemaProperties, validateCoreQueryBody, + validateTimeWindowUnits, } from './lib'; // future enhancement: make these configurable? diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts index 096a928249fd5..a3fe2220a86fd 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts @@ -9,4 +9,5 @@ export { CoreQueryParams, CoreQueryParamsSchemaProperties, validateCoreQueryBody, + validateTimeWindowUnits, } from './core_query_types'; diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts index abd61f2bd3541..5e35293419b17 100644 --- a/x-pack/plugins/triggers_actions_ui/server/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/index.ts @@ -14,6 +14,7 @@ export { CoreQueryParams, CoreQueryParamsSchemaProperties, validateCoreQueryBody, + validateTimeWindowUnits, MAX_INTERVALS, MAX_GROUPS, DEFAULT_GROUPS, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts new file mode 100644 index 0000000000000..a1ae35a29bf23 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -0,0 +1,251 @@ +/* + * 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 { Spaces } from '../../../../scenarios'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { + ESTestIndexTool, + ES_TEST_INDEX_NAME, + getUrlPrefix, + ObjectRemover, +} from '../../../../../common/lib'; +import { createEsDocuments } from './create_test_data'; + +const ALERT_TYPE_ID = '.es-query'; +const ACTION_TYPE_ID = '.index'; +const ES_TEST_INDEX_SOURCE = 'builtin-alert:es-query'; +const ES_TEST_INDEX_REFERENCE = '-na-'; +const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-output`; + +const ALERT_INTERVALS_TO_WRITE = 5; +const ALERT_INTERVAL_SECONDS = 3; +const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000; +const ES_GROUPS_TO_WRITE = 3; + +// eslint-disable-next-line import/no-default-export +export default function alertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + const es = getService('legacyEs'); + const esTestIndexTool = new ESTestIndexTool(es, retry); + const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); + + describe('alert', async () => { + let endDate: string; + let actionId: string; + const objectRemover = new ObjectRemover(supertest); + + beforeEach(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + await esTestIndexToolOutput.destroy(); + await esTestIndexToolOutput.setup(); + + actionId = await createAction(supertest, objectRemover); + + // write documents in the future, figure out the end date + const endDateMillis = Date.now() + (ALERT_INTERVALS_TO_WRITE - 1) * ALERT_INTERVAL_MILLIS; + endDate = new Date(endDateMillis).toISOString(); + + // write documents from now to the future end date in groups + createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + }); + + afterEach(async () => { + await objectRemover.removeAll(); + await esTestIndexTool.destroy(); + await esTestIndexToolOutput.destroy(); + }); + + it('runs correctly: threshold on hit count < >', async () => { + await createAlert({ + name: 'never fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + thresholdComparator: '<', + threshold: [0], + }); + + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + thresholdComparator: '>', + threshold: [-1], + }); + + const docs = await waitForDocs(2); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`alert 'always fire' matched query`); + const messagePattern = /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + + // during the first execution, the latestTimestamp value should be empty + // since this alert always fires, the latestTimestamp value should be updated each execution + if (!i) { + expect(previousTimestamp).to.be.empty(); + } else { + expect(previousTimestamp).not.to.be.empty(); + } + } + }); + + it('runs correctly with query: threshold on hit count < >', async () => { + const rangeQuery = (rangeThreshold: number) => { + return { + query: { + bool: { + filter: [ + { + range: { + testedValue: { + gte: rangeThreshold, + }, + }, + }, + ], + }, + }, + }; + }; + + await createAlert({ + name: 'never fire', + esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)), + thresholdComparator: '>=', + threshold: [0], + }); + + await createAlert({ + name: 'fires once', + esQuery: JSON.stringify( + rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2)) + ), + thresholdComparator: '>=', + threshold: [0], + }); + + const docs = await waitForDocs(1); + for (const doc of docs) { + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('fires once'); + expect(title).to.be(`alert 'fires once' matched query`); + const messagePattern = /alert 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + expect(previousTimestamp).to.be.empty(); + } + }); + + async function createEsDocumentsInGroups(groups: number) { + await createEsDocuments( + es, + esTestIndexTool, + endDate, + ALERT_INTERVALS_TO_WRITE, + ALERT_INTERVAL_MILLIS, + groups + ); + } + + async function waitForDocs(count: number): Promise<any[]> { + return await esTestIndexToolOutput.waitForDocs( + ES_TEST_INDEX_SOURCE, + ES_TEST_INDEX_REFERENCE, + count + ); + } + + interface CreateAlertParams { + name: string; + timeField?: string; + esQuery: string; + thresholdComparator: string; + threshold: number[]; + timeWindowSize?: number; + } + + async function createAlert(params: CreateAlertParams): Promise<string> { + const action = { + id: actionId, + group: 'query matched', + params: { + documents: [ + { + source: ES_TEST_INDEX_SOURCE, + reference: ES_TEST_INDEX_REFERENCE, + params: { + name: '{{{alertName}}}', + value: '{{{context.value}}}', + title: '{{{context.title}}}', + message: '{{{context.message}}}', + }, + hits: '{{context.hits}}', + date: '{{{context.date}}}', + previousTimestamp: '{{{state.latestTimestamp}}}', + }, + ], + }, + }; + + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send({ + name: params.name, + consumer: 'alerts', + enabled: true, + alertTypeId: ALERT_TYPE_ID, + schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` }, + actions: [action], + params: { + index: [ES_TEST_INDEX_NAME], + timeField: params.timeField || 'date', + esQuery: params.esQuery, + timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5, + timeWindowUnit: 's', + thresholdComparator: params.thresholdComparator, + threshold: params.threshold, + }, + }) + .expect(200); + + const alertId = createdAlert.id; + objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); + + return alertId; + } + }); +} + +async function createAction(supertest: any, objectRemover: ObjectRemover): Promise<string> { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'index action for es query FT', + actionTypeId: ACTION_TYPE_ID, + config: { + index: ES_TEST_OUTPUT_INDEX_NAME, + }, + secrets: {}, + }) + .expect(200); + + const actionId = createdAction.id; + objectRemover.add(Spaces.space1.id, actionId, 'action', 'actions'); + + return actionId; +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/create_test_data.ts new file mode 100644 index 0000000000000..7299827a72253 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/create_test_data.ts @@ -0,0 +1,59 @@ +/* + * 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 { times } from 'lodash'; +import { v4 as uuid } from 'uuid'; +import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '../../../../../common/lib'; + +// default end date +export const END_DATE = '2020-01-01T00:00:00Z'; + +export const DOCUMENT_SOURCE = 'queryDataEndpointTests'; +export const DOCUMENT_REFERENCE = '-na-'; + +export async function createEsDocuments( + es: any, + esTestIndexTool: ESTestIndexTool, + endDate: string = END_DATE, + intervals: number = 1, + intervalMillis: number = 1000, + groups: number = 2 +) { + const endDateMillis = Date.parse(endDate) - intervalMillis / 2; + + let testedValue = 0; + times(intervals, (interval) => { + const date = endDateMillis - interval * intervalMillis; + + // don't need await on these, wait at the end of the function + times(groups, () => { + createEsDocument(es, date, testedValue++); + }); + }); + + const totalDocuments = intervals * groups; + await esTestIndexTool.waitForDocs(DOCUMENT_SOURCE, DOCUMENT_REFERENCE, totalDocuments); +} + +async function createEsDocument(es: any, epochMillis: number, testedValue: number) { + const document = { + source: DOCUMENT_SOURCE, + reference: DOCUMENT_REFERENCE, + date: new Date(epochMillis).toISOString(), + date_epoch_millis: epochMillis, + testedValue, + }; + + const response = await es.index({ + id: uuid(), + index: ES_TEST_INDEX_NAME, + body: document, + }); + + if (response.result !== 'created') { + throw new Error(`document not created: ${JSON.stringify(response)}`); + } +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts new file mode 100644 index 0000000000000..574f35e123fe8 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.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. + */ + +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: FtrProviderContext) { + describe('es_query', () => { + loadTestFile(require.resolve('./alert')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts index c0147cbedcdfe..f59ef6829f892 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts @@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function alertingTests({ loadTestFile }: FtrProviderContext) { describe('builtin alertTypes', () => { loadTestFile(require.resolve('./index_threshold')); + loadTestFile(require.resolve('./es_query')); }); } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 352652d9601dc..52e9422da2da4 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -29,10 +29,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - async function defineAlert(alertName: string) { + async function defineAlert(alertName: string, alertType?: string) { + alertType = alertType || '.index-threshold'; await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('alertNameInput', alertName); - await testSubjects.click('.index-threshold-SelectOption'); + await testSubjects.click(`${alertType}-SelectOption`); await testSubjects.click('selectIndexExpression'); const comboBox = await find.byCssSelector('#indexSelectSearchBox'); await comboBox.click(); @@ -217,5 +218,26 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('confirmAlertCloseModal > confirmModalCancelButton'); await testSubjects.missingOrFail('confirmAlertCloseModal'); }); + + it('should successfully test valid es_query alert', async () => { + const alertName = generateUniqueKey(); + await defineAlert(alertName, '.es-query'); + + // Valid query + await testSubjects.setValue('queryJsonEditor', '{"query":{"match_all":{}}}', { + clearWithKeyboard: true, + }); + await testSubjects.click('testQuery'); + await testSubjects.existOrFail('testQuerySuccess'); + await testSubjects.missingOrFail('testQueryError'); + + // Invalid query + await testSubjects.setValue('queryJsonEditor', '{"query":{"foo":{}}}', { + clearWithKeyboard: true, + }); + await testSubjects.click('testQuery'); + await testSubjects.missingOrFail('testQuerySuccess'); + await testSubjects.existOrFail('testQueryError'); + }); }); }; diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index 0328877aae8fe..fcb32fa6c0372 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -7,7 +7,7 @@ import { Unionize, UnionToIntersection } from 'utility-types'; import { ESSearchHit, MaybeReadonlyArray, ESSourceOptions, ESHitsOf } from '.'; -type SortOrder = 'asc' | 'desc'; +export type SortOrder = 'asc' | 'desc'; type SortInstruction = Record<string, SortOrder | { order: SortOrder }>; export type SortOptions = SortOrder | SortInstruction | SortInstruction[]; diff --git a/x-pack/typings/elasticsearch/index.d.ts b/x-pack/typings/elasticsearch/index.d.ts index 049e1e52c66d9..81443947855bc 100644 --- a/x-pack/typings/elasticsearch/index.d.ts +++ b/x-pack/typings/elasticsearch/index.d.ts @@ -70,6 +70,7 @@ export interface ESSearchBody { aggs?: AggregationInputMap; track_total_hits?: boolean | number; collapse?: CollapseQuery; + search_after?: Array<string | number>; _source?: ESSourceOptions; } From da1a4e947a6c49d82adf1cc66d26c376a54e4bd0 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet <nicolas.chaulet@elastic.co> Date: Fri, 29 Jan 2021 08:41:36 -0500 Subject: [PATCH 113/163] [Fleet] Install the Fleet Server package during setup (#89224) --- .../plugins/fleet/common/constants/agent.ts | 1 + x-pack/plugins/fleet/common/constants/epm.ts | 2 ++ .../plugins/fleet/common/constants/index.ts | 9 ++++++ .../server/collectors/agent_collectors.ts | 4 ++- x-pack/plugins/fleet/server/plugin.ts | 11 +++++-- .../server/services/fleet_server_migration.ts | 30 +++++++++++++++++-- x-pack/plugins/fleet/server/services/setup.ts | 29 ++++++++++++++++++ 7 files changed, 80 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/common/constants/agent.ts b/x-pack/plugins/fleet/common/constants/agent.ts index 8bfb32b5ed2b0..2e9161ca1c534 100644 --- a/x-pack/plugins/fleet/common/constants/agent.ts +++ b/x-pack/plugins/fleet/common/constants/agent.ts @@ -24,3 +24,4 @@ export const AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS = 1000; export const AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL = 5; export const AGENTS_INDEX = '.fleet-agents'; +export const AGENT_ACTIONS_INDEX = '.fleet-actions'; diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index 5ba4de914c724..ece669293fdff 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -8,6 +8,8 @@ export const ASSETS_SAVED_OBJECT_TYPE = 'epm-packages-assets'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; export const MAX_TIME_COMPLETE_INSTALL = 60000; +export const FLEET_SERVER_PACKAGE = 'fleet_server'; + export const requiredPackages = { System: 'system', Endpoint: 'endpoint', diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index bdc5714f7e2fe..409375f81d6fe 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -19,3 +19,12 @@ export * from './settings'; // for the actual setting to differ from the default. Can we retrieve the real // setting in the future? export const SO_SEARCH_LIMIT = 10000; + +export const FLEET_SERVER_INDICES = [ + '.fleet-actions', + '.fleet-agents', + '.fleet-enrollment-api-keys', + '.fleet-policies', + '.fleet-policies-leader', + '.fleet-servers', +]; diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index 8925f3386dfb8..fcead1bc89749 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -6,6 +6,7 @@ import { ElasticsearchClient, SavedObjectsClient } from 'kibana/server'; import * as AgentService from '../services/agents'; +import { isFleetServerSetup } from '../services/fleet_server_migration'; export interface AgentUsage { total: number; online: number; @@ -18,7 +19,7 @@ export const getAgentUsage = async ( esClient?: ElasticsearchClient ): Promise<AgentUsage> => { // TODO: unsure if this case is possible at all. - if (!soClient || !esClient) { + if (!soClient || !esClient || !(await isFleetServerSetup())) { return { total: 0, online: 0, @@ -26,6 +27,7 @@ export const getAgentUsage = async ( offline: 0, }; } + const { total, online, error, offline } = await AgentService.getAgentStatusForAgentPolicy( soClient, esClient diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 253b614dc228a..a0eb1547a3d63 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -81,7 +81,7 @@ import { agentCheckinState } from './services/agents/checkin/state'; import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation } from './services/epm/packages'; import { makeRouterEnforcingSuperuser } from './routes/security'; -import { runFleetServerMigration } from './services/fleet_server_migration'; +import { isFleetServerSetup } from './services/fleet_server_migration'; export interface FleetSetupDeps { licensing: LicensingPluginSetup; @@ -299,7 +299,14 @@ export class FleetPlugin if (fleetServerEnabled) { // We need licence to be initialized before using the SO service. await this.licensing$.pipe(first()).toPromise(); - await runFleetServerMigration(); + + const fleetSetup = await isFleetServerSetup(); + + if (!fleetSetup) { + this.logger?.warn( + 'Extra setup is needed to be able to use central management for agent, please visit the Fleet app in Kibana.' + ); + } } return { diff --git a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts index 1a50b5c9df767..44065a9346c5d 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts @@ -9,15 +9,39 @@ import { ENROLLMENT_API_KEYS_INDEX, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, FleetServerEnrollmentAPIKey, + FLEET_SERVER_PACKAGE, + FLEET_SERVER_INDICES, } from '../../common'; import { listEnrollmentApiKeys, getEnrollmentAPIKey } from './api_keys/enrollment_api_key_so'; import { appContextService } from './app_context'; +import { getInstallation } from './epm/packages'; + +export async function isFleetServerSetup() { + const pkgInstall = await getInstallation({ + savedObjectsClient: getInternalUserSOClient(), + pkgName: FLEET_SERVER_PACKAGE, + }); + + if (!pkgInstall) { + return false; + } + + const esClient = appContextService.getInternalUserESClient(); + + const exists = await Promise.all( + FLEET_SERVER_INDICES.map(async (index) => { + const res = await esClient.indices.exists({ + index, + }); + return res.statusCode !== 404; + }) + ); + + return exists.every((exist) => exist === true); +} export async function runFleetServerMigration() { - const logger = appContextService.getLogger(); - logger.info('Starting fleet server migration'); await migrateEnrollmentApiKeys(); - logger.info('Fleet server migration finished'); } function getInternalUserSOClient() { diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 0dcdfeb7b3801..ff96e2724c892 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -11,6 +11,7 @@ import { agentPolicyService } from './agent_policy'; import { outputService } from './output'; import { ensureInstalledDefaultPackages, + ensureInstalledPackage, ensurePackagesCompletedInstall, } from './epm/packages/install'; import { @@ -20,6 +21,8 @@ import { Installation, Output, DEFAULT_AGENT_POLICIES_PACKAGES, + FLEET_SERVER_PACKAGE, + FLEET_SERVER_INDICES, } from '../../common'; import { SO_SEARCH_LIMIT } from '../constants'; import { getPackageInfo } from './epm/packages'; @@ -29,6 +32,8 @@ import { settingsService } from '.'; import { awaitIfPending } from './setup_utils'; import { createDefaultSettings } from './settings'; import { ensureAgentActionPolicyChangeExists } from './agents'; +import { appContextService } from './app_context'; +import { runFleetServerMigration } from './fleet_server_migration'; const FLEET_ENROLL_USERNAME = 'fleet_enroll'; const FLEET_ENROLL_ROLE = 'fleet_enroll'; @@ -77,6 +82,15 @@ async function createSetupSideEffects( // By moving this outside of the Promise.all, the upgrade will occur first, and then we'll attempt to reinstall any // packages that are stuck in the installing state. await ensurePackagesCompletedInstall(soClient, callCluster); + if (appContextService.getConfig()?.agents.fleetServerEnabled) { + await ensureInstalledPackage({ + savedObjectsClient: soClient, + pkgName: FLEET_SERVER_PACKAGE, + callCluster, + }); + await ensureFleetServerIndicesCreated(esClient); + await runFleetServerMigration(); + } // If we just created the default policy, ensure default packages are added to it if (defaultAgentPolicyCreated) { @@ -144,6 +158,21 @@ async function updateFleetRoleIfExists(callCluster: CallESAsCurrentUser) { return putFleetRole(callCluster); } +async function ensureFleetServerIndicesCreated(esClient: ElasticsearchClient) { + await Promise.all( + FLEET_SERVER_INDICES.map(async (index) => { + const res = await esClient.indices.exists({ + index, + }); + if (res.statusCode === 404) { + await esClient.indices.create({ + index, + }); + } + }) + ); +} + async function putFleetRole(callCluster: CallESAsCurrentUser) { return callCluster('transport.request', { method: 'PUT', From e8e8f78b39c65ffd15fc88cb0549f9b8b2fc0bf2 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa <Uladzislau_Lasitsa@epam.com> Date: Fri, 29 Jan 2021 16:49:51 +0300 Subject: [PATCH 114/163] [Vega] Use mapbox instead of leaflet (#88605) * [WIP][Vega] Use mapbox instead of leaflet #78395 add MapServiceSettings class some work add tms_raster_layer add LayerParameters type clenup view.ts some cleeanup fix grammar some refactoring and add attribution control Some refactoring Add some validation for zoom settings and destroy handler Some refactoring some work fix bundle size Move getZoomSettings to the separate file update licence some work move logger to createViewConfig add throttling for updating vega layer * move EMSClient to a separate bundle * [unit testing] add tests for validation_helper.ts * [Bundle optimization] lazy loading of '@elastic/ems-client' only if user open map layer * [Map] fix cursor: crosshair -> auto * [unit testing] add tests for tms_raster_layer.test * [unit testing] add tests for vega_layer.ts * VSI related code was moved into a separate file / unit tests were added * Add functional test for vega map * [unit testing] add tests for map_service_setting.ts * Add unload in function test and delete some unneeded code from test * road_map -> road_map_desaturated * [unit testing] add more tests for map_service_settings.test.ts * Add unit tests for view.ts * Fix some remarks * Fix unit tests * remove tms_tile_layers enum * [unit testing] fix map_service_settings.test.ts * Fix unit test for view.ts * Fix some comments * Fix type check * Fix CI Co-authored-by: Alexey Antonov <alexwizp@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vega_visualization.test.js.snap | 2 - src/plugins/vis_type_vega/public/plugin.ts | 13 +- src/plugins/vis_type_vega/public/services.ts | 11 +- .../public/test_utils/vega_map_test.json | 2 +- .../public/vega_view/vega_base_view.d.ts | 11 +- .../public/vega_view/vega_base_view.js | 9 +- .../public/vega_view/vega_map_layer.js | 28 --- .../public/vega_view/vega_map_view.js | 168 --------------- .../vega_view/vega_map_view/constants.ts | 37 ++++ .../layers/index.ts} | 5 +- .../layers/tms_raster_layer.test.ts | 54 +++++ .../vega_map_view/layers/tms_raster_layer.ts | 37 ++++ .../vega_view/vega_map_view/layers/types.ts | 15 ++ .../vega_map_view/layers/vega_layer.test.ts | 65 ++++++ .../vega_map_view/layers/vega_layer.ts | 47 +++++ .../map_service_settings.test.ts | 105 ++++++++++ .../vega_map_view/map_service_settings.ts | 88 ++++++++ .../vega_view/vega_map_view/utils/index.ts | 10 + .../utils/validation_helper.test.ts | 112 ++++++++++ .../vega_map_view/utils/validation_helper.ts | 80 +++++++ .../vega_map_view/utils/vsi_helper.test.ts | 80 +++++++ .../vega_map_view/utils/vsi_helper.ts | 24 +++ .../vega_map_view/vega_map_view.scss | 7 + .../vega_view/vega_map_view/view.test.ts | 197 ++++++++++++++++++ .../public/vega_view/vega_map_view/view.ts | 181 ++++++++++++++++ .../public/vega_view/vega_view.js | 2 - .../public/vega_visualization.test.js | 30 --- .../public/vega_visualization.ts | 2 +- src/plugins/vis_type_vega/tsconfig.json | 4 +- .../fixtures/es_archiver/visualize/data.json | 21 ++ test/visual_regression/config.ts | 6 +- test/visual_regression/tests/vega/index.ts | 27 +++ .../tests/vega/vega_map_visualization.ts | 34 +++ 33 files changed, 1265 insertions(+), 249 deletions(-) delete mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js delete mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view.js create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts rename src/plugins/vis_type_vega/public/vega_view/{vega_map_view.d.ts => vega_map_view/layers/index.ts} (77%) create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.test.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.test.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/index.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.test.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.test.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts create mode 100644 src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts create mode 100644 test/visual_regression/tests/vega/index.ts create mode 100644 test/visual_regression/tests/vega/vega_map_visualization.ts diff --git a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap index 8b813ee06b1b3..c70c4406a34f2 100644 --- a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap +++ b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap @@ -1,7 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`VegaVisualizations VegaVisualization - basics should show vega blank rectangle on top of a map (vegamap) 1`] = `"<div class=\\"vgaVis__view leaflet-container leaflet-grab leaflet-touch-drag\\" style=\\"height: 100%; position: relative;\\" tabindex=\\"0\\"><div class=\\"leaflet-pane leaflet-map-pane\\" style=\\"left: 0px; top: 0px;\\"><div class=\\"leaflet-pane leaflet-tile-pane\\"></div><div class=\\"leaflet-pane leaflet-shadow-pane\\"></div><div class=\\"leaflet-pane leaflet-overlay-pane\\"><div class=\\"leaflet-vega-container\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\" style=\\"left: 0px; top: 0px; cursor: default;\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"0\\" height=\\"0\\" viewBox=\\"0 0 0 0\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(0,0)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-rect role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"rect mark container\\"><path d=\\"M0,0h0v0h0Z\\" fill=\\"#0f0\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div></div><div class=\\"leaflet-pane leaflet-marker-pane\\"></div><div class=\\"leaflet-pane leaflet-tooltip-pane\\"></div><div class=\\"leaflet-pane leaflet-popup-pane\\"></div></div><div class=\\"leaflet-control-container\\"><div class=\\"leaflet-top leaflet-left\\"><div class=\\"leaflet-control-zoom leaflet-bar leaflet-control\\"><a class=\\"leaflet-control-zoom-in\\" href=\\"#\\" title=\\"Zoom in\\" role=\\"button\\" aria-label=\\"Zoom in\\">+</a><a class=\\"leaflet-control-zoom-out\\" href=\\"#\\" title=\\"Zoom out\\" role=\\"button\\" aria-label=\\"Zoom out\\">−</a></div></div><div class=\\"leaflet-top leaflet-right\\"></div><div class=\\"leaflet-bottom leaflet-left\\"></div><div class=\\"leaflet-bottom leaflet-right\\"><div class=\\"leaflet-control-attribution leaflet-control\\"></div></div></div></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; - exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"<div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"512\\" height=\\"512\\" viewBox=\\"0 0 512 512\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(0,0)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h512v512h-512Z\\"></path><g><g class=\\"mark-group role-scope\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-area role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"area mark container\\"><path d=\\"M0,512C18.962962962962962,512,37.925925925925924,512,56.888888888888886,512C75.85185185185185,512,94.81481481481481,512,113.77777777777777,512C132.74074074074073,512,151.7037037037037,512,170.66666666666666,512C189.62962962962962,512,208.59259259259258,512,227.55555555555554,512C246.5185185185185,512,265.48148148148147,512,284.44444444444446,512C303.4074074074074,512,322.3703703703704,512,341.3333333333333,512C360.29629629629625,512,379.25925925925924,512,398.2222222222222,512C417.18518518518516,512,436.1481481481481,512,455.1111111111111,512C474.0740740740741,512,493.037037037037,512,512,512L512,355.2C493.037037037037,324.79999999999995,474.0740740740741,294.4,455.1111111111111,294.4C436.1481481481481,294.4,417.18518518518516,457.6,398.2222222222222,457.6C379.25925925925924,457.6,360.29629629629625,233.60000000000002,341.3333333333333,233.60000000000002C322.3703703703704,233.60000000000002,303.4074074074074,435.2,284.44444444444446,435.2C265.48148148148147,435.2,246.5185185185185,345.6,227.55555555555554,345.6C208.59259259259258,345.6,189.62962962962962,451.2,170.66666666666666,451.2C151.7037037037037,451.2,132.74074074074073,252.8,113.77777777777777,252.8C94.81481481481481,252.8,75.85185185185185,346.1333333333333,56.888888888888886,374.4C37.925925925925924,402.66666666666663,18.962962962962962,412.5333333333333,0,422.4Z\\" fill=\\"#54B399\\" fill-opacity=\\"1\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-area role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"area mark container\\"><path d=\\"M0,422.4C18.962962962962962,412.5333333333333,37.925925925925924,402.66666666666663,56.888888888888886,374.4C75.85185185185185,346.1333333333333,94.81481481481481,252.8,113.77777777777777,252.8C132.74074074074073,252.8,151.7037037037037,451.2,170.66666666666666,451.2C189.62962962962962,451.2,208.59259259259258,345.6,227.55555555555554,345.6C246.5185185185185,345.6,265.48148148148147,435.2,284.44444444444446,435.2C303.4074074074074,435.2,322.3703703703704,233.60000000000002,341.3333333333333,233.60000000000002C360.29629629629625,233.60000000000002,379.25925925925924,457.6,398.2222222222222,457.6C417.18518518518516,457.6,436.1481481481481,294.4,455.1111111111111,294.4C474.0740740740741,294.4,493.037037037037,324.79999999999995,512,355.2L512,307.2C493.037037037037,275.2,474.0740740740741,243.2,455.1111111111111,243.2C436.1481481481481,243.2,417.18518518518516,371.2,398.2222222222222,371.2C379.25925925925924,371.2,360.29629629629625,22.399999999999977,341.3333333333333,22.399999999999977C322.3703703703704,22.399999999999977,303.4074074074074,278.4,284.44444444444446,278.4C265.48148148148147,278.4,246.5185185185185,204.8,227.55555555555554,192C208.59259259259258,179.20000000000002,189.62962962962962,185.6,170.66666666666666,172.8C151.7037037037037,160.00000000000003,132.74074074074073,83.19999999999999,113.77777777777777,83.19999999999999C94.81481481481481,83.19999999999999,75.85185185185185,83.19999999999999,56.888888888888886,83.19999999999999C37.925925925925924,83.19999999999999,18.962962962962962,164.79999999999998,0,246.39999999999998Z\\" fill=\\"#6092C0\\" fill-opacity=\\"1\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 1`] = `"<ul class=\\"vgaVis__messages\\"><li class=\\"vgaVis__message vgaVis__message--warn\\"><pre class=\\"vgaVis__messageCode\\">\\"width\\" and \\"height\\" params are ignored because \\"autosize\\" is enabled. Set \\"autosize\\": \\"none\\" to disable</pre></li></ul><div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"0\\" height=\\"0\\" viewBox=\\"0 0 0 0\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(7,7)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0.5,0.5h0v0h0Z\\" fill=\\"transparent\\" stroke=\\"#ddd\\"></path><g><g class=\\"mark-line role-mark marks\\" role=\\"graphics-object\\" aria-roledescription=\\"line mark container\\"><path aria-label=\\"key: Dec 11, 2017; doc_count: 0\\" role=\\"graphics-symbol\\" aria-roledescription=\\"line mark\\" d=\\"M0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0\\" stroke=\\"#54B399\\" stroke-width=\\"2\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 376ef84de23c3..c18a7d4dfcfbd 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -17,17 +17,18 @@ import { setData, setInjectedVars, setUISettings, - setMapsLegacyConfig, setInjectedMetadata, + setMapServiceSettings, } from './services'; import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; -import { IServiceSettings } from '../../maps_legacy/public'; +import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { ConfigSchema } from '../config'; import { getVegaInspectorView } from './vega_inspector'; import { getVegaVisRenderer } from './vega_vis_renderer'; +import { MapServiceSettings } from './vega_view/vega_map_view/map_service_settings'; /** @internal */ export interface VegaVisualizationDependencies { @@ -44,7 +45,7 @@ export interface VegaPluginSetupDependencies { visualizations: VisualizationsSetup; inspector: InspectorSetup; data: DataPublicPluginSetup; - mapsLegacy: any; + mapsLegacy: MapsLegacyPluginSetup; } /** @internal */ @@ -68,8 +69,12 @@ export class VegaPlugin implements Plugin<Promise<void>, void> { enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); + setUISettings(core.uiSettings); - setMapsLegacyConfig(mapsLegacy.config); + + setMapServiceSettings( + new MapServiceSettings(mapsLegacy.config, this.initializerContext.env.packageInfo.version) + ); const visualizationDependencies: Readonly<VegaVisualizationDependencies> = { core, diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index 157e355f93434..3e5d890c39ff4 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -10,7 +10,7 @@ import { CoreStart, NotificationsStart, IUiSettingsClient } from 'src/core/publi import { DataPublicPluginStart } from '../../data/public'; import { createGetterSetter } from '../../kibana_utils/public'; -import { MapsLegacyConfig } from '../../maps_legacy/config'; +import { MapServiceSettings } from './vega_view/vega_map_view/map_service_settings'; export const [getData, setData] = createGetterSetter<DataPublicPluginStart>('Data'); @@ -24,13 +24,14 @@ export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< CoreStart['injectedMetadata'] >('InjectedMetadata'); +export const [ + getMapServiceSettings, + setMapServiceSettings, +] = createGetterSetter<MapServiceSettings>('MapServiceSettings'); + export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ enableExternalUrls: boolean; emsTileLayerId: unknown; }>('InjectedVars'); -export const [getMapsLegacyConfig, setMapsLegacyConfig] = createGetterSetter<MapsLegacyConfig>( - 'MapsLegacyConfig' -); - export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; diff --git a/src/plugins/vis_type_vega/public/test_utils/vega_map_test.json b/src/plugins/vis_type_vega/public/test_utils/vega_map_test.json index 9100de38ae387..a7e3b9dc7e024 100644 --- a/src/plugins/vis_type_vega/public/test_utils/vega_map_test.json +++ b/src/plugins/vis_type_vega/public/test_utils/vega_map_test.json @@ -1,7 +1,7 @@ { "$schema": "https://vega.github.io/schema/vega/v5.json", "config": { - "kibana": { "renderer": "svg", "type": "map", "mapStyle": false} + "kibana": { "type": "map", "mapStyle": "default", "latitude": 25, "longitude": -70, "zoom": 3} }, "width": 512, "height": 512, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts index d63288745986c..15132483b3659 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts @@ -18,12 +18,21 @@ interface VegaViewParams { serviceSettings: IServiceSettings; filterManager: DataPublicPluginStart['query']['filterManager']; timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; - // findIndex: (index: string) => Promise<...>; } export class VegaBaseView { constructor(params: VegaViewParams); init(): Promise<void>; onError(error: any): void; + onWarn(error: any): void; + setView(map: any): void; + setDebugValues(view: any, spec: any, vlspec: any): void; + _addDestroyHandler(handler: Function): void; + destroy(): Promise<void>; + + _$container: any; + _parser: any; + _vegaViewConfig: any; + _serviceSettings: any; } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 6971adaa55ec3..7c3915955419f 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -160,8 +160,6 @@ export class VegaBaseView { createViewConfig() { const config = { - // eslint-disable-next-line import/namespace - logLevel: vega.Warn, // note: eslint has a false positive here renderer: this._parser.renderer, }; @@ -189,6 +187,13 @@ export class VegaBaseView { }; config.loader = loader; + const logger = vega.logger(vega.Warn); + + logger.warn = this.onWarn.bind(this); + logger.error = this.onError.bind(this); + + config.logger = logger; + return config; } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js deleted file mode 100644 index bf91b50ed9cf6..0000000000000 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ /dev/null @@ -1,28 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { KibanaMapLayer } from '../../../maps_legacy/public'; - -export class VegaMapLayer extends KibanaMapLayer { - constructor(spec, options, leaflet) { - super(); - - // Used by super.getAttributions() - this._attribution = options.attribution; - delete options.attribution; - this._leafletLayer = leaflet.vega(spec, options); - } - - getVegaView() { - return this._leafletLayer._view; - } - - getVegaSpec() { - return this._leafletLayer._spec; - } -} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js deleted file mode 100644 index 693045edeb7d0..0000000000000 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ /dev/null @@ -1,168 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { vega } from '../lib/vega'; -import { VegaBaseView } from './vega_base_view'; -import { VegaMapLayer } from './vega_map_layer'; -import { getMapsLegacyConfig, getUISettings } from '../services'; -import { lazyLoadMapsLegacyModules, TMS_IN_YML_ID } from '../../../maps_legacy/public'; - -const isUserConfiguredTmsLayer = ({ tilemap }) => Boolean(tilemap.url); - -export class VegaMapView extends VegaBaseView { - constructor(opts) { - super(opts); - } - - async getMapStyleOptions() { - const isDarkMode = getUISettings().get('theme:darkMode'); - const mapsLegacyConfig = getMapsLegacyConfig(); - const tmsServices = await this._serviceSettings.getTMSServices(); - const mapConfig = this._parser.mapConfig; - - let mapStyle; - - if (mapConfig.mapStyle !== 'default') { - mapStyle = mapConfig.mapStyle; - } else { - if (isUserConfiguredTmsLayer(mapsLegacyConfig)) { - mapStyle = TMS_IN_YML_ID; - } else { - mapStyle = mapsLegacyConfig.emsTileLayerId.bright; - } - } - - const mapOptions = tmsServices.find((s) => s.id === mapStyle); - - if (!mapOptions) { - this.onWarn( - i18n.translate('visTypeVega.mapView.mapStyleNotFoundWarningMessage', { - defaultMessage: '{mapStyleParam} was not found', - values: { mapStyleParam: `"mapStyle":${mapStyle}` }, - }) - ); - return null; - } - - return { - ...mapOptions, - ...(await this._serviceSettings.getAttributesForTMSLayer(mapOptions, true, isDarkMode)), - }; - } - - async _initViewCustomizations() { - const mapConfig = this._parser.mapConfig; - let baseMapOpts; - let limitMinZ = 0; - let limitMaxZ = 25; - - // In some cases, Vega may be initialized twice, e.g. after awaiting... - if (!this._$container) return; - - if (mapConfig.mapStyle !== false) { - baseMapOpts = await this.getMapStyleOptions(); - - if (baseMapOpts) { - limitMinZ = baseMapOpts.minZoom; - limitMaxZ = baseMapOpts.maxZoom; - } - } - - const validate = (name, value, dflt, min, max) => { - if (value === undefined) { - value = dflt; - } else if (value < min) { - this.onWarn( - i18n.translate('visTypeVega.mapView.resettingPropertyToMinValueWarningMessage', { - defaultMessage: 'Resetting {name} to {min}', - values: { name: `"${name}"`, min }, - }) - ); - value = min; - } else if (value > max) { - this.onWarn( - i18n.translate('visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage', { - defaultMessage: 'Resetting {name} to {max}', - values: { name: `"${name}"`, max }, - }) - ); - value = max; - } - return value; - }; - - let minZoom = validate('minZoom', mapConfig.minZoom, limitMinZ, limitMinZ, limitMaxZ); - let maxZoom = validate('maxZoom', mapConfig.maxZoom, limitMaxZ, limitMinZ, limitMaxZ); - if (minZoom > maxZoom) { - this.onWarn( - i18n.translate('visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage', { - defaultMessage: '{minZoomPropertyName} and {maxZoomPropertyName} have been swapped', - values: { - minZoomPropertyName: '"minZoom"', - maxZoomPropertyName: '"maxZoom"', - }, - }) - ); - [minZoom, maxZoom] = [maxZoom, minZoom]; - } - const zoom = validate('zoom', mapConfig.zoom, 2, minZoom, maxZoom); - - // let maxBounds = null; - // if (mapConfig.maxBounds) { - // const b = mapConfig.maxBounds; - // eslint-disable-next-line no-undef - // maxBounds = L.latLngBounds(L.latLng(b[1], b[0]), L.latLng(b[3], b[2])); - // } - - const modules = await lazyLoadMapsLegacyModules(); - - this._kibanaMap = new modules.KibanaMap(this._$container.get(0), { - zoom, - minZoom, - maxZoom, - center: [mapConfig.latitude, mapConfig.longitude], - zoomControl: mapConfig.zoomControl, - scrollWheelZoom: mapConfig.scrollWheelZoom, - }); - - if (baseMapOpts) { - this._kibanaMap.setBaseLayer({ - baseLayerType: 'tms', - options: baseMapOpts, - }); - } - - const vegaMapLayer = new VegaMapLayer( - this._parser.spec, - { - vega, - bindingsContainer: this._$controls.get(0), - delayRepaint: mapConfig.delayRepaint, - viewConfig: this._vegaViewConfig, - onWarning: this.onWarn.bind(this), - onError: this.onError.bind(this), - }, - modules.L - ); - - this._kibanaMap.addLayer(vegaMapLayer); - - this._addDestroyHandler(() => { - this._kibanaMap.removeLayer(vegaMapLayer); - if (baseMapOpts) { - this._kibanaMap.setBaseLayer(null); - } - this._kibanaMap.destroy(); - }); - - const vegaView = vegaMapLayer.getVegaView(); - await this.setView(vegaView); - this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec); - } -} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts new file mode 100644 index 0000000000000..ced1dc1bdc217 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { TMS_IN_YML_ID } from '../../../../maps_legacy/public'; + +export const vegaLayerId = 'vega'; +export const userConfiguredLayerId = TMS_IN_YML_ID; +export const defaultMapConfig = { + maxZoom: 20, + minZoom: 0, + tileSize: 256, +}; + +export const defaultMabBoxStyle = { + /** + * according to the MapBox documentation that value should be '8' + * @see (https://docs.mapbox.com/mapbox-gl-js/style-spec/root/#version) + */ + version: 8, + sources: {}, + layers: [], +}; + +export const defaultProjection = { + name: 'projection', + type: 'mercator', + scale: { signal: '512*pow(2,zoom)/2/PI' }, + rotate: [{ signal: '-longitude' }, 0, 0], + center: [0, { signal: 'latitude' }], + translate: [{ signal: 'width/2' }, { signal: 'height/2' }], + fit: false, +}; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/index.ts similarity index 77% rename from src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts rename to src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/index.ts index f101372f5bbce..c0ca7f04810d0 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/index.ts @@ -6,6 +6,5 @@ * Public License, v 1. */ -import { VegaBaseView } from './vega_base_view'; - -export class VegaMapView extends VegaBaseView {} +export { initTmsRasterLayer } from './tms_raster_layer'; +export { initVegaLayer } from './vega_layer'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.test.ts new file mode 100644 index 0000000000000..ea74a48dc9a74 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { initTmsRasterLayer } from './tms_raster_layer'; + +type InitTmsRasterLayerParams = Parameters<typeof initTmsRasterLayer>[0]; + +type IdType = InitTmsRasterLayerParams['id']; +type MapType = InitTmsRasterLayerParams['map']; +type ContextType = InitTmsRasterLayerParams['context']; + +describe('vega_map_view/tms_raster_layer', () => { + let id: IdType; + let map: MapType; + let context: ContextType; + + beforeEach(() => { + id = 'foo_tms_layer_id'; + map = ({ + addSource: jest.fn(), + addLayer: jest.fn(), + } as unknown) as MapType; + context = { + tiles: ['http://some.tile.com/map/{z}/{x}/{y}.jpg'], + maxZoom: 10, + minZoom: 2, + tileSize: 512, + }; + }); + + test('should register a new layer', () => { + initTmsRasterLayer({ id, map, context }); + + expect(map.addLayer).toHaveBeenCalledWith({ + id: 'foo_tms_layer_id', + maxzoom: 10, + minzoom: 2, + source: 'foo_tms_layer_id', + type: 'raster', + }); + + expect(map.addSource).toHaveBeenCalledWith('foo_tms_layer_id', { + scheme: 'xyz', + tileSize: 512, + tiles: ['http://some.tile.com/map/{z}/{x}/{y}.jpg'], + type: 'raster', + }); + }); +}); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.ts new file mode 100644 index 0000000000000..03fdce9bd8d93 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/tms_raster_layer.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import type { LayerParameters } from './types'; + +interface TMSRasterLayerContext { + tiles: string[]; + maxZoom: number; + minZoom: number; + tileSize: number; +} + +export const initTmsRasterLayer = ({ + id, + map, + context: { tiles, maxZoom, minZoom, tileSize }, +}: LayerParameters<TMSRasterLayerContext>) => { + map.addSource(id, { + type: 'raster', + tiles, + tileSize, + scheme: 'xyz', + }); + + map.addLayer({ + id, + type: 'raster', + source: id, + maxzoom: maxZoom, + minzoom: minZoom, + }); +}; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts new file mode 100644 index 0000000000000..1b7ac79312329 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import type { Map } from 'mapbox-gl'; + +export interface LayerParameters<TContext extends Record<string, any> = {}> { + id: string; + map: Map; + context: TContext; +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts new file mode 100644 index 0000000000000..97d231c5f7a6f --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +import { initVegaLayer } from './vega_layer'; + +type InitVegaLayerParams = Parameters<typeof initVegaLayer>[0]; + +type IdType = InitVegaLayerParams['id']; +type MapType = InitVegaLayerParams['map']; +type ContextType = InitVegaLayerParams['context']; + +describe('vega_map_view/tms_raster_layer', () => { + let id: IdType; + let map: MapType; + let context: ContextType; + + beforeEach(() => { + id = 'foo_vega_layer_id'; + map = ({ + getCanvasContainer: () => document.createElement('div'), + getCanvas: () => ({ + style: { + width: 100, + height: 100, + }, + }), + addLayer: jest.fn(), + } as unknown) as MapType; + context = { + vegaView: { + initialize: jest.fn(), + }, + updateVegaView: jest.fn(), + }; + }); + + test('should register a new custom layer', () => { + initVegaLayer({ id, map, context }); + + const calledWith = (map.addLayer as jest.MockedFunction<any>).mock.calls[0][0]; + expect(calledWith).toHaveProperty('id', 'foo_vega_layer_id'); + expect(calledWith).toHaveProperty('type', 'custom'); + }); + + test('should initialize vega container on "onAdd" hook', () => { + initVegaLayer({ id, map, context }); + const { onAdd } = (map.addLayer as jest.MockedFunction<any>).mock.calls[0][0]; + + onAdd(map); + expect(context.vegaView.initialize).toHaveBeenCalled(); + }); + + test('should update vega view on "render" hook', () => { + initVegaLayer({ id, map, context }); + const { render } = (map.addLayer as jest.MockedFunction<any>).mock.calls[0][0]; + + expect(context.updateVegaView).not.toHaveBeenCalled(); + render(); + expect(context.updateVegaView).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts new file mode 100644 index 0000000000000..a9b650fe4c58d --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import type { Map, CustomLayerInterface } from 'mapbox-gl'; +import type { LayerParameters } from './types'; + +// @ts-ignore +import { vega } from '../../lib/vega'; + +export interface VegaLayerContext { + vegaView: vega.View; + updateVegaView: (map: Map, view: vega.View) => void; +} + +export function initVegaLayer({ + id, + map: mapInstance, + context: { vegaView, updateVegaView }, +}: LayerParameters<VegaLayerContext>) { + const vegaLayer: CustomLayerInterface = { + id, + type: 'custom', + onAdd(map: Map) { + const mapContainer = map.getCanvasContainer(); + const mapCanvas = map.getCanvas(); + const vegaContainer = document.createElement('div'); + + vegaContainer.style.position = 'absolute'; + vegaContainer.style.top = '0px'; + vegaContainer.style.width = mapCanvas.style.width; + vegaContainer.style.height = mapCanvas.style.height; + + mapContainer.appendChild(vegaContainer); + vegaView.initialize(vegaContainer); + }, + render() { + updateVegaView(mapInstance, vegaView); + }, + }; + + mapInstance.addLayer(vegaLayer); +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.test.ts new file mode 100644 index 0000000000000..0a477e5f62a7a --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +import { get } from 'lodash'; +import { uiSettingsServiceMock } from 'src/core/public/mocks'; + +import { MapServiceSettings, getAttributionsForTmsService } from './map_service_settings'; +import { MapsLegacyConfig } from '../../../../maps_legacy/config'; +import { EMSClient, TMSService } from '@elastic/ems-client'; +import { setUISettings } from '../../services'; + +const getPrivateField = <T>(mapServiceSettings: MapServiceSettings, privateField: string) => + get(mapServiceSettings, privateField) as T; + +describe('vega_map_view/map_service_settings', () => { + describe('MapServiceSettings', () => { + const appVersion = '99'; + let config: MapsLegacyConfig; + let getUiSettingsMockedValue: any; + + beforeEach(() => { + config = { + emsTileLayerId: { + desaturated: 'road_map_desaturated', + dark: 'dark_map', + }, + } as MapsLegacyConfig; + setUISettings({ + ...uiSettingsServiceMock.createSetupContract(), + get: () => getUiSettingsMockedValue, + }); + }); + + test('should be able to create instance of MapServiceSettings', () => { + const mapServiceSettings = new MapServiceSettings(config, appVersion); + + expect(mapServiceSettings instanceof MapServiceSettings).toBeTruthy(); + expect(mapServiceSettings.hasUserConfiguredTmsLayer()).toBeFalsy(); + expect(mapServiceSettings.defaultTmsLayer()).toBe('road_map_desaturated'); + }); + + test('should be able to set user configured base layer through config', () => { + const mapServiceSettings = new MapServiceSettings( + { + ...config, + tilemap: { + url: 'http://some.tile.com/map/{z}/{x}/{y}.jpg', + options: { + attribution: 'attribution', + minZoom: 0, + maxZoom: 4, + }, + }, + }, + appVersion + ); + + expect(mapServiceSettings.defaultTmsLayer()).toBe('TMS in config/kibana.yml'); + expect(mapServiceSettings.hasUserConfiguredTmsLayer()).toBeTruthy(); + }); + + test('should load ems client only on executing getTmsService method', async () => { + const mapServiceSettings = new MapServiceSettings(config, appVersion); + + expect(getPrivateField<EMSClient>(mapServiceSettings, 'emsClient')).toBeUndefined(); + + await mapServiceSettings.getTmsService('road_map'); + + expect( + getPrivateField<EMSClient>(mapServiceSettings, 'emsClient') instanceof EMSClient + ).toBeTruthy(); + }); + + test('should set isDarkMode value on executing getTmsService method', async () => { + const mapServiceSettings = new MapServiceSettings(config, appVersion); + getUiSettingsMockedValue = true; + + expect(getPrivateField<EMSClient>(mapServiceSettings, 'isDarkMode')).toBeFalsy(); + + await mapServiceSettings.getTmsService('road_map'); + + expect(getPrivateField<EMSClient>(mapServiceSettings, 'isDarkMode')).toBeTruthy(); + }); + + test('getAttributionsForTmsService method should return attributes in a correct form', () => { + const tmsService = ({ + getAttributions: jest.fn(() => [ + { url: 'https://fist_attr.com', label: 'fist_attr' }, + { url: 'https://second_attr.com', label: 'second_attr' }, + ]), + } as unknown) as TMSService; + + expect(getAttributionsForTmsService(tmsService)).toMatchInlineSnapshot(` + Array [ + "<a rel=\\"noreferrer noopener\\" href=\\"https://fist_attr.com\\">fist_attr</a>", + "<a rel=\\"noreferrer noopener\\" href=\\"https://second_attr.com\\">second_attr</a>", + ] + `); + }); + }); +}); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.ts new file mode 100644 index 0000000000000..92dfc873e2715 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/map_service_settings.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +import { i18n } from '@kbn/i18n'; +import type { EMSClient, TMSService } from '@elastic/ems-client'; +import { getUISettings } from '../../services'; +import { userConfiguredLayerId } from './constants'; +import type { MapsLegacyConfig } from '../../../../maps_legacy/config'; + +type EmsClientConfig = ConstructorParameters<typeof EMSClient>[0]; + +const hasUserConfiguredTmsService = (config: MapsLegacyConfig) => Boolean(config.tilemap?.url); + +const initEmsClientAsync = async (config: Partial<EmsClientConfig>) => { + /** + * Build optimization: '@elastic/ems-client' should be loaded from a separate chunk + */ + const emsClientModule = await import('@elastic/ems-client'); + + return new emsClientModule.EMSClient({ + language: i18n.getLocale(), + appName: 'kibana', + // Wrap to avoid errors passing window fetch + fetchFunction(input: RequestInfo, init?: RequestInit) { + return fetch(input, init); + }, + ...config, + } as EmsClientConfig); +}; + +export class MapServiceSettings { + private emsClient?: EMSClient; + private isDarkMode: boolean = false; + + constructor(public config: MapsLegacyConfig, private appVersion: string) {} + + private isInitialized() { + return Boolean(this.emsClient); + } + + public hasUserConfiguredTmsLayer() { + return hasUserConfiguredTmsService(this.config); + } + + public defaultTmsLayer() { + const { dark, desaturated } = this.config.emsTileLayerId; + + if (this.hasUserConfiguredTmsLayer()) { + return userConfiguredLayerId; + } + + return this.isDarkMode ? dark : desaturated; + } + + private async initialize() { + this.isDarkMode = getUISettings().get('theme:darkMode'); + + this.emsClient = await initEmsClientAsync({ + appVersion: this.appVersion, + fileApiUrl: this.config.emsFileApiUrl, + tileApiUrl: this.config.emsTileApiUrl, + landingPageUrl: this.config.emsLandingPageUrl, + }); + } + + public async getTmsService(tmsTileLayer: string) { + if (!this.isInitialized()) { + await this.initialize(); + } + return this.emsClient?.findTMSServiceById(tmsTileLayer); + } +} + +export function getAttributionsForTmsService(tmsService: TMSService) { + return tmsService.getAttributions().map(({ label, url }) => { + const anchorTag = document.createElement('a'); + + anchorTag.textContent = label; + anchorTag.setAttribute('rel', 'noreferrer noopener'); + anchorTag.setAttribute('href', url); + + return anchorTag.outerHTML; + }); +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/index.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/index.ts new file mode 100644 index 0000000000000..921e604354b2e --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export { validateZoomSettings } from './validation_helper'; +export { injectMapPropsIntoSpec } from './vsi_helper'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.test.ts new file mode 100644 index 0000000000000..c2eb37980b741 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +import { validateZoomSettings } from './validation_helper'; + +type ValidateZoomSettingsParams = Parameters<typeof validateZoomSettings>; + +type MapConfigType = ValidateZoomSettingsParams[0]; +type LimitsType = ValidateZoomSettingsParams[1]; +type OnWarnType = ValidateZoomSettingsParams[2]; + +describe('vega_map_view/validation_helper', () => { + describe('validateZoomSettings', () => { + let mapConfig: MapConfigType; + let limits: LimitsType; + let onWarn: OnWarnType; + + beforeEach(() => { + onWarn = jest.fn(); + mapConfig = { + maxZoom: 10, + minZoom: 5, + zoom: 5, + }; + limits = { + maxZoom: 15, + minZoom: 2, + }; + }); + + test('should return validated interval', () => { + expect(validateZoomSettings(mapConfig, limits, onWarn)).toEqual({ + maxZoom: 10, + minZoom: 5, + zoom: 5, + }); + }); + + test('should return default interval in case if mapConfig not provided', () => { + mapConfig = {} as MapConfigType; + expect(validateZoomSettings(mapConfig, limits, onWarn)).toEqual({ + maxZoom: 15, + minZoom: 2, + zoom: 3, + }); + }); + + test('should reset MaxZoom if the passed value is greater than the limit', () => { + mapConfig = { + ...mapConfig, + maxZoom: 20, + }; + + const result = validateZoomSettings(mapConfig, limits, onWarn); + + expect(onWarn).toBeCalledWith('Resetting "maxZoom" to 15'); + expect(result.maxZoom).toEqual(15); + }); + + test('should reset MinZoom if the passed value is greater than the limit', () => { + mapConfig = { + ...mapConfig, + minZoom: 0, + }; + + const result = validateZoomSettings(mapConfig, limits, onWarn); + + expect(onWarn).toBeCalledWith('Resetting "minZoom" to 2'); + expect(result.minZoom).toEqual(2); + }); + + test('should reset Zoom if the passed value is greater than the max limit', () => { + mapConfig = { + ...mapConfig, + zoom: 45, + }; + + const result = validateZoomSettings(mapConfig, limits, onWarn); + + expect(onWarn).toBeCalledWith('Resetting "zoom" to 10'); + expect(result.zoom).toEqual(10); + }); + + test('should reset Zoom if the passed value is greater than the min limit', () => { + mapConfig = { + ...mapConfig, + zoom: 0, + }; + + const result = validateZoomSettings(mapConfig, limits, onWarn); + + expect(onWarn).toBeCalledWith('Resetting "zoom" to 5'); + expect(result.zoom).toEqual(5); + }); + + test('should swap min <--> max values', () => { + mapConfig = { + maxZoom: 10, + minZoom: 15, + }; + + const result = validateZoomSettings(mapConfig, limits, onWarn); + + expect(onWarn).toBeCalledWith('"minZoom" and "maxZoom" have been swapped'); + expect(result).toEqual({ maxZoom: 15, minZoom: 10, zoom: 10 }); + }); + }); +}); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.ts new file mode 100644 index 0000000000000..5e6f45790ae2d --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/validation_helper.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +function validate( + name: string, + value: number, + defaultValue: number, + min: number, + max: number, + onWarn: (message: string) => void +) { + if (value === undefined) { + value = defaultValue; + } else if (value < min) { + onWarn( + i18n.translate('visTypeVega.mapView.resettingPropertyToMinValueWarningMessage', { + defaultMessage: 'Resetting {name} to {min}', + values: { name: `"${name}"`, min }, + }) + ); + value = min; + } else if (value > max) { + onWarn( + i18n.translate('visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage', { + defaultMessage: 'Resetting {name} to {max}', + values: { name: `"${name}"`, max }, + }) + ); + value = max; + } + return value; +} + +export function validateZoomSettings( + mapConfig: { + maxZoom: number; + minZoom: number; + zoom?: number; + }, + limits: { + maxZoom: number; + minZoom: number; + }, + onWarn: (message: any) => void +) { + const DEFAULT_ZOOM = 3; + + let { maxZoom, minZoom, zoom = DEFAULT_ZOOM } = mapConfig; + + minZoom = validate('minZoom', minZoom, limits.minZoom, limits.minZoom, limits.maxZoom, onWarn); + maxZoom = validate('maxZoom', maxZoom, limits.maxZoom, limits.minZoom, limits.maxZoom, onWarn); + + if (minZoom > maxZoom) { + onWarn( + i18n.translate('visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage', { + defaultMessage: '{minZoomPropertyName} and {maxZoomPropertyName} have been swapped', + values: { + minZoomPropertyName: '"minZoom"', + maxZoomPropertyName: '"maxZoom"', + }, + }) + ); + [minZoom, maxZoom] = [maxZoom, minZoom]; + } + + zoom = validate('zoom', zoom, DEFAULT_ZOOM, minZoom, maxZoom, onWarn); + + return { + zoom, + minZoom, + maxZoom, + }; +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.test.ts new file mode 100644 index 0000000000000..e671b9059f358 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { injectMapPropsIntoSpec } from './vsi_helper'; +import { VegaSpec } from '../../../data_model/types'; + +describe('vega_map_view/vsi_helper', () => { + describe('injectMapPropsIntoSpec', () => { + test('should inject map properties into vega spec', () => { + const spec = ({ + $schema: 'https://vega.github.io/schema/vega/v5.json', + config: { + kibana: { type: 'map', latitude: 25, longitude: -70, zoom: 3 }, + }, + } as unknown) as VegaSpec; + + expect(injectMapPropsIntoSpec(spec)).toMatchInlineSnapshot(` + Object { + "$schema": "https://vega.github.io/schema/vega/v5.json", + "autosize": "none", + "config": Object { + "kibana": Object { + "latitude": 25, + "longitude": -70, + "type": "map", + "zoom": 3, + }, + }, + "projections": Array [ + Object { + "center": Array [ + 0, + Object { + "signal": "latitude", + }, + ], + "fit": false, + "name": "projection", + "rotate": Array [ + Object { + "signal": "-longitude", + }, + 0, + 0, + ], + "scale": Object { + "signal": "512*pow(2,zoom)/2/PI", + }, + "translate": Array [ + Object { + "signal": "width/2", + }, + Object { + "signal": "height/2", + }, + ], + "type": "mercator", + }, + ], + "signals": Array [ + Object { + "name": "zoom", + }, + Object { + "name": "latitude", + }, + Object { + "name": "longitude", + }, + ], + } + `); + }); + }); +}); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts new file mode 100644 index 0000000000000..0022f68637659 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +// @ts-expect-error +// eslint-disable-next-line import/no-extraneous-dependencies +import Vsi from 'vega-spec-injector'; + +import { VegaSpec } from '../../../data_model/types'; +import { defaultProjection } from '../constants'; + +export const injectMapPropsIntoSpec = (spec: VegaSpec) => { + const vsi = new Vsi(); + + vsi.overrideField(spec, 'autosize', 'none'); + vsi.addToList(spec, 'signals', ['zoom', 'latitude', 'longitude']); + vsi.addToList(spec, 'projections', [defaultProjection]); + + return spec; +}; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss new file mode 100644 index 0000000000000..33e63e7ef317c --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss @@ -0,0 +1,7 @@ +@import '~mapbox-gl/dist/mapbox-gl.css'; + +.vgaVis { + .mapboxgl-canvas-container { + cursor: auto; + } +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts new file mode 100644 index 0000000000000..fd176e5d20a2f --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import 'jest-canvas-mock'; + +import type { TMSService } from '@elastic/ems-client'; +import { VegaMapView } from './view'; +import { VegaViewParams } from '../vega_base_view'; +import { VegaParser } from '../../data_model/vega_parser'; +import { TimeCache } from '../../data_model/time_cache'; +import { SearchAPI } from '../../data_model/search_api'; +import vegaMap from '../../test_utils/vega_map_test.json'; +import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { IServiceSettings } from '../../../../maps_legacy/public'; +import type { MapsLegacyConfig } from '../../../../maps_legacy/config'; +import { MapServiceSettings } from './map_service_settings'; +import { userConfiguredLayerId } from './constants'; +import { + setInjectedVars, + setData, + setNotifications, + setMapServiceSettings, + setUISettings, +} from '../../services'; + +jest.mock('../../lib/vega', () => ({ + vega: jest.requireActual('vega'), + vegaLite: jest.requireActual('vega-lite'), +})); + +jest.mock('mapbox-gl', () => ({ + Map: jest.fn().mockImplementation(() => ({ + getLayer: () => '', + removeLayer: jest.fn(), + once: (eventName: string, handler: Function) => handler(), + remove: () => jest.fn(), + getCanvas: () => ({ clientWidth: 512, clientHeight: 512 }), + getCenter: () => ({ lat: 20, lng: 20 }), + getZoom: () => 3, + addControl: jest.fn(), + addLayer: jest.fn(), + })), + MapboxOptions: jest.fn(), + NavigationControl: jest.fn(), +})); + +jest.mock('./layers', () => ({ + initVegaLayer: jest.fn(), + initTmsRasterLayer: jest.fn(), +})); + +import { initVegaLayer, initTmsRasterLayer } from './layers'; +import { Map, NavigationControl } from 'mapbox-gl'; + +describe('vega_map_view/view', () => { + describe('VegaMapView', () => { + const coreStart = coreMock.createStart(); + const dataPluginStart = dataPluginMock.createStartContract(); + const mockGetServiceSettings = async () => { + return {} as IServiceSettings; + }; + let vegaParser: VegaParser; + + setInjectedVars({ + emsTileLayerId: {}, + enableExternalUrls: true, + }); + setData(dataPluginStart); + setNotifications(coreStart.notifications); + setUISettings(coreStart.uiSettings); + + const getTmsService = jest.fn().mockReturnValue(({ + getVectorStyleSheet: () => ({ + version: 8, + sources: {}, + layers: [], + }), + getMaxZoom: async () => 20, + getMinZoom: async () => 0, + getAttributions: () => [{ url: 'tms_attributions' }], + } as unknown) as TMSService); + const config = { + tilemap: { + url: 'test', + options: { + attribution: 'tilemap-attribution', + minZoom: 0, + maxZoom: 20, + }, + }, + } as MapsLegacyConfig; + + function setMapService(defaultTmsLayer: string) { + setMapServiceSettings(({ + getTmsService, + defaultTmsLayer: () => defaultTmsLayer, + config, + } as unknown) as MapServiceSettings); + } + + async function createVegaMapView() { + await vegaParser.parseAsync(); + return new VegaMapView({ + vegaParser, + filterManager: dataPluginStart.query.filterManager, + timefilter: dataPluginStart.query.timefilter.timefilter, + fireEvent: (event: any) => {}, + parentEl: document.createElement('div'), + } as VegaViewParams); + } + + beforeEach(() => { + vegaParser = new VegaParser( + JSON.stringify(vegaMap), + new SearchAPI({ + search: dataPluginStart.search, + uiSettings: coreStart.uiSettings, + injectedMetadata: coreStart.injectedMetadata, + }), + new TimeCache(dataPluginStart.query.timefilter.timefilter, 0), + {}, + mockGetServiceSettings + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should be added TmsRasterLayer and do not use tmsService if mapStyle is "user_configured"', async () => { + setMapService(userConfiguredLayerId); + const vegaMapView = await createVegaMapView(); + + await vegaMapView.init(); + + const { longitude, latitude, scrollWheelZoom } = vegaMapView._parser.mapConfig; + expect(Map).toHaveBeenCalledWith({ + style: { + version: 8, + sources: {}, + layers: [], + }, + customAttribution: 'tilemap-attribution', + container: vegaMapView._$container.get(0), + minZoom: 0, + maxZoom: 20, + zoom: 3, + scrollZoom: scrollWheelZoom, + center: [longitude, latitude], + }); + expect(getTmsService).not.toHaveBeenCalled(); + expect(initTmsRasterLayer).toHaveBeenCalled(); + expect(initVegaLayer).toHaveBeenCalled(); + }); + + test('should not be added TmsRasterLayer and use tmsService if mapStyle is not "user_configured"', async () => { + setMapService('road_map_desaturated'); + const vegaMapView = await createVegaMapView(); + + await vegaMapView.init(); + + const { longitude, latitude, scrollWheelZoom } = vegaMapView._parser.mapConfig; + expect(Map).toHaveBeenCalledWith({ + style: { + version: 8, + sources: {}, + layers: [], + }, + customAttribution: ['<a rel="noreferrer noopener" href="tms_attributions"></a>'], + container: vegaMapView._$container.get(0), + minZoom: 0, + maxZoom: 20, + zoom: 3, + scrollZoom: scrollWheelZoom, + center: [longitude, latitude], + }); + expect(getTmsService).toHaveBeenCalled(); + expect(initTmsRasterLayer).not.toHaveBeenCalled(); + expect(initVegaLayer).toHaveBeenCalled(); + }); + + test('should be added NavigationControl', async () => { + setMapService('road_map_desaturated'); + const vegaMapView = await createVegaMapView(); + + await vegaMapView.init(); + + expect(NavigationControl).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts new file mode 100644 index 0000000000000..6a31eb0b37833 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { Map, Style, NavigationControl, MapboxOptions } from 'mapbox-gl'; + +import { initTmsRasterLayer, initVegaLayer } from './layers'; +import { VegaBaseView } from '../vega_base_view'; +import { getMapServiceSettings } from '../../services'; +import { getAttributionsForTmsService } from './map_service_settings'; +import type { MapServiceSettings } from './map_service_settings'; + +import { + defaultMapConfig, + defaultMabBoxStyle, + userConfiguredLayerId, + vegaLayerId, +} from './constants'; + +import { validateZoomSettings, injectMapPropsIntoSpec } from './utils'; + +// @ts-expect-error +import { vega } from '../../lib/vega'; + +import './vega_map_view.scss'; + +async function updateVegaView(mapBoxInstance: Map, vegaView: vega.View) { + const mapCanvas = mapBoxInstance.getCanvas(); + const { lat, lng } = mapBoxInstance.getCenter(); + let shouldRender = false; + + const sendSignal = (sig: string, value: any) => { + if (vegaView.signal(sig) !== value) { + vegaView.signal(sig, value); + shouldRender = true; + } + }; + + sendSignal('width', mapCanvas.clientWidth); + sendSignal('height', mapCanvas.clientHeight); + sendSignal('latitude', lat); + sendSignal('longitude', lng); + sendSignal('zoom', mapBoxInstance.getZoom()); + + if (shouldRender) { + await vegaView.runAsync(); + } +} + +export class VegaMapView extends VegaBaseView { + private mapServiceSettings: MapServiceSettings = getMapServiceSettings(); + private mapStyle = this.getMapStyle(); + + private getMapStyle() { + const { mapStyle } = this._parser.mapConfig; + + return mapStyle === 'default' ? this.mapServiceSettings.defaultTmsLayer() : mapStyle; + } + + private get shouldShowZoomControl() { + return Boolean(this._parser.mapConfig.zoomControl); + } + + private getMapParams(defaults: { maxZoom: number; minZoom: number }): Partial<MapboxOptions> { + const { longitude, latitude, scrollWheelZoom } = this._parser.mapConfig; + const zoomSettings = validateZoomSettings(this._parser.mapConfig, defaults, this.onWarn); + + return { + ...zoomSettings, + center: [longitude, latitude], + scrollZoom: scrollWheelZoom, + }; + } + + private async initMapContainer(vegaView: vega.View) { + let style: Style = defaultMabBoxStyle; + let customAttribution: MapboxOptions['customAttribution'] = []; + const zoomSettings = { + minZoom: defaultMapConfig.minZoom, + maxZoom: defaultMapConfig.maxZoom, + }; + + if (this.mapStyle && this.mapStyle !== userConfiguredLayerId) { + const tmsService = await this.mapServiceSettings.getTmsService(this.mapStyle); + + if (!tmsService) { + this.onWarn( + i18n.translate('visTypeVega.mapView.mapStyleNotFoundWarningMessage', { + defaultMessage: '{mapStyleParam} was not found', + values: { mapStyleParam: `"mapStyle":${this.mapStyle}` }, + }) + ); + return; + } + zoomSettings.maxZoom = (await tmsService.getMaxZoom()) ?? defaultMapConfig.maxZoom; + zoomSettings.minZoom = (await tmsService.getMinZoom()) ?? defaultMapConfig.minZoom; + customAttribution = getAttributionsForTmsService(tmsService); + style = (await tmsService.getVectorStyleSheet()) as Style; + } else { + customAttribution = this.mapServiceSettings.config.tilemap.options.attribution; + } + + // In some cases, Vega may be initialized twice, e.g. after awaiting... + if (!this._$container) return; + + const mapBoxInstance = new Map({ + style, + customAttribution, + container: this._$container.get(0), + ...this.getMapParams({ ...zoomSettings }), + }); + + const initMapComponents = () => { + this.initControls(mapBoxInstance); + this.initLayers(mapBoxInstance, vegaView); + + this._addDestroyHandler(() => { + if (mapBoxInstance.getLayer(vegaLayerId)) { + mapBoxInstance.removeLayer(vegaLayerId); + } + if (mapBoxInstance.getLayer(userConfiguredLayerId)) { + mapBoxInstance.removeLayer(userConfiguredLayerId); + } + mapBoxInstance.remove(); + }); + }; + + mapBoxInstance.once('load', initMapComponents); + } + + private initControls(mapBoxInstance: Map) { + if (this.shouldShowZoomControl) { + mapBoxInstance.addControl(new NavigationControl({ showCompass: false }), 'top-left'); + } + } + + private initLayers(mapBoxInstance: Map, vegaView: vega.View) { + const shouldShowUserConfiguredLayer = this.mapStyle === userConfiguredLayerId; + + if (shouldShowUserConfiguredLayer) { + const { url, options } = this.mapServiceSettings.config.tilemap; + + initTmsRasterLayer({ + id: userConfiguredLayerId, + map: mapBoxInstance, + context: { + tiles: [url!], + maxZoom: options.maxZoom ?? defaultMapConfig.maxZoom, + minZoom: options.minZoom ?? defaultMapConfig.minZoom, + tileSize: options.tileSize ?? defaultMapConfig.tileSize, + }, + }); + } + + initVegaLayer({ + id: vegaLayerId, + map: mapBoxInstance, + context: { + vegaView, + updateVegaView, + }, + }); + } + + protected async _initViewCustomizations() { + const vegaView = new vega.View( + vega.parse(injectMapPropsIntoSpec(this._parser.spec)), + this._vegaViewConfig + ); + + this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec); + this.setView(vegaView); + + await this.initMapContainer(vegaView); + } +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index 8b6ebbe9c7594..2fd7e4fd606fd 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -16,8 +16,6 @@ export class VegaView extends VegaBaseView { const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig); - view.warn = this.onWarn.bind(this); - view.error = this.onError.bind(this); if (this._parser.useResize) this.updateVegaSize(view); view.initialize(this._$container.get(0), this._$controls.get(0)); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index af396dbf778d2..926c03e79bff9 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -10,13 +10,10 @@ import 'jest-canvas-mock'; import $ from 'jquery'; -import 'leaflet/dist/leaflet.js'; -import 'leaflet-vega'; import { createVegaVisualization } from './vega_visualization'; import vegaliteGraph from './test_utils/vegalite_graph.json'; import vegaGraph from './test_utils/vega_graph.json'; -import vegaMapGraph from './test_utils/vega_map_test.json'; import { VegaParser } from './data_model/vega_parser'; import { SearchAPI } from './data_model/search_api'; @@ -146,32 +143,5 @@ describe('VegaVisualizations', () => { vegaVis.destroy(); } }); - - test('should show vega blank rectangle on top of a map (vegamap)', async () => { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, jest.fn()); - const vegaParser = new VegaParser( - JSON.stringify(vegaMapGraph), - new SearchAPI({ - search: dataPluginStart.search, - uiSettings: coreStart.uiSettings, - injectedMetadata: coreStart.injectedMetadata, - }), - 0, - 0, - mockGetServiceSettings - ); - await vegaParser.parseAsync(); - - mockedWidthValue = 256; - mockedHeightValue = 256; - - await vegaVis.render(vegaParser); - expect(domNode.innerHTML).toMatchSnapshot(); - } finally { - vegaVis.destroy(); - } - }); }); }); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.ts b/src/plugins/vis_type_vega/public/vega_visualization.ts index 26647ecca93ec..14dea362bc8c5 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.ts +++ b/src/plugins/vis_type_vega/public/vega_visualization.ts @@ -78,7 +78,7 @@ export const createVegaVisualization = ({ }; if (vegaParser.useMap) { - const { VegaMapView } = await import('./vega_view/vega_map_view'); + const { VegaMapView } = await import('./vega_view/vega_map_view/view'); this.vegaView = new VegaMapView(vegaViewParams); } else { const { VegaView: VegaViewClass } = await import('./vega_view/vega_view'); diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index e28839612bca7..c013056ba4566 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -10,7 +10,9 @@ "include": [ "server/**/*", "public/**/*", - "*.ts" + "*.ts", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "public/test_utils/vega_map_test.json" ], "references": [ { "path": "../../core/tsconfig.json" }, diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json index 56397351562de..66941e201e9ba 100644 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ b/test/functional/fixtures/es_archiver/visualize/data.json @@ -269,3 +269,24 @@ } } } + +{ + "type": "doc", + "value": { + "id": "visualization:VegaMap", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "VegaMap", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "VegaMap", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}" + } + } + } +} diff --git a/test/visual_regression/config.ts b/test/visual_regression/config.ts index c4951760fc756..60219efc61e6c 100644 --- a/test/visual_regression/config.ts +++ b/test/visual_regression/config.ts @@ -15,7 +15,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), - testFiles: [require.resolve('./tests/console_app'), require.resolve('./tests/discover')], + testFiles: [ + require.resolve('./tests/console_app'), + require.resolve('./tests/discover'), + require.resolve('./tests/vega'), + ], services, diff --git a/test/visual_regression/tests/vega/index.ts b/test/visual_regression/tests/vega/index.ts new file mode 100644 index 0000000000000..6f79ee834b3dc --- /dev/null +++ b/test/visual_regression/tests/vega/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { DEFAULT_OPTIONS } from '../../services/visual_testing/visual_testing'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +// Width must be the same as visual_testing or canvas image widths will get skewed +const [SCREEN_WIDTH] = DEFAULT_OPTIONS.widths || []; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + + describe('vega app', function () { + this.tags('ciGroup6'); + + before(function () { + return browser.setWindowSize(SCREEN_WIDTH, 1000); + }); + + loadTestFile(require.resolve('./vega_map_visualization')); + }); +} diff --git a/test/visual_regression/tests/vega/vega_map_visualization.ts b/test/visual_regression/tests/vega/vega_map_visualization.ts new file mode 100644 index 0000000000000..98aad0cb87795 --- /dev/null +++ b/test/visual_regression/tests/vega/vega_map_visualization.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'visualize', 'visChart', 'visEditor', 'vegaChart']); + const visualTesting = getService('visualTesting'); + + describe('vega chart in visualize app', () => { + before(async () => { + await esArchiver.loadIfNeeded('kibana_sample_data_flights'); + await esArchiver.loadIfNeeded('visualize'); + }); + + after(async () => { + await esArchiver.unload('kibana_sample_data_flights'); + await esArchiver.unload('visualize'); + }); + + it('should show map with vega layer', async function () { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await PageObjects.visualize.openSavedVisualization('VegaMap'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await visualTesting.snapshot(); + }); + }); +} From a29d4d3b1b41f7159621c1666cf9d604b9f09d5c Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" <christiane.heiligers@elastic.co> Date: Fri, 29 Jan 2021 07:10:12 -0700 Subject: [PATCH 115/163] Fix flights sample data dashboard visualization (#89460) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/discover/_data_grid_doc_table.ts | 5 +++++ test/functional/apps/home/_sample_data.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts index 8481065c18466..10cdd7e866af9 100644 --- a/test/functional/apps/discover/_data_grid_doc_table.ts +++ b/test/functional/apps/discover/_data_grid_doc_table.ts @@ -33,6 +33,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); }); + after(async function () { + log.debug('reset uiSettings'); + await kibanaServer.uiSettings.replace({}); + }); + it('should show the first 50 rows by default', async function () { // with the default range the number of hits is ~14000 const rows = await dataGrid.getDocTableRows(); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 438dd6f8adce2..a9fe2026112b6 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -20,8 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardExpect = getService('dashboardExpect'); const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']); - // Failing: See https://github.com/elastic/kibana/issues/89379 - describe.skip('sample data', function describeIndexTests() { + describe('sample data', function describeIndexTests() { before(async () => { await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { From 5c45e7dfcfe1f2e43171162a2962f4473e399f83 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" <christiane.heiligers@elastic.co> Date: Fri, 29 Jan 2021 07:48:51 -0700 Subject: [PATCH 116/163] Migrates watcher to a TS project ref (#89622) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../helpers/jest_constants.ts | 2 +- x-pack/plugins/watcher/tsconfig.json | 29 +++++++++++++++++++ x-pack/test/tsconfig.json | 3 +- x-pack/tsconfig.json | 2 ++ x-pack/tsconfig.refs.json | 3 +- 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/watcher/tsconfig.json diff --git a/x-pack/plugins/watcher/tests_client_integration/helpers/jest_constants.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/jest_constants.ts index 6f243e130c235..7b4876f542292 100644 --- a/x-pack/plugins/watcher/tests_client_integration/helpers/jest_constants.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/jest_constants.ts @@ -8,4 +8,4 @@ import { getWatch } from '../../__fixtures__'; export const WATCH_ID = 'my-test-watch'; -export const WATCH = { watch: getWatch({ id: WATCH_ID }) }; +export const WATCH: any = { watch: getWatch({ id: WATCH_ID }) }; diff --git a/x-pack/plugins/watcher/tsconfig.json b/x-pack/plugins/watcher/tsconfig.json new file mode 100644 index 0000000000000..4680847ba486d --- /dev/null +++ b/x-pack/plugins/watcher/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*", + "public/**/*", + "common/**/*", + "tests_client_integration/**/*", + "__fixtures__/*", + "../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../../../src/plugins/charts/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 6a75f0c7e02d3..cc36a2c93b1a0 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -59,6 +59,7 @@ { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, { "path": "../plugins/global_search_bar/tsconfig.json" }, - { "path": "../plugins/license_management/tsconfig.json" } + { "path": "../plugins/license_management/tsconfig.json" }, + { "path": "../plugins/watcher/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 7ed53ca0abb6b..956bd409f979d 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -38,6 +38,7 @@ "plugins/saved_objects_tagging/**/*", "plugins/global_search_bar/**/*", "plugins/license_management/**/*", + "plugins/watcher/**/*", "test/**/*" ], "compilerOptions": { @@ -109,5 +110,6 @@ { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, { "path": "./plugins/stack_alerts/tsconfig.json"}, { "path": "./plugins/license_management/tsconfig.json" }, + { "path": "./plugins/watcher/tsconfig.json" }, ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index eeba8dd770da6..1724cb2afbffa 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -32,6 +32,7 @@ { "path": "./plugins/cloud/tsconfig.json" }, { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, { "path": "./plugins/global_search_bar/tsconfig.json" }, - { "path": "./plugins/license_management/tsconfig.json" } + { "path": "./plugins/license_management/tsconfig.json" }, + { "path": "./plugins/watcher/tsconfig.json" } ] } From 1fc45a7c370b38fe2a3ad0727f956f79d922b5b3 Mon Sep 17 00:00:00 2001 From: Devon Thomson <devon.thomson@hotmail.com> Date: Fri, 29 Jan 2021 09:59:05 -0500 Subject: [PATCH 117/163] Fix Lens Save and Return Removing Tags (#89613) * use last saved tag ids in save and return... --- x-pack/plugins/lens/public/app_plugin/app.tsx | 17 +-- x-pack/test/functional/apps/lens/index.ts | 1 + .../test/functional/apps/lens/lens_tagging.ts | 118 ++++++++++++++++++ 3 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 x-pack/test/functional/apps/lens/lens_tagging.ts diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index c7764684029c7..2dcda656c779b 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -370,6 +370,11 @@ export function App({ state.persistedDoc?.state, ]); + const tagsIds = + state.persistedDoc && savedObjectsTagging + ? savedObjectsTagging.ui.getTagIdsFromReferences(state.persistedDoc.references) + : []; + const runSave = async ( saveProps: Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & { returnToOrigin: boolean; @@ -385,8 +390,11 @@ export function App({ } let references = lastKnownDoc.references; - if (savedObjectsTagging && saveProps.newTags) { - references = savedObjectsTagging.ui.updateTagsReferences(references, saveProps.newTags); + if (savedObjectsTagging) { + references = savedObjectsTagging.ui.updateTagsReferences( + references, + saveProps.newTags || tagsIds + ); } const docToSave = { @@ -586,11 +594,6 @@ export function App({ }, }); - const tagsIds = - state.persistedDoc && savedObjectsTagging - ? savedObjectsTagging.ui.getTagIdsFromReferences(state.persistedDoc.references) - : []; - return ( <> <div className="lnsApp"> diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 642526d74b687..db8ede58ca9d4 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -34,6 +34,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./chart_data')); loadTestFile(require.resolve('./drag_and_drop')); loadTestFile(require.resolve('./lens_reporting')); + loadTestFile(require.resolve('./lens_tagging')); // has to be last one in the suite because it overrides saved objects loadTestFile(require.resolve('./rollup')); diff --git a/x-pack/test/functional/apps/lens/lens_tagging.ts b/x-pack/test/functional/apps/lens/lens_tagging.ts new file mode 100644 index 0000000000000..970eaa89548d2 --- /dev/null +++ b/x-pack/test/functional/apps/lens/lens_tagging.ts @@ -0,0 +1,118 @@ +/* + * 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'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const find = getService('find'); + const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const PageObjects = getPageObjects([ + 'common', + 'tagManagement', + 'header', + 'dashboard', + 'visualize', + 'lens', + ]); + + const lensTag = 'extreme-lens-tag'; + const lensTitle = 'lens tag test'; + + describe('lens tagging', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('lens/basic'); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('adds a new tag to a Lens visualization', async () => { + // create lens + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickLensWidget(); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'ip', + }); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('lnsApp_saveButton'); + + await PageObjects.visualize.setSaveModalValues(lensTitle, { + saveAsNew: false, + redirectToOrigin: true, + }); + await testSubjects.click('savedObjectTagSelector'); + await testSubjects.click(`tagSelectorOption-action__create`); + + expect(await PageObjects.tagManagement.tagModal.isOpened()).to.be(true); + + await PageObjects.tagManagement.tagModal.fillForm( + { + name: lensTag, + color: '#FFCC33', + description: '', + }, + { + submit: true, + } + ); + + expect(await PageObjects.tagManagement.tagModal.isOpened()).to.be(false); + await testSubjects.click('confirmSaveSavedObjectButton'); + await retry.waitForWithTimeout('Save modal to disappear', 1000, () => + testSubjects + .missingOrFail('confirmSaveSavedObjectButton') + .then(() => true) + .catch(() => false) + ); + }); + + it('retains its saved object tags after save and return', async () => { + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.lens.saveAndReturn(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.waitUntilTableIsLoaded(); + + // open the filter dropdown + const filterButton = await find.byCssSelector('.euiFilterGroup .euiFilterButton'); + await filterButton.click(); + await testSubjects.click( + `tag-searchbar-option-${PageObjects.tagManagement.testSubjFriendly(lensTag)}` + ); + // click elsewhere to close the filter dropdown + const searchFilter = await find.byCssSelector('main .euiFieldSearch'); + await searchFilter.click(); + // wait until the table refreshes + await listingTable.waitUntilTableIsLoaded(); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain(lensTitle); + }); + }); +} From f732b2c3c5627ab17521f60598e3097d542cded4 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering <skaapgif@gmail.com> Date: Fri, 29 Jan 2021 15:59:17 +0100 Subject: [PATCH 118/163] Fix rendering of Saved object indices and aliases per {kib} version table (#89700) Collapsing rows and applying vertical alignment doesn't work well when published to the docs website. So I removed the last column. --- docs/setup/upgrade/upgrade-migrations.asciidoc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 7436536d22781..cc6e363872808 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -19,17 +19,16 @@ Saved objects are stored in two indices: * `.kibana_{kibana_version}_001`, or if the `kibana.index` configuration setting is set `.{kibana.index}_{kibana_version}_001`. E.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. * `.kibana_task_manager_{kibana_version}_001`, or if the `xpack.tasks.index` configuration setting is set `.{xpack.tasks.index}_{kibana_version}_001` E.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. -The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date version indices. +The index aliases `.kibana` and `.kibana_task_manager` will always point to +the most up-to-date saved object indices. The first time a newer {kib} starts, it will first perform an upgrade migration before starting plugins or serving HTTP traffic. To prevent losing acknowledged writes old nodes should be shutdown before starting the upgrade. To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later will add a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}. .Saved object indices and aliases per {kib} version [options="header"] -[cols="a,a,a"] |======================= -|Upgrading from version | Outdated index (alias) | Upgraded index (alias) -| 6.0.0 through 6.4.x | `.kibana` 1.3+^.^| `.kibana_7.12.0_001` -(`.kibana` alias) +|Upgrading from version | Outdated index (alias) +| 6.0.0 through 6.4.x | `.kibana` `.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias) | 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias) From e7cbdd3050d65c8e875e7baeac25983cfbb7b733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= <alejandro.haro@elastic.co> Date: Fri, 29 Jan 2021 15:00:39 +0000 Subject: [PATCH 119/163] @kbn/telemetry-tools: Better CI error message (#89688) --- .../src/tools/tasks/check_matching_schemas_task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts index b6dcd40b53d2e..08bfa5eb404ca 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts @@ -23,7 +23,7 @@ export function checkMatchingSchemasTask({ roots }: TaskContext, throwOnDiff: bo root.esMappingDiffs = Object.keys(differences); if (root.esMappingDiffs.length && throwOnDiff) { throw Error( - `The following changes must be persisted in ${fullPath} file. Use '--fix' to update.\n${JSON.stringify( + `The following changes must be persisted in ${fullPath} file. Run 'node scripts/telemetry_check --fix' to update.\n${JSON.stringify( differences, null, 2 From a08895dbfc20ea883afb526a8597014ded4571b7 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez <melissa.alvarez@elastic.co> Date: Fri, 29 Jan 2021 10:42:35 -0500 Subject: [PATCH 120/163] [ML] Anomaly Detection: add anomalies map to explorer for jobs with 'lat_long' function (#88416) * wip: create embedded map component for explorer * add embeddedMap component to explorer * use geo_results * remove charts callout when map is shown * add translation, round geo coordinates * create GEO_MAP chart type and move embedded map to charts area * remove embedded map that is no longer used * fix type and fail silently if plugin not available * fix multiple type of jobs charts view * fix tooltip function and remove single viewer link for latlong * ensure diff types of jobs show correct charts. fix jest test * show errorCallout if maps not enabled and is lat_long job * use shared MlEmbeddedMapComponent in explorer * ensure latLong jobs not viewable in single metric viewer * update jest test --- x-pack/plugins/ml/common/util/job_utils.ts | 12 ++ .../ml_embedded_map/ml_embedded_map.tsx | 11 +- .../explorer_chart_embedded_map.tsx | 33 ++++ .../explorer_charts_container.js | 76 +++++++-- .../explorer_charts_container_service.js | 111 +++++++++--- .../explorer/explorer_charts/map_config.ts | 161 ++++++++++++++++++ .../explorer/explorer_constants.ts | 1 + .../application/explorer/explorer_utils.js | 1 + .../advanced_detector_modal/descriptions.tsx | 2 +- .../results_service/result_service_rx.ts | 3 +- .../application/util/chart_config_builder.js | 5 +- .../ml/public/application/util/chart_utils.js | 5 + 12 files changed, 364 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 4f4d9851c4957..d20ad4a368948 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -78,6 +78,18 @@ export function isTimeSeriesViewDetector(job: CombinedJob, detectorIndex: number ); } +// Returns a flag to indicate whether the specified job is suitable for embedded map viewing. +export function isMappableJob(job: CombinedJob, detectorIndex: number): boolean { + let isMappable = false; + const { detectors } = job.analysis_config; + if (detectorIndex >= 0 && detectorIndex < detectors.length) { + const dtr = detectors[detectorIndex]; + const functionName = dtr.function; + isMappable = functionName === ML_JOB_AGGREGATION.LAT_LONG; + } + return isMappable; +} + // Returns a flag to indicate whether the source data can be plotted in a time // series chart for the specified detector. export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex: number): boolean { diff --git a/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx b/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx index d5fdc9d52a102..12c7d6ac69bb1 100644 --- a/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { htmlIdGenerator } from '@elastic/eui'; import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; +import { INITIAL_LOCATION } from '../../../../../maps/common/constants'; import { MapEmbeddable, MapEmbeddableInput, @@ -81,21 +82,13 @@ export function MlEmbeddedMapComponent({ viewMode: ViewMode.VIEW, isLayerTOCOpen: false, hideFilterActions: true, - // Zoom Lat/Lon values are set to make sure map is in center in the panel - // It will also omit Greenland/Antarctica etc. NOTE: Can be removed when initialLocation is set - mapCenter: { - lon: 11, - lat: 20, - zoom: 1, - }, // can use mapSettings to center map on anomalies mapSettings: { disableInteractive: false, hideToolbarOverlay: false, hideLayerControl: false, hideViewControl: false, - // Doesn't currently work with GEO_JSON. Will uncomment when https://github.com/elastic/kibana/pull/88294 is in - // initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS, // this will startup based on data-extent + initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS, // this will startup based on data-extent autoFitToDataBounds: true, // this will auto-fit when there are changes to the filter and/or query }, }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx new file mode 100644 index 0000000000000..fc1621e962f36 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; +import { Dictionary } from '../../../../common/types/common'; +import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; +import { getMLAnomaliesActualLayer, getMLAnomaliesTypicalLayer } from './map_config'; +import { MlEmbeddedMapComponent } from '../../components/ml_embedded_map'; +interface Props { + seriesConfig: Dictionary<any>; +} + +export function EmbeddedMapComponentWrapper({ seriesConfig }: Props) { + const [layerList, setLayerList] = useState<LayerDescriptor[]>([]); + + useEffect(() => { + if (seriesConfig.mapData && seriesConfig.mapData.length > 0) { + setLayerList([ + getMLAnomaliesActualLayer(seriesConfig.mapData), + getMLAnomaliesTypicalLayer(seriesConfig.mapData), + ]); + } + }, [seriesConfig]); + + return ( + <div data-test-subj="xpack.ml.explorer.embeddedMap" style={{ width: '100%', height: 300 }}> + <MlEmbeddedMapComponent layerList={layerList} /> + </div> + ); +} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 774372f678c9b..9921b5f991844 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -22,6 +22,7 @@ import { } from '../../util/chart_utils'; import { ExplorerChartDistribution } from './explorer_chart_distribution'; import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; +import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map'; import { ExplorerChartLabel } from './components/explorer_chart_label'; import { CHART_TYPE } from '../explorer_constants'; @@ -30,6 +31,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { MlTooltipComponent } from '../../components/chart_tooltip'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; +import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; import { ExplorerChartsErrorCallOuts } from './explorer_charts_error_callouts'; @@ -43,6 +45,9 @@ const textViewButton = i18n.translate( defaultMessage: 'Open in Single Metric Viewer', } ); +const mapsPluginMessage = i18n.translate('xpack.ml.explorer.charts.mapsPluginMissingMessage', { + defaultMessage: 'maps or embeddable start plugin not found', +}); // create a somewhat unique ID // from charts metadata for React's key attribute @@ -67,8 +72,8 @@ function ExplorerChartContainer({ useEffect(() => { let isCancelled = false; const generateLink = async () => { - const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); - if (!isCancelled) { + if (!isCancelled && series.functionDescription !== ML_JOB_AGGREGATION.LAT_LONG) { + const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); setExplorerSeriesLink(singleMetricViewerLink); } }; @@ -150,6 +155,18 @@ function ExplorerChartContainer({ </EuiFlexItem> </EuiFlexGroup> {(() => { + if (chartType === CHART_TYPE.GEO_MAP) { + return ( + <MlTooltipComponent> + {(tooltipService) => ( + <EmbeddedMapComponentWrapper + seriesConfig={series} + tooltipService={tooltipService} + /> + )} + </MlTooltipComponent> + ); + } if ( chartType === CHART_TYPE.EVENT_DISTRIBUTION || chartType === CHART_TYPE.POPULATION_DISTRIBUTION @@ -167,18 +184,20 @@ function ExplorerChartContainer({ </MlTooltipComponent> ); } - return ( - <MlTooltipComponent> - {(tooltipService) => ( - <ExplorerChartSingleMetric - tooManyBuckets={tooManyBuckets} - seriesConfig={series} - severity={severity} - tooltipService={tooltipService} - /> - )} - </MlTooltipComponent> - ); + if (chartType === CHART_TYPE.SINGLE_METRIC) { + return ( + <MlTooltipComponent> + {(tooltipService) => ( + <ExplorerChartSingleMetric + tooManyBuckets={tooManyBuckets} + seriesConfig={series} + severity={severity} + tooltipService={tooltipService} + /> + )} + </MlTooltipComponent> + ); + } })()} </React.Fragment> ); @@ -199,8 +218,31 @@ export const ExplorerChartsContainerUI = ({ share: { urlGenerators: { getUrlGenerator }, }, + embeddable: embeddablePlugin, + maps: mapsPlugin, }, } = kibana; + + let seriesToPlotFiltered; + + if (!embeddablePlugin || !mapsPlugin) { + seriesToPlotFiltered = []; + // Show missing plugin callout + seriesToPlot.forEach((series) => { + if (series.functionDescription === 'lat_long') { + if (errorMessages[mapsPluginMessage] === undefined) { + errorMessages[mapsPluginMessage] = new Set([series.jobId]); + } else { + errorMessages[mapsPluginMessage].add(series.jobId); + } + } else { + seriesToPlotFiltered.push(series); + } + }); + } + + const seriesToUse = seriesToPlotFiltered !== undefined ? seriesToPlotFiltered : seriesToPlot; + const mlUrlGenerator = useMemo(() => getUrlGenerator(ML_APP_URL_GENERATOR), [getUrlGenerator]); // <EuiFlexGrid> doesn't allow a setting of `columns={1}` when chartsPerRow would be 1. @@ -208,13 +250,13 @@ export const ExplorerChartsContainerUI = ({ const chartsWidth = chartsPerRow === 1 ? 'calc(100% - 20px)' : 'auto'; const chartsColumns = chartsPerRow === 1 ? 0 : chartsPerRow; - const wrapLabel = seriesToPlot.some((series) => isLabelLengthAboveThreshold(series)); + const wrapLabel = seriesToUse.some((series) => isLabelLengthAboveThreshold(series)); return ( <> <ExplorerChartsErrorCallOuts errorMessagesByType={errorMessages} /> <EuiFlexGrid columns={chartsColumns}> - {seriesToPlot.length > 0 && - seriesToPlot.map((series) => ( + {seriesToUse.length > 0 && + seriesToUse.map((series) => ( <EuiFlexItem key={getChartId(series)} className="ml-explorer-chart-container" diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 3dc1c0234584d..077e60db4760a 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -22,12 +22,14 @@ import { isSourceDataChartableForDetector, isModelPlotChartableForDetector, isModelPlotEnabled, + isMappableJob, } from '../../../../common/util/job_utils'; import { mlResultsService } from '../../services/results_service'; import { mlJobService } from '../../services/job_service'; import { explorerService } from '../explorer_dashboard_service'; import { CHART_TYPE } from '../explorer_constants'; +import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { i18n } from '@kbn/i18n'; import { SWIM_LANE_LABEL_WIDTH } from '../swimlane_container'; @@ -77,7 +79,50 @@ export const anomalyDataChange = function ( // For now just take first 6 (or 8 if 4 charts per row). const maxSeriesToPlot = Math.max(chartsPerRow * 2, 6); const recordsToPlot = allSeriesRecords.slice(0, maxSeriesToPlot); + const hasGeoData = recordsToPlot.find( + (record) => + (record.function_description || recordsToPlot.function) === ML_JOB_AGGREGATION.LAT_LONG + ); + const seriesConfigs = recordsToPlot.map(buildConfig); + const seriesConfigsNoGeoData = []; + + // initialize the charts with loading indicators + data.seriesToPlot = seriesConfigs.map((config) => ({ + ...config, + loading: true, + chartData: null, + })); + + const mapData = []; + + if (hasGeoData !== undefined) { + for (let i = 0; i < seriesConfigs.length; i++) { + const config = seriesConfigs[i]; + let records; + if (config.detectorLabel.includes(ML_JOB_AGGREGATION.LAT_LONG)) { + if (config.entityFields.length) { + records = [ + recordsToPlot.find((record) => { + const entityFieldName = config.entityFields[0].fieldName; + const entityFieldValue = config.entityFields[0].fieldValue; + return (record[entityFieldName] && record[entityFieldName][0]) === entityFieldValue; + }), + ]; + } else { + records = recordsToPlot; + } + + mapData.push({ + ...config, + loading: false, + mapData: records, + }); + } else { + seriesConfigsNoGeoData.push(config); + } + } + } // Calculate the time range of the charts, which is a function of the chart width and max job bucket span. data.tooManyBuckets = false; @@ -92,13 +137,6 @@ export const anomalyDataChange = function ( ); data.tooManyBuckets = tooManyBuckets; - // initialize the charts with loading indicators - data.seriesToPlot = seriesConfigs.map((config) => ({ - ...config, - loading: true, - chartData: null, - })); - data.errorMessages = errorMessages; explorerService.setCharts({ ...data }); @@ -269,22 +307,27 @@ export const anomalyDataChange = function ( // only after that trigger data processing and page render. // TODO - if query returns no results e.g. source data has been deleted, // display a message saying 'No data between earliest/latest'. - const seriesPromises = seriesConfigs.map((seriesConfig) => - Promise.all([ - getMetricData(seriesConfig, chartRange), - getRecordsForCriteria(seriesConfig, chartRange), - getScheduledEvents(seriesConfig, chartRange), - getEventDistribution(seriesConfig, chartRange), - ]) - ); + const seriesPromises = []; + // Use seriesConfigs list without geo data config so indices match up after seriesPromises are resolved and we map through the responses + const seriesCongifsForPromises = hasGeoData ? seriesConfigsNoGeoData : seriesConfigs; + seriesCongifsForPromises.forEach((seriesConfig) => { + seriesPromises.push( + Promise.all([ + getMetricData(seriesConfig, chartRange), + getRecordsForCriteria(seriesConfig, chartRange), + getScheduledEvents(seriesConfig, chartRange), + getEventDistribution(seriesConfig, chartRange), + ]) + ); + }); function processChartData(response, seriesIndex) { const metricData = response[0].results; const records = response[1].records; - const jobId = seriesConfigs[seriesIndex].jobId; + const jobId = seriesCongifsForPromises[seriesIndex].jobId; const scheduledEvents = response[2].events[jobId]; const eventDistribution = response[3]; - const chartType = getChartType(seriesConfigs[seriesIndex]); + const chartType = getChartType(seriesCongifsForPromises[seriesIndex]); // Sort records in ascending time order matching up with chart data records.sort((recordA, recordB) => { @@ -409,16 +452,25 @@ export const anomalyDataChange = function ( ); const overallChartLimits = chartLimits(allDataPoints); - data.seriesToPlot = response.map((d, i) => ({ - ...seriesConfigs[i], - loading: false, - chartData: processedData[i], - plotEarliest: chartRange.min, - plotLatest: chartRange.max, - selectedEarliest: selectedEarliestMs, - selectedLatest: selectedLatestMs, - chartLimits: USE_OVERALL_CHART_LIMITS ? overallChartLimits : chartLimits(processedData[i]), - })); + data.seriesToPlot = response.map((d, i) => { + return { + ...seriesCongifsForPromises[i], + loading: false, + chartData: processedData[i], + plotEarliest: chartRange.min, + plotLatest: chartRange.max, + selectedEarliest: selectedEarliestMs, + selectedLatest: selectedLatestMs, + chartLimits: USE_OVERALL_CHART_LIMITS + ? overallChartLimits + : chartLimits(processedData[i]), + }; + }); + + if (mapData.length) { + // push map data in if it's available + data.seriesToPlot.push(...mapData); + } explorerService.setCharts({ ...data }); }) .catch((error) => { @@ -447,7 +499,10 @@ function processRecordsForDisplay(anomalyRecords) { return; } - let isChartable = isSourceDataChartableForDetector(job, record.detector_index); + let isChartable = + isSourceDataChartableForDetector(job, record.detector_index) || + isMappableJob(job, record.detector_index); + if (isChartable === false) { if (isModelPlotChartableForDetector(job, record.detector_index)) { // Check if model plot is enabled for this job. diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts new file mode 100644 index 0000000000000..451fa602315d7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts @@ -0,0 +1,161 @@ +/* + * 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 { FIELD_ORIGIN, STYLE_TYPE } from '../../../../../maps/common/constants'; +import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../../common'; + +const FEATURE = 'Feature'; +const POINT = 'Point'; +const SEVERITY_COLOR_RAMP = [ + { + stop: ANOMALY_THRESHOLD.LOW, + color: SEVERITY_COLORS.WARNING, + }, + { + stop: ANOMALY_THRESHOLD.MINOR, + color: SEVERITY_COLORS.MINOR, + }, + { + stop: ANOMALY_THRESHOLD.MAJOR, + color: SEVERITY_COLORS.MAJOR, + }, + { + stop: ANOMALY_THRESHOLD.CRITICAL, + color: SEVERITY_COLORS.CRITICAL, + }, +]; + +function getAnomalyFeatures(anomalies: any[], type: 'actual_point' | 'typical_point') { + const anomalyFeatures = []; + for (let i = 0; i < anomalies.length; i++) { + const anomaly = anomalies[i]; + const geoResults = anomaly.geo_results || (anomaly?.causes && anomaly?.causes[0]?.geo_results); + const coordinateStr = geoResults && geoResults[type]; + if (coordinateStr !== undefined) { + // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs + const coordinates = coordinateStr + .split(',') + .map((point: string) => Number(point)) + .reverse(); + + anomalyFeatures.push({ + type: FEATURE, + geometry: { + type: POINT, + coordinates, + }, + properties: { + record_score: Math.floor(anomaly.record_score), + [type]: coordinates.map((point: number) => point.toFixed(2)), + }, + }); + } + } + return anomalyFeatures; +} + +export const getMLAnomaliesTypicalLayer = (anomalies: any) => { + return { + id: 'anomalies_typical_layer', + label: 'Typical', + sourceDescriptor: { + id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854e', + type: 'GEOJSON_FILE', + __featureCollection: { + features: getAnomalyFeatures(anomalies, 'typical_point'), + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#98A2B2', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; + +export const getMLAnomaliesActualLayer = (anomalies: any) => { + return { + id: 'anomalies_actual_layer', + label: 'Actual', + sourceDescriptor: { + id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854d', + type: 'GEOJSON_FILE', + __fields: [ + { + name: 'record_score', + type: 'number', + }, + ], + __featureCollection: { + features: getAnomalyFeatures(anomalies, 'actual_point'), + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + customColorRamp: SEVERITY_COLOR_RAMP, + field: { + name: 'record_score', + origin: FIELD_ORIGIN.SOURCE, + }, + useCustomColorRamp: true, + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts index 3f5f016fc365a..2178c837458e9 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -48,6 +48,7 @@ export const CHART_TYPE = { EVENT_DISTRIBUTION: 'event_distribution', POPULATION_DISTRIBUTION: 'population_distribution', SINGLE_METRIC: 'single_metric', + GEO_MAP: 'geo_map', }; export const MAX_CATEGORY_EXAMPLES = 10; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index f6889c9a6f24c..4ba9d4ea14f10 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -511,6 +511,7 @@ export async function loadAnomaliesTableData( const entityFields = getEntityFieldList(anomaly.source); isChartable = isModelPlotEnabled(job, anomaly.detectorIndex, entityFields); } + anomaly.isTimeSeriesViewRecord = isChartable; if (mlJobService.customUrlsByJob[jobId] !== undefined) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx index 280ac85a5a2bc..470fe11759d27 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx @@ -46,7 +46,7 @@ export const FieldDescription: FC = memo(({ children }) => { description={ <FormattedMessage id="xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorModal.fieldSelect.description" - defaultMessage="Required for functions: sum, mean, median, max, min, info_content, distinct_count." + defaultMessage="Required for functions: sum, mean, median, max, min, info_content, distinct_count, lat_long." /> } > diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 514449385bf0b..3747e84f43765 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -156,7 +156,8 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { } body.aggs.byTime.aggs = {}; - if (metricFieldName !== undefined && metricFieldName !== '') { + + if (metricFieldName !== undefined && metricFieldName !== '' && metricFunction) { const metricAgg: any = { [metricFunction]: {}, }; diff --git a/x-pack/plugins/ml/public/application/util/chart_config_builder.js b/x-pack/plugins/ml/public/application/util/chart_config_builder.js index a30280f1220c0..a306211defc87 100644 --- a/x-pack/plugins/ml/public/application/util/chart_config_builder.js +++ b/x-pack/plugins/ml/public/application/util/chart_config_builder.js @@ -24,7 +24,10 @@ export function buildConfigFromDetector(job, detectorIndex) { const config = { jobId: job.job_id, detectorIndex: detectorIndex, - metricFunction: mlFunctionToESAggregation(detector.function), + metricFunction: + detector.function === ML_JOB_AGGREGATION.LAT_LONG + ? ML_JOB_AGGREGATION.LAT_LONG + : mlFunctionToESAggregation(detector.function), timeField: job.data_description.time_field, interval: job.analysis_config.bucket_span, datafeedConfig: job.datafeed_config, diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 402c922a0034f..799187cc37dfd 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -176,6 +176,11 @@ const POPULATION_DISTRIBUTION_ENABLED = true; // get the chart type based on its configuration export function getChartType(config) { let chartType = CHART_TYPE.SINGLE_METRIC; + + if (config.functionDescription === 'lat_long' || config.mapData !== undefined) { + return CHART_TYPE.GEO_MAP; + } + if ( EVENT_DISTRIBUTION_ENABLED && config.functionDescription === 'rare' && From 98b80484b5e22463b4e421ae5353281fd424e022 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" <christiane.heiligers@elastic.co> Date: Fri, 29 Jan 2021 09:47:15 -0700 Subject: [PATCH 121/163] Converts painlessLab to a TS project reference (#89626) --- x-pack/plugins/painless_lab/tsconfig.json | 23 +++++++++++++++++++++++ x-pack/test/tsconfig.json | 1 + x-pack/tsconfig.json | 2 ++ x-pack/tsconfig.refs.json | 1 + 4 files changed, 27 insertions(+) create mode 100644 x-pack/plugins/painless_lab/tsconfig.json diff --git a/x-pack/plugins/painless_lab/tsconfig.json b/x-pack/plugins/painless_lab/tsconfig.json new file mode 100644 index 0000000000000..a869b21e06d4d --- /dev/null +++ b/x-pack/plugins/painless_lab/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/dev_tools/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" } + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index cc36a2c93b1a0..461ebfe15b109 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -60,6 +60,7 @@ { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, { "path": "../plugins/global_search_bar/tsconfig.json" }, { "path": "../plugins/license_management/tsconfig.json" }, + { "path": "../plugins/painless_lab/tsconfig.json" }, { "path": "../plugins/watcher/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 956bd409f979d..d64b17813f660 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -38,6 +38,7 @@ "plugins/saved_objects_tagging/**/*", "plugins/global_search_bar/**/*", "plugins/license_management/**/*", + "plugins/painless_lab/**/*", "plugins/watcher/**/*", "test/**/*" ], @@ -110,6 +111,7 @@ { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, { "path": "./plugins/stack_alerts/tsconfig.json"}, { "path": "./plugins/license_management/tsconfig.json" }, + { "path": "./plugins/painless_lab/tsconfig.json" }, { "path": "./plugins/watcher/tsconfig.json" }, ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 1724cb2afbffa..694d359b6a05d 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -33,6 +33,7 @@ { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, { "path": "./plugins/global_search_bar/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, + { "path": "./plugins/painless_lab/tsconfig.json" }, { "path": "./plugins/watcher/tsconfig.json" } ] } From d7b1cbbed5bc74175f4b81ae56a08377eb92cd20 Mon Sep 17 00:00:00 2001 From: Lukas Olson <olson.lukas@gmail.com> Date: Fri, 29 Jan 2021 10:01:35 -0700 Subject: [PATCH 122/163] [data.search.searchSource] Add fetch$ observable for partial results (#89211) * [data.search.searchSource] Add fetch$ observable for partial results * Fix mocks & add tests * Update docs * Update docs * Review feedback --- ...-plugins-data-public.searchsource.fetch.md | 6 +- ...plugins-data-public.searchsource.fetch_.md | 24 ++++++++ ...plugin-plugins-data-public.searchsource.md | 1 + .../data/common/search/search_source/mocks.ts | 3 +- .../search_source/search_source.test.ts | 37 +++++++++++- .../search/search_source/search_source.ts | 58 ++++++++++--------- src/plugins/data/public/public.api.md | 4 +- 7 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md index 8fd17e6b1a1d9..e96fe8b8e08dc 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md @@ -4,8 +4,12 @@ ## SearchSource.fetch() method -Fetch this source and reject the returned Promise on error +> Warning: This API is now obsolete. +> +> Use fetch$ instead +> +Fetch this source and reject the returned Promise on error <b>Signature:</b> diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md new file mode 100644 index 0000000000000..bcf220a9a27e6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md @@ -0,0 +1,24 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [fetch$](./kibana-plugin-plugins-data-public.searchsource.fetch_.md) + +## SearchSource.fetch$() method + +Fetch this source from Elasticsearch, returning an observable over the response(s) + +<b>Signature:</b> + +```typescript +fetch$(options?: ISearchOptions): import("rxjs").Observable<import("elasticsearch").SearchResponse<any>>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | <code>ISearchOptions</code> | | + +<b>Returns:</b> + +`import("rxjs").Observable<import("elasticsearch").SearchResponse<any>>` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md index df302e9f3b0d3..2af9cc14e3668 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md @@ -33,6 +33,7 @@ export declare class SearchSource | [createCopy()](./kibana-plugin-plugins-data-public.searchsource.createcopy.md) | | creates a copy of this search source (without its children) | | [destroy()](./kibana-plugin-plugins-data-public.searchsource.destroy.md) | | Completely destroy the SearchSource. {<!-- -->undefined<!-- -->} | | [fetch(options)](./kibana-plugin-plugins-data-public.searchsource.fetch.md) | | Fetch this source and reject the returned Promise on error | +| [fetch$(options)](./kibana-plugin-plugins-data-public.searchsource.fetch_.md) | | Fetch this source from Elasticsearch, returning an observable over the response(s) | | [getField(field, recurse)](./kibana-plugin-plugins-data-public.searchsource.getfield.md) | | Gets a single field from the fields | | [getFields()](./kibana-plugin-plugins-data-public.searchsource.getfields.md) | | returns all search source fields | | [getId()](./kibana-plugin-plugins-data-public.searchsource.getid.md) | | returns search source id | diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index 328f05fac8594..08fe2b07096bb 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; import type { MockedKeys } from '@kbn/utility-types/jest'; import { uiSettingsServiceMock } from '../../../../../core/public/mocks'; @@ -27,6 +27,7 @@ export const searchSourceInstanceMock: MockedKeys<ISearchSource> = { createChild: jest.fn().mockReturnThis(), setParent: jest.fn(), getParent: jest.fn().mockReturnThis(), + fetch$: jest.fn().mockReturnValue(of({})), fetch: jest.fn().mockResolvedValue({}), onRequestStart: jest.fn(), getSearchRequestBody: jest.fn(), diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 6d7654c6659f2..c2a4beb9b61a5 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -51,7 +51,14 @@ describe('SearchSource', () => { let searchSource: SearchSource; beforeEach(() => { - mockSearchMethod = jest.fn().mockReturnValue(of({ rawResponse: '' })); + mockSearchMethod = jest + .fn() + .mockReturnValue( + of( + { rawResponse: { isPartial: true, isRunning: true } }, + { rawResponse: { isPartial: false, isRunning: false } } + ) + ); searchSourceDependencies = { getConfig: jest.fn(), @@ -564,6 +571,34 @@ describe('SearchSource', () => { await searchSource.fetch(options); expect(mockSearchMethod).toBeCalledTimes(1); }); + + test('should return partial results', (done) => { + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + const options = {}; + + const next = jest.fn(); + const complete = () => { + expect(next).toBeCalledTimes(2); + expect(next.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "isPartial": true, + "isRunning": true, + }, + ] + `); + expect(next.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "isPartial": false, + "isRunning": false, + }, + ] + `); + done(); + }; + searchSource.fetch$(options).subscribe({ next, complete }); + }); }); describe('#serialize', () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 554e8385881f2..bb60f0d7b4ad4 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -60,7 +60,8 @@ import { setWith } from '@elastic/safer-lodash-set'; import { uniqueId, keyBy, pick, difference, omit, isObject, isFunction } from 'lodash'; -import { map } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { defer, from } from 'rxjs'; import { normalizeSortRequest } from './normalize_sort_request'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern } from '../../index_patterns'; @@ -244,30 +245,35 @@ export class SearchSource { } /** - * Fetch this source and reject the returned Promise on error - * - * @async + * Fetch this source from Elasticsearch, returning an observable over the response(s) + * @param options */ - async fetch(options: ISearchOptions = {}) { + fetch$(options: ISearchOptions = {}) { const { getConfig } = this.dependencies; - await this.requestIsStarting(options); - - const searchRequest = await this.flatten(); - this.history = [searchRequest]; - - let response; - if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) { - response = await this.legacyFetch(searchRequest, options); - } else { - response = await this.fetchSearch(searchRequest, options); - } - - // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved - if ((response as any).error) { - throw new RequestFailure(null, response); - } + return defer(() => this.requestIsStarting(options)).pipe( + switchMap(() => { + const searchRequest = this.flatten(); + this.history = [searchRequest]; + + return getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES) + ? from(this.legacyFetch(searchRequest, options)) + : this.fetchSearch$(searchRequest, options); + }), + tap((response) => { + // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved + if ((response as any).error) { + throw new RequestFailure(null, response); + } + }) + ); + } - return response; + /** + * Fetch this source and reject the returned Promise on error + * @deprecated Use fetch$ instead + */ + fetch(options: ISearchOptions = {}) { + return this.fetch$(options).toPromise(); } /** @@ -305,16 +311,16 @@ export class SearchSource { * Run a search using the search service * @return {Promise<SearchResponse<unknown>>} */ - private fetchSearch(searchRequest: SearchRequest, options: ISearchOptions) { + private fetchSearch$(searchRequest: SearchRequest, options: ISearchOptions) { const { search, getConfig, onResponse } = this.dependencies; const params = getSearchParamsFromRequest(searchRequest, { getConfig, }); - return search({ params, indexType: searchRequest.indexType }, options) - .pipe(map(({ rawResponse }) => onResponse(searchRequest, rawResponse))) - .toPromise(); + return search({ params, indexType: searchRequest.indexType }, options).pipe( + map(({ rawResponse }) => onResponse(searchRequest, rawResponse)) + ); } /** diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 9e493f46b0781..5b1462e5d506b 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2360,6 +2360,8 @@ export class SearchSource { createChild(options?: {}): SearchSource; createCopy(): SearchSource; destroy(): void; + fetch$(options?: ISearchOptions): import("rxjs").Observable<import("elasticsearch").SearchResponse<any>>; + // @deprecated fetch(options?: ISearchOptions): Promise<import("elasticsearch").SearchResponse<any>>; getField<K extends keyof SearchSourceFields>(field: K, recurse?: boolean): SearchSourceFields[K]; getFields(): { @@ -2601,7 +2603,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:139:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/search/search_source/search_source.ts:186:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/search_source/search_source.ts:187:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts From 61d4d870e28bb99f7cad88cbef71fa5a6b32ccf6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas <christos.nasikas@elastic.co> Date: Fri, 29 Jan 2021 19:19:19 +0200 Subject: [PATCH 123/163] [Security Solution][Case] Allow users with Gold license to use Jira (#89406) --- .../case/common/api/cases/configure.ts | 3 +- .../routes/api/__fixtures__/route_contexts.ts | 4 +- .../routes/api/__mocks__/request_responses.ts | 44 ++++++++ .../cases/configure/get_connectors.test.ts | 62 +++++++++++ .../api/cases/configure/get_connectors.ts | 17 ++- .../cases/components/all_cases/index.test.tsx | 63 +++++++++++ .../configure_cases/__mock__/index.tsx | 11 +- .../components/configure_cases/button.tsx | 2 + .../components/configure_cases/index.test.tsx | 27 ++++- .../components/configure_cases/index.tsx | 22 ++-- .../use_push_to_service/helpers.tsx | 11 +- .../use_push_to_service/translations.ts | 9 +- .../containers/configure/__mocks__/api.ts | 6 +- .../cases/containers/configure/api.test.ts | 29 ++++- .../public/cases/containers/configure/api.ts | 11 ++ .../public/cases/containers/configure/mock.ts | 45 ++++++++ .../cases/containers/configure/types.ts | 11 +- .../configure/use_action_types.test.tsx | 101 ++++++++++++++++++ .../containers/configure/use_action_types.tsx | 72 +++++++++++++ .../public/cases/containers/mock.ts | 7 ++ .../use_get_action_license.test.tsx | 2 +- .../containers/use_get_action_license.tsx | 5 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 24 files changed, 539 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index b82c6de8fc363..398f73f2721a6 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -6,11 +6,12 @@ import * as rt from 'io-ts'; -import { ActionResult } from '../../../../actions/common'; +import { ActionResult, ActionType } from '../../../../actions/common'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; export type ActionConnector = ActionResult; +export type ActionTypeConnector = ActionType; // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index 40911496d6494..3a12b50cf8f68 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -14,13 +14,15 @@ import { CaseConfigureService, ConnectorMappingsService, } from '../../../services'; -import { getActions } from '../__mocks__/request_responses'; +import { getActions, getActionTypes } from '../__mocks__/request_responses'; import { authenticationMock } from '../__fixtures__'; import type { CasesRequestHandlerContext } from '../../../types'; export const createRouteContext = async (client: any, badAuth = false) => { const actionsMock = actionsClientMock.create(); actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + actionsMock.listTypes.mockImplementation(() => Promise.resolve(getActionTypes())); + const log = loggingSystemMock.create().get('case'); const esClientMock = elasticsearchServiceMock.createClusterClient(); diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index efc3b6044a804..236deb9c7462c 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { + ActionTypeConnector, CasePostRequest, CasesConfigureRequest, ConnectorTypes, @@ -73,6 +74,49 @@ export const getActions = (): FindActionResult[] => [ }, ]; +export const getActionTypes = (): ActionTypeConnector[] => [ + { + id: '.email', + name: 'Email', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.index', + name: 'Index', + minimumLicenseRequired: 'basic', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.servicenow', + name: 'ServiceNow', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.jira', + name: 'Jira', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.resilient', + name: 'IBM Resilient', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + enabledInLicense: true, + }, +]; + export const newConfiguration: CasesConfigureRequest = { connector: { id: '456', diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index b744a6dc04810..974ae9283dd98 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -42,10 +42,72 @@ describe('GET connectors', () => { expect(res.status).toEqual(200); const expected = getActions(); + // The first connector returned by getActions is of type .webhook and we expect to be filtered expected.shift(); expect(res.payload).toEqual(expected); }); + it('filters out connectors that are not enabled in license', async () => { + const req = httpServerMock.createKibanaRequest({ + path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, + method: 'get', + }); + + const context = await createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, + }) + ); + + const actionsClient = context.actions.getActionsClient(); + (actionsClient.listTypes as jest.Mock).mockImplementation(() => + Promise.resolve([ + { + id: '.servicenow', + name: 'ServiceNow', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + // User does not have a platinum license + enabledInLicense: false, + }, + { + id: '.jira', + name: 'Jira', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.resilient', + name: 'IBM Resilient', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + // User does not have a platinum license + enabledInLicense: false, + }, + ]) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual([ + { + id: '456', + actionTypeId: '.jira', + name: 'Connector without isCaseOwned', + config: { + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + ]); + }); + it('it throws an error when actions client is null', async () => { const req = httpServerMock.createKibanaRequest({ path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index cb88f04a9b835..cf854df9f04f2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { ActionType } from '../../../../../../actions/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../../../actions/server/types'; @@ -17,10 +18,13 @@ import { RESILIENT_ACTION_TYPE_ID, } from '../../../../../common/constants'; -const isConnectorSupported = (action: FindActionResult): boolean => +const isConnectorSupported = ( + action: FindActionResult, + actionTypes: Record<string, ActionType> +): boolean => [SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( action.actionTypeId - ); + ) && actionTypes[action.actionTypeId]?.enabledInLicense; /* * Be aware that this api will only return 20 connectors @@ -40,7 +44,14 @@ export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter(isConnectorSupported); + const actionTypes = (await actionsClient.listTypes()).reduce( + (types, type) => ({ ...types, [type.id]: type }), + {} + ); + + const results = (await actionsClient.getAll()).filter((action) => + isConnectorSupported(action, actionTypes) + ); return response.ok({ body: results }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 71fd74570c16a..009053067064a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -20,6 +20,7 @@ import { useDeleteCases } from '../../containers/use_delete_cases'; import { useGetCases } from '../../containers/use_get_cases'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { useUpdateCases } from '../../containers/use_bulk_update_case'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; import { getCasesColumns } from './columns'; import { AllCases } from '.'; @@ -27,12 +28,14 @@ jest.mock('../../containers/use_bulk_update_case'); jest.mock('../../containers/use_delete_cases'); jest.mock('../../containers/use_get_cases'); jest.mock('../../containers/use_get_cases_status'); +jest.mock('../../containers/use_get_action_license'); const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>; const useDeleteCasesMock = useDeleteCases as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useUpdateCasesMock = useUpdateCases as jest.Mock; +const useGetActionLicenseMock = useGetActionLicense as jest.Mock; jest.mock('../../../common/components/link_to'); @@ -86,6 +89,12 @@ describe('AllCases', () => { updateBulkStatus, }; + const defaultActionLicense = { + actionLicense: null, + isLoading: false, + isError: false, + }; + let navigateToApp: jest.Mock; beforeEach(() => { @@ -96,6 +105,7 @@ describe('AllCases', () => { useGetCasesMock.mockReturnValue(defaultGetCases); useDeleteCasesMock.mockReturnValue(defaultDeleteCases); useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); + useGetActionLicenseMock.mockReturnValue(defaultActionLicense); moment.tz.setDefault('UTC'); }); @@ -398,6 +408,7 @@ describe('AllCases', () => { expect(dispatchResetIsDeleted).toBeCalled(); }); }); + it('isUpdated is true, refetch', async () => { useUpdateCasesMock.mockReturnValue({ ...defaultUpdateCases, @@ -627,4 +638,56 @@ describe('AllCases', () => { ); }); }); + + it('should not allow the user to enter configuration page with basic license', async () => { + useGetActionLicenseMock.mockReturnValue({ + ...defaultActionLicense, + actionLicense: { + id: '.jira', + name: 'Jira', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: false, + }, + }); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="configure-case-button"]').first().prop('isDisabled') + ).toBeTruthy(); + }); + }); + + it('should allow the user to enter configuration page with gold license and above', async () => { + useGetActionLicenseMock.mockReturnValue({ + ...defaultActionLicense, + actionLicense: { + id: '.jira', + name: 'Jira', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + }); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="configure-case-button"]').first().prop('isDisabled') + ).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx index b1b5f2b087eee..93890656b4a7f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ConnectorTypes } from '../../../../../../case/common/api'; import { ActionConnector } from '../../../containers/configure/types'; import { UseConnectorsResponse } from '../../../containers/configure/use_connectors'; -import { connectorsMock } from '../../../containers/configure/mock'; import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; -import { ConnectorTypes } from '../../../../../../case/common/api'; +import { UseActionTypesResponse } from '../../../containers/configure/use_action_types'; +import { connectorsMock, actionTypesMock } from '../../../containers/configure/mock'; export { mappings } from '../../../containers/configure/mock'; export const connectors: ActionConnector[] = connectorsMock; @@ -51,3 +52,9 @@ export const useConnectorsResponse: UseConnectorsResponse = { connectors, refetchConnectors: jest.fn(), }; + +export const useActionTypesResponse: UseActionTypesResponse = { + loading: false, + actionTypes: actionTypesMock, + refetchActionTypes: jest.fn(), +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx index 44767471dd9e7..9f3dcd168ba5f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx @@ -38,6 +38,7 @@ const ConfigureCaseButtonComponent: React.FC<ConfigureCaseButtonProps> = ({ }, [history, urlSearch] ); + const configureCaseButton = useMemo( () => ( <LinkButton @@ -53,6 +54,7 @@ const ConfigureCaseButtonComponent: React.FC<ConfigureCaseButtonProps> = ({ ), [label, isDisabled, formatUrl, goToCaseConfigure] ); + return showToolTip ? ( <EuiToolTip position="top" diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index 2656e2496c2fc..dbdf3d914efab 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -22,20 +22,29 @@ import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/publi import { useKibana } from '../../../common/lib/kibana'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useActionTypes } from '../../containers/configure/use_action_types'; import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { connectors, searchURL, useCaseConfigureResponse, useConnectorsResponse } from './__mock__'; +import { + connectors, + searchURL, + useCaseConfigureResponse, + useConnectorsResponse, + useActionTypesResponse, +} from './__mock__'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/configure/use_configure'); +jest.mock('../../containers/configure/use_action_types'); jest.mock('../../../common/components/navigation/use_get_url_search'); const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>; const useConnectorsMock = useConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; +const useActionTypesMock = useActionTypes as jest.Mock; describe('ConfigureCases', () => { beforeEach(() => { @@ -83,6 +92,8 @@ describe('ConfigureCases', () => { /> )), } as unknown) as TriggersAndActionsUIPublicPluginStart; + + useActionTypesMock.mockImplementation(() => useActionTypesResponse); }); describe('rendering', () => { @@ -265,10 +276,12 @@ describe('ConfigureCases', () => { closureType: 'close-by-user', }, })); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, loading: true, })); + useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); }); @@ -294,6 +307,18 @@ describe('ConfigureCases', () => { .prop('disabled') ).toBe(true); }); + + test('it shows isLoading when loading action types', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: false, + })); + + useActionTypesMock.mockImplementation(() => ({ ...useActionTypesResponse, loading: true })); + + wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + expect(wrapper.find(Connectors).prop('isLoading')).toBe(true); + }); }); describe('saving configuration', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx index d34dc168ba7a2..bc56e404c891d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx @@ -9,16 +9,16 @@ import styled, { css } from 'styled-components'; import { EuiCallOut } from '@elastic/eui'; +import { SUPPORTED_CONNECTORS } from '../../../../../case/common/constants'; import { useKibana } from '../../../common/lib/kibana'; import { useConnectors } from '../../containers/configure/use_connectors'; +import { useActionTypes } from '../../containers/configure/use_action_types'; import { useCaseConfigure } from '../../containers/configure/use_configure'; -import { ActionType } from '../../../../../triggers_actions_ui/public'; import { ClosureType } from '../../containers/configure/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ActionConnectorTableItem } from '../../../../../triggers_actions_ui/public/types'; -import { connectorsConfiguration } from '../connectors'; import { SectionWrapper } from '../wrappers'; import { Connectors } from './connectors'; @@ -49,8 +49,6 @@ const FormWrapper = styled.div` `} `; -const actionTypes: ActionType[] = Object.values(connectorsConfiguration); - interface ConfigureCasesComponentProps { userCanCrud: boolean; } @@ -78,12 +76,20 @@ const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userC } = useCaseConfigure(); const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); + const { loading: isLoadingActionTypes, actionTypes, refetchActionTypes } = useActionTypes(); + const supportedActionTypes = useMemo( + () => actionTypes.filter((actionType) => SUPPORTED_CONNECTORS.includes(actionType.id)), + [actionTypes] + ); const onConnectorUpdate = useCallback(async () => { refetchConnectors(); + refetchActionTypes(); refetchCaseConfigure(); - }, [refetchCaseConfigure, refetchConnectors]); - const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; + }, [refetchActionTypes, refetchCaseConfigure, refetchConnectors]); + + const isLoadingAny = + isLoadingConnectors || persistLoading || loadingCaseConfigure || isLoadingActionTypes; const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connector.id === 'none'; const onClickUpdateConnector = useCallback(() => { setEditFlyoutVisibility(true); @@ -154,11 +160,11 @@ const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userC triggersActionsUi.getAddConnectorFlyout({ consumer: 'case', onClose: onCloseAddFlyout, - actionTypes, + actionTypes: supportedActionTypes, reloadConnectors: onConnectorUpdate, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [supportedActionTypes] ); const ConnectorEditFlyout = useMemo( diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx index 43f2a2a6e12f1..396ce0725eb3a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx @@ -17,11 +17,16 @@ export const getLicenseError = () => ({ title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE, description: ( <FormattedMessage - defaultMessage="To open cases in external systems, you must update your license to Platinum, start a free 30-day trial, or spin up a {link} on AWS, GCP, or Azure." + defaultMessage="Opening cases in external systems is available when you have the {appropriateLicense}, are using a {cloud}, or are testing out a Free Trial." id="xpack.securitySolution.case.caseView.pushToServiceDisableByLicenseDescription" values={{ - link: ( - <EuiLink href="https://www.elastic.co/cloud/" target="_blank"> + appropriateLicense: ( + <EuiLink href="https://www.elastic.co/subscriptions" target="_blank"> + {i18n.LINK_APPROPRIATE_LICENSE} + </EuiLink> + ), + cloud: ( + <EuiLink href="https://www.elastic.co/cloud/elasticsearch-service/signup" target="_blank"> {i18n.LINK_CLOUD_DEPLOYMENT} </EuiLink> ), diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/translations.ts b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/translations.ts index f4539b8019d43..16f1b8965bb0b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/translations.ts @@ -69,7 +69,7 @@ export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate( export const PUSH_DISABLE_BY_LICENSE_TITLE = i18n.translate( 'xpack.securitySolution.case.caseView.pushToServiceDisableByLicenseTitle', { - defaultMessage: 'Upgrade to Elastic Platinum', + defaultMessage: 'Upgrade to an appropriate license', } ); @@ -80,6 +80,13 @@ export const LINK_CLOUD_DEPLOYMENT = i18n.translate( } ); +export const LINK_APPROPRIATE_LICENSE = i18n.translate( + 'xpack.securitySolution.case.caseView.appropiateLicense', + { + defaultMessage: 'appropriate license', + } +); + export const LINK_CONNECTOR_CONFIGURE = i18n.translate( 'xpack.securitySolution.case.caseView.connectorConfigureLink', { diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/__mocks__/api.ts index 257cb171a4a9a..ed2f77657fb5e 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/__mocks__/api.ts @@ -8,11 +8,12 @@ import { CasesConfigurePatch, CasesConfigureRequest, ActionConnector, + ActionTypeConnector, } from '../../../../../../case/common/api'; import { ApiProps } from '../../types'; import { CaseConfigure } from '../types'; -import { connectorsMock, caseConfigurationCamelCaseResponseMock } from '../mock'; +import { connectorsMock, caseConfigurationCamelCaseResponseMock, actionTypesMock } from '../mock'; export const fetchConnectors = async ({ signal }: ApiProps): Promise<ActionConnector[]> => Promise.resolve(connectorsMock); @@ -29,3 +30,6 @@ export const patchCaseConfigure = async ( caseConfiguration: CasesConfigurePatch, signal: AbortSignal ): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const fetchActionTypes = async ({ signal }: ApiProps): Promise<ActionTypeConnector[]> => + Promise.resolve(actionTypesMock); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts index f9115963c745d..70576482fbe89 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts @@ -5,9 +5,16 @@ */ import { KibanaServices } from '../../../common/lib/kibana'; -import { fetchConnectors, getCaseConfigure, postCaseConfigure, patchCaseConfigure } from './api'; +import { + fetchConnectors, + getCaseConfigure, + postCaseConfigure, + patchCaseConfigure, + fetchActionTypes, +} from './api'; import { connectorsMock, + actionTypesMock, caseConfigurationMock, caseConfigurationResposeMock, caseConfigurationCamelCaseResponseMock, @@ -123,4 +130,24 @@ describe('Case Configuration API', () => { expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); }); }); + + describe('fetch actionTypes', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(actionTypesMock); + }); + + test('check url, method, signal', async () => { + await fetchActionTypes({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/actions/list_action_types', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await fetchActionTypes({ signal: abortCtrl.signal }); + expect(resp).toEqual(actionTypesMock); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts index 8652e48fd834d..2b2bd1a782f75 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts @@ -7,6 +7,7 @@ import { isEmpty } from 'lodash/fp'; import { ActionConnector, + ActionTypeConnector, CasesConfigurePatch, CasesConfigureResponse, CasesConfigureRequest, @@ -16,6 +17,7 @@ import { KibanaServices } from '../../../common/lib/kibana'; import { CASE_CONFIGURE_CONNECTORS_URL, CASE_CONFIGURE_URL, + ACTION_TYPES_URL, } from '../../../../../case/common/constants'; import { ApiProps } from '../types'; @@ -89,3 +91,12 @@ export const patchCaseConfigure = async ( decodeCaseConfigureResponse(response) ); }; + +export const fetchActionTypes = async ({ signal }: ApiProps): Promise<ActionTypeConnector[]> => { + const response = await KibanaServices.get().http.fetch(ACTION_TYPES_URL, { + method: 'GET', + signal, + }); + + return response; +}; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts index fabd1187698a7..79aaaab61324e 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts @@ -6,6 +6,7 @@ import { ActionConnector, + ActionTypeConnector, CasesConfigureResponse, CasesConfigureRequest, ConnectorTypes, @@ -29,6 +30,7 @@ export const mappings: CaseConnectorMapping[] = [ actionType: 'append', }, ]; + export const connectorsMock: ActionConnector[] = [ { id: 'servicenow-1', @@ -60,6 +62,49 @@ export const connectorsMock: ActionConnector[] = [ }, ]; +export const actionTypesMock: ActionTypeConnector[] = [ + { + id: '.email', + name: 'Email', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.index', + name: 'Index', + minimumLicenseRequired: 'basic', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.servicenow', + name: 'ServiceNow', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.jira', + name: 'Jira', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.resilient', + name: 'IBM Resilient', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + enabledInLicense: true, + }, +]; + export const caseConfigurationResposeMock: CasesConfigureResponse = { created_at: '2020-04-06T13:03:18.657Z', created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts index 41acb91f2ae96..ff2441d361c2c 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts @@ -7,6 +7,7 @@ import { ElasticUser } from '../types'; import { ActionConnector, + ActionTypeConnector, ActionType, CaseConnector, CaseField, @@ -15,7 +16,15 @@ import { ThirdPartyField, } from '../../../../../case/common/api'; -export { ActionConnector, ActionType, CaseConnector, CaseField, ClosureType, ThirdPartyField }; +export { + ActionConnector, + ActionTypeConnector, + ActionType, + CaseConnector, + CaseField, + ClosureType, + ThirdPartyField, +}; export interface CaseConnectorMapping { actionType: ActionType; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.test.tsx new file mode 100644 index 0000000000000..b2213fb8fc8c4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useActionTypes, UseActionTypesResponse } from './use_action_types'; +import { actionTypesMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useActionTypes', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + actionTypes: [], + refetchActionTypes: result.current.refetchActionTypes, + }); + }); + }); + + test('fetch action types', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + actionTypes: actionTypesMock, + refetchActionTypes: result.current.refetchActionTypes, + }); + }); + }); + + test('refetch actionTypes', async () => { + const spyOnfetchActionTypes = jest.spyOn(api, 'fetchActionTypes'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.refetchActionTypes(); + expect(spyOnfetchActionTypes).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when refetching actionTypes', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.refetchActionTypes(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('unhappy path', async () => { + const spyOnfetchActionTypes = jest.spyOn(api, 'fetchActionTypes'); + spyOnfetchActionTypes.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + actionTypes: [], + refetchActionTypes: result.current.refetchActionTypes, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx new file mode 100644 index 0000000000000..980db8ed61f8f --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx @@ -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. + */ + +import { useState, useEffect, useCallback, useRef } from 'react'; + +import { useStateToaster, errorToToaster } from '../../../common/components/toasters'; +import * as i18n from '../translations'; +import { fetchActionTypes } from './api'; +import { ActionTypeConnector } from './types'; + +export interface UseActionTypesResponse { + loading: boolean; + actionTypes: ActionTypeConnector[]; + refetchActionTypes: () => void; +} + +export const useActionTypes = (): UseActionTypesResponse => { + const [, dispatchToaster] = useStateToaster(); + const [loading, setLoading] = useState(true); + const [actionTypes, setActionTypes] = useState<ActionTypeConnector[]>([]); + const didCancel = useRef(false); + const abortCtrl = useRef(new AbortController()); + const queryFirstTime = useRef(true); + + const refetchActionTypes = useCallback(async () => { + try { + setLoading(true); + didCancel.current = false; + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + const res = await fetchActionTypes({ signal: abortCtrl.current.signal }); + + if (!didCancel.current) { + setLoading(false); + setActionTypes(res); + } + } catch (error) { + if (!didCancel.current) { + setLoading(false); + setActionTypes([]); + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + } + }, [dispatchToaster]); + + useEffect(() => { + if (queryFirstTime.current) { + refetchActionTypes(); + queryFirstTime.current = false; + } + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + queryFirstTime.current = true; + }; + }, [refetchActionTypes]); + + return { + loading, + actionTypes, + refetchActionTypes, + }; +}; diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index fd24a8451fcbe..3fb962df232bc 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -199,6 +199,13 @@ export const actionLicenses: ActionLicense[] = [ enabledInConfig: true, enabledInLicense: true, }, + { + id: '.jira', + name: 'Jira', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, ]; // Snake case for mock api responses diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.test.tsx index 23c9ff5e49586..3e501a5276d5b 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.test.tsx @@ -51,7 +51,7 @@ describe('useGetActionLicense', () => { expect(result.current).toEqual({ isLoading: false, isError: false, - actionLicense: actionLicenses[0], + actionLicense: actionLicenses[1], }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx index e289a1973cf6e..8ce5c4aeef4b6 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx @@ -23,6 +23,8 @@ export const initialData: ActionLicenseState = { isError: false, }; +const MINIMUM_LICENSE_REQUIRED_CONNECTOR = '.jira'; + export const useGetActionLicense = (): ActionLicenseState => { const [actionLicenseState, setActionLicensesState] = useState<ActionLicenseState>(initialData); @@ -40,7 +42,8 @@ export const useGetActionLicense = (): ActionLicenseState => { const response = await getActionLicense(abortCtrl.signal); if (!didCancel) { setActionLicensesState({ - actionLicense: response.find((l) => l.id === '.servicenow') ?? null, + actionLicense: + response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null, isLoading: false, isError: false, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1c058245f04cd..d6aeb3a293f67 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17338,7 +17338,6 @@ "xpack.securitySolution.case.caseView.pushToServiceDisableByConfigDescription": "kibana.ymlファイルは、特定のコネクターのみを許可するように構成されています。外部システムでケースを開けるようにするには、xpack.actions.enabledActiontypes設定に.[actionTypeId](例:.servicenow | .jira)を追加します。詳細は{link}をご覧ください。", "xpack.securitySolution.case.caseView.pushToServiceDisableByConfigTitle": "Kibanaの構成ファイルで外部サービスを有効にする", "xpack.securitySolution.case.caseView.pushToServiceDisableByInvalidConnector": "外部サービスに更新を送信するために使用されるコネクターが削除されました。外部システムでケースを更新するには、別のコネクターを選択するか、新しいコネクターを作成してください。", - "xpack.securitySolution.case.caseView.pushToServiceDisableByLicenseDescription": "外部システムでケースを開くには、ライセンスをプラチナに更新するか、30日間の無料トライアルを開始するか、AWS、GCP、またはAzureで{link}にサインアップする必要があります。", "xpack.securitySolution.case.caseView.pushToServiceDisableByLicenseTitle": "E lastic Platinumへのアップグレード", "xpack.securitySolution.case.caseView.pushToServiceDisableByNoCaseConfigDescription": "外部システムでケースを開いて更新するには、このケースの外部インシデント管理システムを選択する必要があります。", "xpack.securitySolution.case.caseView.pushToServiceDisableByNoCaseConfigTitle": "外部コネクターを選択", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e7dbc0c161a37..c47be2f09ef82 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17382,7 +17382,6 @@ "xpack.securitySolution.case.caseView.pushToServiceDisableByConfigDescription": "kibana.yml 文件已配置为仅允许特定连接器。要在外部系统中打开案例,请将 .[actionTypeId](例如:.servicenow | .jira)添加到 xpack.actions.enabledActiontypes 设置。有关更多信息,请参阅{link}。", "xpack.securitySolution.case.caseView.pushToServiceDisableByConfigTitle": "在 Kibana 配置文件中启用外部服务", "xpack.securitySolution.case.caseView.pushToServiceDisableByInvalidConnector": "用于将更新发送到外部服务的连接器已删除。要在外部系统中更新案例,请选择不同的连接器或创建新的连接器。", - "xpack.securitySolution.case.caseView.pushToServiceDisableByLicenseDescription": "要在外部系统中打开案例,必须将许可证更新到白金级,开始为期 30 天的免费试用,或在 AWS、GCP 或 Azure 上快速部署 {link}。", "xpack.securitySolution.case.caseView.pushToServiceDisableByLicenseTitle": "升级到 Elastic 白金级", "xpack.securitySolution.case.caseView.pushToServiceDisableByNoCaseConfigDescription": "要在外部系统中打开和更新案例,必须为此案例选择外部事件管理系统。", "xpack.securitySolution.case.caseView.pushToServiceDisableByNoCaseConfigTitle": "选择外部连接器", From 9286b1352e25dae615b6516fc76fca4698da9bdd Mon Sep 17 00:00:00 2001 From: CJ Cenizal <cj@cenizal.com> Date: Fri, 29 Jan 2021 09:21:51 -0800 Subject: [PATCH 124/163] Rename PipelineProcessorsEditor to PipelineEditor to shorten import path to a length that Windows can handle, and to disambiguate with child component of the same name. (#89645) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../README.md | 0 .../__jest__/constants.ts | 0 .../__jest__/http_requests.helpers.ts | 0 .../__jest__/pipeline_processors_editor.helpers.tsx | 0 .../__jest__/pipeline_processors_editor.test.tsx | 0 .../__jest__/processors/processor.helpers.tsx | 0 .../__jest__/processors/uri_parts.test.tsx | 0 .../__jest__/processors_editor.tsx | 4 ++-- .../__jest__/test_pipeline.helpers.tsx | 0 .../__jest__/test_pipeline.test.tsx | 0 .../components/_shared.scss | 0 .../components/add_processor_button.tsx | 0 .../components/index.ts | 0 .../components/load_from_json/button.tsx | 0 .../components/load_from_json/index.ts | 0 .../components/load_from_json/modal_provider.test.tsx | 0 .../components/load_from_json/modal_provider.tsx | 0 .../components/on_failure_processors_title.tsx | 2 +- .../components/pipeline_processors_editor.tsx | 0 .../pipeline_processors_editor_item/context_menu.tsx | 0 .../pipeline_processors_editor_item/i18n_texts.ts | 0 .../pipeline_processors_editor_item/index.ts | 0 .../inline_text_input.tsx | 0 .../pipeline_processors_editor_item.container.tsx | 0 .../pipeline_processors_editor_item.scss | 0 .../pipeline_processors_editor_item.tsx | 0 .../pipeline_processors_editor_item/types.ts | 0 .../pipeline_processors_editor_item_status.tsx | 0 .../pipeline_processors_editor_item_tooltip/index.ts | 0 .../pipeline_processors_editor_item_toolip.scss | 0 .../pipeline_processors_editor_item_tooltip.tsx | 0 .../processor_information.tsx | 0 .../components/processor_form/add_processor_form.tsx | 0 .../processor_form/documentation_button.tsx | 0 .../components/processor_form/edit_processor_form.tsx | 0 .../field_components/drag_and_drop_text_list.scss | 0 .../field_components/drag_and_drop_text_list.tsx | 0 .../processor_form/field_components/index.ts | 0 .../processor_form/field_components/text_editor.scss | 0 .../processor_form/field_components/text_editor.tsx | 0 .../processor_form/field_components/xjson_editor.tsx | 0 .../components/processor_form/index.ts | 0 .../processor_form/processor_form.container.tsx | 0 .../processor_form/processor_output/index.ts | 0 .../processor_output/processor_output.scss | 0 .../processor_output/processor_output.tsx | 0 .../processor_form/processor_settings_fields.tsx | 0 .../components/processor_form/processors/append.tsx | 0 .../components/processor_form/processors/bytes.tsx | 0 .../components/processor_form/processors/circle.tsx | 0 .../common_fields/common_processor_fields.tsx | 0 .../processors/common_fields/field_name_field.tsx | 0 .../processors/common_fields/ignore_missing_field.tsx | 0 .../processor_form/processors/common_fields/index.ts | 0 .../processors/common_fields/processor_type_field.tsx | 0 .../processors/common_fields/properties_field.tsx | 0 .../processors/common_fields/target_field.tsx | 0 .../components/processor_form/processors/convert.tsx | 0 .../components/processor_form/processors/csv.tsx | 0 .../components/processor_form/processors/custom.tsx | 0 .../components/processor_form/processors/date.tsx | 0 .../processor_form/processors/date_index_name.tsx | 0 .../components/processor_form/processors/dissect.tsx | 0 .../processor_form/processors/dot_expander.tsx | 0 .../components/processor_form/processors/drop.tsx | 0 .../components/processor_form/processors/enrich.tsx | 0 .../components/processor_form/processors/fail.tsx | 0 .../components/processor_form/processors/foreach.tsx | 0 .../components/processor_form/processors/geoip.tsx | 0 .../processor_form/processors/grok.test.tsx | 0 .../components/processor_form/processors/grok.tsx | 0 .../components/processor_form/processors/gsub.tsx | 0 .../processor_form/processors/html_strip.tsx | 0 .../components/processor_form/processors/index.ts | 0 .../processor_form/processors/inference.tsx | 0 .../components/processor_form/processors/join.tsx | 0 .../components/processor_form/processors/json.tsx | 0 .../components/processor_form/processors/kv.tsx | 0 .../processor_form/processors/lowercase.tsx | 0 .../components/processor_form/processors/pipeline.tsx | 0 .../components/processor_form/processors/remove.tsx | 0 .../components/processor_form/processors/rename.tsx | 0 .../components/processor_form/processors/script.tsx | 0 .../components/processor_form/processors/set.tsx | 0 .../processor_form/processors/set_security_user.tsx | 0 .../components/processor_form/processors/shared.ts | 0 .../components/processor_form/processors/sort.tsx | 0 .../components/processor_form/processors/split.tsx | 0 .../components/processor_form/processors/trim.tsx | 0 .../processor_form/processors/uppercase.tsx | 0 .../processor_form/processors/uri_parts.tsx | 0 .../processor_form/processors/url_decode.tsx | 0 .../processor_form/processors/user_agent.tsx | 0 .../components/processor_remove_modal.tsx | 0 .../components/processors_empty_prompt.tsx | 0 .../components/processors_header.tsx | 0 .../processors_tree/components/drop_zone_button.tsx | 0 .../components/processors_tree/components/index.ts | 0 .../processors_tree/components/private_tree.tsx | 0 .../processors_tree/components/tree_node.tsx | 0 .../components/processors_tree/index.ts | 0 .../components/processors_tree/processors_tree.scss | 0 .../components/processors_tree/processors_tree.tsx | 0 .../components/processors_tree/utils.ts | 0 .../components/shared/index.ts | 0 .../components/shared/map_processor_type_to_form.tsx | 0 .../components/shared/status_icons/error_icon.tsx | 0 .../shared/status_icons/error_ignored_icon.tsx | 0 .../components/shared/status_icons/index.ts | 0 .../components/shared/status_icons/skipped_icon.tsx | 0 .../components/test_pipeline/add_documents_button.tsx | 0 .../documents_dropdown/documents_dropdown.scss | 0 .../documents_dropdown/documents_dropdown.tsx | 0 .../test_pipeline/documents_dropdown/index.ts | 0 .../components/test_pipeline/index.ts | 0 .../components/test_pipeline/test_output_button.tsx | 0 .../test_pipeline/test_pipeline_actions.tsx | 0 .../test_pipeline/test_pipeline_flyout.container.tsx | 0 .../components/test_pipeline/test_pipeline_flyout.tsx | 0 .../test_pipeline/test_pipeline_tabs/index.ts | 0 .../tab_documents/add_document_form.tsx | 0 .../add_documents_accordion.scss | 0 .../add_documents_accordion.tsx | 0 .../tab_documents/add_documents_accordion/index.ts | 0 .../test_pipeline_tabs/tab_documents/index.ts | 0 .../tab_documents/reset_documents_modal.tsx | 0 .../tab_documents/tab_documents.scss | 0 .../tab_documents/tab_documents.tsx | 0 .../test_pipeline/test_pipeline_tabs/tab_output.tsx | 0 .../test_pipeline_tabs/test_pipeline_tabs.tsx | 0 .../constants.ts | 0 .../context/context.tsx | 0 .../context/index.ts | 0 .../context/processors_context.tsx | 0 .../context/test_pipeline_context.tsx | 0 .../deserialize.test.ts | 0 .../deserialize.ts | 0 .../editors/global_on_failure_processors_editor.tsx | 0 .../editors/index.ts | 0 .../editors/processors_editor.tsx | 0 .../index.ts | 2 +- .../components/pipeline_editor/pipeline_editor.scss | 11 +++++++++++ .../pipeline_editor.tsx} | 8 ++++---- .../processors_reducer/constants.ts | 0 .../processors_reducer/index.ts | 0 .../processors_reducer/processors_reducer.test.ts | 0 .../processors_reducer/processors_reducer.ts | 0 .../processors_reducer/utils.ts | 0 .../serialize.ts | 0 .../types.ts | 0 .../use_is_mounted.ts | 0 .../utils.test.ts | 0 .../utils.ts | 0 .../components/pipeline_form/pipeline_form.tsx | 2 +- .../components/pipeline_form/pipeline_form_fields.tsx | 6 +++--- .../pipeline_processors_editor.scss | 11 ----------- 156 files changed, 23 insertions(+), 23 deletions(-) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/README.md (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/constants.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/http_requests.helpers.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/pipeline_processors_editor.helpers.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/pipeline_processors_editor.test.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/processors/processor.helpers.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/processors/uri_parts.test.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/processors_editor.tsx (89%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/test_pipeline.helpers.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/__jest__/test_pipeline.test.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/_shared.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/add_processor_button.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/load_from_json/button.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/load_from_json/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/load_from_json/modal_provider.test.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/load_from_json/modal_provider.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/on_failure_processors_title.tsx (96%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item/context_menu.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item/i18n_texts.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item/inline_text_input.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item/pipeline_processors_editor_item.container.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item/types.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item_status.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item_tooltip/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_toolip.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_tooltip.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/pipeline_processors_editor_item_tooltip/processor_information.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/add_processor_form.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/documentation_button.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/edit_processor_form.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/field_components/drag_and_drop_text_list.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/field_components/drag_and_drop_text_list.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/field_components/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/field_components/text_editor.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/field_components/text_editor.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/field_components/xjson_editor.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processor_form.container.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processor_output/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processor_output/processor_output.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processor_output/processor_output.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processor_settings_fields.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/append.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/bytes.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/circle.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/common_fields/common_processor_fields.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/common_fields/field_name_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/common_fields/ignore_missing_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/common_fields/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/common_fields/processor_type_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/common_fields/properties_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/common_fields/target_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/convert.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/csv.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/custom.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/date.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/date_index_name.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/dissect.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/dot_expander.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/drop.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/enrich.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/fail.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/foreach.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/geoip.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/grok.test.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/grok.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/gsub.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/html_strip.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/inference.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/join.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/json.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/kv.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/lowercase.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/pipeline.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/remove.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/rename.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/script.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/set.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/set_security_user.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/shared.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/sort.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/split.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/trim.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/uppercase.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/uri_parts.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/url_decode.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_form/processors/user_agent.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processor_remove_modal.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_empty_prompt.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_header.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_tree/components/drop_zone_button.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_tree/components/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_tree/components/private_tree.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_tree/components/tree_node.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_tree/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_tree/processors_tree.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_tree/processors_tree.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/processors_tree/utils.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/shared/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/shared/map_processor_type_to_form.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/shared/status_icons/error_icon.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/shared/status_icons/error_ignored_icon.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/shared/status_icons/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/shared/status_icons/skipped_icon.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/add_documents_button.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/documents_dropdown/documents_dropdown.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/documents_dropdown/documents_dropdown.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/documents_dropdown/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_output_button.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_actions.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_flyout.container.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_flyout.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_documents/add_document_form.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_documents/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.scss (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/tab_output.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/components/test_pipeline/test_pipeline_tabs/test_pipeline_tabs.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/constants.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/context/context.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/context/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/context/processors_context.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/context/test_pipeline_context.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/deserialize.test.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/deserialize.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/editors/global_on_failure_processors_editor.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/editors/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/editors/processors_editor.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/index.ts (86%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/pipeline_editor.scss rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor/pipeline_processors_editor.tsx => pipeline_editor/pipeline_editor.tsx} (85%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/processors_reducer/constants.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/processors_reducer/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/processors_reducer/processors_reducer.test.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/processors_reducer/processors_reducer.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/processors_reducer/utils.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/serialize.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/types.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/use_is_mounted.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/utils.test.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor => pipeline_editor}/utils.ts (100%) delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/README.md similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/README.md diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/constants.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/constants.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/constants.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/http_requests.helpers.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/http_requests.helpers.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/http_requests.helpers.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/http_requests.helpers.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/processor.helpers.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/uri_parts.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/uri_parts.test.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors_editor.tsx similarity index 89% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors_editor.tsx index 8fb51ade921a9..3fa245ff96d37 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors_editor.tsx @@ -9,7 +9,7 @@ import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mock import { LocationDescriptorObject } from 'history'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; -import { ProcessorsEditorContextProvider, Props, PipelineProcessorsEditor } from '../'; +import { ProcessorsEditorContextProvider, Props, PipelineEditor } from '../'; import { breadcrumbService, @@ -36,7 +36,7 @@ export const ProcessorsEditorWithDeps: React.FunctionComponent<Props> = (props) return ( <KibanaContextProvider services={appServices}> <ProcessorsEditorContextProvider {...props}> - <PipelineProcessorsEditor onLoadJson={jest.fn()} /> + <PipelineEditor onLoadJson={jest.fn()} /> </ProcessorsEditorContextProvider> </KibanaContextProvider> ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/_shared.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/_shared.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/add_processor_button.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/add_processor_button.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/button.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/button.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.test.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.test.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/on_failure_processors_title.tsx similarity index 96% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/on_failure_processors_title.tsx index 7adc37d1897d1..fe3e6d79f84d7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/on_failure_processors_title.tsx @@ -14,7 +14,7 @@ export const OnFailureProcessorsTitle: FunctionComponent = () => { const { services } = useKibana(); return ( - <div className="pipelineProcessorsEditor__onFailureTitle"> + <div className="pipelineEditor__onFailureTitle"> <EuiTitle size="xs"> <h4> <FormattedMessage diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/context_menu.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/context_menu.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/i18n_texts.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/i18n_texts.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/inline_text_input.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/inline_text_input.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.container.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.container.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.container.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/types.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/types.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/types.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_status.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_status.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_toolip.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_toolip.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_toolip.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_toolip.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_tooltip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_tooltip.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_tooltip.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_tooltip.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/add_processor_form.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/add_processor_form.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/documentation_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/documentation_button.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/documentation_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/documentation_button.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/edit_processor_form.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/edit_processor_form.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/text_editor.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/text_editor.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/text_editor.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/text_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/xjson_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/xjson_editor.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/xjson_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/xjson_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_output/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_output/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_output/processor_output.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_output/processor_output.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_output/processor_output.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_output/processor_output.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_settings_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_settings_fields.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_settings_fields.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_settings_fields.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/append.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/append.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/append.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/append.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/bytes.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/bytes.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/bytes.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/bytes.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/circle.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/field_name_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/field_name_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/field_name_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/field_name_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/processor_type_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/processor_type_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/processor_type_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/properties_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/properties_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/target_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/target_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/convert.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/convert.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/convert.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/convert.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/csv.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/custom.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/custom.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/custom.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/custom.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/date.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/date_index_name.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/dissect.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/dot_expander.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/drop.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/drop.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/drop.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/drop.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/enrich.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/fail.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/fail.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/fail.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/fail.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/foreach.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/foreach.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/foreach.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/foreach.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/geoip.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.test.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.test.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.test.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/gsub.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/html_strip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/html_strip.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/html_strip.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/html_strip.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/inference.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/join.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/join.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/join.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/json.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/json.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/kv.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/lowercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/lowercase.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/lowercase.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/lowercase.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/pipeline.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/pipeline.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/pipeline.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/pipeline.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/remove.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/remove.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/remove.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/remove.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/rename.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/rename.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/rename.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/rename.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/set.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/set_security_user.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set_security_user.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/set_security_user.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set_security_user.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/shared.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/sort.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/split.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/split.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/split.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/split.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/trim.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/trim.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/trim.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/trim.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uppercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/uppercase.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uppercase.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/uppercase.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uri_parts.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/uri_parts.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uri_parts.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/uri_parts.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/url_decode.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/url_decode.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/url_decode.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/url_decode.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/user_agent.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_empty_prompt.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_empty_prompt.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_header.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_header.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_header.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_header.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/components/drop_zone_button.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/components/drop_zone_button.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/components/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/components/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/components/private_tree.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/components/private_tree.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/components/tree_node.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/components/tree_node.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/processors_tree.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/processors_tree.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/processors_tree.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/processors_tree.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/utils.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processors_tree/utils.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_icon.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/status_icons/error_icon.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_icon.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/status_icons/error_icon.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_ignored_icon.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/status_icons/error_ignored_icon.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_ignored_icon.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/status_icons/error_ignored_icon.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/status_icons/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/status_icons/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/skipped_icon.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/status_icons/skipped_icon.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/skipped_icon.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/status_icons/skipped_icon.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/add_documents_button.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/add_documents_button.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_output_button.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_output_button.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_actions.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_actions.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_flyout.container.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.container.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_flyout.container.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_flyout.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_flyout.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_document_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_document_form.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_document_form.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_document_form.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_output.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_output.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_output.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/test_pipeline_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/test_pipeline_tabs.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/test_pipeline_tabs.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/test_pipeline_tabs.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/constants.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/constants.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/context.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/context.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/context.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/test_pipeline_context.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/test_pipeline_context.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/deserialize.test.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/deserialize.test.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/deserialize.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/deserialize.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/editors/global_on_failure_processors_editor.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/editors/global_on_failure_processors_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/editors/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/editors/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/editors/processors_editor.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/editors/processors_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/index.ts similarity index 86% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/index.ts index ae3dd9d673ebe..05de8c7079eab 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/index.ts @@ -12,4 +12,4 @@ export { SerializeResult } from './serialize'; export { OnDoneLoadJsonHandler } from './components'; -export { PipelineProcessorsEditor } from './pipeline_processors_editor'; +export { PipelineEditor } from './pipeline_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/pipeline_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/pipeline_editor.scss new file mode 100644 index 0000000000000..6a51f4f54f27c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/pipeline_editor.scss @@ -0,0 +1,11 @@ +.pipelineEditor { + margin-bottom: $euiSizeXL; +} + +.pipelineEditor__container { + background-color: $euiColorLightestShade; +} + +.pipelineEditor__onFailureTitle { + padding-left: $euiSizeS; +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/pipeline_editor.tsx similarity index 85% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/pipeline_editor.tsx index beb165973d3cd..ce079f87da6c5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/pipeline_editor.tsx @@ -16,13 +16,13 @@ import { } from './components'; import { ProcessorsEditor, GlobalOnFailureProcessorsEditor } from './editors'; -import './pipeline_processors_editor.scss'; +import './pipeline_editor.scss'; interface Props { onLoadJson: OnDoneLoadJsonHandler; } -export const PipelineProcessorsEditor: React.FunctionComponent<Props> = ({ onLoadJson }) => { +export const PipelineEditor: React.FunctionComponent<Props> = ({ onLoadJson }) => { const { state: { processors: allProcessors }, } = usePipelineProcessorsContext(); @@ -52,12 +52,12 @@ export const PipelineProcessorsEditor: React.FunctionComponent<Props> = ({ onLoa } return ( - <div className="pipelineProcessorsEditor"> + <div className="pipelineEditor"> <EuiFlexGroup gutterSize="m" responsive={false} direction="column"> <EuiFlexItem grow={false}> <ProcessorsHeader onLoadJson={onLoadJson} hasProcessors={processors.length > 0} /> </EuiFlexItem> - <EuiFlexItem grow={false} className="pipelineProcessorsEditor__container"> + <EuiFlexItem grow={false} className="pipelineEditor__container"> {content} </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/constants.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/constants.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/constants.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/processors_reducer.test.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/processors_reducer.test.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/processors_reducer.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/processors_reducer.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/utils.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/processors_reducer/utils.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/serialize.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/serialize.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/types.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/types.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/use_is_mounted.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/use_is_mounted.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/use_is_mounted.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/use_is_mounted.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.test.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.test.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index ffd82b0bbaf35..ac8612a36dd7e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -11,7 +11,7 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from import { useForm, Form, FormConfig } from '../../../shared_imports'; import { Pipeline, Processor } from '../../../../common/types'; -import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_processors_editor'; +import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_editor'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index a7ffe7ba02caa..b1b2e04e7d0dc 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -16,8 +16,8 @@ import { ProcessorsEditorContextProvider, OnUpdateHandler, OnDoneLoadJsonHandler, - PipelineProcessorsEditor, -} from '../pipeline_processors_editor'; + PipelineEditor, +} from '../pipeline_editor'; interface Props { processors: Processor[]; @@ -119,7 +119,7 @@ export const PipelineFormFields: React.FunctionComponent<Props> = ({ onUpdate={onProcessorsUpdate} value={{ processors, onFailure }} > - <PipelineProcessorsEditor onLoadJson={onLoadJson} /> + <PipelineEditor onLoadJson={onLoadJson} /> </ProcessorsEditorContextProvider> </> ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss deleted file mode 100644 index d5592b87dda51..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss +++ /dev/null @@ -1,11 +0,0 @@ -.pipelineProcessorsEditor { - margin-bottom: $euiSizeXL; - - &__container { - background-color: $euiColorLightestShade; - } - - &__onFailureTitle { - padding-left: $euiSizeS; - } -} From ad8a2fb920b8b66045617f64f7a60453de275b2a Mon Sep 17 00:00:00 2001 From: Nathan Reese <reese.nathan@gmail.com> Date: Fri, 29 Jan 2021 10:28:48 -0700 Subject: [PATCH 125/163] [Maps] Implement searchSessionId in MapEmbeddable (#89342) * [Maps] Implement searchSessionId in MapEmbeddable * clean up * update method name * fix _unsubscribeFromStore subscription * fix unit test * add maps assertion to send_to_background_relative_time functional test * fix functional assertion Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data_request_descriptor_types.ts | 1 + .../maps/public/actions/map_actions.test.js | 20 ++++++++++ .../maps/public/actions/map_actions.ts | 5 +++ .../blended_vector_layer.ts | 5 ++- .../layers/vector_layer/vector_layer.tsx | 2 + .../es_geo_grid_source/es_geo_grid_source.tsx | 8 ++++ .../es_geo_line_source/es_geo_line_source.tsx | 2 + .../es_pew_pew_source/es_pew_pew_source.js | 1 + .../es_search_source/es_search_source.tsx | 2 + .../classes/sources/es_source/es_source.ts | 21 +++++++--- .../sources/es_term_source/es_term_source.ts | 1 + .../public/classes/util/can_skip_fetch.ts | 17 +++++++- .../maps/public/embeddable/map_embeddable.tsx | 40 ++++++++++--------- x-pack/plugins/maps/public/reducers/map.d.ts | 1 + x-pack/plugins/maps/public/reducers/map.js | 3 +- .../maps/public/selectors/map_selectors.ts | 16 +++++++- .../send_to_background_relative_time.ts | 11 +++++ 17 files changed, 128 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index b00281588734d..c9391e1aac749 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -18,6 +18,7 @@ export type MapFilters = { filters: Filter[]; query?: MapQuery; refreshTimerLastTriggeredAt?: string; + searchSessionId?: string; timeFilters: TimeRange; zoom: number; }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js index 1d1f8a511c4fa..c091aba14687a 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -260,6 +260,7 @@ describe('map_actions', () => { $state: { store: 'appState' }, }, ]; + const searchSessionId = '1234'; beforeEach(() => { //Mocks the "previous" state @@ -272,6 +273,9 @@ describe('map_actions', () => { require('../selectors/map_selectors').getFilters = () => { return filters; }; + require('../selectors/map_selectors').getSearchSessionId = () => { + return searchSessionId; + }; require('../selectors/map_selectors').getMapSettings = () => { return { autoFitToDataBounds: false, @@ -288,12 +292,14 @@ describe('map_actions', () => { const setQueryAction = await setQuery({ query: newQuery, filters, + searchSessionId, }); await setQueryAction(dispatchMock, getStoreMock); expect(dispatchMock.mock.calls).toEqual([ [ { + searchSessionId, timeFilters, query: newQuery, filters, @@ -304,11 +310,25 @@ describe('map_actions', () => { ]); }); + it('should dispatch query action when searchSessionId changes', async () => { + const setQueryAction = await setQuery({ + timeFilters, + query, + filters, + searchSessionId: '5678', + }); + await setQueryAction(dispatchMock, getStoreMock); + + // dispatchMock calls: dispatch(SET_QUERY) and dispatch(syncDataForAllLayers()) + expect(dispatchMock.mock.calls.length).toEqual(2); + }); + it('should not dispatch query action when nothing changes', async () => { const setQueryAction = await setQuery({ timeFilters, query, filters, + searchSessionId, }); await setQueryAction(dispatchMock, getStoreMock); diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 64c35bd207439..afb3df5be73de 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -19,6 +19,7 @@ import { getQuery, getTimeFilters, getLayerList, + getSearchSessionId, } from '../selectors/map_selectors'; import { CLEAR_GOTO, @@ -225,11 +226,13 @@ export function setQuery({ timeFilters, filters = [], forceRefresh = false, + searchSessionId, }: { filters?: Filter[]; query?: Query; timeFilters?: TimeRange; forceRefresh?: boolean; + searchSessionId?: string; }) { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, @@ -249,12 +252,14 @@ export function setQuery({ queryLastTriggeredAt: forceRefresh ? generateQueryTimestamp() : prevTriggeredAt, }, filters: filters ? filters : getFilters(getState()), + searchSessionId, }; const prevQueryContext = { timeFilters: getTimeFilters(getState()), query: getQuery(getState()), filters: getFilters(getState()), + searchSessionId: getSearchSessionId(getState()), }; if (_.isEqual(nextQueryContext, prevQueryContext)) { diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 5b33738a91a28..88150da84f23f 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -316,7 +316,10 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { const abortController = new AbortController(); syncContext.registerCancelCallback(requestToken, () => abortController.abort()); const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); - const resp = await searchSource.fetch({ abortSignal: abortController.signal }); + const resp = await searchSource.fetch({ + abortSignal: abortController.signal, + sessionId: syncContext.dataFilters.searchSessionId, + }); const maxResultWindow = await this._documentSource.getMaxResultWindow(); isSyncClustered = resp.hits.total > maxResultWindow; const countData = { isSyncClustered } as CountData; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 0cb24be445c6e..2304bb277da49 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -616,6 +616,7 @@ export class VectorLayer extends AbstractLayer { sourceQuery, isTimeAware: this.getCurrentStyle().isTimeAware() && (await source.isTimeAware()), timeFilters: dataFilters.timeFilters, + searchSessionId: dataFilters.searchSessionId, } as VectorStyleRequestMeta; const prevDataRequest = this.getDataRequest(dataRequestId); const canSkipFetch = canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }); @@ -635,6 +636,7 @@ export class VectorLayer extends AbstractLayer { registerCancelCallback: registerCancelCallback.bind(null, requestToken), sourceQuery: nextMeta.sourceQuery, timeFilters: nextMeta.timeFilters, + searchSessionId: dataFilters.searchSessionId, }); stopLoading(dataRequestId, requestToken, styleMeta, nextMeta); } catch (error) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 6ec51b8e118cb..24b7e0dec519c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -195,6 +195,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle async _compositeAggRequest({ searchSource, + searchSessionId, indexPattern, precision, layerName, @@ -204,6 +205,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle bufferedExtent, }: { searchSource: ISearchSource; + searchSessionId?: string; indexPattern: IndexPattern; precision: number; layerName: string; @@ -280,6 +282,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle values: { requestId }, } ), + searchSessionId, }); features.push(...convertCompositeRespToGeoJson(esResponse, this._descriptor.requestType)); @@ -325,6 +328,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle // see https://github.com/elastic/kibana/pull/57875#issuecomment-590515482 for explanation on using separate code paths async _nonCompositeAggRequest({ searchSource, + searchSessionId, indexPattern, precision, layerName, @@ -332,6 +336,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle bufferedExtent, }: { searchSource: ISearchSource; + searchSessionId?: string; indexPattern: IndexPattern; precision: number; layerName: string; @@ -348,6 +353,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle requestDescription: i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { defaultMessage: 'Elasticsearch geo grid aggregation request', }), + searchSessionId, }); return convertRegularRespToGeoJson(esResponse, this._descriptor.requestType); @@ -373,6 +379,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle bucketsPerGrid === 1 ? await this._nonCompositeAggRequest({ searchSource, + searchSessionId: searchFilters.searchSessionId, indexPattern, precision: searchFilters.geogridPrecision || 0, layerName, @@ -381,6 +388,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle }) : await this._compositeAggRequest({ searchSource, + searchSessionId: searchFilters.searchSessionId, indexPattern, precision: searchFilters.geogridPrecision || 0, layerName, diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index 9c851dcedb3fa..916a8a291e6b4 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -213,6 +213,7 @@ export class ESGeoLineSource extends AbstractESAggSource { requestDescription: i18n.translate('xpack.maps.source.esGeoLine.entityRequestDescription', { defaultMessage: 'Elasticsearch terms request to fetch entities within map buffer.', }), + searchSessionId: searchFilters.searchSessionId, }); const entityBuckets: Array<{ key: string; doc_count: number }> = _.get( entityResp, @@ -282,6 +283,7 @@ export class ESGeoLineSource extends AbstractESAggSource { defaultMessage: 'Elasticsearch geo_line request to fetch tracks for entities. Tracks are not filtered by map buffer.', }), + searchSessionId: searchFilters.searchSessionId, }); const { featureCollection, numTrimmedTracks } = convertToGeoJson( tracksResp, diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 504212ea1ea84..98d3ba6267c6d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -148,6 +148,7 @@ export class ESPewPewSource extends AbstractESAggSource { requestDescription: i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { defaultMessage: 'Source-destination connections request', }), + searchSessionId: searchFilters.searchSessionId, }); const { featureCollection } = convertToLines(esResponse); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 5a923f0ce4292..b70a433f2c729 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -325,6 +325,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye searchSource, registerCancelCallback, requestDescription: 'Elasticsearch document top hits request', + searchSessionId: searchFilters.searchSessionId, }); const allHits: any[] = []; @@ -391,6 +392,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye searchSource, registerCancelCallback, requestDescription: 'Elasticsearch document request', + searchSessionId: searchFilters.searchSessionId, }); return { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 967131e900fc6..64a5cd575a19d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -57,6 +57,7 @@ export interface IESSource extends IVectorSource { registerCancelCallback, sourceQuery, timeFilters, + searchSessionId, }: { layerName: string; style: IVectorStyle; @@ -64,6 +65,7 @@ export interface IESSource extends IVectorSource { registerCancelCallback: (callback: () => void) => void; sourceQuery?: MapQuery; timeFilters: TimeRange; + searchSessionId?: string; }): Promise<object>; } @@ -151,17 +153,19 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource } async _runEsQuery({ + registerCancelCallback, + requestDescription, requestId, requestName, - requestDescription, + searchSessionId, searchSource, - registerCancelCallback, }: { + registerCancelCallback: (callback: () => void) => void; + requestDescription: string; requestId: string; requestName: string; - requestDescription: string; + searchSessionId?: string; searchSource: ISearchSource; - registerCancelCallback: (callback: () => void) => void; }): Promise<any> { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); @@ -172,6 +176,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource inspectorRequest = inspectorAdapters.requests.start(requestName, { id: requestId, description: requestDescription, + searchSessionId, }); } @@ -186,7 +191,10 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource } }); } - resp = await searchSource.fetch({ abortSignal: abortController.signal }); + resp = await searchSource.fetch({ + abortSignal: abortController.signal, + sessionId: searchSessionId, + }); if (inspectorRequest) { const responseStats = search.getResponseInspectorStats(resp, searchSource); inspectorRequest.stats(responseStats).ok({ json: resp }); @@ -404,6 +412,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource registerCancelCallback, sourceQuery, timeFilters, + searchSessionId, }: { layerName: string; style: IVectorStyle; @@ -411,6 +420,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource registerCancelCallback: (callback: () => void) => void; sourceQuery?: MapQuery; timeFilters: TimeRange; + searchSessionId?: string; }): Promise<object> { const promises = dynamicStyleProps.map((dynamicStyleProp) => { return dynamicStyleProp.getFieldMetaRequest(); @@ -456,6 +466,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource 'Elasticsearch request retrieving field metadata used for calculating symbolization bands.', } ), + searchSessionId, }); return resp.aggregations; diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index 12f1ef4829a4a..235e8e3a651ee 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -147,6 +147,7 @@ export class ESTermSource extends AbstractESAggSource { rightSource: `${this._descriptor.indexPatternTitle}:${this._termField.getName()}`, }, }), + searchSessionId: searchFilters.searchSessionId, }); const countPropertyName = this.getAggKey(AGG_TYPE.COUNT); diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index a7919ad058e4b..d7a5eea151602 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -113,6 +113,7 @@ export async function canSkipSourceUpdate({ if (isQueryAware) { updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + if (nextMeta.applyGlobalQuery) { updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); @@ -123,6 +124,11 @@ export async function canSkipSourceUpdate({ } } + let updateDueToSearchSessionId = false; + if (timeAware || isQueryAware) { + updateDueToSearchSessionId = prevMeta.searchSessionId !== nextMeta.searchSessionId; + } + let updateDueToPrecisionChange = false; if (isGeoGridPrecisionAware) { updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); @@ -146,7 +152,8 @@ export async function canSkipSourceUpdate({ !updateDueToSourceQuery && !updateDueToApplyGlobalQuery && !updateDueToPrecisionChange && - !updateDueToSourceMetaChange + !updateDueToSourceMetaChange && + !updateDueToSearchSessionId ); } @@ -174,8 +181,14 @@ export function canSkipStyleMetaUpdate({ ? !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters) : false; + const updateDueToSearchSessionId = prevMeta.searchSessionId !== nextMeta.searchSessionId; + return ( - !updateDueToFields && !updateDueToSourceQuery && !updateDueToIsTimeAware && !updateDueToTime + !updateDueToFields && + !updateDueToSourceQuery && + !updateDueToIsTimeAware && + !updateDueToTime && + !updateDueToSearchSessionId ); } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index bcdc23bddd2eb..623e548aa85fa 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -82,6 +82,7 @@ export class MapEmbeddable private _prevQuery?: Query; private _prevRefreshConfig?: RefreshInterval; private _prevFilters?: Filter[]; + private _prevSearchSessionId?: string; private _domNode?: HTMLElement; private _unsubscribeFromStore?: Unsubscribe; private _isInitialized = false; @@ -99,9 +100,7 @@ export class MapEmbeddable this._savedMap = new SavedMap({ mapEmbeddableInput: initialInput }); this._initializeSaveMap(); - this._subscription = this.getUpdated$().subscribe(() => - this.onContainerStateChanged(this.input) - ); + this._subscription = this.getUpdated$().subscribe(() => this.onUpdate()); } private async _initializeSaveMap() { @@ -135,6 +134,7 @@ export class MapEmbeddable timeRange: this.input.timeRange, filters: this.input.filters, forceRefresh: false, + searchSessionId: this.input.searchSessionId, }); if (this.input.refreshConfig) { this._dispatchSetRefreshConfig(this.input.refreshConfig); @@ -201,25 +201,24 @@ export class MapEmbeddable return getInspectorAdapters(this._savedMap.getStore().getState()); } - onContainerStateChanged(containerState: MapEmbeddableInput) { + onUpdate() { if ( - !_.isEqual(containerState.timeRange, this._prevTimeRange) || - !_.isEqual(containerState.query, this._prevQuery) || - !esFilters.onlyDisabledFiltersChanged(containerState.filters, this._prevFilters) + !_.isEqual(this.input.timeRange, this._prevTimeRange) || + !_.isEqual(this.input.query, this._prevQuery) || + !esFilters.onlyDisabledFiltersChanged(this.input.filters, this._prevFilters) || + this.input.searchSessionId !== this._prevSearchSessionId ) { this._dispatchSetQuery({ - query: containerState.query, - timeRange: containerState.timeRange, - filters: containerState.filters, + query: this.input.query, + timeRange: this.input.timeRange, + filters: this.input.filters, forceRefresh: false, + searchSessionId: this.input.searchSessionId, }); } - if ( - containerState.refreshConfig && - !_.isEqual(containerState.refreshConfig, this._prevRefreshConfig) - ) { - this._dispatchSetRefreshConfig(containerState.refreshConfig); + if (this.input.refreshConfig && !_.isEqual(this.input.refreshConfig, this._prevRefreshConfig)) { + this._dispatchSetRefreshConfig(this.input.refreshConfig); } } @@ -228,21 +227,25 @@ export class MapEmbeddable timeRange, filters = [], forceRefresh, + searchSessionId, }: { query?: Query; timeRange?: TimeRange; filters?: Filter[]; forceRefresh: boolean; + searchSessionId?: string; }) { this._prevTimeRange = timeRange; this._prevQuery = query; this._prevFilters = filters; + this._prevSearchSessionId = searchSessionId; this._savedMap.getStore().dispatch<any>( setQuery({ filters: filters.filter((filter) => !filter.meta.disabled), query, timeFilters: timeRange, forceRefresh, + searchSessionId, }) ); } @@ -380,10 +383,11 @@ export class MapEmbeddable reload() { this._dispatchSetQuery({ - query: this._prevQuery, - timeRange: this._prevTimeRange, - filters: this._prevFilters ?? [], + query: this.input.query, + timeRange: this.input.timeRange, + filters: this.input.filters, forceRefresh: true, + searchSessionId: this.input.searchSessionId, }); } diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index 273d1de6fddfe..52df65d6d2ecc 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -34,6 +34,7 @@ export type MapContext = { refreshConfig?: MapRefreshConfig; refreshTimerLastTriggeredAt?: string; drawState?: DrawState; + searchSessionId?: string; }; export type MapSettings = { diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index 1395f2c5ce2fe..f068abee48b93 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -240,7 +240,7 @@ export function map(state = DEFAULT_MAP_STATE, action) { }; return { ...state, mapState: { ...state.mapState, ...newMapState } }; case SET_QUERY: - const { query, timeFilters, filters } = action; + const { query, timeFilters, filters, searchSessionId } = action; return { ...state, mapState: { @@ -248,6 +248,7 @@ export function map(state = DEFAULT_MAP_STATE, action) { query, timeFilters, filters, + searchSessionId, }, }; case SET_REFRESH_CONFIG: diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 8876b9536ce92..21ce5993b7c89 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -169,6 +169,9 @@ export const getQuery = ({ map }: MapStoreState): MapQuery | undefined => map.ma export const getFilters = ({ map }: MapStoreState): Filter[] => map.mapState.filters; +export const getSearchSessionId = ({ map }: MapStoreState): string | undefined => + map.mapState.searchSessionId; + export const isUsingSearch = (state: MapStoreState): boolean => { const filters = getFilters(state).filter((filter) => !filter.meta.disabled); const queryString = _.get(getQuery(state), 'query', ''); @@ -220,7 +223,17 @@ export const getDataFilters = createSelector( getRefreshTimerLastTriggeredAt, getQuery, getFilters, - (mapExtent, mapBuffer, mapZoom, timeFilters, refreshTimerLastTriggeredAt, query, filters) => { + getSearchSessionId, + ( + mapExtent, + mapBuffer, + mapZoom, + timeFilters, + refreshTimerLastTriggeredAt, + query, + filters, + searchSessionId + ) => { return { extent: mapExtent, buffer: mapBuffer, @@ -229,6 +242,7 @@ export const getDataFilters = createSelector( refreshTimerLastTriggeredAt, query, filters, + searchSessionId, }; } ); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts index ce6c8978c7d67..25291fd74b322 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'home', 'timePicker', + 'maps', ]); const dashboardPanelActions = getService('dashboardPanelActions'); const inspector = getService('inspector'); @@ -112,5 +113,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('Checking vega chart rendered'); const tsvb = await find.existsByCssSelector('.vgaVis__view'); expect(tsvb).to.be(true); + log.debug('Checking map rendered'); + await dashboardPanelActions.openInspectorByTitle( + '[Flights] Origin and Destination Flight Time' + ); + await testSubjects.click('inspectorRequestChooser'); + await testSubjects.click(`inspectorRequestChooserFlight Origin Location`); + const requestStats = await inspector.getTableData(); + const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); + expect(totalHits).to.equal('0'); + await inspector.close(); } } From 6a0f97fca738791cf7a5f2539814173c637ac39a Mon Sep 17 00:00:00 2001 From: Constance <constancecchen@users.noreply.github.com> Date: Fri, 29 Jan 2021 09:35:20 -0800 Subject: [PATCH 126/163] [Enterprise Search] Minor Elastic Cloud setup guide instructions fixes (#89620) * Fix Cloud instructions copy when cloudDeploymentLink is missing * Fix missing i18n translations on copy nested within links Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../shared/setup_guide/cloud/instructions.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx index 26bbc8814d108..9af5bfc0c3d40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx @@ -34,10 +34,16 @@ export const CloudSetupInstructions: React.FC<Props> = ({ productName, cloudDepl values={{ editDeploymentLink: cloudDeploymentLink ? ( <EuiLink href={cloudDeploymentLink + '/edit'} target="_blank"> - edit your deployment + {i18n.translate( + 'xpack.enterpriseSearch.setupGuide.cloud.step1.instruction1LinkText', + { defaultMessage: 'edit your deployment' } + )} </EuiLink> ) : ( - 'Visit the Elastic Cloud console' + i18n.translate( + 'xpack.enterpriseSearch.setupGuide.cloud.step1.instruction1LinkText', + { defaultMessage: 'edit your deployment' } + ) ), }} /> @@ -76,7 +82,10 @@ export const CloudSetupInstructions: React.FC<Props> = ({ productName, cloudDepl href={`${docLinks.enterpriseSearchBase}/configuration.html`} target="_blank" > - configurable options + {i18n.translate( + 'xpack.enterpriseSearch.setupGuide.cloud.step3.instruction1LinkText', + { defaultMessage: 'configurable options' } + )} </EuiLink> ), }} @@ -118,7 +127,10 @@ export const CloudSetupInstructions: React.FC<Props> = ({ productName, cloudDepl href={`${docLinks.cloudBase}/ec-configure-index-management.html`} target="_blank" > - configure an index lifecycle policy + {i18n.translate( + 'xpack.enterpriseSearch.setupGuide.cloud.step5.instruction1LinkText', + { defaultMessage: 'configure an index lifecycle policy' } + )} </EuiLink> ), }} From 32058f9998addfff1b7e8dae4dfcf0cb3b33118a Mon Sep 17 00:00:00 2001 From: Aaron Caldwell <aaron.caldwell@elastic.co> Date: Fri, 29 Jan 2021 10:36:52 -0700 Subject: [PATCH 127/163] Remove geo threshold alert type (#89632) --- .../public/alert_types/geo_threshold/index.ts | 25 -- ...eshold_alert_type_expression.test.tsx.snap | 240 ----------- .../expressions/boundary_index_expression.tsx | 172 -------- .../expressions/entity_by_expression.tsx | 86 ---- .../expressions/entity_index_expression.tsx | 162 -------- ...o_threshold_alert_type_expression.test.tsx | 83 ---- .../geo_threshold/query_builder/index.tsx | 386 ------------------ .../expression_with_popover.tsx | 78 ---- .../geo_index_pattern_select.tsx | 150 ------- .../util_components/single_field_select.tsx | 84 ---- .../public/alert_types/geo_threshold/types.ts | 35 -- .../geo_threshold/validation.test.ts | 171 -------- .../alert_types/geo_threshold/validation.ts | 101 ----- .../stack_alerts/public/alert_types/index.ts | 2 - .../alert_types/geo_threshold/alert_type.ts | 240 ----------- .../geo_threshold/es_query_builder.ts | 202 --------- .../geo_threshold/geo_threshold.ts | 293 ------------- .../server/alert_types/geo_threshold/index.ts | 19 - .../__snapshots__/alert_type.test.ts.snap | 60 --- .../geo_threshold/tests/alert_type.test.ts | 66 --- .../tests/es_query_builder.test.ts | 67 --- .../tests/es_sample_response.json | 170 -------- .../es_sample_response_with_nesting.json | 170 -------- .../geo_threshold/tests/geo_threshold.test.ts | 268 ------------ .../stack_alerts/server/alert_types/index.ts | 2 - x-pack/plugins/stack_alerts/server/feature.ts | 7 +- .../stack_alerts/server/plugin.test.ts | 12 +- .../translations/translations/ja-JP.json | 52 --- .../translations/translations/zh-CN.json | 52 --- 29 files changed, 9 insertions(+), 3446 deletions(-) delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.test.ts delete mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/__snapshots__/alert_type.test.ts.snap delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response.json delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response_with_nesting.json delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts deleted file mode 100644 index 8ba632633a3af..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { lazy } from 'react'; -import { i18n } from '@kbn/i18n'; -import { validateExpression } from './validation'; -import { GeoThresholdAlertParams } from './types'; -import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; - -export function getAlertType(): AlertTypeModel<GeoThresholdAlertParams> { - return { - id: '.geo-threshold', - description: i18n.translate('xpack.stackAlerts.geoThreshold.descriptionText', { - defaultMessage: 'Alert when an entity enters or leaves a geo boundary.', - }), - iconClass: 'globe', - // TODO: Add documentation for geo threshold alert - documentationUrl: null, - alertParamsExpression: lazy(() => import('./query_builder')), - validate: validateExpression, - requiresAppContext: false, - }; -} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap deleted file mode 100644 index ce59adc688c36..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap +++ /dev/null @@ -1,240 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render BoundaryIndexExpression 1`] = ` -<ExpressionWithPopover - defaultValue="Select an index pattern and geo shape field" - expressionDescription="index" - popoverContent={ - <React.Fragment> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="geoIndexPatternSelect" - labelType="label" - > - <GeoIndexPatternSelect - IndexPatternSelectComponent={[MockFunction]} - includedGeoTypes={ - Array [ - "geo_shape", - ] - } - indexPatternService={ - Object { - "clearCache": [MockFunction], - "createField": [MockFunction], - "createFieldList": [MockFunction], - "ensureDefaultIndexPattern": [MockFunction], - "find": [MockFunction], - "get": [MockFunction], - "make": [Function], - } - } - onChange={[Function]} - /> - </EuiFormRow> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="geoField" - label="Geospatial field" - labelType="label" - > - <SingleFieldSelect - fields={Array []} - onChange={[Function]} - placeholder="Select geo field" - value="" - /> - </EuiFormRow> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="boundaryNameFieldSelect" - label="Human-readable boundary name (optional)" - labelType="label" - > - <SingleFieldSelect - fields={Array []} - onChange={[Function]} - placeholder="Select boundary name" - value="testNameField" - /> - </EuiFormRow> - </React.Fragment> - } -/> -`; - -exports[`should render EntityIndexExpression 1`] = ` -<ExpressionWithPopover - defaultValue="Select an index pattern and geo point field" - expressionDescription="index" - isInvalid={false} - popoverContent={ - <React.Fragment> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="geoIndexPatternSelect" - labelType="label" - > - <GeoIndexPatternSelect - IndexPatternSelectComponent={[MockFunction]} - includedGeoTypes={ - Array [ - "geo_point", - ] - } - indexPatternService={ - Object { - "clearCache": [MockFunction], - "createField": [MockFunction], - "createFieldList": [MockFunction], - "ensureDefaultIndexPattern": [MockFunction], - "find": [MockFunction], - "get": [MockFunction], - "make": [Function], - } - } - onChange={[Function]} - /> - </EuiFormRow> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="thresholdTimeField" - label={ - <FormattedMessage - defaultMessage="Time field" - id="xpack.stackAlerts.geoThreshold.timeFieldLabel" - values={Object {}} - /> - } - labelType="label" - > - <SingleFieldSelect - fields={Array []} - onChange={[Function]} - placeholder="Select time field" - value="testDateField" - /> - </EuiFormRow> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="geoField" - label="Geospatial field" - labelType="label" - > - <SingleFieldSelect - fields={Array []} - onChange={[Function]} - placeholder="Select geo field" - value="testGeoField" - /> - </EuiFormRow> - </React.Fragment> - } -/> -`; - -exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = ` -<ExpressionWithPopover - defaultValue="Select an index pattern and geo point field" - expressionDescription="index" - isInvalid={true} - popoverContent={ - <React.Fragment> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="geoIndexPatternSelect" - labelType="label" - > - <GeoIndexPatternSelect - IndexPatternSelectComponent={[MockFunction]} - includedGeoTypes={ - Array [ - "geo_point", - ] - } - indexPatternService={ - Object { - "clearCache": [MockFunction], - "createField": [MockFunction], - "createFieldList": [MockFunction], - "ensureDefaultIndexPattern": [MockFunction], - "find": [MockFunction], - "get": [MockFunction], - "make": [Function], - } - } - onChange={[Function]} - /> - </EuiFormRow> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="thresholdTimeField" - label={ - <FormattedMessage - defaultMessage="Time field" - id="xpack.stackAlerts.geoThreshold.timeFieldLabel" - values={Object {}} - /> - } - labelType="label" - > - <SingleFieldSelect - fields={Array []} - onChange={[Function]} - placeholder="Select time field" - value="testDateField" - /> - </EuiFormRow> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={true} - hasChildLabel={true} - hasEmptyLabelSpace={false} - id="geoField" - label="Geospatial field" - labelType="label" - > - <SingleFieldSelect - fields={Array []} - onChange={[Function]} - placeholder="Select geo field" - value="testGeoField" - /> - </EuiFormRow> - </React.Fragment> - } -/> -`; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx deleted file mode 100644 index 93918c82d664c..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx +++ /dev/null @@ -1,172 +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, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { HttpSetup } from 'kibana/public'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { IErrorObject } from '../../../../../../triggers_actions_ui/public'; -import { ES_GEO_SHAPE_TYPES, GeoThresholdAlertParams } from '../../types'; -import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; -import { SingleFieldSelect } from '../util_components/single_field_select'; -import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; - -interface Props { - alertParams: GeoThresholdAlertParams; - errors: IErrorObject; - boundaryIndexPattern: IIndexPattern; - boundaryNameField?: string; - setBoundaryIndexPattern: (boundaryIndexPattern?: IIndexPattern) => void; - setBoundaryGeoField: (boundaryGeoField?: string) => void; - setBoundaryNameField: (boundaryNameField?: string) => void; - data: DataPublicPluginStart; -} - -interface KibanaDeps { - http: HttpSetup; -} - -export const BoundaryIndexExpression: FunctionComponent<Props> = ({ - alertParams, - errors, - boundaryIndexPattern, - boundaryNameField, - setBoundaryIndexPattern, - setBoundaryGeoField, - setBoundaryNameField, - data, -}) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - const BOUNDARY_NAME_ENTITY_TYPES = ['string', 'number', 'ip']; - const { http } = useKibana<KibanaDeps>().services; - const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null; - const { boundaryGeoField } = alertParams; - // eslint-disable-next-line react-hooks/exhaustive-deps - const nothingSelected: IFieldType = { - name: '<nothing selected>', - type: 'string', - }; - - const usePrevious = <T extends unknown>(value: T): T | undefined => { - const ref = useRef<T>(); - useEffect(() => { - ref.current = value; - }); - return ref.current; - }; - - const oldIndexPattern = usePrevious(boundaryIndexPattern); - const fields = useRef<{ - geoFields: IFieldType[]; - boundaryNameFields: IFieldType[]; - }>({ - geoFields: [], - boundaryNameFields: [], - }); - useEffect(() => { - if (oldIndexPattern !== boundaryIndexPattern) { - fields.current.geoFields = - (boundaryIndexPattern.fields.length && - boundaryIndexPattern.fields.filter((field: IFieldType) => - ES_GEO_SHAPE_TYPES.includes(field.type) - )) || - []; - if (fields.current.geoFields.length) { - setBoundaryGeoField(fields.current.geoFields[0].name); - } - - fields.current.boundaryNameFields = [ - ...boundaryIndexPattern.fields.filter((field: IFieldType) => { - return ( - BOUNDARY_NAME_ENTITY_TYPES.includes(field.type) && - !field.name.startsWith('_') && - !field.name.endsWith('keyword') - ); - }), - nothingSelected, - ]; - if (fields.current.boundaryNameFields.length) { - setBoundaryNameField(fields.current.boundaryNameFields[0].name); - } - } - }, [ - BOUNDARY_NAME_ENTITY_TYPES, - boundaryIndexPattern, - nothingSelected, - oldIndexPattern, - setBoundaryGeoField, - setBoundaryNameField, - ]); - - const indexPopover = ( - <Fragment> - <EuiFormRow id="geoIndexPatternSelect" fullWidth error={errors.index}> - <GeoIndexPatternSelect - onChange={(_indexPattern) => { - if (!_indexPattern) { - return; - } - setBoundaryIndexPattern(_indexPattern); - }} - value={boundaryIndexPattern.id} - IndexPatternSelectComponent={IndexPatternSelect} - indexPatternService={data.indexPatterns} - http={http} - includedGeoTypes={ES_GEO_SHAPE_TYPES} - /> - </EuiFormRow> - <EuiFormRow - id="geoField" - fullWidth - label={i18n.translate('xpack.stackAlerts.geoThreshold.geofieldLabel', { - defaultMessage: 'Geospatial field', - })} - > - <SingleFieldSelect - placeholder={i18n.translate('xpack.stackAlerts.geoThreshold.selectLabel', { - defaultMessage: 'Select geo field', - })} - value={boundaryGeoField} - onChange={setBoundaryGeoField} - fields={fields.current.geoFields} - /> - </EuiFormRow> - <EuiFormRow - id="boundaryNameFieldSelect" - fullWidth - label={i18n.translate('xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel', { - defaultMessage: 'Human-readable boundary name (optional)', - })} - > - <SingleFieldSelect - placeholder={i18n.translate('xpack.stackAlerts.geoThreshold.boundaryNameSelect', { - defaultMessage: 'Select boundary name', - })} - value={boundaryNameField || null} - onChange={(name) => { - setBoundaryNameField(name === nothingSelected.name ? undefined : name); - }} - fields={fields.current.boundaryNameFields} - /> - </EuiFormRow> - </Fragment> - ); - - return ( - <ExpressionWithPopover - defaultValue={'Select an index pattern and geo shape field'} - value={boundaryIndexPattern.title} - popoverContent={indexPopover} - expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.indexLabel', { - defaultMessage: 'index', - })} - /> - ); -}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx deleted file mode 100644 index 0cff207e674e5..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx +++ /dev/null @@ -1,86 +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, { FunctionComponent, useEffect, useRef } from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; -import { IErrorObject } from '../../../../../../triggers_actions_ui/public'; -import { SingleFieldSelect } from '../util_components/single_field_select'; -import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; - -interface Props { - errors: IErrorObject; - entity: string; - setAlertParamsEntity: (entity: string) => void; - indexFields: IFieldType[]; - isInvalid: boolean; -} - -export const EntityByExpression: FunctionComponent<Props> = ({ - errors, - entity, - setAlertParamsEntity, - indexFields, - isInvalid, -}) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - const ENTITY_TYPES = ['string', 'number', 'ip']; - - const usePrevious = <T extends unknown>(value: T): T | undefined => { - const ref = useRef<T>(); - useEffect(() => { - ref.current = value; - }); - return ref.current; - }; - - const oldIndexFields = usePrevious(indexFields); - const fields = useRef<{ - indexFields: IFieldType[]; - }>({ - indexFields: [], - }); - useEffect(() => { - if (!_.isEqual(oldIndexFields, indexFields)) { - fields.current.indexFields = indexFields.filter( - (field: IFieldType) => ENTITY_TYPES.includes(field.type) && !field.name.startsWith('_') - ); - if (!entity && fields.current.indexFields.length) { - setAlertParamsEntity(fields.current.indexFields[0].name); - } - } - }, [ENTITY_TYPES, indexFields, oldIndexFields, setAlertParamsEntity, entity]); - - const indexPopover = ( - <EuiFormRow id="entitySelect" fullWidth error={errors.index}> - <SingleFieldSelect - placeholder={i18n.translate( - 'xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder', - { - defaultMessage: 'Select entity field', - } - )} - value={entity} - onChange={(_entity) => _entity && setAlertParamsEntity(_entity)} - fields={fields.current.indexFields} - /> - </EuiFormRow> - ); - - return ( - <ExpressionWithPopover - isInvalid={isInvalid} - value={entity} - defaultValue={'Select entity field'} - popoverContent={indexPopover} - expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.entityByLabel', { - defaultMessage: 'by', - })} - /> - ); -}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx deleted file mode 100644 index f2d2f7848a4f9..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx +++ /dev/null @@ -1,162 +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, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { - IErrorObject, - AlertTypeParamsExpressionProps, -} from '../../../../../../triggers_actions_ui/public'; -import { ES_GEO_FIELD_TYPES } from '../../types'; -import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; -import { SingleFieldSelect } from '../util_components/single_field_select'; -import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; - -interface Props { - dateField: string; - geoField: string; - errors: IErrorObject; - setAlertParamsDate: (date: string) => void; - setAlertParamsGeoField: (geoField: string) => void; - setAlertProperty: AlertTypeParamsExpressionProps['setAlertProperty']; - setIndexPattern: (indexPattern: IIndexPattern) => void; - indexPattern: IIndexPattern; - isInvalid: boolean; - data: DataPublicPluginStart; -} - -export const EntityIndexExpression: FunctionComponent<Props> = ({ - setAlertParamsDate, - setAlertParamsGeoField, - errors, - setIndexPattern, - indexPattern, - isInvalid, - dateField: timeField, - geoField, - data, -}) => { - const { http } = useKibana().services; - const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null; - - const usePrevious = <T extends unknown>(value: T): T | undefined => { - const ref = useRef<T>(); - useEffect(() => { - ref.current = value; - }); - return ref.current; - }; - - const oldIndexPattern = usePrevious(indexPattern); - const fields = useRef<{ - dateFields: IFieldType[]; - geoFields: IFieldType[]; - }>({ - dateFields: [], - geoFields: [], - }); - useEffect(() => { - if (oldIndexPattern !== indexPattern) { - fields.current.geoFields = - (indexPattern.fields.length && - indexPattern.fields.filter((field: IFieldType) => - ES_GEO_FIELD_TYPES.includes(field.type) - )) || - []; - if (fields.current.geoFields.length) { - setAlertParamsGeoField(fields.current.geoFields[0].name); - } - - fields.current.dateFields = - (indexPattern.fields.length && - indexPattern.fields.filter((field: IFieldType) => field.type === 'date')) || - []; - if (fields.current.dateFields.length) { - setAlertParamsDate(fields.current.dateFields[0].name); - } - } - }, [indexPattern, oldIndexPattern, setAlertParamsDate, setAlertParamsGeoField]); - - const indexPopover = ( - <Fragment> - <EuiFormRow id="geoIndexPatternSelect" fullWidth error={errors.index}> - <GeoIndexPatternSelect - onChange={(_indexPattern) => { - // reset time field and expression fields if indices are deleted - if (!_indexPattern) { - return; - } - setIndexPattern(_indexPattern); - }} - value={indexPattern.id} - IndexPatternSelectComponent={IndexPatternSelect} - indexPatternService={data.indexPatterns} - http={http!} - includedGeoTypes={ES_GEO_FIELD_TYPES} - /> - </EuiFormRow> - <EuiFormRow - id="thresholdTimeField" - fullWidth - label={ - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.timeFieldLabel" - defaultMessage="Time field" - /> - } - > - <SingleFieldSelect - placeholder={i18n.translate('xpack.stackAlerts.geoThreshold.selectTimeLabel', { - defaultMessage: 'Select time field', - })} - value={timeField} - onChange={(_timeField: string | undefined) => - _timeField && setAlertParamsDate(_timeField) - } - fields={fields.current.dateFields} - /> - </EuiFormRow> - <EuiFormRow - id="geoField" - fullWidth - label={i18n.translate('xpack.stackAlerts.geoThreshold.geofieldLabel', { - defaultMessage: 'Geospatial field', - })} - > - <SingleFieldSelect - placeholder={i18n.translate('xpack.stackAlerts.geoThreshold.selectGeoLabel', { - defaultMessage: 'Select geo field', - })} - value={geoField} - onChange={(_geoField: string | undefined) => - _geoField && setAlertParamsGeoField(_geoField) - } - fields={fields.current.geoFields} - /> - </EuiFormRow> - </Fragment> - ); - - return ( - <ExpressionWithPopover - isInvalid={isInvalid} - value={indexPattern.title} - defaultValue={i18n.translate('xpack.stackAlerts.geoThreshold.entityIndexSelect', { - defaultMessage: 'Select an index pattern and geo point field', - })} - popoverContent={indexPopover} - expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.entityIndexLabel', { - defaultMessage: 'index', - })} - /> - ); -}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx deleted file mode 100644 index c8158b0a6feaa..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx +++ /dev/null @@ -1,83 +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 { shallow } from 'enzyme'; -import { EntityIndexExpression } from './expressions/entity_index_expression'; -import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; -import { IErrorObject } from '../../../../../triggers_actions_ui/public'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { dataPluginMock } from 'src/plugins/data/public/mocks'; - -const alertParams = { - index: '', - indexId: '', - geoField: '', - entity: '', - dateField: '', - trackingEvent: '', - boundaryType: '', - boundaryIndexTitle: '', - boundaryIndexId: '', - boundaryGeoField: '', -}; - -const dataStartMock = dataPluginMock.createStartContract(); - -test('should render EntityIndexExpression', async () => { - const component = shallow( - <EntityIndexExpression - dateField={'testDateField'} - geoField={'testGeoField'} - errors={{} as IErrorObject} - setAlertParamsDate={() => {}} - setAlertParamsGeoField={() => {}} - setAlertProperty={() => {}} - setIndexPattern={() => {}} - indexPattern={('' as unknown) as IIndexPattern} - isInvalid={false} - data={dataStartMock} - /> - ); - - expect(component).toMatchSnapshot(); -}); - -test('should render EntityIndexExpression w/ invalid flag if invalid', async () => { - const component = shallow( - <EntityIndexExpression - dateField={'testDateField'} - geoField={'testGeoField'} - errors={{} as IErrorObject} - setAlertParamsDate={() => {}} - setAlertParamsGeoField={() => {}} - setAlertProperty={() => {}} - setIndexPattern={() => {}} - indexPattern={('' as unknown) as IIndexPattern} - isInvalid={true} - data={dataStartMock} - /> - ); - - expect(component).toMatchSnapshot(); -}); - -test('should render BoundaryIndexExpression', async () => { - const component = shallow( - <BoundaryIndexExpression - alertParams={alertParams} - errors={{} as IErrorObject} - boundaryIndexPattern={('' as unknown) as IIndexPattern} - setBoundaryIndexPattern={() => {}} - setBoundaryGeoField={() => {}} - setBoundaryNameField={() => {}} - boundaryNameField={'testNameField'} - data={dataStartMock} - /> - ); - - expect(component).toMatchSnapshot(); -}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx deleted file mode 100644 index 2a08a4b32f076..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx +++ /dev/null @@ -1,386 +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, { Fragment, useEffect, useState } from 'react'; -import { - EuiCallOut, - EuiFieldNumber, - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiIconTip, - EuiSelect, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - AlertTypeParamsExpressionProps, - getTimeOptions, -} from '../../../../../triggers_actions_ui/public'; -import { GeoThresholdAlertParams, TrackingEvent } from '../types'; -import { ExpressionWithPopover } from './util_components/expression_with_popover'; -import { EntityIndexExpression } from './expressions/entity_index_expression'; -import { EntityByExpression } from './expressions/entity_by_expression'; -import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; -import { - esQuery, - esKuery, - Query, - QueryStringInput, -} from '../../../../../../../src/plugins/data/public'; - -const DEFAULT_VALUES = { - TRACKING_EVENT: '', - ENTITY: '', - INDEX: '', - INDEX_ID: '', - DATE_FIELD: '', - BOUNDARY_TYPE: 'entireIndex', // Only one supported currently. Will eventually be more - GEO_FIELD: '', - BOUNDARY_INDEX: '', - BOUNDARY_INDEX_ID: '', - BOUNDARY_GEO_FIELD: '', - BOUNDARY_NAME_FIELD: '', - DELAY_OFFSET_WITH_UNITS: '0m', -}; - -const conditionOptions = Object.keys(TrackingEvent).map((key) => ({ - text: TrackingEvent[key as TrackingEvent], - value: TrackingEvent[key as TrackingEvent], -})); - -const labelForDelayOffset = ( - <> - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.delayOffset" - defaultMessage="Delayed evaluation offset" - />{' '} - <EuiIconTip - position="right" - type="questionInCircle" - content={i18n.translate('xpack.stackAlerts.geoThreshold.delayOffsetTooltip', { - defaultMessage: 'Evaluate alerts on a delayed cycle to adjust for data latency', - })} - /> - </> -); - -function validateQuery(query: Query) { - try { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - query.language === 'kuery' - ? esKuery.fromKueryExpression(query.query) - : esQuery.luceneStringToDsl(query.query); - } catch (err) { - return false; - } - return true; -} - -export const GeoThresholdAlertTypeExpression: React.FunctionComponent< - AlertTypeParamsExpressionProps<GeoThresholdAlertParams> -> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, data }) => { - const { - index, - indexId, - indexQuery, - geoField, - entity, - dateField, - trackingEvent, - boundaryType, - boundaryIndexTitle, - boundaryIndexId, - boundaryIndexQuery, - boundaryGeoField, - boundaryNameField, - delayOffsetWithUnits, - } = alertParams; - - const [indexPattern, _setIndexPattern] = useState<IIndexPattern>({ - id: '', - fields: [], - title: '', - }); - const setIndexPattern = (_indexPattern?: IIndexPattern) => { - if (_indexPattern) { - _setIndexPattern(_indexPattern); - if (_indexPattern.title) { - setAlertParams('index', _indexPattern.title); - } - if (_indexPattern.id) { - setAlertParams('indexId', _indexPattern.id); - } - } - }; - const [indexQueryInput, setIndexQueryInput] = useState<Query>( - indexQuery || { - query: '', - language: 'kuery', - } - ); - const [boundaryIndexPattern, _setBoundaryIndexPattern] = useState<IIndexPattern>({ - id: '', - fields: [], - title: '', - }); - const setBoundaryIndexPattern = (_indexPattern?: IIndexPattern) => { - if (_indexPattern) { - _setBoundaryIndexPattern(_indexPattern); - if (_indexPattern.title) { - setAlertParams('boundaryIndexTitle', _indexPattern.title); - } - if (_indexPattern.id) { - setAlertParams('boundaryIndexId', _indexPattern.id); - } - } - }; - const [boundaryIndexQueryInput, setBoundaryIndexQueryInput] = useState<Query>( - boundaryIndexQuery || { - query: '', - language: 'kuery', - } - ); - const [delayOffset, _setDelayOffset] = useState<number>(0); - function setDelayOffset(_delayOffset: number) { - setAlertParams('delayOffsetWithUnits', `${_delayOffset}${delayOffsetUnit}`); - _setDelayOffset(_delayOffset); - } - const [delayOffsetUnit, setDelayOffsetUnit] = useState<string>('m'); - - const hasExpressionErrors = false; - const expressionErrorMessage = i18n.translate( - 'xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage', - { - defaultMessage: 'Expression contains errors.', - } - ); - - useEffect(() => { - const initToDefaultParams = async () => { - setAlertProperty('params', { - ...alertParams, - index: index ?? DEFAULT_VALUES.INDEX, - indexId: indexId ?? DEFAULT_VALUES.INDEX_ID, - entity: entity ?? DEFAULT_VALUES.ENTITY, - dateField: dateField ?? DEFAULT_VALUES.DATE_FIELD, - trackingEvent: trackingEvent ?? DEFAULT_VALUES.TRACKING_EVENT, - boundaryType: boundaryType ?? DEFAULT_VALUES.BOUNDARY_TYPE, - geoField: geoField ?? DEFAULT_VALUES.GEO_FIELD, - boundaryIndexTitle: boundaryIndexTitle ?? DEFAULT_VALUES.BOUNDARY_INDEX, - boundaryIndexId: boundaryIndexId ?? DEFAULT_VALUES.BOUNDARY_INDEX_ID, - boundaryGeoField: boundaryGeoField ?? DEFAULT_VALUES.BOUNDARY_GEO_FIELD, - boundaryNameField: boundaryNameField ?? DEFAULT_VALUES.BOUNDARY_NAME_FIELD, - delayOffsetWithUnits: delayOffsetWithUnits ?? DEFAULT_VALUES.DELAY_OFFSET_WITH_UNITS, - }); - if (!data?.indexPatterns) { - return; - } - if (indexId) { - const _indexPattern = await data?.indexPatterns.get(indexId); - setIndexPattern(_indexPattern); - } - if (boundaryIndexId) { - const _boundaryIndexPattern = await data?.indexPatterns.get(boundaryIndexId); - setBoundaryIndexPattern(_boundaryIndexPattern); - } - if (delayOffsetWithUnits) { - setDelayOffset(+delayOffsetWithUnits.replace(/\D/g, '')); - } - }; - initToDefaultParams(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <Fragment> - {hasExpressionErrors ? ( - <Fragment> - <EuiSpacer /> - <EuiCallOut color="danger" size="s" title={expressionErrorMessage} /> - <EuiSpacer /> - </Fragment> - ) : null} - <EuiSpacer size="l" /> - <EuiTitle size="xs"> - <h5> - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.selectOffset" - defaultMessage="Select offset (optional)" - /> - </h5> - </EuiTitle> - <EuiSpacer size="m" /> - <EuiFlexGrid columns={2}> - <EuiFlexItem> - <EuiFormRow fullWidth display="rowCompressed" label={labelForDelayOffset}> - <EuiFlexGroup gutterSize="s"> - <EuiFlexItem> - <EuiFieldNumber - fullWidth - min={0} - compressed - value={delayOffset || 0} - name="delayOffset" - onChange={(e) => { - setDelayOffset(+e.target.value); - }} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiSelect - fullWidth - compressed - value={delayOffsetUnit} - options={getTimeOptions(+alertInterval ?? 1)} - onChange={(e) => { - setDelayOffsetUnit(e.target.value); - }} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGrid> - <EuiSpacer size="m" /> - <EuiTitle size="xs"> - <h5> - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.selectEntity" - defaultMessage="Select entity" - /> - </h5> - </EuiTitle> - <EuiSpacer size="s" /> - <EntityIndexExpression - dateField={dateField} - geoField={geoField} - errors={errors} - setAlertParamsDate={(_date) => setAlertParams('dateField', _date)} - setAlertParamsGeoField={(_geoField) => setAlertParams('geoField', _geoField)} - setAlertProperty={setAlertProperty} - setIndexPattern={setIndexPattern} - indexPattern={indexPattern} - isInvalid={!indexId || !dateField || !geoField} - data={data} - /> - <EntityByExpression - errors={errors} - entity={entity} - setAlertParamsEntity={(entityName) => setAlertParams('entity', entityName)} - indexFields={indexPattern.fields} - isInvalid={indexId && dateField && geoField ? !entity : false} - /> - <EuiSpacer size="s" /> - <EuiFlexItem> - <QueryStringInput - disableAutoFocus - bubbleSubmitEvent - indexPatterns={indexPattern ? [indexPattern] : []} - query={indexQueryInput} - onChange={(query) => { - if (query.language) { - if (validateQuery(query)) { - setAlertParams('indexQuery', query); - } - setIndexQueryInput(query); - } - }} - /> - </EuiFlexItem> - - <EuiSpacer size="l" /> - <EuiTitle size="xs"> - <h5> - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.selectIndex" - defaultMessage="Define the condition" - /> - </h5> - </EuiTitle> - <EuiSpacer size="s" /> - <ExpressionWithPopover - isInvalid={entity ? !trackingEvent : false} - defaultValue={'Select crossing option'} - value={trackingEvent} - popoverContent={ - <EuiFormRow id="someSelect" fullWidth error={errors.index}> - <div> - <EuiSelect - data-test-subj="whenExpressionSelect" - value={ - (trackingEvent && trackingEvent) || - (entity && - setAlertParams('trackingEvent', conditionOptions[0].text) && - conditionOptions[0].text) || - undefined - } - fullWidth - onChange={(e) => setAlertParams('trackingEvent', e.target.value)} - options={conditionOptions} - /> - </div> - </EuiFormRow> - } - expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.whenEntityLabel', { - defaultMessage: 'when entity', - })} - /> - - <EuiSpacer size="l" /> - <EuiTitle size="xs"> - <h5> - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.selectBoundaryIndex" - defaultMessage="Select boundary:" - /> - </h5> - </EuiTitle> - <EuiSpacer size="s" /> - <BoundaryIndexExpression - alertParams={alertParams} - errors={errors} - boundaryIndexPattern={boundaryIndexPattern} - setBoundaryIndexPattern={setBoundaryIndexPattern} - setBoundaryGeoField={(_geoField: string | undefined) => - _geoField && setAlertParams('boundaryGeoField', _geoField) - } - setBoundaryNameField={(_boundaryNameField: string | undefined) => - _boundaryNameField - ? setAlertParams('boundaryNameField', _boundaryNameField) - : setAlertParams('boundaryNameField', '') - } - boundaryNameField={boundaryNameField} - data={data} - /> - <EuiSpacer size="s" /> - <EuiFlexItem> - <QueryStringInput - disableAutoFocus - bubbleSubmitEvent - indexPatterns={boundaryIndexPattern ? [boundaryIndexPattern] : []} - query={boundaryIndexQueryInput} - onChange={(query) => { - if (query.language) { - if (validateQuery(query)) { - setAlertParams('boundaryIndexQuery', query); - } - setBoundaryIndexQueryInput(query); - } - }} - /> - </EuiFlexItem> - <EuiSpacer size="l" /> - </Fragment> - ); -}; - -// eslint-disable-next-line import/no-default-export -export { GeoThresholdAlertTypeExpression as default }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx deleted file mode 100644 index a83667cfd92c6..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx +++ /dev/null @@ -1,78 +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, { ReactNode, useState } from 'react'; -import { - EuiButtonIcon, - EuiExpression, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiPopoverTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export const ExpressionWithPopover: ({ - popoverContent, - expressionDescription, - defaultValue, - value, - isInvalid, -}: { - popoverContent: ReactNode; - expressionDescription: ReactNode; - defaultValue?: ReactNode; - value?: ReactNode; - isInvalid?: boolean; -}) => JSX.Element = ({ popoverContent, expressionDescription, defaultValue, value, isInvalid }) => { - const [popoverOpen, setPopoverOpen] = useState(false); - - return ( - <EuiPopover - id="popoverForExpression" - button={ - <EuiExpression - display="columns" - data-test-subj="selectIndexExpression" - description={expressionDescription} - value={value || defaultValue} - isActive={popoverOpen} - onClick={() => setPopoverOpen(true)} - isInvalid={isInvalid} - /> - } - isOpen={popoverOpen} - closePopover={() => setPopoverOpen(false)} - ownFocus - anchorPosition="downLeft" - zIndex={8000} - display="block" - > - <div style={{ width: '450px' }}> - <EuiPopoverTitle> - <EuiFlexGroup alignItems="center" gutterSize="s"> - <EuiFlexItem>{expressionDescription}</EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonIcon - data-test-subj="closePopover" - iconType="cross" - color="danger" - aria-label={i18n.translate( - 'xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel', - { - defaultMessage: 'Close', - } - )} - onClick={() => setPopoverOpen(false)} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPopoverTitle> - {popoverContent} - </div> - </EuiPopover> - ); -}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx deleted file mode 100644 index a552d6d998c7e..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx +++ /dev/null @@ -1,150 +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, { Component } from 'react'; -import { EuiCallOut, EuiFormRow, EuiLink, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexPattern, IndexPatternsContract } from 'src/plugins/data/public'; -import { HttpSetup } from 'kibana/public'; - -interface Props { - onChange: (indexPattern: IndexPattern) => void; - value: string | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - IndexPatternSelectComponent: any; - indexPatternService: IndexPatternsContract | undefined; - http: HttpSetup; - includedGeoTypes: string[]; -} - -interface State { - noGeoIndexPatternsExist: boolean; -} - -export class GeoIndexPatternSelect extends Component<Props, State> { - private _isMounted: boolean = false; - - state = { - noGeoIndexPatternsExist: false, - }; - - componentWillUnmount() { - this._isMounted = false; - } - - componentDidMount() { - this._isMounted = true; - } - - _onIndexPatternSelect = async (indexPatternId: string) => { - if (!indexPatternId || indexPatternId.length === 0 || !this.props.indexPatternService) { - return; - } - - let indexPattern; - try { - indexPattern = await this.props.indexPatternService.get(indexPatternId); - } catch (err) { - return; - } - - // method may be called again before 'get' returns - // ignore response when fetched index pattern does not match active index pattern - if (this._isMounted && indexPattern.id === indexPatternId) { - this.props.onChange(indexPattern); - } - }; - - _onNoIndexPatterns = () => { - this.setState({ noGeoIndexPatternsExist: true }); - }; - - _renderNoIndexPatternWarning() { - if (!this.state.noGeoIndexPatternsExist) { - return null; - } - - return ( - <> - <EuiCallOut - title={i18n.translate('xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle', { - defaultMessage: `Couldn't find any index patterns with geospatial fields`, - })} - color="warning" - > - <p> - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription" - defaultMessage="You'll need to " - /> - <EuiLink - href={this.props.http.basePath.prepend(`/app/management/kibana/indexPatterns`)} - > - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription" - defaultMessage="create an index pattern" - /> - </EuiLink> - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription" - defaultMessage=" with geospatial fields." - /> - </p> - <p> - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription" - defaultMessage="Don't have any geospatial data sets? " - /> - <EuiLink - href={this.props.http.basePath.prepend('/app/home#/tutorial_directory/sampleData')} - > - <FormattedMessage - id="xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText" - defaultMessage="Get started with some sample data sets." - /> - </EuiLink> - </p> - </EuiCallOut> - <EuiSpacer size="s" /> - </> - ); - } - - render() { - const IndexPatternSelectComponent = this.props.IndexPatternSelectComponent; - return ( - <> - {this._renderNoIndexPatternWarning()} - - <EuiFormRow - label={i18n.translate('xpack.stackAlerts.geoThreshold.indexPatternSelectLabel', { - defaultMessage: 'Index pattern', - })} - > - {IndexPatternSelectComponent ? ( - <IndexPatternSelectComponent - isDisabled={this.state.noGeoIndexPatternsExist} - indexPatternId={this.props.value} - onChange={this._onIndexPatternSelect} - placeholder={i18n.translate( - 'xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder', - { - defaultMessage: 'Select index pattern', - } - )} - fieldTypes={this.props.includedGeoTypes} - onNoIndexPatterns={this._onNoIndexPatterns} - isClearable={false} - /> - ) : ( - <div /> - )} - </EuiFormRow> - </> - ); - } -} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx deleted file mode 100644 index ef6e6f6f5e18f..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx +++ /dev/null @@ -1,84 +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 _ from 'lodash'; -import React from 'react'; -import { - EuiComboBox, - EuiComboBoxOptionOption, - EuiHighlight, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { IFieldType } from 'src/plugins/data/public'; -import { FieldIcon } from '../../../../../../../../src/plugins/kibana_react/public'; - -function fieldsToOptions(fields?: IFieldType[]): Array<EuiComboBoxOptionOption<IFieldType>> { - if (!fields) { - return []; - } - - return fields - .map((field) => ({ - value: field, - label: field.name, - })) - .sort((a, b) => { - return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); - }); -} - -interface Props { - placeholder: string; - value: string | null; // index pattern field name - onChange: (fieldName?: string) => void; - fields: IFieldType[]; -} - -export function SingleFieldSelect({ placeholder, value, onChange, fields }: Props) { - function renderOption( - option: EuiComboBoxOptionOption<IFieldType>, - searchValue: string, - contentClassName: string - ) { - return ( - <EuiFlexGroup className={contentClassName} gutterSize="s" alignItems="center"> - <EuiFlexItem grow={null}> - <FieldIcon type={option.value!.type} fill="none" /> - </EuiFlexItem> - <EuiFlexItem> - <EuiHighlight search={searchValue}>{option.label}</EuiHighlight> - </EuiFlexItem> - </EuiFlexGroup> - ); - } - - const onSelection = (selectedOptions: Array<EuiComboBoxOptionOption<IFieldType>>) => { - onChange(_.get(selectedOptions, '0.value.name')); - }; - - const selectedOptions: Array<EuiComboBoxOptionOption<IFieldType>> = []; - if (value && fields) { - const selectedField = fields.find((field: IFieldType) => field.name === value); - if (selectedField) { - selectedOptions.push({ value: selectedField, label: value }); - } - } - - return ( - <EuiComboBox - singleSelection={true} - options={fieldsToOptions(fields)} - selectedOptions={selectedOptions} - onChange={onSelection} - isDisabled={!fields} - renderOption={renderOption} - isClearable={false} - placeholder={placeholder} - compressed - /> - ); -} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts deleted file mode 100644 index 3f487135f0474..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts +++ /dev/null @@ -1,35 +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 { AlertTypeParams } from '../../../../alerts/common'; -import { Query } from '../../../../../../src/plugins/data/common'; - -export enum TrackingEvent { - entered = 'entered', - exited = 'exited', - crossed = 'crossed', -} - -export interface GeoThresholdAlertParams extends AlertTypeParams { - index: string; - indexId: string; - geoField: string; - entity: string; - dateField: string; - trackingEvent: string; - boundaryType: string; - boundaryIndexTitle: string; - boundaryIndexId: string; - boundaryGeoField: string; - boundaryNameField?: string; - delayOffsetWithUnits?: string; - indexQuery?: Query; - boundaryIndexQuery?: Query; -} - -// Will eventually include 'geo_shape' -export const ES_GEO_FIELD_TYPES = ['geo_point']; -export const ES_GEO_SHAPE_TYPES = ['geo_shape']; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.test.ts deleted file mode 100644 index 9cc5b1eb069ae..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.test.ts +++ /dev/null @@ -1,171 +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 { GeoThresholdAlertParams } from './types'; -import { validateExpression } from './validation'; - -describe('expression params validation', () => { - test('if index property is invalid should return proper error message', () => { - const initialParams: GeoThresholdAlertParams = { - index: '', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: 'testField', - trackingEvent: 'testEvent', - boundaryType: 'testType', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndexId', - boundaryGeoField: 'testField', - }; - expect(validateExpression(initialParams).errors.index.length).toBeGreaterThan(0); - expect(validateExpression(initialParams).errors.index[0]).toBe('Index pattern is required.'); - }); - - test('if geoField property is invalid should return proper error message', () => { - const initialParams: GeoThresholdAlertParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: '', - entity: 'testField', - dateField: 'testField', - trackingEvent: 'testEvent', - boundaryType: 'testType', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndexId', - boundaryGeoField: 'testField', - }; - expect(validateExpression(initialParams).errors.geoField.length).toBeGreaterThan(0); - expect(validateExpression(initialParams).errors.geoField[0]).toBe('Geo field is required.'); - }); - - test('if entity property is invalid should return proper error message', () => { - const initialParams: GeoThresholdAlertParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: '', - dateField: 'testField', - trackingEvent: 'testEvent', - boundaryType: 'testType', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndexId', - boundaryGeoField: 'testField', - }; - expect(validateExpression(initialParams).errors.entity.length).toBeGreaterThan(0); - expect(validateExpression(initialParams).errors.entity[0]).toBe('Entity is required.'); - }); - - test('if dateField property is invalid should return proper error message', () => { - const initialParams: GeoThresholdAlertParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: '', - trackingEvent: 'testEvent', - boundaryType: 'testType', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndexId', - boundaryGeoField: 'testField', - }; - expect(validateExpression(initialParams).errors.dateField.length).toBeGreaterThan(0); - expect(validateExpression(initialParams).errors.dateField[0]).toBe('Date field is required.'); - }); - - test('if trackingEvent property is invalid should return proper error message', () => { - const initialParams: GeoThresholdAlertParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: 'testField', - trackingEvent: '', - boundaryType: 'testType', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndexId', - boundaryGeoField: 'testField', - }; - expect(validateExpression(initialParams).errors.trackingEvent.length).toBeGreaterThan(0); - expect(validateExpression(initialParams).errors.trackingEvent[0]).toBe( - 'Tracking event is required.' - ); - }); - - test('if boundaryType property is invalid should return proper error message', () => { - const initialParams: GeoThresholdAlertParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: 'testField', - trackingEvent: 'testEvent', - boundaryType: '', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndexId', - boundaryGeoField: 'testField', - }; - expect(validateExpression(initialParams).errors.boundaryType.length).toBeGreaterThan(0); - expect(validateExpression(initialParams).errors.boundaryType[0]).toBe( - 'Boundary type is required.' - ); - }); - - test('if boundaryIndexTitle property is invalid should return proper error message', () => { - const initialParams: GeoThresholdAlertParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: 'testField', - trackingEvent: 'testEvent', - boundaryType: 'testType', - boundaryIndexTitle: '', - boundaryIndexId: 'testIndexId', - boundaryGeoField: 'testField', - }; - expect(validateExpression(initialParams).errors.boundaryIndexTitle.length).toBeGreaterThan(0); - expect(validateExpression(initialParams).errors.boundaryIndexTitle[0]).toBe( - 'Boundary index pattern title is required.' - ); - }); - - test('if boundaryGeoField property is invalid should return proper error message', () => { - const initialParams: GeoThresholdAlertParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: 'testField', - trackingEvent: 'testEvent', - boundaryType: 'testType', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndexId', - boundaryGeoField: '', - }; - expect(validateExpression(initialParams).errors.boundaryGeoField.length).toBeGreaterThan(0); - expect(validateExpression(initialParams).errors.boundaryGeoField[0]).toBe( - 'Boundary geo field is required.' - ); - }); - - test('if boundaryNameField property is missing should not return error', () => { - const initialParams: GeoThresholdAlertParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: 'testField', - trackingEvent: 'testEvent', - boundaryType: 'testType', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndexId', - boundaryGeoField: 'testField', - boundaryNameField: '', - }; - expect(validateExpression(initialParams).errors.boundaryGeoField.length).toBe(0); - }); -}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts deleted file mode 100644 index 7a511f681ecaa..0000000000000 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts +++ /dev/null @@ -1,101 +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 { i18n } from '@kbn/i18n'; -import { ValidationResult } from '../../../../triggers_actions_ui/public'; -import { GeoThresholdAlertParams } from './types'; - -export const validateExpression = (alertParams: GeoThresholdAlertParams): ValidationResult => { - const { - index, - geoField, - entity, - dateField, - trackingEvent, - boundaryType, - boundaryIndexTitle, - boundaryGeoField, - } = alertParams; - const validationResult = { errors: {} }; - const errors = { - index: new Array<string>(), - indexId: new Array<string>(), - geoField: new Array<string>(), - entity: new Array<string>(), - dateField: new Array<string>(), - trackingEvent: new Array<string>(), - boundaryType: new Array<string>(), - boundaryIndexTitle: new Array<string>(), - boundaryIndexId: new Array<string>(), - boundaryGeoField: new Array<string>(), - }; - validationResult.errors = errors; - - if (!index) { - errors.index.push( - i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText', { - defaultMessage: 'Index pattern is required.', - }) - ); - } - - if (!geoField) { - errors.geoField.push( - i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText', { - defaultMessage: 'Geo field is required.', - }) - ); - } - - if (!entity) { - errors.entity.push( - i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredEntityText', { - defaultMessage: 'Entity is required.', - }) - ); - } - - if (!dateField) { - errors.dateField.push( - i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredDateFieldText', { - defaultMessage: 'Date field is required.', - }) - ); - } - - if (!trackingEvent) { - errors.trackingEvent.push( - i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText', { - defaultMessage: 'Tracking event is required.', - }) - ); - } - - if (!boundaryType) { - errors.boundaryType.push( - i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText', { - defaultMessage: 'Boundary type is required.', - }) - ); - } - - if (!boundaryIndexTitle) { - errors.boundaryIndexTitle.push( - i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText', { - defaultMessage: 'Boundary index pattern title is required.', - }) - ); - } - - if (!boundaryGeoField) { - errors.boundaryGeoField.push( - i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText', { - defaultMessage: 'Boundary geo field is required.', - }) - ); - } - - return validationResult; -}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/index.ts index 654bf0a424f09..026383cd92f20 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getAlertType as getGeoThresholdAlertType } from './geo_threshold'; import { getAlertType as getGeoContainmentAlertType } from './geo_containment'; import { getAlertType as getThresholdAlertType } from './threshold'; import { getAlertType as getEsQueryAlertType } from './es_query'; @@ -19,7 +18,6 @@ export function registerAlertTypes({ config: Config; }) { if (config.enableGeoAlerting) { - alertTypeRegistry.register(getGeoThresholdAlertType()); alertTypeRegistry.register(getGeoContainmentAlertType()); } alertTypeRegistry.register(getThresholdAlertType()); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts deleted file mode 100644 index 27478049d4880..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts +++ /dev/null @@ -1,240 +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 { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; -import { Logger } from 'src/core/server'; -import { STACK_ALERTS_FEATURE_ID } from '../../../common'; -import { getGeoThresholdExecutor } from './geo_threshold'; -import { - AlertType, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - AlertTypeParams, -} from '../../../../alerts/server'; -import { Query } from '../../../../../../src/plugins/data/common/query'; - -export const GEO_THRESHOLD_ID = '.geo-threshold'; -export type TrackingEvent = 'entered' | 'exited'; -export const ActionGroupId = 'tracking threshold met'; - -const actionVariableContextToEntityDateTimeLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextToEntityDateTimeLabel', - { - defaultMessage: `The time the entity was detected in the current boundary`, - } -); - -const actionVariableContextFromEntityDateTimeLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDateTimeLabel', - { - defaultMessage: `The last time the entity was recorded in the previous boundary`, - } -); - -const actionVariableContextToEntityLocationLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextToEntityLocationLabel', - { - defaultMessage: 'The most recently captured location of the entity', - } -); - -const actionVariableContextCrossingLineLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextCrossingLineLabel', - { - defaultMessage: - 'GeoJSON line connecting the two locations that were used to determine the crossing event', - } -); - -const actionVariableContextFromEntityLocationLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityLocationLabel', - { - defaultMessage: 'The previously captured location of the entity', - } -); - -const actionVariableContextToBoundaryIdLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextCurrentBoundaryIdLabel', - { - defaultMessage: 'The current boundary id containing the entity (if any)', - } -); - -const actionVariableContextToBoundaryNameLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextToBoundaryNameLabel', - { - defaultMessage: 'The boundary (if any) the entity has crossed into and is currently located', - } -); - -const actionVariableContextFromBoundaryNameLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryNameLabel', - { - defaultMessage: 'The boundary (if any) the entity has crossed from and was previously located', - } -); - -const actionVariableContextFromBoundaryIdLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryIdLabel', - { - defaultMessage: 'The previous boundary id containing the entity (if any)', - } -); - -const actionVariableContextToEntityDocumentIdLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextCrossingDocumentIdLabel', - { - defaultMessage: 'The id of the crossing entity document', - } -); - -const actionVariableContextFromEntityDocumentIdLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDocumentIdLabel', - { - defaultMessage: 'The id of the crossing entity document', - } -); - -const actionVariableContextTimeOfDetectionLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextTimeOfDetectionLabel', - { - defaultMessage: 'The alert interval end time this change was recorded', - } -); - -const actionVariableContextEntityIdLabel = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionVariableContextEntityIdLabel', - { - defaultMessage: 'The entity ID of the document that triggered the alert', - } -); - -const actionVariables = { - context: [ - // Alert-specific data - { name: 'entityId', description: actionVariableContextEntityIdLabel }, - { name: 'timeOfDetection', description: actionVariableContextTimeOfDetectionLabel }, - { name: 'crossingLine', description: actionVariableContextCrossingLineLabel }, - - // Corresponds to a specific document in the entity-index - { name: 'toEntityLocation', description: actionVariableContextToEntityLocationLabel }, - { - name: 'toEntityDateTime', - description: actionVariableContextToEntityDateTimeLabel, - }, - { name: 'toEntityDocumentId', description: actionVariableContextToEntityDocumentIdLabel }, - - // Corresponds to a specific document in the boundary-index - { name: 'toBoundaryId', description: actionVariableContextToBoundaryIdLabel }, - { name: 'toBoundaryName', description: actionVariableContextToBoundaryNameLabel }, - - // Corresponds to a specific document in the entity-index (from) - { name: 'fromEntityLocation', description: actionVariableContextFromEntityLocationLabel }, - { name: 'fromEntityDateTime', description: actionVariableContextFromEntityDateTimeLabel }, - { name: 'fromEntityDocumentId', description: actionVariableContextFromEntityDocumentIdLabel }, - - // Corresponds to a specific document in the boundary-index (from) - { name: 'fromBoundaryId', description: actionVariableContextFromBoundaryIdLabel }, - { name: 'fromBoundaryName', description: actionVariableContextFromBoundaryNameLabel }, - ], -}; - -export const ParamsSchema = schema.object({ - index: schema.string({ minLength: 1 }), - indexId: schema.string({ minLength: 1 }), - geoField: schema.string({ minLength: 1 }), - entity: schema.string({ minLength: 1 }), - dateField: schema.string({ minLength: 1 }), - trackingEvent: schema.string({ minLength: 1 }), - boundaryType: schema.string({ minLength: 1 }), - boundaryIndexTitle: schema.string({ minLength: 1 }), - boundaryIndexId: schema.string({ minLength: 1 }), - boundaryGeoField: schema.string({ minLength: 1 }), - boundaryNameField: schema.maybe(schema.string({ minLength: 1 })), - delayOffsetWithUnits: schema.maybe(schema.string({ minLength: 1 })), - indexQuery: schema.maybe(schema.any({})), - boundaryIndexQuery: schema.maybe(schema.any({})), -}); - -export interface GeoThresholdParams extends AlertTypeParams { - index: string; - indexId: string; - geoField: string; - entity: string; - dateField: string; - trackingEvent: string; - boundaryType: string; - boundaryIndexTitle: string; - boundaryIndexId: string; - boundaryGeoField: string; - boundaryNameField?: string; - delayOffsetWithUnits?: string; - indexQuery?: Query; - boundaryIndexQuery?: Query; -} -export interface GeoThresholdState extends AlertTypeState { - shapesFilters: Record<string, unknown>; - shapesIdsNamesMap: Record<string, unknown>; - prevLocationArr: GeoThresholdInstanceState[]; -} -export interface GeoThresholdInstanceState extends AlertInstanceState { - location: number[]; - shapeLocationId: string; - entityName: string; - dateInShape: string | null; - docId: string; -} -export interface GeoThresholdInstanceContext extends AlertInstanceContext { - entityId: string; - timeOfDetection: number; - crossingLine: string; - toEntityLocation: string; - toEntityDateTime: string | null; - toEntityDocumentId: string; - toBoundaryId: string; - toBoundaryName: unknown; - fromEntityLocation: string; - fromEntityDateTime: string | null; - fromEntityDocumentId: string; - fromBoundaryId: string; - fromBoundaryName: unknown; -} - -export type GeoThresholdAlertType = AlertType< - GeoThresholdParams, - GeoThresholdState, - GeoThresholdInstanceState, - GeoThresholdInstanceContext, - typeof ActionGroupId ->; -export function getAlertType(logger: Logger): GeoThresholdAlertType { - const alertTypeName = i18n.translate('xpack.stackAlerts.geoThreshold.alertTypeTitle', { - defaultMessage: 'Tracking threshold', - }); - - const actionGroupName = i18n.translate( - 'xpack.stackAlerts.geoThreshold.actionGroupThresholdMetTitle', - { - defaultMessage: 'Tracking threshold met', - } - ); - - return { - id: GEO_THRESHOLD_ID, - name: alertTypeName, - actionGroups: [{ id: ActionGroupId, name: actionGroupName }], - defaultActionGroupId: ActionGroupId, - executor: getGeoThresholdExecutor(logger), - producer: STACK_ALERTS_FEATURE_ID, - validate: { - params: ParamsSchema, - }, - actionVariables, - minimumLicenseRequired: 'gold', - }; -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts deleted file mode 100644 index 02ac19e7b6f1e..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts +++ /dev/null @@ -1,202 +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 { ILegacyScopedClusterClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; -import { Logger } from 'src/core/server'; -import { - Query, - IIndexPattern, - fromKueryExpression, - toElasticsearchQuery, - luceneStringToDsl, -} from '../../../../../../src/plugins/data/common'; - -export const OTHER_CATEGORY = 'other'; -// Consider dynamically obtaining from config? -const MAX_TOP_LEVEL_QUERY_SIZE = 0; -const MAX_SHAPES_QUERY_SIZE = 10000; -const MAX_BUCKETS_LIMIT = 65535; - -export const getEsFormattedQuery = (query: Query, indexPattern?: IIndexPattern) => { - let esFormattedQuery; - - const queryLanguage = query.language; - if (queryLanguage === 'kuery') { - const ast = fromKueryExpression(query.query); - esFormattedQuery = toElasticsearchQuery(ast, indexPattern); - } else { - esFormattedQuery = luceneStringToDsl(query.query); - } - return esFormattedQuery; -}; - -export async function getShapesFilters( - boundaryIndexTitle: string, - boundaryGeoField: string, - geoField: string, - callCluster: ILegacyScopedClusterClient['callAsCurrentUser'], - log: Logger, - alertId: string, - boundaryNameField?: string, - boundaryIndexQuery?: Query -) { - const filters: Record<string, unknown> = {}; - const shapesIdsNamesMap: Record<string, unknown> = {}; - // Get all shapes in index - const boundaryData: SearchResponse<Record<string, unknown>> = await callCluster('search', { - index: boundaryIndexTitle, - body: { - size: MAX_SHAPES_QUERY_SIZE, - ...(boundaryIndexQuery ? { query: getEsFormattedQuery(boundaryIndexQuery) } : {}), - }, - }); - - boundaryData.hits.hits.forEach(({ _index, _id }) => { - filters[_id] = { - geo_shape: { - [geoField]: { - indexed_shape: { - index: _index, - id: _id, - path: boundaryGeoField, - }, - }, - }, - }; - }); - if (boundaryNameField) { - boundaryData.hits.hits.forEach( - ({ _source, _id }: { _source: Record<string, unknown>; _id: string }) => { - shapesIdsNamesMap[_id] = _source[boundaryNameField]; - } - ); - } - return { - shapesFilters: filters, - shapesIdsNamesMap, - }; -} - -export async function executeEsQueryFactory( - { - entity, - index, - dateField, - boundaryGeoField, - geoField, - boundaryIndexTitle, - indexQuery, - }: { - entity: string; - index: string; - dateField: string; - boundaryGeoField: string; - geoField: string; - boundaryIndexTitle: string; - boundaryNameField?: string; - indexQuery?: Query; - }, - { callCluster }: { callCluster: ILegacyScopedClusterClient['callAsCurrentUser'] }, - log: Logger, - shapesFilters: Record<string, unknown> -) { - return async ( - gteDateTime: Date | null, - ltDateTime: Date | null - ): Promise<SearchResponse<unknown> | undefined> => { - let esFormattedQuery; - if (indexQuery) { - const gteEpochDateTime = gteDateTime ? new Date(gteDateTime).getTime() : null; - const ltEpochDateTime = ltDateTime ? new Date(ltDateTime).getTime() : null; - const dateRangeUpdatedQuery = - indexQuery.language === 'kuery' - ? `(${dateField} >= "${gteEpochDateTime}" and ${dateField} < "${ltEpochDateTime}") and (${indexQuery.query})` - : `(${dateField}:[${gteDateTime} TO ${ltDateTime}]) AND (${indexQuery.query})`; - esFormattedQuery = getEsFormattedQuery({ - query: dateRangeUpdatedQuery, - language: indexQuery.language, - }); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const esQuery: Record<string, any> = { - index, - body: { - size: MAX_TOP_LEVEL_QUERY_SIZE, - aggs: { - shapes: { - filters: { - other_bucket_key: OTHER_CATEGORY, - filters: shapesFilters, - }, - aggs: { - entitySplit: { - terms: { - size: MAX_BUCKETS_LIMIT / ((Object.keys(shapesFilters).length || 1) * 2), - field: entity, - }, - aggs: { - entityHits: { - top_hits: { - size: 1, - sort: [ - { - [dateField]: { - order: 'desc', - }, - }, - ], - docvalue_fields: [entity, dateField, geoField], - _source: false, - }, - }, - }, - }, - }, - }, - }, - query: esFormattedQuery - ? esFormattedQuery - : { - bool: { - must: [], - filter: [ - { - match_all: {}, - }, - { - range: { - [dateField]: { - ...(gteDateTime ? { gte: gteDateTime } : {}), - lt: ltDateTime, // 'less than' to prevent overlap between intervals - format: 'strict_date_optional_time', - }, - }, - }, - ], - should: [], - must_not: [], - }, - }, - stored_fields: ['*'], - docvalue_fields: [ - { - field: dateField, - format: 'date_time', - }, - ], - }, - }; - - let esResult: SearchResponse<unknown> | undefined; - try { - esResult = await callCluster('search', esQuery); - } catch (err) { - log.warn(`${err.message}`); - } - return esResult; - }; -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts deleted file mode 100644 index a2375537ae6e5..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts +++ /dev/null @@ -1,293 +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 _ from 'lodash'; -import { SearchResponse } from 'elasticsearch'; -import { Logger } from 'src/core/server'; -import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_query_builder'; -import { - ActionGroupId, - GEO_THRESHOLD_ID, - GeoThresholdAlertType, - GeoThresholdInstanceState, -} from './alert_type'; - -export type LatestEntityLocation = GeoThresholdInstanceState; - -// Flatten agg results and get latest locations for each entity -export function transformResults( - results: SearchResponse<unknown> | undefined, - dateField: string, - geoField: string -): LatestEntityLocation[] { - if (!results) { - return []; - } - - return ( - _.chain(results) - .get('aggregations.shapes.buckets', {}) - // @ts-expect-error - .flatMap((bucket: unknown, bucketKey: string) => { - const subBuckets = _.get(bucket, 'entitySplit.buckets', []); - return _.map(subBuckets, (subBucket) => { - const locationFieldResult = _.get( - subBucket, - `entityHits.hits.hits[0].fields["${geoField}"][0]`, - '' - ); - const location = locationFieldResult - ? _.chain(locationFieldResult) - .split(', ') - .map((coordString) => +coordString) - .reverse() - .value() - : null; - const dateInShape = _.get( - subBucket, - `entityHits.hits.hits[0].fields["${dateField}"][0]`, - null - ); - const docId = _.get(subBucket, `entityHits.hits.hits[0]._id`); - - return { - location, - shapeLocationId: bucketKey, - entityName: subBucket.key, - dateInShape, - docId, - }; - }); - }) - .orderBy(['entityName', 'dateInShape'], ['asc', 'desc']) - .reduce((accu: LatestEntityLocation[], el: LatestEntityLocation) => { - if (!accu.length || el.entityName !== accu[accu.length - 1].entityName) { - accu.push(el); - } - return accu; - }, []) - .value() - ); -} - -interface EntityMovementDescriptor { - entityName: string; - currLocation: { - location: number[]; - shapeId: string; - date: string | null; - docId: string; - }; - prevLocation: { - location: number[]; - shapeId: string; - date: string | null; - docId: string; - }; -} - -export function getMovedEntities( - currLocationArr: LatestEntityLocation[], - prevLocationArr: LatestEntityLocation[], - trackingEvent: string -): EntityMovementDescriptor[] { - return ( - currLocationArr - // Check if shape has a previous location and has moved - .reduce( - ( - accu: EntityMovementDescriptor[], - { - entityName, - shapeLocationId, - dateInShape, - location, - docId, - }: { - entityName: string; - shapeLocationId: string; - dateInShape: string | null; - location: number[]; - docId: string; - } - ) => { - const prevLocationObj = prevLocationArr.find( - (locationObj: LatestEntityLocation) => locationObj.entityName === entityName - ); - if (!prevLocationObj) { - return accu; - } - if (shapeLocationId !== prevLocationObj.shapeLocationId) { - accu.push({ - entityName, - currLocation: { - location, - shapeId: shapeLocationId, - date: dateInShape, - docId, - }, - prevLocation: { - location: prevLocationObj.location, - shapeId: prevLocationObj.shapeLocationId, - date: prevLocationObj.dateInShape, - docId: prevLocationObj.docId, - }, - }); - } - return accu; - }, - [] - ) - // Do not track entries to or exits from 'other' - .filter((entityMovementDescriptor: EntityMovementDescriptor) => { - if (trackingEvent !== 'crossed') { - return trackingEvent === 'entered' - ? entityMovementDescriptor.currLocation.shapeId !== OTHER_CATEGORY - : entityMovementDescriptor.prevLocation.shapeId !== OTHER_CATEGORY; - } - return true; - }) - ); -} - -function getOffsetTime(delayOffsetWithUnits: string, oldTime: Date): Date { - const timeUnit = delayOffsetWithUnits.slice(-1); - const time: number = +delayOffsetWithUnits.slice(0, -1); - - const adjustedDate = new Date(oldTime.getTime()); - if (timeUnit === 's') { - adjustedDate.setSeconds(adjustedDate.getSeconds() - time); - } else if (timeUnit === 'm') { - adjustedDate.setMinutes(adjustedDate.getMinutes() - time); - } else if (timeUnit === 'h') { - adjustedDate.setHours(adjustedDate.getHours() - time); - } else if (timeUnit === 'd') { - adjustedDate.setDate(adjustedDate.getDate() - time); - } - return adjustedDate; -} - -export const getGeoThresholdExecutor = (log: Logger): GeoThresholdAlertType['executor'] => - async function ({ previousStartedAt, startedAt, services, params, alertId, state }) { - const { shapesFilters, shapesIdsNamesMap } = state.shapesFilters - ? state - : await getShapesFilters( - params.boundaryIndexTitle, - params.boundaryGeoField, - params.geoField, - services.callCluster, - log, - alertId, - params.boundaryNameField, - params.boundaryIndexQuery - ); - - const executeEsQuery = await executeEsQueryFactory(params, services, log, shapesFilters); - - let currIntervalStartTime = previousStartedAt; - let currIntervalEndTime = startedAt; - if (params.delayOffsetWithUnits) { - if (currIntervalStartTime) { - currIntervalStartTime = getOffsetTime(params.delayOffsetWithUnits, currIntervalStartTime); - } - currIntervalEndTime = getOffsetTime(params.delayOffsetWithUnits, currIntervalEndTime); - } - - // Start collecting data only on the first cycle - if (!currIntervalStartTime) { - log.debug(`alert ${GEO_THRESHOLD_ID}:${alertId} alert initialized. Collecting data`); - // Consider making first time window configurable? - const tempPreviousEndTime = new Date(currIntervalEndTime); - tempPreviousEndTime.setMinutes(tempPreviousEndTime.getMinutes() - 5); - const prevToCurrentIntervalResults: - | SearchResponse<unknown> - | undefined = await executeEsQuery(tempPreviousEndTime, currIntervalEndTime); - return { - prevLocationArr: transformResults( - prevToCurrentIntervalResults, - params.dateField, - params.geoField - ), - shapesFilters, - shapesIdsNamesMap, - }; - } - - const currentIntervalResults: SearchResponse<unknown> | undefined = await executeEsQuery( - currIntervalStartTime, - currIntervalEndTime - ); - // No need to compare if no changes in current interval - if (!_.get(currentIntervalResults, 'hits.total.value')) { - return state; - } - - const currLocationArr: LatestEntityLocation[] = transformResults( - currentIntervalResults, - params.dateField, - params.geoField - ); - - const movedEntities: EntityMovementDescriptor[] = getMovedEntities( - currLocationArr, - state.prevLocationArr, - params.trackingEvent - ); - - // Create alert instances - movedEntities.forEach(({ entityName, currLocation, prevLocation }) => { - const toBoundaryName = shapesIdsNamesMap[currLocation.shapeId] || currLocation.shapeId; - const fromBoundaryName = shapesIdsNamesMap[prevLocation.shapeId] || prevLocation.shapeId; - let alertInstance; - if (params.trackingEvent === 'entered') { - alertInstance = `${entityName}-${toBoundaryName || currLocation.shapeId}`; - } else if (params.trackingEvent === 'exited') { - alertInstance = `${entityName}-${fromBoundaryName || prevLocation.shapeId}`; - } else { - // == 'crossed' - alertInstance = `${entityName}-${fromBoundaryName || prevLocation.shapeId}-${ - toBoundaryName || currLocation.shapeId - }`; - } - services.alertInstanceFactory(alertInstance).scheduleActions(ActionGroupId, { - entityId: entityName, - timeOfDetection: new Date(currIntervalEndTime).getTime(), - crossingLine: `LINESTRING (${prevLocation.location[0]} ${prevLocation.location[1]}, ${currLocation.location[0]} ${currLocation.location[1]})`, - - toEntityLocation: `POINT (${currLocation.location[0]} ${currLocation.location[1]})`, - toEntityDateTime: currLocation.date, - toEntityDocumentId: currLocation.docId, - - toBoundaryId: currLocation.shapeId, - toBoundaryName, - - fromEntityLocation: `POINT (${prevLocation.location[0]} ${prevLocation.location[1]})`, - fromEntityDateTime: prevLocation.date, - fromEntityDocumentId: prevLocation.docId, - - fromBoundaryId: prevLocation.shapeId, - fromBoundaryName, - }); - }); - - // Combine previous results w/ current results for state of next run - const prevLocationArr = _.chain(currLocationArr) - .concat(state.prevLocationArr) - .orderBy(['entityName', 'dateInShape'], ['asc', 'desc']) - .reduce((accu: LatestEntityLocation[], el: LatestEntityLocation) => { - if (!accu.length || el.entityName !== accu[accu.length - 1].entityName) { - accu.push(el); - } - return accu; - }, []) - .value(); - - return { - prevLocationArr, - shapesFilters, - shapesIdsNamesMap, - }; - }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts deleted file mode 100644 index 2fa2bed9d8419..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts +++ /dev/null @@ -1,19 +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 { Logger } from 'src/core/server'; -import { AlertingSetup } from '../../types'; -import { getAlertType } from './alert_type'; - -interface RegisterParams { - logger: Logger; - alerts: AlertingSetup; -} - -export function register(params: RegisterParams) { - const { logger, alerts } = params; - alerts.registerType(getAlertType(logger)); -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/__snapshots__/alert_type.test.ts.snap b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/__snapshots__/alert_type.test.ts.snap deleted file mode 100644 index 0cb04144fdb78..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/__snapshots__/alert_type.test.ts.snap +++ /dev/null @@ -1,60 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`alertType alert type creation structure is the expected value 1`] = ` -Object { - "context": Array [ - Object { - "description": "The entity ID of the document that triggered the alert", - "name": "entityId", - }, - Object { - "description": "The alert interval end time this change was recorded", - "name": "timeOfDetection", - }, - Object { - "description": "GeoJSON line connecting the two locations that were used to determine the crossing event", - "name": "crossingLine", - }, - Object { - "description": "The most recently captured location of the entity", - "name": "toEntityLocation", - }, - Object { - "description": "The time the entity was detected in the current boundary", - "name": "toEntityDateTime", - }, - Object { - "description": "The id of the crossing entity document", - "name": "toEntityDocumentId", - }, - Object { - "description": "The current boundary id containing the entity (if any)", - "name": "toBoundaryId", - }, - Object { - "description": "The boundary (if any) the entity has crossed into and is currently located", - "name": "toBoundaryName", - }, - Object { - "description": "The previously captured location of the entity", - "name": "fromEntityLocation", - }, - Object { - "description": "The last time the entity was recorded in the previous boundary", - "name": "fromEntityDateTime", - }, - Object { - "description": "The id of the crossing entity document", - "name": "fromEntityDocumentId", - }, - Object { - "description": "The previous boundary id containing the entity (if any)", - "name": "fromBoundaryId", - }, - Object { - "description": "The boundary (if any) the entity has crossed from and was previously located", - "name": "fromBoundaryName", - }, - ], -} -`; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts deleted file mode 100644 index 0cfce2d47f189..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts +++ /dev/null @@ -1,66 +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 { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; -import { getAlertType, GeoThresholdParams } from '../alert_type'; - -describe('alertType', () => { - const logger = loggingSystemMock.create().get(); - - const alertType = getAlertType(logger); - - it('alert type creation structure is the expected value', async () => { - expect(alertType.id).toBe('.geo-threshold'); - expect(alertType.name).toBe('Tracking threshold'); - expect(alertType.actionGroups).toEqual([ - { id: 'tracking threshold met', name: 'Tracking threshold met' }, - ]); - - expect(alertType.actionVariables).toMatchSnapshot(); - }); - - it('validator succeeds with valid params', async () => { - const params: GeoThresholdParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: 'testField', - trackingEvent: 'testEvent', - boundaryType: 'testType', - boundaryIndexTitle: 'testIndex', - boundaryIndexId: 'testIndex', - boundaryGeoField: 'testField', - boundaryNameField: 'testField', - delayOffsetWithUnits: 'testOffset', - }; - - expect(alertType.validate?.params?.validate(params)).toBeTruthy(); - }); - - it('validator fails with invalid params', async () => { - const paramsSchema = alertType.validate?.params; - if (!paramsSchema) throw new Error('params validator not set'); - - const params: GeoThresholdParams = { - index: 'testIndex', - indexId: 'testIndexId', - geoField: 'testField', - entity: 'testField', - dateField: 'testField', - trackingEvent: '', - boundaryType: 'testType', - boundaryIndexTitle: '', - boundaryIndexId: 'testIndex', - boundaryGeoField: 'testField', - boundaryNameField: 'testField', - }; - - expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( - `"[trackingEvent]: value has length [0] but it must have a minimum length of [1]."` - ); - }); -}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts deleted file mode 100644 index d577a88e8e2f8..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getEsFormattedQuery } from '../es_query_builder'; - -describe('esFormattedQuery', () => { - it('lucene queries are converted correctly', async () => { - const testLuceneQuery1 = { - query: `"airport": "Denver"`, - language: 'lucene', - }; - const esFormattedQuery1 = getEsFormattedQuery(testLuceneQuery1); - expect(esFormattedQuery1).toStrictEqual({ query_string: { query: '"airport": "Denver"' } }); - const testLuceneQuery2 = { - query: `title:"Fun with turnips" AND text:Cabbage, cabbage and more cabbage!`, - language: 'lucene', - }; - const esFormattedQuery2 = getEsFormattedQuery(testLuceneQuery2); - expect(esFormattedQuery2).toStrictEqual({ - query_string: { - query: `title:"Fun with turnips" AND text:Cabbage, cabbage and more cabbage!`, - }, - }); - }); - - it('kuery queries are converted correctly', async () => { - const testKueryQuery1 = { - query: `"airport": "Denver"`, - language: 'kuery', - }; - const esFormattedQuery1 = getEsFormattedQuery(testKueryQuery1); - expect(esFormattedQuery1).toStrictEqual({ - bool: { minimum_should_match: 1, should: [{ match_phrase: { airport: 'Denver' } }] }, - }); - const testKueryQuery2 = { - query: `"airport": "Denver" and ("animal": "goat" or "animal": "narwhal")`, - language: 'kuery', - }; - const esFormattedQuery2 = getEsFormattedQuery(testKueryQuery2); - expect(esFormattedQuery2).toStrictEqual({ - bool: { - filter: [ - { bool: { should: [{ match_phrase: { airport: 'Denver' } }], minimum_should_match: 1 } }, - { - bool: { - should: [ - { - bool: { should: [{ match_phrase: { animal: 'goat' } }], minimum_should_match: 1 }, - }, - { - bool: { - should: [{ match_phrase: { animal: 'narwhal' } }], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }); - }); -}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response.json b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response.json deleted file mode 100644 index 70edbd09aa5a1..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "took" : 2760, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 - }, - "hits" : { - "total" : { - "value" : 10000, - "relation" : "gte" - }, - "max_score" : 0.0, - "hits" : [] - }, - "aggregations" : { - "shapes" : { - "meta" : { }, - "buckets" : { - "0DrJu3QB6yyY-xQxv6Ip" : { - "doc_count" : 1047, - "entitySplit" : { - "doc_count_error_upper_bound" : 0, - "sum_other_doc_count" : 957, - "buckets" : [ - { - "key" : "936", - "doc_count" : 9, - "entityHits" : { - "hits" : { - "total" : { - "value" : 9, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "N-ng1XQB6yyY-xQxnGSM", - "_score" : null, - "fields" : { - "@timestamp" : [ - "2020-09-28T18:01:41.190Z" - ], - "location" : [ - "40.62806099653244, -82.8814151789993" - ], - "entity_id" : [ - "936" - ] - }, - "sort" : [ - 1601316101190 - ] - } - ] - } - } - }, - { - "key" : "AAL2019", - "doc_count" : 9, - "entityHits" : { - "hits" : { - "total" : { - "value" : 9, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "iOng1XQB6yyY-xQxnGSM", - "_score" : null, - "fields" : { - "@timestamp" : [ - "2020-09-28T18:01:41.191Z" - ], - "location" : [ - "39.006176185794175, -82.22068064846098" - ], - "entity_id" : [ - "AAL2019" - ] - }, - "sort" : [ - 1601316101191 - ] - } - ] - } - } - }, - { - "key" : "AAL2323", - "doc_count" : 9, - "entityHits" : { - "hits" : { - "total" : { - "value" : 9, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "n-ng1XQB6yyY-xQxnGSM", - "_score" : null, - "fields" : { - "@timestamp" : [ - "2020-09-28T18:01:41.191Z" - ], - "location" : [ - "41.6677269525826, -84.71324851736426" - ], - "entity_id" : [ - "AAL2323" - ] - }, - "sort" : [ - 1601316101191 - ] - } - ] - } - } - }, - { - "key" : "ABD5250", - "doc_count" : 9, - "entityHits" : { - "hits" : { - "total" : { - "value" : 9, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "GOng1XQB6yyY-xQxnGWM", - "_score" : null, - "fields" : { - "@timestamp" : [ - "2020-09-28T18:01:41.192Z" - ], - "location" : [ - "39.07997465226799, 6.073727197945118" - ], - "entity_id" : [ - "ABD5250" - ] - }, - "sort" : [ - 1601316101192 - ] - } - ] - } - } - } - ] - } - } - } - } - } -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response_with_nesting.json b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response_with_nesting.json deleted file mode 100644 index a4b7b6872b341..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response_with_nesting.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "took" : 2760, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 - }, - "hits" : { - "total" : { - "value" : 10000, - "relation" : "gte" - }, - "max_score" : 0.0, - "hits" : [] - }, - "aggregations" : { - "shapes" : { - "meta" : { }, - "buckets" : { - "0DrJu3QB6yyY-xQxv6Ip" : { - "doc_count" : 1047, - "entitySplit" : { - "doc_count_error_upper_bound" : 0, - "sum_other_doc_count" : 957, - "buckets" : [ - { - "key" : "936", - "doc_count" : 9, - "entityHits" : { - "hits" : { - "total" : { - "value" : 9, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "N-ng1XQB6yyY-xQxnGSM", - "_score" : null, - "fields" : { - "time_data.@timestamp" : [ - "2020-09-28T18:01:41.190Z" - ], - "geo.coords.location" : [ - "40.62806099653244, -82.8814151789993" - ], - "entity_id" : [ - "936" - ] - }, - "sort" : [ - 1601316101190 - ] - } - ] - } - } - }, - { - "key" : "AAL2019", - "doc_count" : 9, - "entityHits" : { - "hits" : { - "total" : { - "value" : 9, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "iOng1XQB6yyY-xQxnGSM", - "_score" : null, - "fields" : { - "time_data.@timestamp" : [ - "2020-09-28T18:01:41.191Z" - ], - "geo.coords.location" : [ - "39.006176185794175, -82.22068064846098" - ], - "entity_id" : [ - "AAL2019" - ] - }, - "sort" : [ - 1601316101191 - ] - } - ] - } - } - }, - { - "key" : "AAL2323", - "doc_count" : 9, - "entityHits" : { - "hits" : { - "total" : { - "value" : 9, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "n-ng1XQB6yyY-xQxnGSM", - "_score" : null, - "fields" : { - "time_data.@timestamp" : [ - "2020-09-28T18:01:41.191Z" - ], - "geo.coords.location" : [ - "41.6677269525826, -84.71324851736426" - ], - "entity_id" : [ - "AAL2323" - ] - }, - "sort" : [ - 1601316101191 - ] - } - ] - } - } - }, - { - "key" : "ABD5250", - "doc_count" : 9, - "entityHits" : { - "hits" : { - "total" : { - "value" : 9, - "relation" : "eq" - }, - "max_score" : null, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "GOng1XQB6yyY-xQxnGWM", - "_score" : null, - "fields" : { - "time_data.@timestamp" : [ - "2020-09-28T18:01:41.192Z" - ], - "geo.coords.location" : [ - "39.07997465226799, 6.073727197945118" - ], - "entity_id" : [ - "ABD5250" - ] - }, - "sort" : [ - 1601316101192 - ] - } - ] - } - } - } - ] - } - } - } - } - } -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts deleted file mode 100644 index 5b5197ac62a39..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts +++ /dev/null @@ -1,268 +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 sampleJsonResponse from './es_sample_response.json'; -import sampleJsonResponseWithNesting from './es_sample_response_with_nesting.json'; -import { getMovedEntities, transformResults } from '../geo_threshold'; -import { OTHER_CATEGORY } from '../es_query_builder'; -import { SearchResponse } from 'elasticsearch'; - -describe('geo_threshold', () => { - describe('transformResults', () => { - const dateField = '@timestamp'; - const geoField = 'location'; - it('should correctly transform expected results', async () => { - const transformedResults = transformResults( - (sampleJsonResponse as unknown) as SearchResponse<unknown>, - dateField, - geoField - ); - expect(transformedResults).toEqual([ - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'iOng1XQB6yyY-xQxnGSM', - entityName: 'AAL2019', - location: [-82.22068064846098, 39.006176185794175], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'n-ng1XQB6yyY-xQxnGSM', - entityName: 'AAL2323', - location: [-84.71324851736426, 41.6677269525826], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, - { - dateInShape: '2020-09-28T18:01:41.192Z', - docId: 'GOng1XQB6yyY-xQxnGWM', - entityName: 'ABD5250', - location: [6.073727197945118, 39.07997465226799], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, - ]); - }); - - const nestedDateField = 'time_data.@timestamp'; - const nestedGeoField = 'geo.coords.location'; - it('should correctly transform expected results if fields are nested', async () => { - const transformedResults = transformResults( - (sampleJsonResponseWithNesting as unknown) as SearchResponse<unknown>, - nestedDateField, - nestedGeoField - ); - expect(transformedResults).toEqual([ - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'iOng1XQB6yyY-xQxnGSM', - entityName: 'AAL2019', - location: [-82.22068064846098, 39.006176185794175], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'n-ng1XQB6yyY-xQxnGSM', - entityName: 'AAL2323', - location: [-84.71324851736426, 41.6677269525826], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, - { - dateInShape: '2020-09-28T18:01:41.192Z', - docId: 'GOng1XQB6yyY-xQxnGWM', - entityName: 'ABD5250', - location: [6.073727197945118, 39.07997465226799], - shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', - }, - ]); - }); - - it('should return an empty array if no results', async () => { - const transformedResults = transformResults(undefined, dateField, geoField); - expect(transformedResults).toEqual([]); - }); - }); - - describe('getMovedEntities', () => { - it('should return empty array if only movements were within same shapes', async () => { - const currLocationArr = [ - { - dateInShape: '2020-08-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 41.62806099653244], - shapeLocationId: 'sameShape1', - }, - { - dateInShape: '2020-08-28T18:01:41.191Z', - docId: 'iOng1XQB6yyY-xQxnGSM', - entityName: 'AAL2019', - location: [-82.22068064846098, 38.006176185794175], - shapeLocationId: 'sameShape2', - }, - ]; - const prevLocationArr = [ - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: 'sameShape1', - }, - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'iOng1XQB6yyY-xQxnGSM', - entityName: 'AAL2019', - location: [-82.22068064846098, 39.006176185794175], - shapeLocationId: 'sameShape2', - }, - ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); - expect(movedEntities).toEqual([]); - }); - - it('should return result if entity has moved to different shape', async () => { - const currLocationArr = [ - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'currLocationDoc1', - entityName: '936', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: 'newShapeLocation', - }, - { - dateInShape: '2020-09-28T18:01:41.191Z', - docId: 'currLocationDoc2', - entityName: 'AAL2019', - location: [-82.22068064846098, 39.006176185794175], - shapeLocationId: 'thisOneDidntMove', - }, - ]; - const prevLocationArr = [ - { - dateInShape: '2020-09-27T18:01:41.190Z', - docId: 'prevLocationDoc1', - entityName: '936', - location: [-82.8814151789993, 20.62806099653244], - shapeLocationId: 'oldShapeLocation', - }, - { - dateInShape: '2020-09-27T18:01:41.191Z', - docId: 'prevLocationDoc2', - entityName: 'AAL2019', - location: [-82.22068064846098, 39.006176185794175], - shapeLocationId: 'thisOneDidntMove', - }, - ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); - expect(movedEntities.length).toEqual(1); - }); - - it('should ignore "entered" results to "other"', async () => { - const currLocationArr = [ - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 41.62806099653244], - shapeLocationId: OTHER_CATEGORY, - }, - ]; - const prevLocationArr = [ - { - dateInShape: '2020-08-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: 'oldShapeLocation', - }, - ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); - expect(movedEntities).toEqual([]); - }); - - it('should ignore "exited" results from "other"', async () => { - const currLocationArr = [ - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 41.62806099653244], - shapeLocationId: 'newShapeLocation', - }, - ]; - const prevLocationArr = [ - { - dateInShape: '2020-08-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: OTHER_CATEGORY, - }, - ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'exited'); - expect(movedEntities).toEqual([]); - }); - - it('should not ignore "crossed" results from "other"', async () => { - const currLocationArr = [ - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 41.62806099653244], - shapeLocationId: 'newShapeLocation', - }, - ]; - const prevLocationArr = [ - { - dateInShape: '2020-08-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: OTHER_CATEGORY, - }, - ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'crossed'); - expect(movedEntities.length).toEqual(1); - }); - - it('should not ignore "crossed" results to "other"', async () => { - const currLocationArr = [ - { - dateInShape: '2020-08-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 40.62806099653244], - shapeLocationId: OTHER_CATEGORY, - }, - ]; - const prevLocationArr = [ - { - dateInShape: '2020-09-28T18:01:41.190Z', - docId: 'N-ng1XQB6yyY-xQxnGSM', - entityName: '936', - location: [-82.8814151789993, 41.62806099653244], - shapeLocationId: 'newShapeLocation', - }, - ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'crossed'); - expect(movedEntities.length).toEqual(1); - }); - }); -}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index.ts index 2a343cb49a91b..5c35af5e344b9 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index.ts @@ -7,7 +7,6 @@ import { Logger } from 'src/core/server'; import { AlertingSetup, StackAlertsStartDeps } from '../types'; import { register as registerIndexThreshold } from './index_threshold'; -import { register as registerGeoThreshold } from './geo_threshold'; import { register as registerGeoContainment } from './geo_containment'; import { register as registerEsQuery } from './es_query'; interface RegisterAlertTypesParams { @@ -18,7 +17,6 @@ interface RegisterAlertTypesParams { export function registerBuiltInAlertTypes(params: RegisterAlertTypesParams) { registerIndexThreshold(params); - registerGeoThreshold(params); registerGeoContainment(params); registerEsQuery(params); } diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 448e1e698858b..e334b4642a00a 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; -import { GEO_THRESHOLD_ID as GeoThreshold } from './alert_types/geo_threshold/alert_type'; import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containment/alert_type'; import { STACK_ALERTS_FEATURE_ID } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; @@ -21,7 +20,7 @@ export const BUILT_IN_ALERTS_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [IndexThreshold, GeoThreshold, GeoContainment], + alerting: [IndexThreshold, GeoContainment], privileges: { all: { app: [], @@ -30,7 +29,7 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: [IndexThreshold, GeoThreshold, GeoContainment], + all: [IndexThreshold, GeoContainment], read: [], }, savedObject: { @@ -48,7 +47,7 @@ export const BUILT_IN_ALERTS_FEATURE = { }, alerting: { all: [], - read: [IndexThreshold, GeoThreshold, GeoContainment], + read: [IndexThreshold, GeoContainment], }, savedObject: { all: [], diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index 8d69fad4afa46..0273f373734fa 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -27,7 +27,7 @@ describe('AlertingBuiltins Plugin', () => { const featuresSetup = featuresPluginMock.createSetup(); await plugin.setup(coreSetup, { alerts: alertingSetup, features: featuresSetup }); - expect(alertingSetup.registerType).toHaveBeenCalledTimes(4); + expect(alertingSetup.registerType).toHaveBeenCalledTimes(3); const indexThresholdArgs = alertingSetup.registerType.mock.calls[0][0]; const testedIndexThresholdArgs = { @@ -58,16 +58,16 @@ describe('AlertingBuiltins Plugin', () => { Object { "actionGroups": Array [ Object { - "id": "tracking threshold met", - "name": "Tracking threshold met", + "id": "Tracked entity contained", + "name": "Tracking containment met", }, ], - "id": ".geo-threshold", - "name": "Tracking threshold", + "id": ".geo-containment", + "name": "Tracking containment", } `); - const esQueryArgs = alertingSetup.registerType.mock.calls[3][0]; + const esQueryArgs = alertingSetup.registerType.mock.calls[2][0]; const testedEsQueryArgs = { id: esQueryArgs.id, name: esQueryArgs.name, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d6aeb3a293f67..28ef79beb72cf 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20805,58 +20805,6 @@ "xpack.stackAlerts.geoContainment.timeFieldLabel": "時間フィールド", "xpack.stackAlerts.geoContainment.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", "xpack.stackAlerts.geoContainment.ui.expressionPopover.closePopoverLabel": "閉じる", - "xpack.stackAlerts.geoThreshold.actionGroupThresholdMetTitle": "追跡しきい値が満たされました", - "xpack.stackAlerts.geoThreshold.actionVariableContextCrossingDocumentIdLabel": "クロスエンティティドキュメントのID", - "xpack.stackAlerts.geoThreshold.actionVariableContextCrossingLineLabel": "クロスイベントを決定するために使用された2つの場所を接続するGeoJSON行", - "xpack.stackAlerts.geoThreshold.actionVariableContextCurrentBoundaryIdLabel": "エンティティを含む現在の境界ID(該当する場合)", - "xpack.stackAlerts.geoThreshold.actionVariableContextEntityIdLabel": "アラートをトリガーしたドキュメントのエンティティ ID", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryIdLabel": "エンティティを含む以前の境界ID(該当する場合)", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryNameLabel": "エンティティがそこからクロスし、以前に検出された境界(該当する場合)", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDateTimeLabel": "前回エンティティが前の境界で記録された日時", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDocumentIdLabel": "クロスエンティティドキュメントのID", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityLocationLabel": "エンティティの以前に取り込まれた場所", - "xpack.stackAlerts.geoThreshold.actionVariableContextTimeOfDetectionLabel": "この変更が記録された、アラート間隔終了日時", - "xpack.stackAlerts.geoThreshold.actionVariableContextToBoundaryNameLabel": "エンティティがその中にクロスし、現在検出されている境界(該当する場合)", - "xpack.stackAlerts.geoThreshold.actionVariableContextToEntityDateTimeLabel": "現在の境界でエンティティが検出された日時", - "xpack.stackAlerts.geoThreshold.actionVariableContextToEntityLocationLabel": "エンティティの直近に取り込まれた場所", - "xpack.stackAlerts.geoThreshold.alertTypeTitle": "地理追跡しきい値", - "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "境界名を選択", - "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", - "xpack.stackAlerts.geoThreshold.delayOffset": "遅延評価オフセット", - "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します", - "xpack.stackAlerts.geoThreshold.descriptionText": "エンティティが地理的境界に出入りするときにアラートを発行します。", - "xpack.stackAlerts.geoThreshold.entityByLabel": "グループ基準", - "xpack.stackAlerts.geoThreshold.entityIndexLabel": "インデックス", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。", - "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "エンティティは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。", - "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。", - "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。", - "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", - "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空間フィールド", - "xpack.stackAlerts.geoThreshold.indexLabel": "インデックス", - "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "インデックスパターン", - "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", - "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", - "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか?", - "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした", - "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "境界を選択:", - "xpack.stackAlerts.geoThreshold.selectEntity": "エンティティを選択", - "xpack.stackAlerts.geoThreshold.selectGeoLabel": "ジオフィールドを選択", - "xpack.stackAlerts.geoThreshold.selectIndex": "条件を定義してください", - "xpack.stackAlerts.geoThreshold.selectLabel": "ジオフィールドを選択", - "xpack.stackAlerts.geoThreshold.selectOffset": "オフセットを選択(任意)", - "xpack.stackAlerts.geoThreshold.selectTimeLabel": "時刻フィールドを選択", - "xpack.stackAlerts.geoThreshold.timeFieldLabel": "時間フィールド", - "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", - "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "閉じる", - "xpack.stackAlerts.geoThreshold.whenEntityLabel": "エンティティ", "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "しきい値一致", "xpack.stackAlerts.indexThreshold.actionVariableContextConditionsLabel": "しきい値比較基準としきい値を説明する文字列", "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "アラートがしきい値を超えた日付。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c47be2f09ef82..052a00b1aefa4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20853,58 +20853,6 @@ "xpack.stackAlerts.geoContainment.timeFieldLabel": "时间字段", "xpack.stackAlerts.geoContainment.topHitsSplitFieldSelectPlaceholder": "选择实体字段", "xpack.stackAlerts.geoContainment.ui.expressionPopover.closePopoverLabel": "关闭", - "xpack.stackAlerts.geoThreshold.actionGroupThresholdMetTitle": "已达到跟踪阈值", - "xpack.stackAlerts.geoThreshold.actionVariableContextCrossingDocumentIdLabel": "穿越实体文档的 ID", - "xpack.stackAlerts.geoThreshold.actionVariableContextCrossingLineLabel": "连接用于确定穿越事件的两个位置的 GeoJSON 线", - "xpack.stackAlerts.geoThreshold.actionVariableContextCurrentBoundaryIdLabel": "包含实体的当前边界 ID(如果有)", - "xpack.stackAlerts.geoThreshold.actionVariableContextEntityIdLabel": "触发了告警的文档的实体 ID", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryIdLabel": "包含实体的上一边界 ID(如果有)", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryNameLabel": "实体从中穿越出且先前所位于的边界(如果有)", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDateTimeLabel": "实体上次在上一边界中记录的时间", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDocumentIdLabel": "穿越实体文档的 ID", - "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityLocationLabel": "实体的先前捕获位置", - "xpack.stackAlerts.geoThreshold.actionVariableContextTimeOfDetectionLabel": "记录此更改的告警时间间隔结束时间", - "xpack.stackAlerts.geoThreshold.actionVariableContextToBoundaryNameLabel": "实体已穿越进且当前位于的边界(如果有)", - "xpack.stackAlerts.geoThreshold.actionVariableContextToEntityDateTimeLabel": "在当前边界中检测到实体的时间", - "xpack.stackAlerts.geoThreshold.actionVariableContextToEntityLocationLabel": "实体的最近捕获位置", - "xpack.stackAlerts.geoThreshold.alertTypeTitle": "地理跟踪阈值", - "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "选择边界名称", - "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", - "xpack.stackAlerts.geoThreshold.delayOffset": "已延迟的评估偏移", - "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整", - "xpack.stackAlerts.geoThreshold.descriptionText": "实体进入或离开地理边界时告警。", - "xpack.stackAlerts.geoThreshold.entityByLabel": "依据", - "xpack.stackAlerts.geoThreshold.entityIndexLabel": "索引", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。", - "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。", - "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。", - "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "“实体”必填。", - "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。", - "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。", - "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。", - "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", - "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空间字段", - "xpack.stackAlerts.geoThreshold.indexLabel": "索引", - "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "索引模式", - "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ", - "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。", - "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", - "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集?", - "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式", - "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "选择边界:", - "xpack.stackAlerts.geoThreshold.selectEntity": "选择实体", - "xpack.stackAlerts.geoThreshold.selectGeoLabel": "选择地理字段", - "xpack.stackAlerts.geoThreshold.selectIndex": "定义条件", - "xpack.stackAlerts.geoThreshold.selectLabel": "选择地理字段", - "xpack.stackAlerts.geoThreshold.selectOffset": "选择偏移(可选)", - "xpack.stackAlerts.geoThreshold.selectTimeLabel": "选择时间字段", - "xpack.stackAlerts.geoThreshold.timeFieldLabel": "时间字段", - "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段", - "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "关闭", - "xpack.stackAlerts.geoThreshold.whenEntityLabel": "当实体", "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "已达到阈值", "xpack.stackAlerts.indexThreshold.actionVariableContextConditionsLabel": "描述阈值比较运算符和阈值的字符串", "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "告警超过阈值的日期。", From 5feca52dea33fafae81662b4a60582e94f63f278 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger <scotty.bollinger@elastic.co> Date: Fri, 29 Jan 2021 11:43:34 -0600 Subject: [PATCH 128/163] [Enterprise Search] Migrate Kibana plugin to TS project references (#87683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Enterprise Search] Migrate Kibana plugin to TS project references Part of #80508 * Add charts and un-comment added ‘features’ Also alphabetize. * Uncomment recently added security and spaces * Add last remaining reference * Add shared typings to cover svgs * Include package.json for version.ts * REvery adding package.json to include This did not fix the issue * Add correct references --- .../plugins/enterprise_search/tsconfig.json | 27 +++++++++++++++++++ x-pack/test/tsconfig.json | 1 + x-pack/tsconfig.json | 2 ++ x-pack/tsconfig.refs.json | 1 + 4 files changed, 31 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/tsconfig.json diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json new file mode 100644 index 0000000000000..6b4c50770b49f --- /dev/null +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "../../../typings/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/charts/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 461ebfe15b109..5232af0dd304b 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -40,6 +40,7 @@ { "path": "../plugins/alerts/tsconfig.json"}, { "path": "../plugins/console_extensions/tsconfig.json" }, { "path": "../plugins/data_enhanced/tsconfig.json" }, + { "path": "../plugins/enterprise_search/tsconfig.json" }, { "path": "../plugins/global_search/tsconfig.json" }, { "path": "../plugins/global_search_providers/tsconfig.json" }, { "path": "../plugins/features/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index d64b17813f660..4b161e3559849 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -17,6 +17,7 @@ "plugins/features/**/*", "plugins/embeddable_enhanced/**/*", "plugins/event_log/**/*", + "plugins/enterprise_search/**/*", "plugins/licensing/**/*", "plugins/lens/**/*", "plugins/maps/**/*", @@ -85,6 +86,7 @@ { "path": "./plugins/discover_enhanced/tsconfig.json" }, { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/global_search_providers/tsconfig.json" }, + { "path": "./plugins/enterprise_search/tsconfig.json" }, { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 694d359b6a05d..f5b35c9429a1c 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -15,6 +15,7 @@ { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "./plugins/enterprise_search/tsconfig.json" }, { "path": "./plugins/maps/tsconfig.json" }, { "path": "./plugins/maps_file_upload/tsconfig.json" }, { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, From 8780a2de6e8178d4084ce431afff58bd0edf19bc Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus <jastoltz24@gmail.com> Date: Fri, 29 Jan 2021 12:55:06 -0500 Subject: [PATCH 129/163] Better async (#89636) --- .../analytics/analytics_logic.test.ts | 33 ++++----- .../credentials/credentials_logic.test.ts | 57 +++++++-------- .../document_creation_logic.test.ts | 25 +++---- .../documents/document_detail_logic.test.ts | 26 +++---- .../components/engine/engine_logic.test.ts | 14 ++-- .../engine_overview_logic.test.ts | 19 ++--- .../components/engines/engines_logic.test.ts | 12 ++-- .../log_retention/log_retention_logic.test.ts | 29 +++----- .../indexing_status_logic.test.ts | 24 +++---- .../add_source/add_source_logic.test.ts | 69 ++++++++----------- .../display_settings_logic.test.ts | 33 ++++----- .../components/schema/schema_logic.test.ts | 59 +++++++--------- .../views/groups/group_logic.test.ts | 68 ++++++++---------- .../views/groups/groups_logic.test.ts | 69 +++++++++---------- .../views/settings/settings_logic.test.ts | 59 +++++++--------- 15 files changed, 247 insertions(+), 349 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts index 0901ff2737803..cb3273cc69387 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts @@ -9,13 +9,14 @@ import { mockKibanaValues, mockHttpValues, mockFlashMessageHelpers, - expectedAsyncError, } from '../../../__mocks__'; jest.mock('../engine', () => ({ EngineLogic: { values: { engineName: 'test-engine' } }, })); +import { nextTick } from '@kbn/test/jest'; + import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants'; import { AnalyticsLogic } from './'; @@ -176,13 +177,12 @@ describe('AnalyticsLogic', () => { }); it('should make an API call and set state based on the response', async () => { - const promise = Promise.resolve(MOCK_ANALYTICS_RESPONSE); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.resolve(MOCK_ANALYTICS_RESPONSE)); mount(); jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsDataLoad'); AnalyticsLogic.actions.loadAnalyticsData(); - await promise; + await nextTick(); expect(http.get).toHaveBeenCalledWith( '/api/app_search/engines/test-engine/analytics/queries', @@ -220,25 +220,23 @@ describe('AnalyticsLogic', () => { }); it('calls onAnalyticsUnavailable if analyticsUnavailable is in response', async () => { - const promise = Promise.resolve({ analyticsUnavailable: true }); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.resolve({ analyticsUnavailable: true })); mount(); jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsUnavailable'); AnalyticsLogic.actions.loadAnalyticsData(); - await promise; + await nextTick(); expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled(); }); it('handles errors', async () => { - const promise = Promise.reject('error'); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.reject('error')); mount(); jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsUnavailable'); AnalyticsLogic.actions.loadAnalyticsData(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('error'); expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled(); @@ -258,13 +256,12 @@ describe('AnalyticsLogic', () => { }); it('should make an API call and set state based on the response', async () => { - const promise = Promise.resolve(MOCK_QUERY_RESPONSE); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.resolve(MOCK_QUERY_RESPONSE)); mount(); jest.spyOn(AnalyticsLogic.actions, 'onQueryDataLoad'); AnalyticsLogic.actions.loadQueryData('some-query'); - await promise; + await nextTick(); expect(http.get).toHaveBeenCalledWith( '/api/app_search/engines/test-engine/analytics/queries/some-query', @@ -298,25 +295,23 @@ describe('AnalyticsLogic', () => { }); it('calls onAnalyticsUnavailable if analyticsUnavailable is in response', async () => { - const promise = Promise.resolve({ analyticsUnavailable: true }); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.resolve({ analyticsUnavailable: true })); mount(); jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsUnavailable'); AnalyticsLogic.actions.loadQueryData('some-query'); - await promise; + await nextTick(); expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled(); }); it('handles errors', async () => { - const promise = Promise.reject('error'); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.reject('error')); mount(); jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsUnavailable'); AnalyticsLogic.actions.loadQueryData('some-query'); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('error'); expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index cdd055fd367ef..2374bcb1b2d03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, - expectedAsyncError, -} from '../../../__mocks__'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; jest.mock('../../app_logic', () => ({ AppLogic: { @@ -17,9 +12,12 @@ jest.mock('../../app_logic', () => ({ values: { myRole: jest.fn(() => ({})) }, }, })); -import { AppLogic } from '../../app_logic'; +import { nextTick } from '@kbn/test/jest'; + +import { AppLogic } from '../../app_logic'; import { ApiTokenTypes } from './constants'; + import { CredentialsLogic } from './credentials_logic'; describe('CredentialsLogic', () => { @@ -1064,8 +1062,7 @@ describe('CredentialsLogic', () => { it('will call an API endpoint and set the results with the `setCredentialsData` action', async () => { mount(); jest.spyOn(CredentialsLogic.actions, 'setCredentialsData').mockImplementationOnce(() => {}); - const promise = Promise.resolve({ meta, results }); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve({ meta, results })); CredentialsLogic.actions.fetchCredentials(2); expect(http.get).toHaveBeenCalledWith('/api/app_search/credentials', { @@ -1073,17 +1070,16 @@ describe('CredentialsLogic', () => { 'page[current]': 2, }, }); - await promise; + await nextTick(); expect(CredentialsLogic.actions.setCredentialsData).toHaveBeenCalledWith(meta, results); }); it('handles errors', async () => { mount(); - const promise = Promise.reject('An error occured'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('An error occured')); CredentialsLogic.actions.fetchCredentials(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); @@ -1095,12 +1091,11 @@ describe('CredentialsLogic', () => { jest .spyOn(CredentialsLogic.actions, 'setCredentialsDetails') .mockImplementationOnce(() => {}); - const promise = Promise.resolve(credentialsDetails); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(credentialsDetails)); CredentialsLogic.actions.fetchDetails(); expect(http.get).toHaveBeenCalledWith('/api/app_search/credentials/details'); - await promise; + await nextTick(); expect(CredentialsLogic.actions.setCredentialsDetails).toHaveBeenCalledWith( credentialsDetails ); @@ -1108,11 +1103,10 @@ describe('CredentialsLogic', () => { it('handles errors', async () => { mount(); - const promise = Promise.reject('An error occured'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('An error occured')); CredentialsLogic.actions.fetchDetails(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); @@ -1124,23 +1118,21 @@ describe('CredentialsLogic', () => { it('will call an API endpoint and set the results with the `onApiKeyDelete` action', async () => { mount(); jest.spyOn(CredentialsLogic.actions, 'onApiKeyDelete').mockImplementationOnce(() => {}); - const promise = Promise.resolve(); - http.delete.mockReturnValue(promise); + http.delete.mockReturnValue(Promise.resolve()); CredentialsLogic.actions.deleteApiKey(tokenName); expect(http.delete).toHaveBeenCalledWith(`/api/app_search/credentials/${tokenName}`); - await promise; + await nextTick(); expect(CredentialsLogic.actions.onApiKeyDelete).toHaveBeenCalledWith(tokenName); expect(setSuccessMessage).toHaveBeenCalled(); }); it('handles errors', async () => { mount(); - const promise = Promise.reject('An error occured'); - http.delete.mockReturnValue(promise); + http.delete.mockReturnValue(Promise.reject('An error occured')); CredentialsLogic.actions.deleteApiKey(tokenName); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); @@ -1156,14 +1148,13 @@ describe('CredentialsLogic', () => { activeApiToken: createdToken, }); jest.spyOn(CredentialsLogic.actions, 'onApiTokenCreateSuccess'); - const promise = Promise.resolve(createdToken); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(createdToken)); CredentialsLogic.actions.onApiTokenChange(); expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', { body: JSON.stringify(createdToken), }); - await promise; + await nextTick(); expect(CredentialsLogic.actions.onApiTokenCreateSuccess).toHaveBeenCalledWith(createdToken); expect(setSuccessMessage).toHaveBeenCalled(); }); @@ -1184,25 +1175,23 @@ describe('CredentialsLogic', () => { }, }); jest.spyOn(CredentialsLogic.actions, 'onApiTokenUpdateSuccess'); - const promise = Promise.resolve(updatedToken); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.resolve(updatedToken)); CredentialsLogic.actions.onApiTokenChange(); expect(http.put).toHaveBeenCalledWith('/api/app_search/credentials/test-key', { body: JSON.stringify(updatedToken), }); - await promise; + await nextTick(); expect(CredentialsLogic.actions.onApiTokenUpdateSuccess).toHaveBeenCalledWith(updatedToken); expect(setSuccessMessage).toHaveBeenCalled(); }); it('handles errors', async () => { mount(); - const promise = Promise.reject('An error occured'); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject('An error occured')); CredentialsLogic.actions.onApiTokenChange(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts index 2256d5ae7946a..e1b562d9561ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts @@ -6,6 +6,7 @@ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; +import { nextTick } from '@kbn/test/jest'; import dedent from 'dedent'; jest.mock('./utils', () => ({ @@ -443,10 +444,10 @@ describe('DocumentCreationLogic', () => { }); it('should set and show summary from the returned response', async () => { - const promise = http.post.mockReturnValueOnce(Promise.resolve(mockValidResponse)); + http.post.mockReturnValueOnce(Promise.resolve(mockValidResponse)); await DocumentCreationLogic.actions.uploadDocuments({ documents: mockValidDocuments }); - await promise; + await nextTick(); expect(DocumentCreationLogic.actions.setSummary).toHaveBeenCalledWith(mockValidResponse); expect(DocumentCreationLogic.actions.setCreationStep).toHaveBeenCalledWith( @@ -462,7 +463,7 @@ describe('DocumentCreationLogic', () => { }); it('handles API errors', async () => { - const promise = http.post.mockReturnValueOnce( + http.post.mockReturnValueOnce( Promise.reject({ body: { statusCode: 400, @@ -473,7 +474,7 @@ describe('DocumentCreationLogic', () => { ); await DocumentCreationLogic.actions.uploadDocuments({ documents: [{}] }); - await promise; + await nextTick(); expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith( '[400 Bad Request] Invalid request payload JSON format' @@ -481,10 +482,10 @@ describe('DocumentCreationLogic', () => { }); it('handles client-side errors', async () => { - const promise = (http.post as jest.Mock).mockReturnValueOnce(new Error()); + (http.post as jest.Mock).mockReturnValueOnce(new Error()); await DocumentCreationLogic.actions.uploadDocuments({ documents: [{}] }); - await promise; + await nextTick(); expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith( "Cannot read property 'total' of undefined" @@ -493,14 +494,14 @@ describe('DocumentCreationLogic', () => { // NOTE: I can't seem to reproduce this in a production setting. it('handles errors returned from the API', async () => { - const promise = http.post.mockReturnValueOnce( + http.post.mockReturnValueOnce( Promise.resolve({ errors: ['JSON cannot be empty'], }) ); await DocumentCreationLogic.actions.uploadDocuments({ documents: [{}] }); - await promise; + await nextTick(); expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith([ 'JSON cannot be empty', @@ -536,12 +537,12 @@ describe('DocumentCreationLogic', () => { }); it('should correctly merge multiple API calls into a single summary obj', async () => { - const promise = (http.post as jest.Mock) + (http.post as jest.Mock) .mockReturnValueOnce(mockFirstResponse) .mockReturnValueOnce(mockSecondResponse); await DocumentCreationLogic.actions.uploadDocuments({ documents: largeDocumentsArray }); - await promise; + await nextTick(); expect(http.post).toHaveBeenCalledTimes(2); expect(DocumentCreationLogic.actions.setSummary).toHaveBeenCalledWith({ @@ -562,12 +563,12 @@ describe('DocumentCreationLogic', () => { }); it('should correctly merge response errors', async () => { - const promise = (http.post as jest.Mock) + (http.post as jest.Mock) .mockReturnValueOnce({ ...mockFirstResponse, errors: ['JSON cannot be empty'] }) .mockReturnValueOnce({ ...mockSecondResponse, errors: ['Too large to render'] }); await DocumentCreationLogic.actions.uploadDocuments({ documents: largeDocumentsArray }); - await promise; + await nextTick(); expect(http.post).toHaveBeenCalledTimes(2); expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith([ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index e33cd9b0e9e71..3a8861ee1e20e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -9,10 +9,11 @@ import { mockHttpValues, mockKibanaValues, mockFlashMessageHelpers, - expectedAsyncError, } from '../../../__mocks__'; import { mockEngineValues } from '../../__mocks__'; +import { nextTick } from '@kbn/test/jest'; + import { DocumentDetailLogic } from './document_detail_logic'; import { InternalSchemaTypes } from '../../../shared/types'; @@ -56,23 +57,21 @@ describe('DocumentDetailLogic', () => { it('will call an API endpoint and then store the result', async () => { const fields = [{ name: 'name', value: 'python', type: 'string' }]; jest.spyOn(DocumentDetailLogic.actions, 'setFields'); - const promise = Promise.resolve({ fields }); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve({ fields })); DocumentDetailLogic.actions.getDocumentDetails('1'); expect(http.get).toHaveBeenCalledWith(`/api/app_search/engines/engine1/documents/1`); - await promise; + await nextTick(); expect(DocumentDetailLogic.actions.setFields).toHaveBeenCalledWith(fields); }); it('handles errors', async () => { mount(); - const promise = Promise.reject('An error occurred'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('An error occurred')); DocumentDetailLogic.actions.getDocumentDetails('1'); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred', { isQueued: true }); expect(navigateToUrl).toHaveBeenCalledWith('/engines/engine1/documents'); @@ -81,13 +80,11 @@ describe('DocumentDetailLogic', () => { describe('deleteDocument', () => { let confirmSpy: any; - let promise: Promise<any>; beforeEach(() => { confirmSpy = jest.spyOn(window, 'confirm'); confirmSpy.mockImplementation(jest.fn(() => true)); - promise = Promise.resolve({}); - http.delete.mockReturnValue(promise); + http.delete.mockReturnValue(Promise.resolve({})); }); afterEach(() => { @@ -99,7 +96,7 @@ describe('DocumentDetailLogic', () => { DocumentDetailLogic.actions.deleteDocument('1'); expect(http.delete).toHaveBeenCalledWith(`/api/app_search/engines/engine1/documents/1`); - await promise; + await nextTick(); expect(setQueuedSuccessMessage).toHaveBeenCalledWith( 'Successfully marked document for deletion. It will be deleted momentarily.' ); @@ -113,16 +110,15 @@ describe('DocumentDetailLogic', () => { DocumentDetailLogic.actions.deleteDocument('1'); expect(http.delete).not.toHaveBeenCalled(); - await promise; + await nextTick(); }); it('handles errors', async () => { mount(); - promise = Promise.reject('An error occured'); - http.delete.mockReturnValue(promise); + http.delete.mockReturnValue(Promise.reject('An error occured')); DocumentDetailLogic.actions.deleteDocument('1'); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index 48cbaeef70c1a..616dae98e29f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LogicMounter, mockHttpValues, expectedAsyncError } from '../../../__mocks__'; +import { LogicMounter, mockHttpValues } from '../../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; import { EngineLogic } from './'; @@ -172,11 +174,10 @@ describe('EngineLogic', () => { it('fetches and sets engine data', async () => { mount({ engineName: 'some-engine' }); jest.spyOn(EngineLogic.actions, 'setEngineData'); - const promise = Promise.resolve(mockEngineData); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.resolve(mockEngineData)); EngineLogic.actions.initializeEngine(); - await promise; + await nextTick(); expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine'); expect(EngineLogic.actions.setEngineData).toHaveBeenCalledWith(mockEngineData); @@ -185,11 +186,10 @@ describe('EngineLogic', () => { it('handles errors', async () => { mount(); jest.spyOn(EngineLogic.actions, 'setEngineNotFound'); - const promise = Promise.reject('An error occured'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('An error occured')); EngineLogic.actions.initializeEngine(); - await expectedAsyncError(promise); + await nextTick(); expect(EngineLogic.actions.setEngineNotFound).toHaveBeenCalledWith(true); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts index b6620756699d5..9832387a563e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -4,17 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, - expectedAsyncError, -} from '../../../__mocks__'; +import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; jest.mock('../engine', () => ({ EngineLogic: { values: { engineName: 'some-engine' } }, })); +import { nextTick } from '@kbn/test/jest'; + import { EngineOverviewLogic } from './'; describe('EngineOverviewLogic', () => { @@ -85,11 +82,10 @@ describe('EngineOverviewLogic', () => { it('fetches data and calls onPollingSuccess', async () => { mount(); jest.spyOn(EngineOverviewLogic.actions, 'onPollingSuccess'); - const promise = Promise.resolve(mockEngineMetrics); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.resolve(mockEngineMetrics)); EngineOverviewLogic.actions.pollForOverviewMetrics(); - await promise; + await nextTick(); expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/overview'); expect(EngineOverviewLogic.actions.onPollingSuccess).toHaveBeenCalledWith( @@ -99,11 +95,10 @@ describe('EngineOverviewLogic', () => { it('handles errors', async () => { mount(); - const promise = Promise.reject('An error occurred'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('An error occurred')); EngineOverviewLogic.actions.pollForOverviewMetrics(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts index 5a83717aa0030..2e22c9b76cf6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts @@ -6,6 +6,8 @@ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; +import { nextTick } from '@kbn/test/jest'; + import { EngineDetails } from '../engine/types'; import { EnginesLogic } from './'; @@ -124,13 +126,12 @@ describe('EnginesLogic', () => { describe('loadEngines', () => { it('should call the engines API endpoint and set state based on the results', async () => { - const promise = Promise.resolve(MOCK_ENGINES_API_RESPONSE); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.resolve(MOCK_ENGINES_API_RESPONSE)); mount({ enginesPage: 10 }); jest.spyOn(EnginesLogic.actions, 'onEnginesLoad'); EnginesLogic.actions.loadEngines(); - await promise; + await nextTick(); expect(http.get).toHaveBeenCalledWith('/api/app_search/engines', { query: { type: 'indexed', pageIndex: 10 }, @@ -144,13 +145,12 @@ describe('EnginesLogic', () => { describe('loadMetaEngines', () => { it('should call the engines API endpoint and set state based on the results', async () => { - const promise = Promise.resolve(MOCK_ENGINES_API_RESPONSE); - http.get.mockReturnValueOnce(promise); + http.get.mockReturnValueOnce(Promise.resolve(MOCK_ENGINES_API_RESPONSE)); mount({ metaEnginesPage: 99 }); jest.spyOn(EnginesLogic.actions, 'onMetaEnginesLoad'); EnginesLogic.actions.loadMetaEngines(); - await promise; + await nextTick(); expect(http.get).toHaveBeenCalledWith('/api/app_search/engines', { query: { type: 'meta', pageIndex: 99 }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts index bfdca6791edc1..18ab05a3676c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, - expectedAsyncError, -} from '../../../__mocks__'; +import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; import { LogRetentionOptions } from './types'; import { LogRetentionLogic } from './log_retention_logic'; @@ -202,8 +199,7 @@ describe('LogRetentionLogic', () => { it('will call an API endpoint and update log retention', async () => { jest.spyOn(LogRetentionLogic.actions, 'updateLogRetention'); - const promise = Promise.resolve(TYPICAL_SERVER_LOG_RETENTION); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.resolve(TYPICAL_SERVER_LOG_RETENTION)); LogRetentionLogic.actions.saveLogRetention(LogRetentionOptions.Analytics, true); @@ -215,7 +211,7 @@ describe('LogRetentionLogic', () => { }), }); - await promise; + await nextTick(); expect(LogRetentionLogic.actions.updateLogRetention).toHaveBeenCalledWith( TYPICAL_CLIENT_LOG_RETENTION ); @@ -224,11 +220,10 @@ describe('LogRetentionLogic', () => { }); it('handles errors', async () => { - const promise = Promise.reject('An error occured'); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.reject('An error occured')); LogRetentionLogic.actions.saveLogRetention(LogRetentionOptions.Analytics, true); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); @@ -276,14 +271,13 @@ describe('LogRetentionLogic', () => { .spyOn(LogRetentionLogic.actions, 'updateLogRetention') .mockImplementationOnce(() => {}); - const promise = Promise.resolve(TYPICAL_SERVER_LOG_RETENTION); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(TYPICAL_SERVER_LOG_RETENTION)); LogRetentionLogic.actions.fetchLogRetention(); expect(LogRetentionLogic.values.isLogRetentionUpdating).toBe(true); expect(http.get).toHaveBeenCalledWith('/api/app_search/log_settings'); - await promise; + await nextTick(); expect(LogRetentionLogic.actions.updateLogRetention).toHaveBeenCalledWith( TYPICAL_CLIENT_LOG_RETENTION ); @@ -293,11 +287,10 @@ describe('LogRetentionLogic', () => { it('handles errors', async () => { mount(); jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); - const promise = Promise.reject('An error occured'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('An error occured')); LogRetentionLogic.actions.fetchLogRetention(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts index 0a80f8e361025..cfff8cc557836 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, - expectedAsyncError, -} from '../../__mocks__'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; import { IndexingStatusLogic } from './indexing_status_logic'; @@ -57,37 +54,34 @@ describe('IndexingStatusLogic', () => { it('calls API and sets values', async () => { const setIndexingStatusSpy = jest.spyOn(IndexingStatusLogic.actions, 'setIndexingStatus'); - const promise = Promise.resolve(mockStatusResponse); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(mockStatusResponse)); IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); jest.advanceTimersByTime(TIMEOUT); expect(http.get).toHaveBeenCalledWith(statusPath); - await promise; + await nextTick(); expect(setIndexingStatusSpy).toHaveBeenCalledWith(mockStatusResponse); }); it('handles error', async () => { - const promise = Promise.reject('An error occured'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('An error occured')); IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); jest.advanceTimersByTime(TIMEOUT); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); it('handles indexing complete state', async () => { - const promise = Promise.resolve({ ...mockStatusResponse, percentageComplete: 100 }); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve({ ...mockStatusResponse, percentageComplete: 100 })); IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); jest.advanceTimersByTime(TIMEOUT); - await promise; + await nextTick(); expect(clearInterval).toHaveBeenCalled(); expect(onComplete).toHaveBeenCalledWith(mockStatusResponse.numDocumentsWithErrors); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index d08f807691c2b..058645bd30862 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -4,18 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, - expectedAsyncError, -} from '../../../../../__mocks__'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; import { AppLogic } from '../../../../app_logic'; jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { nextTick } from '@kbn/test/jest'; + import { CustomSource } from '../../../../types'; import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; @@ -271,23 +268,21 @@ describe('AddSourceLogic', () => { describe('getSourceConfigData', () => { it('calls API and sets values', async () => { const setSourceConfigDataSpy = jest.spyOn(AddSourceLogic.actions, 'setSourceConfigData'); - const promise = Promise.resolve(sourceConfigData); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(sourceConfigData)); AddSourceLogic.actions.getSourceConfigData('github'); expect(http.get).toHaveBeenCalledWith( '/api/workplace_search/org/settings/connectors/github' ); - await promise; + await nextTick(); expect(setSourceConfigDataSpy).toHaveBeenCalledWith(sourceConfigData); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.getSourceConfigData('github'); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -302,15 +297,14 @@ describe('AddSourceLogic', () => { AddSourceLogic.actions, 'setSourceConnectData' ); - const promise = Promise.resolve(sourceConnectData); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(sourceConnectData)); AddSourceLogic.actions.getSourceConnectData('github', successCallback); expect(clearFlashMessages).toHaveBeenCalled(); expect(AddSourceLogic.values.buttonLoading).toEqual(true); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/sources/github/prepare'); - await promise; + await nextTick(); expect(setSourceConnectDataSpy).toHaveBeenCalledWith(sourceConnectData); expect(successCallback).toHaveBeenCalledWith(sourceConnectData.oauthUrl); expect(setButtonNotLoadingSpy).toHaveBeenCalled(); @@ -327,11 +321,10 @@ describe('AddSourceLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.getSourceConnectData('github', successCallback); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -343,24 +336,22 @@ describe('AddSourceLogic', () => { AddSourceLogic.actions, 'setSourceConnectData' ); - const promise = Promise.resolve(sourceConnectData); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(sourceConnectData)); AddSourceLogic.actions.getSourceReConnectData('github'); expect(http.get).toHaveBeenCalledWith( '/api/workplace_search/org/sources/github/reauth_prepare' ); - await promise; + await nextTick(); expect(setSourceConnectDataSpy).toHaveBeenCalledWith(sourceConnectData); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.getSourceReConnectData('github'); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -372,22 +363,20 @@ describe('AddSourceLogic', () => { AddSourceLogic.actions, 'setPreContentSourceConfigData' ); - const promise = Promise.resolve(config); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(config)); AddSourceLogic.actions.getPreContentSourceConfigData('123'); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/pre_sources/123'); - await promise; + await nextTick(); expect(setPreContentSourceConfigDataSpy).toHaveBeenCalledWith(config); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.getPreContentSourceConfigData('123'); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -414,8 +403,7 @@ describe('AddSourceLogic', () => { const successCallback = jest.fn(); const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading'); const setSourceConfigDataSpy = jest.spyOn(AddSourceLogic.actions, 'setSourceConfigData'); - const promise = Promise.resolve({ sourceConfigData }); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.resolve({ sourceConfigData })); AddSourceLogic.actions.saveSourceConfig(true, successCallback); @@ -428,7 +416,7 @@ describe('AddSourceLogic', () => { { body: JSON.stringify({ params }) } ); - await promise; + await nextTick(); expect(successCallback).toHaveBeenCalled(); expect(setSourceConfigDataSpy).toHaveBeenCalledWith({ sourceConfigData }); expect(setButtonNotLoadingSpy).toHaveBeenCalled(); @@ -453,11 +441,10 @@ describe('AddSourceLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.saveSourceConfig(true); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -495,8 +482,7 @@ describe('AddSourceLogic', () => { it('calls API and sets values', async () => { const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading'); const setCustomSourceDataSpy = jest.spyOn(AddSourceLogic.actions, 'setCustomSourceData'); - const promise = Promise.resolve({ sourceConfigData }); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve({ sourceConfigData })); AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback); @@ -505,18 +491,17 @@ describe('AddSourceLogic', () => { expect(http.post).toHaveBeenCalledWith('/api/workplace_search/org/create_source', { body: JSON.stringify({ ...params }), }); - await promise; + await nextTick(); expect(setCustomSourceDataSpy).toHaveBeenCalledWith({ sourceConfigData }); expect(successCallback).toHaveBeenCalled(); expect(setButtonNotLoadingSpy).toHaveBeenCalled(); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback); - await expectedAsyncError(promise); + await nextTick(); expect(errorCallback).toHaveBeenCalled(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts index aed99bdd950c5..d43afd589468f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts @@ -6,11 +6,7 @@ import { LogicMounter } from '../../../../../__mocks__/kea.mock'; -import { - mockFlashMessageHelpers, - mockHttpValues, - expectedAsyncError, -} from '../../../../../__mocks__'; +import { mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; const contentSource = { id: 'source123' }; jest.mock('../../source_logic', () => ({ @@ -22,6 +18,8 @@ jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { nextTick } from '@kbn/test/jest'; + import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import { LEAVE_UNASSIGNED_FIELD } from './constants'; @@ -286,14 +284,13 @@ describe('DisplaySettingsLogic', () => { DisplaySettingsLogic.actions, 'onInitializeDisplaySettings' ); - const promise = Promise.resolve(serverProps); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(serverProps)); DisplaySettingsLogic.actions.initializeDisplaySettings(); expect(http.get).toHaveBeenCalledWith( '/api/workplace_search/org/sources/source123/display_settings/config' ); - await promise; + await nextTick(); expect(onInitializeDisplaySettingsSpy).toHaveBeenCalledWith({ ...serverProps, isOrganization: true, @@ -307,14 +304,13 @@ describe('DisplaySettingsLogic', () => { DisplaySettingsLogic.actions, 'onInitializeDisplaySettings' ); - const promise = Promise.resolve(serverProps); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(serverProps)); DisplaySettingsLogic.actions.initializeDisplaySettings(); expect(http.get).toHaveBeenCalledWith( '/api/workplace_search/account/sources/source123/display_settings/config' ); - await promise; + await nextTick(); expect(onInitializeDisplaySettingsSpy).toHaveBeenCalledWith({ ...serverProps, isOrganization: false, @@ -322,10 +318,9 @@ describe('DisplaySettingsLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); DisplaySettingsLogic.actions.initializeDisplaySettings(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -337,25 +332,23 @@ describe('DisplaySettingsLogic', () => { DisplaySettingsLogic.actions, 'setServerResponseData' ); - const promise = Promise.resolve(serverProps); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(serverProps)); DisplaySettingsLogic.actions.onInitializeDisplaySettings(serverProps); DisplaySettingsLogic.actions.setServerData(); expect(http.post).toHaveBeenCalledWith(serverProps.serverRoute, { body: JSON.stringify({ ...searchResultConfig }), }); - await promise; + await nextTick(); expect(setServerResponseDataSpy).toHaveBeenCalledWith({ ...serverProps, }); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject('this is an error')); DisplaySettingsLogic.actions.setServerData(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts index 2c3aa6114c7da..c9d68201f33ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, - expectedAsyncError, -} from '../../../../../__mocks__'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; const contentSource = { id: 'source123' }; jest.mock('../../source_logic', () => ({ @@ -198,14 +195,13 @@ describe('SchemaLogic', () => { describe('initializeSchema', () => { it('calls API and sets values (org)', async () => { const onInitializeSchemaSpy = jest.spyOn(SchemaLogic.actions, 'onInitializeSchema'); - const promise = Promise.resolve(serverResponse); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(serverResponse)); SchemaLogic.actions.initializeSchema(); expect(http.get).toHaveBeenCalledWith( '/api/workplace_search/org/sources/source123/schemas' ); - await promise; + await nextTick(); expect(onInitializeSchemaSpy).toHaveBeenCalledWith(serverResponse); }); @@ -213,22 +209,20 @@ describe('SchemaLogic', () => { AppLogic.values.isOrganization = false; const onInitializeSchemaSpy = jest.spyOn(SchemaLogic.actions, 'onInitializeSchema'); - const promise = Promise.resolve(serverResponse); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(serverResponse)); SchemaLogic.actions.initializeSchema(); expect(http.get).toHaveBeenCalledWith( '/api/workplace_search/account/sources/source123/schemas' ); - await promise; + await nextTick(); expect(onInitializeSchemaSpy).toHaveBeenCalledWith(serverResponse); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); SchemaLogic.actions.initializeSchema(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -297,13 +291,12 @@ describe('SchemaLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject({ error: 'this is an error' }); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject({ error: 'this is an error' })); SchemaLogic.actions.initializeSchemaFieldErrors( mostRecentIndexJob.activeReindexJobId, contentSource.id ); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith({ error: 'this is an error', @@ -352,8 +345,7 @@ describe('SchemaLogic', () => { it('calls API and sets values (org)', async () => { AppLogic.values.isOrganization = true; const onSchemaSetSuccessSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetSuccess'); - const promise = Promise.resolve(serverResponse); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(serverResponse)); SchemaLogic.actions.setServerField(schema, ADD); expect(http.post).toHaveBeenCalledWith( @@ -362,7 +354,7 @@ describe('SchemaLogic', () => { body: JSON.stringify({ ...schema }), } ); - await promise; + await nextTick(); expect(setSuccessMessage).toHaveBeenCalledWith(SCHEMA_FIELD_ADDED_MESSAGE); expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); @@ -371,8 +363,7 @@ describe('SchemaLogic', () => { AppLogic.values.isOrganization = false; const onSchemaSetSuccessSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetSuccess'); - const promise = Promise.resolve(serverResponse); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(serverResponse)); SchemaLogic.actions.setServerField(schema, ADD); expect(http.post).toHaveBeenCalledWith( @@ -381,16 +372,15 @@ describe('SchemaLogic', () => { body: JSON.stringify({ ...schema }), } ); - await promise; + await nextTick(); expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); it('handles error', async () => { const onSchemaSetFormErrorsSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetFormErrors'); - const promise = Promise.reject({ message: 'this is an error' }); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject({ message: 'this is an error' })); SchemaLogic.actions.setServerField(schema, ADD); - await expectedAsyncError(promise); + await nextTick(); expect(onSchemaSetFormErrorsSpy).toHaveBeenCalledWith('this is an error'); }); @@ -400,8 +390,7 @@ describe('SchemaLogic', () => { it('calls API and sets values (org)', async () => { AppLogic.values.isOrganization = true; const onSchemaSetSuccessSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetSuccess'); - const promise = Promise.resolve(serverResponse); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(serverResponse)); SchemaLogic.actions.setServerField(schema, UPDATE); expect(http.post).toHaveBeenCalledWith( @@ -410,7 +399,7 @@ describe('SchemaLogic', () => { body: JSON.stringify({ ...schema }), } ); - await promise; + await nextTick(); expect(setSuccessMessage).toHaveBeenCalledWith(SCHEMA_UPDATED_MESSAGE); expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); @@ -419,8 +408,7 @@ describe('SchemaLogic', () => { AppLogic.values.isOrganization = false; const onSchemaSetSuccessSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetSuccess'); - const promise = Promise.resolve(serverResponse); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(serverResponse)); SchemaLogic.actions.setServerField(schema, UPDATE); expect(http.post).toHaveBeenCalledWith( @@ -429,15 +417,14 @@ describe('SchemaLogic', () => { body: JSON.stringify({ ...schema }), } ); - await promise; + await nextTick(); expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject('this is an error')); SchemaLogic.actions.setServerField(schema, UPDATE); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts index e90acd929a990..2e7a028e43aec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -9,9 +9,10 @@ import { mockKibanaValues, mockFlashMessageHelpers, mockHttpValues, - expectedAsyncError, } from '../../../__mocks__'; +import { nextTick } from '@kbn/test/jest'; + import { groups } from '../../__mocks__/groups.mock'; import { mockGroupValues } from './__mocks__/group_logic.mock'; import { GroupLogic } from './group_logic'; @@ -229,32 +230,29 @@ describe('GroupLogic', () => { describe('initializeGroup', () => { it('calls API and sets values', async () => { const onInitializeGroupSpy = jest.spyOn(GroupLogic.actions, 'onInitializeGroup'); - const promise = Promise.resolve(group); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(group)); GroupLogic.actions.initializeGroup(sourceIds[0]); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/groups/123'); - await promise; + await nextTick(); expect(onInitializeGroupSpy).toHaveBeenCalledWith(group); }); it('handles 404 error', async () => { - const promise = Promise.reject({ response: { status: 404 } }); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject({ response: { status: 404 } })); GroupLogic.actions.initializeGroup(sourceIds[0]); - await expectedAsyncError(promise); + await nextTick(); expect(navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); expect(setQueuedErrorMessage).toHaveBeenCalledWith('Unable to find group with ID: "123".'); }); it('handles non-404 error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); GroupLogic.actions.initializeGroup(sourceIds[0]); - await expectedAsyncError(promise); + await nextTick(); expect(navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); expect(setQueuedErrorMessage).toHaveBeenCalledWith('this is an error'); @@ -266,13 +264,12 @@ describe('GroupLogic', () => { GroupLogic.actions.onInitializeGroup(group); }); it('deletes a group', async () => { - const promise = Promise.resolve(true); - http.delete.mockReturnValue(promise); + http.delete.mockReturnValue(Promise.resolve(true)); GroupLogic.actions.deleteGroup(); expect(http.delete).toHaveBeenCalledWith('/api/workplace_search/groups/123'); - await promise; + await nextTick(); expect(navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); expect(setQueuedSuccessMessage).toHaveBeenCalledWith( 'Group "group" was successfully deleted.' @@ -280,11 +277,10 @@ describe('GroupLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.delete.mockReturnValue(promise); + http.delete.mockReturnValue(Promise.reject('this is an error')); GroupLogic.actions.deleteGroup(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -297,15 +293,14 @@ describe('GroupLogic', () => { }); it('updates name', async () => { const onGroupNameChangedSpy = jest.spyOn(GroupLogic.actions, 'onGroupNameChanged'); - const promise = Promise.resolve(group); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.resolve(group)); GroupLogic.actions.updateGroupName(); expect(http.put).toHaveBeenCalledWith('/api/workplace_search/groups/123', { body: JSON.stringify({ group: { name: 'new name' } }), }); - await promise; + await nextTick(); expect(onGroupNameChangedSpy).toHaveBeenCalledWith(group); expect(setSuccessMessage).toHaveBeenCalledWith( 'Successfully renamed this group to "group".' @@ -313,11 +308,10 @@ describe('GroupLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.reject('this is an error')); GroupLogic.actions.updateGroupName(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -330,15 +324,14 @@ describe('GroupLogic', () => { }); it('updates name', async () => { const onGroupSourcesSavedSpy = jest.spyOn(GroupLogic.actions, 'onGroupSourcesSaved'); - const promise = Promise.resolve(group); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(group)); GroupLogic.actions.saveGroupSources(); expect(http.post).toHaveBeenCalledWith('/api/workplace_search/groups/123/share', { body: JSON.stringify({ content_source_ids: sourceIds }), }); - await promise; + await nextTick(); expect(onGroupSourcesSavedSpy).toHaveBeenCalledWith(group); expect(setSuccessMessage).toHaveBeenCalledWith( 'Successfully updated shared content sources.' @@ -346,11 +339,10 @@ describe('GroupLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject('this is an error')); GroupLogic.actions.saveGroupSources(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -362,15 +354,14 @@ describe('GroupLogic', () => { }); it('updates name', async () => { const onGroupUsersSavedSpy = jest.spyOn(GroupLogic.actions, 'onGroupUsersSaved'); - const promise = Promise.resolve(group); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(group)); GroupLogic.actions.saveGroupUsers(); expect(http.post).toHaveBeenCalledWith('/api/workplace_search/groups/123/assign', { body: JSON.stringify({ user_ids: userIds }), }); - await promise; + await nextTick(); expect(onGroupUsersSavedSpy).toHaveBeenCalledWith(group); expect(setSuccessMessage).toHaveBeenCalledWith( 'Successfully updated the users of this group.' @@ -378,11 +369,10 @@ describe('GroupLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject('this is an error')); GroupLogic.actions.saveGroupUsers(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -397,8 +387,7 @@ describe('GroupLogic', () => { GroupLogic.actions, 'onGroupPrioritiesChanged' ); - const promise = Promise.resolve(group); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.resolve(group)); GroupLogic.actions.saveGroupSourcePrioritization(); expect(http.put).toHaveBeenCalledWith('/api/workplace_search/groups/123/boosts', { @@ -410,7 +399,7 @@ describe('GroupLogic', () => { }), }); - await promise; + await nextTick(); expect(setSuccessMessage).toHaveBeenCalledWith( 'Successfully updated shared source prioritization.' ); @@ -418,11 +407,10 @@ describe('GroupLogic', () => { }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.reject('this is an error')); GroupLogic.actions.saveGroupSourcePrioritization(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts index 76352a6670650..6c9f912a98ce8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, - expectedAsyncError, -} from '../../../__mocks__'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../shared/constants'; import { JSON_HEADER as headers } from '../../../../../common/constants'; @@ -22,7 +19,6 @@ import { GroupsLogic } from './groups_logic'; // We need to mock out the debounced functionality const TIMEOUT = 400; -const delay = () => new Promise((resolve) => setTimeout(resolve, TIMEOUT)); describe('GroupsLogic', () => { const { mount } = new LogicMounter(GroupsLogic); @@ -218,21 +214,19 @@ describe('GroupsLogic', () => { describe('initializeGroups', () => { it('calls API and sets values', async () => { const onInitializeGroupsSpy = jest.spyOn(GroupsLogic.actions, 'onInitializeGroups'); - const promise = Promise.resolve(groupsResponse); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(groupsResponse)); GroupsLogic.actions.initializeGroups(); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/groups'); - await promise; + await nextTick(); expect(onInitializeGroupsSpy).toHaveBeenCalledWith(groupsResponse); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); GroupsLogic.actions.initializeGroups(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -256,15 +250,22 @@ describe('GroupsLogic', () => { headers, }; + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + it('calls API and sets values', async () => { const setSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'setSearchResults'); - const promise = Promise.resolve(groups); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(groups)); GroupsLogic.actions.getSearchResults(); - await delay(); + jest.advanceTimersByTime(TIMEOUT); + await nextTick(); expect(http.post).toHaveBeenCalledWith('/api/workplace_search/groups/search', payload); - await promise; expect(setSearchResultsSpy).toHaveBeenCalledWith(groups); }); @@ -272,24 +273,22 @@ describe('GroupsLogic', () => { // Set active page to 2 to confirm resetting sends the `payload` value of 1 for the current page. GroupsLogic.actions.setActivePage(2); const setSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'setSearchResults'); - const promise = Promise.resolve(groups); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(groups)); GroupsLogic.actions.getSearchResults(true); // Account for `breakpoint` that debounces filter value. - await delay(); + jest.advanceTimersByTime(TIMEOUT); + await nextTick(); expect(http.post).toHaveBeenCalledWith('/api/workplace_search/groups/search', payload); - await promise; expect(setSearchResultsSpy).toHaveBeenCalledWith(groups); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject('this is an error')); GroupsLogic.actions.getSearchResults(); - await expectedAsyncError(promise); - await delay(); + jest.advanceTimersByTime(TIMEOUT); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -298,21 +297,19 @@ describe('GroupsLogic', () => { describe('fetchGroupUsers', () => { it('calls API and sets values', async () => { const setGroupUsersSpy = jest.spyOn(GroupsLogic.actions, 'setGroupUsers'); - const promise = Promise.resolve(users); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(users)); GroupsLogic.actions.fetchGroupUsers('123'); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/groups/123/group_users'); - await promise; + await nextTick(); expect(setGroupUsersSpy).toHaveBeenCalledWith(users); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); GroupsLogic.actions.fetchGroupUsers('123'); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -323,24 +320,22 @@ describe('GroupsLogic', () => { const GROUP_NAME = 'new group'; GroupsLogic.actions.setNewGroupName(GROUP_NAME); const setNewGroupSpy = jest.spyOn(GroupsLogic.actions, 'setNewGroup'); - const promise = Promise.resolve(groups[0]); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.resolve(groups[0])); GroupsLogic.actions.saveNewGroup(); expect(http.post).toHaveBeenCalledWith('/api/workplace_search/groups', { body: JSON.stringify({ group_name: GROUP_NAME }), headers, }); - await promise; + await nextTick(); expect(setNewGroupSpy).toHaveBeenCalledWith(groups[0]); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.post.mockReturnValue(promise); + http.post.mockReturnValue(Promise.reject('this is an error')); GroupsLogic.actions.saveNewGroup(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts index aaeae08d552d4..e21b62b500067 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts @@ -6,12 +6,9 @@ import { LogicMounter } from '../../../__mocks__/kea.mock'; -import { - mockFlashMessageHelpers, - mockHttpValues, - expectedAsyncError, - mockKibanaValues, -} from '../../../__mocks__'; +import { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; import { configuredSources, oauthApplication } from '../../__mocks__/content_sources.mock'; @@ -89,20 +86,18 @@ describe('SettingsLogic', () => { describe('initializeSettings', () => { it('calls API and sets values', async () => { const setServerPropsSpy = jest.spyOn(SettingsLogic.actions, 'setServerProps'); - const promise = Promise.resolve(configuredSources); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(configuredSources)); SettingsLogic.actions.initializeSettings(); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/settings'); - await promise; + await nextTick(); expect(setServerPropsSpy).toHaveBeenCalledWith(configuredSources); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); SettingsLogic.actions.initializeSettings(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -114,20 +109,18 @@ describe('SettingsLogic', () => { SettingsLogic.actions, 'onInitializeConnectors' ); - const promise = Promise.resolve(serverProps); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.resolve(serverProps)); SettingsLogic.actions.initializeConnectors(); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/settings/connectors'); - await promise; + await nextTick(); expect(onInitializeConnectorsSpy).toHaveBeenCalledWith(serverProps); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.get.mockReturnValue(promise); + http.get.mockReturnValue(Promise.reject('this is an error')); SettingsLogic.actions.initializeConnectors(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -138,25 +131,23 @@ describe('SettingsLogic', () => { const NAME = 'updated name'; SettingsLogic.actions.onOrgNameInputChange(NAME); const setUpdatedNameSpy = jest.spyOn(SettingsLogic.actions, 'setUpdatedName'); - const promise = Promise.resolve({ organizationName: NAME }); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.resolve({ organizationName: NAME })); SettingsLogic.actions.updateOrgName(); expect(http.put).toHaveBeenCalledWith('/api/workplace_search/org/settings/customize', { body: JSON.stringify({ name: NAME }), }); - await promise; + await nextTick(); expect(setSuccessMessage).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); expect(setUpdatedNameSpy).toHaveBeenCalledWith({ organizationName: NAME }); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.reject('this is an error')); SettingsLogic.actions.updateOrgName(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -168,8 +159,7 @@ describe('SettingsLogic', () => { SettingsLogic.actions, 'setUpdatedOauthApplication' ); - const promise = Promise.resolve({ oauthApplication }); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.resolve({ oauthApplication })); SettingsLogic.actions.setOauthApplication(oauthApplication); SettingsLogic.actions.updateOauthApplication(); @@ -183,16 +173,15 @@ describe('SettingsLogic', () => { }), } ); - await promise; + await nextTick(); expect(setUpdatedOauthApplicationSpy).toHaveBeenCalledWith({ oauthApplication }); expect(setSuccessMessage).toHaveBeenCalledWith(OAUTH_APP_UPDATED_MESSAGE); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.put.mockReturnValue(promise); + http.put.mockReturnValue(Promise.reject('this is an error')); SettingsLogic.actions.updateOauthApplication(); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); @@ -203,20 +192,18 @@ describe('SettingsLogic', () => { const NAME = 'baz'; it('calls API and sets values', async () => { - const promise = Promise.resolve({}); - http.delete.mockReturnValue(promise); + http.delete.mockReturnValue(Promise.resolve({})); SettingsLogic.actions.deleteSourceConfig(SERVICE_TYPE, NAME); - await promise; + await nextTick(); expect(navigateToUrl).toHaveBeenCalledWith('/settings/connectors'); expect(setQueuedSuccessMessage).toHaveBeenCalled(); }); it('handles error', async () => { - const promise = Promise.reject('this is an error'); - http.delete.mockReturnValue(promise); + http.delete.mockReturnValue(Promise.reject('this is an error')); SettingsLogic.actions.deleteSourceConfig(SERVICE_TYPE, NAME); - await expectedAsyncError(promise); + await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); From d6227fbb307c2cb1d3185250f99597b29fbc80d5 Mon Sep 17 00:00:00 2001 From: Alison Goryachev <alison.goryachev@elastic.co> Date: Fri, 29 Jan 2021 13:18:06 -0500 Subject: [PATCH 130/163] [Upgrade Assistant] Clean up i18n (#89661) --- .../checkup/deprecations/index_table.test.tsx | 6 +- .../tabs/checkup/deprecations/index_table.tsx | 29 ++++---- .../overview/deprecation_logging_toggle.tsx | 42 +++++------ .../components/tabs/overview/steps.tsx | 70 ++++++++++--------- 4 files changed, 77 insertions(+), 70 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx index 1c9a079bcf1eb..772d558a0d20d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; +import { shallow } from 'enzyme'; -import { IndexDeprecationTableProps, IndexDeprecationTableUI } from './index_table'; +import { IndexDeprecationTableProps, IndexDeprecationTable } from './index_table'; describe('IndexDeprecationTable', () => { const defaultProps = { @@ -22,7 +22,7 @@ describe('IndexDeprecationTable', () => { // This test simply verifies that the props passed to EuiBaseTable are the ones // expected. test('render', () => { - expect(shallowWithIntl(<IndexDeprecationTableUI {...defaultProps} />)).toMatchInlineSnapshot(` + expect(shallow(<IndexDeprecationTable {...defaultProps} />)).toMatchInlineSnapshot(` <EuiBasicTable columns={ Array [ diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx index fff8215e77ae6..d360e2a44d8c1 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx @@ -8,7 +8,7 @@ import { sortBy } from 'lodash'; import React from 'react'; import { EuiBasicTable } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { ReindexButton } from './reindex'; import { AppContext } from '../../../../app_context'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; @@ -22,7 +22,7 @@ export interface IndexDeprecationDetails { details?: string; } -export interface IndexDeprecationTableProps extends ReactIntl.InjectedIntlProps { +export interface IndexDeprecationTableProps { indices: IndexDeprecationDetails[]; } @@ -33,7 +33,7 @@ interface IndexDeprecationTableState { pageSize: number; } -export class IndexDeprecationTableUI extends React.Component< +export class IndexDeprecationTable extends React.Component< IndexDeprecationTableProps, IndexDeprecationTableState > { @@ -49,24 +49,27 @@ export class IndexDeprecationTableUI extends React.Component< } public render() { - const { intl } = this.props; const { pageIndex, pageSize, sortField, sortDirection } = this.state; const columns = [ { field: 'index', - name: intl.formatMessage({ - id: 'xpack.upgradeAssistant.checkupTab.deprecations.indexTable.indexColumnLabel', - defaultMessage: 'Index', - }), + name: i18n.translate( + 'xpack.upgradeAssistant.checkupTab.deprecations.indexTable.indexColumnLabel', + { + defaultMessage: 'Index', + } + ), sortable: true, }, { field: 'details', - name: intl.formatMessage({ - id: 'xpack.upgradeAssistant.checkupTab.deprecations.indexTable.detailsColumnLabel', - defaultMessage: 'Details', - }), + name: i18n.translate( + 'xpack.upgradeAssistant.checkupTab.deprecations.indexTable.detailsColumnLabel', + { + defaultMessage: 'Details', + } + ), }, ]; @@ -169,5 +172,3 @@ export class IndexDeprecationTableUI extends React.Component< }; } } - -export const IndexDeprecationTable = injectI18n(IndexDeprecationTableUI); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx index 0e6c79dc47b53..7a1ffb955db5c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { HttpSetup } from 'src/core/public'; import { LoadingState } from '../../types'; -interface DeprecationLoggingTabProps extends ReactIntl.InjectedIntlProps { +interface DeprecationLoggingTabProps { http: HttpSetup; } @@ -22,7 +22,7 @@ interface DeprecationLoggingTabState { loggingEnabled?: boolean; } -export class DeprecationLoggingToggleUI extends React.Component< +export class DeprecationLoggingToggle extends React.Component< DeprecationLoggingTabProps, DeprecationLoggingTabState > { @@ -59,27 +59,29 @@ export class DeprecationLoggingToggleUI extends React.Component< } private renderLoggingState() { - const { intl } = this.props; const { loggingEnabled, loadingState } = this.state; if (loadingState === LoadingState.Error) { - return intl.formatMessage({ - id: - 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch.errorLabel', - defaultMessage: 'Could not load logging state', - }); + return i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch.errorLabel', + { + defaultMessage: 'Could not load logging state', + } + ); } else if (loggingEnabled) { - return intl.formatMessage({ - id: - 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch.enabledLabel', - defaultMessage: 'On', - }); + return i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch.enabledLabel', + { + defaultMessage: 'On', + } + ); } else { - return intl.formatMessage({ - id: - 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch.disabledLabel', - defaultMessage: 'Off', - }); + return i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch.disabledLabel', + { + defaultMessage: 'Off', + } + ); } } @@ -117,5 +119,3 @@ export class DeprecationLoggingToggleUI extends React.Component< } }; } - -export const DeprecationLoggingToggle = injectI18n(DeprecationLoggingToggleUI); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx index 1a1ea48a350c8..dd392f6d1b294 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx @@ -17,7 +17,7 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../../common/version'; import { UpgradeAssistantTabProps } from '../../types'; @@ -89,10 +89,9 @@ const START_UPGRADE_STEP = (isCloudEnabled: boolean, esDocBasePath: string) => ( ), }); -export const StepsUI: FunctionComponent<UpgradeAssistantTabProps & ReactIntl.InjectedIntlProps> = ({ +export const Steps: FunctionComponent<UpgradeAssistantTabProps> = ({ checkupData, setSelectedTabIndex, - intl, }) => { const checkupDataTyped = (checkupData! as unknown) as { [checkupType: string]: any[] }; const countByType = Object.keys(checkupDataTyped).reduce((counts, checkupType) => { @@ -113,15 +112,18 @@ export const StepsUI: FunctionComponent<UpgradeAssistantTabProps & ReactIntl.Inj steps={[ { title: countByType.cluster - ? intl.formatMessage({ - id: 'xpack.upgradeAssistant.overviewTab.steps.clusterStep.issuesRemainingStepTitle', - defaultMessage: 'Check for issues with your cluster', - }) - : intl.formatMessage({ - id: - 'xpack.upgradeAssistant.overviewTab.steps.clusterStep.noIssuesRemainingStepTitle', - defaultMessage: 'Your cluster settings are ready', - }), + ? i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.clusterStep.issuesRemainingStepTitle', + { + defaultMessage: 'Check for issues with your cluster', + } + ) + : i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.clusterStep.noIssuesRemainingStepTitle', + { + defaultMessage: 'Your cluster settings are ready', + } + ), status: countByType.cluster ? 'warning' : 'complete', children: ( <EuiText> @@ -168,15 +170,18 @@ export const StepsUI: FunctionComponent<UpgradeAssistantTabProps & ReactIntl.Inj }, { title: countByType.indices - ? intl.formatMessage({ - id: 'xpack.upgradeAssistant.overviewTab.steps.indicesStep.issuesRemainingStepTitle', - defaultMessage: 'Check for issues with your indices', - }) - : intl.formatMessage({ - id: - 'xpack.upgradeAssistant.overviewTab.steps.indicesStep.noIssuesRemainingStepTitle', - defaultMessage: 'Your index settings are ready', - }), + ? i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.indicesStep.issuesRemainingStepTitle', + { + defaultMessage: 'Check for issues with your indices', + } + ) + : i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.indicesStep.noIssuesRemainingStepTitle', + { + defaultMessage: 'Your index settings are ready', + } + ), status: countByType.indices ? 'warning' : 'complete', children: ( <EuiText> @@ -222,10 +227,12 @@ export const StepsUI: FunctionComponent<UpgradeAssistantTabProps & ReactIntl.Inj ), }, { - title: intl.formatMessage({ - id: 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.stepTitle', - defaultMessage: 'Review the Elasticsearch deprecation logs', - }), + title: i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.stepTitle', + { + defaultMessage: 'Review the Elasticsearch deprecation logs', + } + ), children: ( <Fragment> <EuiText grow={false}> @@ -256,11 +263,12 @@ export const StepsUI: FunctionComponent<UpgradeAssistantTabProps & ReactIntl.Inj <EuiSpacer /> <EuiFormRow - label={intl.formatMessage({ - id: - 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingLabel', - defaultMessage: 'Enable deprecation logging?', - })} + label={i18n.translate( + 'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingLabel', + { + defaultMessage: 'Enable deprecation logging?', + } + )} describedByIds={['deprecation-logging']} > <DeprecationLoggingToggle http={http} /> @@ -276,5 +284,3 @@ export const StepsUI: FunctionComponent<UpgradeAssistantTabProps & ReactIntl.Inj /> ); }; - -export const Steps = injectI18n(StepsUI); From 7609fb9351f7e9c7289e96eea19421d68fb9112c Mon Sep 17 00:00:00 2001 From: Pier-Hugues Pellerin <phpellerin@gmail.com> Date: Fri, 29 Jan 2021 13:21:53 -0500 Subject: [PATCH 131/163] Update code owners for Fleet (#89715) Rename ingest-management to fleet. --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dea2c12756b08..3343544d57fad 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -99,7 +99,7 @@ # Observability UIs /x-pack/plugins/infra/ @elastic/logs-metrics-ui -/x-pack/plugins/fleet/ @elastic/ingest-management +/x-pack/plugins/fleet/ @elastic/fleet /x-pack/plugins/observability/ @elastic/observability-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime From a6fe0a2de78a8766ff0f34272e6d81daa11a2568 Mon Sep 17 00:00:00 2001 From: Brandon Kobel <brandon.kobel@elastic.co> Date: Fri, 29 Jan 2021 10:36:50 -0800 Subject: [PATCH 132/163] Fix error thrown when Kibana is sent a SIGHUP to reload logging config (#89218) * Fix error thrown when Kibana is sent a SIGHUP to reload logging config * Adding a simple unit test to catch a future regression Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/setup_logging.test.ts | 35 +++++++++++++++++++ .../kbn-legacy-logging/src/setup_logging.ts | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 packages/kbn-legacy-logging/src/setup_logging.test.ts diff --git a/packages/kbn-legacy-logging/src/setup_logging.test.ts b/packages/kbn-legacy-logging/src/setup_logging.test.ts new file mode 100644 index 0000000000000..6386b400329b9 --- /dev/null +++ b/packages/kbn-legacy-logging/src/setup_logging.test.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Server } from '@hapi/hapi'; +import { reconfigureLogging, setupLogging } from './setup_logging'; +import { LegacyLoggingConfig } from './schema'; + +describe('reconfigureLogging', () => { + test(`doesn't throw an error`, () => { + const server = new Server(); + const config: LegacyLoggingConfig = { + silent: false, + quiet: false, + verbose: true, + events: {}, + dest: '/tmp/foo', + filter: {}, + json: true, + rotate: { + enabled: false, + everyBytes: 0, + keepFiles: 0, + pollingInterval: 0, + usePolling: false, + }, + }; + setupLogging(server, config, 10); + reconfigureLogging(server, { ...config, dest: '/tmp/bar' }, 0); + }); +}); diff --git a/packages/kbn-legacy-logging/src/setup_logging.ts b/packages/kbn-legacy-logging/src/setup_logging.ts index 4370e4ab77d68..ffe3be558f366 100644 --- a/packages/kbn-legacy-logging/src/setup_logging.ts +++ b/packages/kbn-legacy-logging/src/setup_logging.ts @@ -37,5 +37,5 @@ export function reconfigureLogging( opsInterval: number ) { const loggingOptions = getLoggingConfiguration(config, opsInterval); - (server.plugins as any)['@elastic/good'].reconfigure(loggingOptions); + (server.plugins as any).good.reconfigure(loggingOptions); } From 2055cb96bae7850deba68220edb3ce4545f0e8b3 Mon Sep 17 00:00:00 2001 From: Corey Robertson <corey.robertson@elastic.co> Date: Fri, 29 Jan 2021 13:38:18 -0500 Subject: [PATCH 133/163] Adds find by value embeddables helper (#89629) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/dashboard/server/index.ts | 1 + .../usage/find_by_value_embeddables.test.ts | 60 +++++++++++++++++++ .../server/usage/find_by_value_embeddables.ts | 34 +++++++++++ 3 files changed, 95 insertions(+) create mode 100644 src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts create mode 100644 src/plugins/dashboard/server/usage/find_by_value_embeddables.ts diff --git a/src/plugins/dashboard/server/index.ts b/src/plugins/dashboard/server/index.ts index cc784f5f81c9e..4bd43d1cd64a9 100644 --- a/src/plugins/dashboard/server/index.ts +++ b/src/plugins/dashboard/server/index.ts @@ -25,3 +25,4 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { DashboardPluginSetup, DashboardPluginStart } from './types'; +export { findByValueEmbeddables } from './usage/find_by_value_embeddables'; diff --git a/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts b/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts new file mode 100644 index 0000000000000..3da6a8050f14c --- /dev/null +++ b/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedDashboardPanel730ToLatest } from '../../common'; +import { findByValueEmbeddables } from './find_by_value_embeddables'; + +const visualizationByValue = ({ + embeddableConfig: { + value: 'visualization-by-value', + }, + type: 'visualization', +} as unknown) as SavedDashboardPanel730ToLatest; + +const mapByValue = ({ + embeddableConfig: { + value: 'map-by-value', + }, + type: 'map', +} as unknown) as SavedDashboardPanel730ToLatest; + +const embeddableByRef = ({ + panelRefName: 'panel_ref_1', +} as unknown) as SavedDashboardPanel730ToLatest; + +describe('findByValueEmbeddables', () => { + it('finds the by value embeddables for the given type', async () => { + const savedObjectsResult = { + saved_objects: [ + { + attributes: { + panelsJSON: JSON.stringify([visualizationByValue, mapByValue, embeddableByRef]), + }, + }, + { + attributes: { + panelsJSON: JSON.stringify([embeddableByRef, mapByValue, visualizationByValue]), + }, + }, + ], + }; + const savedObjectClient = { find: jest.fn().mockResolvedValue(savedObjectsResult) }; + + const maps = await findByValueEmbeddables(savedObjectClient, 'map'); + + expect(maps.length).toBe(2); + expect(maps[0]).toEqual(mapByValue.embeddableConfig); + expect(maps[1]).toEqual(mapByValue.embeddableConfig); + + const visualizations = await findByValueEmbeddables(savedObjectClient, 'visualization'); + + expect(visualizations.length).toBe(2); + expect(visualizations[0]).toEqual(visualizationByValue.embeddableConfig); + expect(visualizations[1]).toEqual(visualizationByValue.embeddableConfig); + }); +}); diff --git a/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts b/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts new file mode 100644 index 0000000000000..0ae14cdcf7197 --- /dev/null +++ b/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { ISavedObjectsRepository, SavedObjectAttributes } from 'kibana/server'; +import { SavedDashboardPanel730ToLatest } from '../../common'; + +export const findByValueEmbeddables = async ( + savedObjectClient: Pick<ISavedObjectsRepository, 'find'>, + embeddableType: string +) => { + const dashboards = await savedObjectClient.find<SavedObjectAttributes>({ + type: 'dashboard', + }); + + return dashboards.saved_objects + .map((dashboard) => { + try { + return (JSON.parse( + dashboard.attributes.panelsJSON as string + ) as unknown) as SavedDashboardPanel730ToLatest[]; + } catch (exception) { + return []; + } + }) + .flat() + .filter((panel) => (panel as Record<string, any>).panelRefName === undefined) + .filter((panel) => panel.type === embeddableType) + .map((panel) => panel.embeddableConfig); +}; From c5ad2ca5dd9de87d82e2b2908f7c82a78ea2563d Mon Sep 17 00:00:00 2001 From: Pier-Hugues Pellerin <phpellerin@gmail.com> Date: Fri, 29 Jan 2021 13:39:28 -0500 Subject: [PATCH 134/163] Adjust Path labeller for Team:Fleet (#89769) Move from Team:Ingest management to Team:Fleet --- .github/paths-labeller.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index f74870578ecb1..81d57be9b2d95 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -10,7 +10,7 @@ - "src/plugins/bfetch/**/*.*" - "Team:apm": - "x-pack/plugins/apm/**/*.*" - - "Team:Ingest Management": + - "Team:Fleet": - "x-pack/plugins/fleet/**/*.*" - "x-pack/test/fleet_api_integration/**/*.*" - "Team:uptime": From 4f6de5a407d2f06edad2599883aac8668eb69272 Mon Sep 17 00:00:00 2001 From: Constance <constancecchen@users.noreply.github.com> Date: Fri, 29 Jan 2021 11:42:37 -0800 Subject: [PATCH 135/163] [App Search] Add final Analytics table components (#89233) * Add new AnalyticsSection component * Update views that use AnalyticsSection * [Setup] Update types + final API logic data - export query types so that new table components can use them - reorganize type keys by their (upcoming) table column order, remove unused tags from document obj * [Setup] Migrate InlineTagsList component - used for tags columns in all tables * Create basic AnalyticsTable component - there's a lot of logic separated out into constants.tsx right now, I promise it will make more sense when the one-off tables get added * Update all views that use AnalyticsTable + add 'view all' button links to overview tables * Add RecentQueriesTable component - Why is the API for this specific table so different? who knows, but it do be that way * Update views with RecentQueryTable * Add QueryClicksTable component to QueryDetails view * Create AnalyticsSearch bar for queries subpages * [Polish] Add some space to the bottom of analytics pages * [Design feedback] Tweak header + search form layout - Have analytics filter form be on its own row separate from page title - Change AnalyticsSearch to stretch to full width + add placeholder text + match header gutter + remain one line on mobile * [PR feedback] Type clarification * [PR feedback] Clear mocks * [PR suggestion] File rename constants.tsx -> shared_columns.tsx Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/analytics/analytics_layout.tsx | 2 + .../analytics/analytics_logic.test.ts | 24 ++--- .../components/analytics/analytics_logic.ts | 36 +++++++ .../components/analytics_header.scss | 14 +++ .../analytics/components/analytics_header.tsx | 12 ++- .../components/analytics_search.test.tsx | 56 +++++++++++ .../analytics/components/analytics_search.tsx | 53 ++++++++++ .../components/analytics_section.test.tsx | 24 +++++ .../components/analytics_section.tsx | 28 ++++++ .../analytics_tables/analytics_table.test.tsx | 90 +++++++++++++++++ .../analytics_tables/analytics_table.tsx | 76 ++++++++++++++ .../components/analytics_tables/index.ts | 9 ++ .../inline_tags_list.test.tsx | 38 +++++++ .../analytics_tables/inline_tags_list.tsx | 44 +++++++++ .../query_clicks_table.test.tsx | 77 +++++++++++++++ .../analytics_tables/query_clicks_table.tsx | 78 +++++++++++++++ .../recent_queries_table.test.tsx | 85 ++++++++++++++++ .../analytics_tables/recent_queries_table.tsx | 82 +++++++++++++++ .../analytics_tables/shared_columns.tsx | 99 +++++++++++++++++++ .../components/analytics/components/index.ts | 3 + .../app_search/components/analytics/types.ts | 16 ++- .../analytics/views/analytics.test.tsx | 28 +++++- .../components/analytics/views/analytics.tsx | 96 +++++++++++++++++- .../analytics/views/query_detail.test.tsx | 3 +- .../analytics/views/query_detail.tsx | 18 +++- .../analytics/views/recent_queries.test.tsx | 6 +- .../analytics/views/recent_queries.tsx | 8 +- .../analytics/views/top_queries.test.tsx | 6 +- .../analytics/views/top_queries.tsx | 8 +- .../views/top_queries_no_clicks.test.tsx | 6 +- .../analytics/views/top_queries_no_clicks.tsx | 8 +- .../views/top_queries_no_results.test.tsx | 6 +- .../views/top_queries_no_results.tsx | 8 +- .../views/top_queries_with_clicks.test.tsx | 6 +- .../views/top_queries_with_clicks.tsx | 8 +- 35 files changed, 1114 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx index 68906e2927a0d..22847843826da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx @@ -7,6 +7,7 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; +import { EuiSpacer } from '@elastic/eui'; import { KibanaLogic } from '../../../shared/kibana'; import { FlashMessages } from '../../../shared/flash_messages'; @@ -47,6 +48,7 @@ export const AnalyticsLayout: React.FC<Props> = ({ <FlashMessages /> <LogRetentionCallout type={LogRetentionOptions.Analytics} /> {children} + <EuiSpacer /> </> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts index cb3273cc69387..59e33893a18eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts @@ -30,6 +30,11 @@ describe('AnalyticsLogic', () => { dataLoading: true, analyticsUnavailable: false, allTags: [], + recentQueries: [], + topQueries: [], + topQueriesNoResults: [], + topQueriesNoClicks: [], + topQueriesWithClicks: [], totalQueries: 0, totalQueriesNoResults: 0, totalClicks: 0, @@ -38,6 +43,7 @@ describe('AnalyticsLogic', () => { queriesNoResultsPerDay: [], clicksPerDay: [], queriesPerDayForQuery: [], + topClicksForQuery: [], startDate: '', }; @@ -130,16 +136,7 @@ describe('AnalyticsLogic', () => { expect(AnalyticsLogic.values).toEqual({ ...DEFAULT_VALUES, dataLoading: false, - analyticsUnavailable: false, - allTags: ['some-tag'], - startDate: '1970-01-01', - totalClicks: 1000, - totalQueries: 5000, - totalQueriesNoResults: 500, - queriesPerDay: [10, 50, 100], - queriesNoResultsPerDay: [1, 2, 3], - clicksPerDay: [0, 10, 50], - // TODO: Replace this with ...MOCK_ANALYTICS_RESPONSE once all data is set + ...MOCK_ANALYTICS_RESPONSE, }); }); }); @@ -152,12 +149,7 @@ describe('AnalyticsLogic', () => { expect(AnalyticsLogic.values).toEqual({ ...DEFAULT_VALUES, dataLoading: false, - analyticsUnavailable: false, - allTags: ['some-tag'], - startDate: '1970-01-01', - totalQueriesForQuery: 50, - queriesPerDayForQuery: [25, 0, 25], - // TODO: Replace this with ...MOCK_QUERY_RESPONSE once all data is set + ...MOCK_QUERY_RESPONSE, }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts index 537de02a0fee5..0caf804ea2a08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts @@ -62,6 +62,36 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction onQueryDataLoad: (_, { allTags }) => allTags, }, ], + recentQueries: [ + [], + { + onAnalyticsDataLoad: (_, { recentQueries }) => recentQueries, + }, + ], + topQueries: [ + [], + { + onAnalyticsDataLoad: (_, { topQueries }) => topQueries, + }, + ], + topQueriesNoResults: [ + [], + { + onAnalyticsDataLoad: (_, { topQueriesNoResults }) => topQueriesNoResults, + }, + ], + topQueriesNoClicks: [ + [], + { + onAnalyticsDataLoad: (_, { topQueriesNoClicks }) => topQueriesNoClicks, + }, + ], + topQueriesWithClicks: [ + [], + { + onAnalyticsDataLoad: (_, { topQueriesWithClicks }) => topQueriesWithClicks, + }, + ], totalQueries: [ 0, { @@ -110,6 +140,12 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction onQueryDataLoad: (_, { queriesPerDayForQuery }) => queriesPerDayForQuery, }, ], + topClicksForQuery: [ + [], + { + onQueryDataLoad: (_, { topClicksForQuery }) => topClicksForQuery, + }, + ], startDate: [ '', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss new file mode 100644 index 0000000000000..f3c503d4b27cb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss @@ -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. + */ + +.analyticsHeader { + flex-wrap: wrap; + + &__filters.euiPageHeaderSection { + width: 100%; + margin: $euiSizeM 0; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx index 6866a89687a74..e82c3aff70119 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx @@ -30,6 +30,8 @@ import { AnalyticsLogic } from '../'; import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants'; import { convertTagsToSelectOptions } from '../utils'; +import './analytics_header.scss'; + interface Props { title: string; } @@ -60,7 +62,7 @@ export const AnalyticsHeader: React.FC<Props> = ({ title }) => { const hasInvalidDateRange = startDate > endDate; return ( - <EuiPageHeader> + <EuiPageHeader className="analyticsHeader"> <EuiPageHeaderSection> <EuiFlexGroup alignItems="center" justifyContent="flexStart" responsive={false}> <EuiFlexItem grow={false}> @@ -69,13 +71,13 @@ export const AnalyticsHeader: React.FC<Props> = ({ title }) => { </EuiTitle> </EuiFlexItem> <EuiFlexItem grow={false}> - <LogRetentionTooltip type={LogRetentionOptions.Analytics} /> + <LogRetentionTooltip type={LogRetentionOptions.Analytics} position="right" /> </EuiFlexItem> </EuiFlexGroup> </EuiPageHeaderSection> - <EuiPageHeaderSection> + <EuiPageHeaderSection className="analyticsHeader__filters"> <EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="m"> - <EuiFlexItem grow={false}> + <EuiFlexItem> <EuiSelect options={convertTagsToSelectOptions(allTags)} value={currentTag} @@ -87,7 +89,7 @@ export const AnalyticsHeader: React.FC<Props> = ({ title }) => { fullWidth /> </EuiFlexItem> - <EuiFlexItem grow={false}> + <EuiFlexItem> <EuiDatePickerRange startDateControl={ <EuiDatePicker diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx new file mode 100644 index 0000000000000..34161ba80dab8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockKibanaValues } from '../../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFieldSearch } from '@elastic/eui'; + +import { AnalyticsSearch } from './'; + +describe('AnalyticsSearch', () => { + const { navigateToUrl } = mockKibanaValues; + const preventDefault = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const wrapper = shallow(<AnalyticsSearch />); + const setSearchValue = (value: string) => + wrapper.find(EuiFieldSearch).simulate('change', { target: { value } }); + + it('renders', () => { + expect(wrapper.find(EuiFieldSearch)).toHaveLength(1); + }); + + it('updates searchValue state on input change', () => { + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual(''); + + setSearchValue('some-query'); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('some-query'); + }); + + it('sends the user to the query detail page on search', () => { + wrapper.find('form').simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(navigateToUrl).toHaveBeenCalledWith( + '/engines/some-engine/analytics/query_detail/some-query' + ); + }); + + it('falls back to showing the "" query if searchValue is empty', () => { + setSearchValue(''); + wrapper.find('form').simulate('submit', { preventDefault }); + + expect(navigateToUrl).toHaveBeenCalledWith( + '/engines/some-engine/analytics/query_detail/%22%22' // "" gets encoded + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx new file mode 100644 index 0000000000000..fc2639d87a2f9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { useValues } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton, EuiSpacer } from '@elastic/eui'; + +import { KibanaLogic } from '../../../../shared/kibana'; +import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../routes'; +import { generateEnginePath } from '../../engine'; + +export const AnalyticsSearch: React.FC = () => { + const [searchValue, setSearchValue] = useState(''); + + const { navigateToUrl } = useValues(KibanaLogic); + const viewQueryDetails = (e: React.SyntheticEvent) => { + e.preventDefault(); + const query = searchValue || '""'; + navigateToUrl(generateEnginePath(ENGINE_ANALYTICS_QUERY_DETAIL_PATH, { query })); + }; + + return ( + <form onSubmit={viewQueryDetails}> + <EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}> + <EuiFlexItem> + <EuiFieldSearch + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetailSearchPlaceholder', + { defaultMessage: 'Go to search term' } + )} + fullWidth + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton type="submit"> + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetailSearchButtonLabel', + { defaultMessage: 'View details' } + )} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + </form> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx new file mode 100644 index 0000000000000..1814aba7497f6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { AnalyticsSection } from './'; + +describe('AnalyticsSection', () => { + it('renders', () => { + const wrapper = shallow( + <AnalyticsSection title="Lorem ipsum" subtitle="Dolor sit amet."> + <div data-test-subj="HelloWorld">Test</div> + </AnalyticsSection> + ); + + expect(wrapper.find('h2').text()).toEqual('Lorem ipsum'); + expect(wrapper.find('p').text()).toEqual('Dolor sit amet.'); + expect(wrapper.find('[data-test-subj="HelloWorld"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.tsx new file mode 100644 index 0000000000000..e14ef0b1f2631 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.tsx @@ -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 React from 'react'; + +import { EuiPageContentBody, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; + +interface Props { + title: string; + subtitle: string; +} +export const AnalyticsSection: React.FC<Props> = ({ title, subtitle, children }) => ( + <section> + <header> + <EuiTitle size="m"> + <h2>{title}</h2> + </EuiTitle> + <EuiText size="s" color="subdued"> + <p>{subtitle}</p> + </EuiText> + </header> + <EuiSpacer size="m" /> + <EuiPageContentBody>{children}</EuiPageContentBody> + </section> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx new file mode 100644 index 0000000000000..88f7e858bef62 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__'; +import '../../../../__mocks__/engine_logic.mock'; + +import React from 'react'; +import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; + +import { AnalyticsTable } from './'; + +describe('AnalyticsTable', () => { + const { navigateToUrl } = mockKibanaValues; + + const items = [ + { + key: 'some search', + tags: ['tagA'], + searches: { doc_count: 100 }, + clicks: { doc_count: 10 }, + }, + { + key: 'another search', + tags: ['tagB'], + searches: { doc_count: 99 }, + clicks: { doc_count: 9 }, + }, + { + key: '', + tags: ['tagA', 'tagB'], + searches: { doc_count: 1 }, + clicks: { doc_count: 0 }, + }, + ]; + + it('renders', () => { + const wrapper = mountWithIntl(<AnalyticsTable items={items} />); + const tableContent = wrapper.find(EuiBasicTable).text(); + + expect(tableContent).toContain('Search term'); + expect(tableContent).toContain('some search'); + expect(tableContent).toContain('another search'); + expect(tableContent).toContain('""'); + + expect(tableContent).toContain('Analytics tags'); + expect(tableContent).toContain('tagA'); + expect(tableContent).toContain('tagB'); + expect(wrapper.find(EuiBadge)).toHaveLength(4); + + expect(tableContent).toContain('Queries'); + expect(tableContent).toContain('100'); + expect(tableContent).toContain('99'); + expect(tableContent).toContain('1'); + expect(tableContent).not.toContain('Clicks'); + }); + + it('renders a clicks column if hasClicks is passed', () => { + const wrapper = mountWithIntl(<AnalyticsTable items={items} hasClicks />); + const tableContent = wrapper.find(EuiBasicTable).text(); + + expect(tableContent).toContain('Clicks'); + expect(tableContent).toContain('10'); + expect(tableContent).toContain('9'); + expect(tableContent).toContain('0'); + }); + + it('renders an action column', () => { + const wrapper = mountWithIntl(<AnalyticsTable items={items} />); + const viewQuery = wrapper.find('[data-test-subj="AnalyticsTableViewQueryButton"]').first(); + const editQuery = wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first(); + + viewQuery.simulate('click'); + expect(navigateToUrl).toHaveBeenCalledWith( + '/engines/some-engine/analytics/query_detail/some%20search' + ); + + editQuery.simulate('click'); + // TODO + }); + + it('renders an empty prompt if no items are passed', () => { + const wrapper = mountWithIntl(<AnalyticsTable items={[]} />); + const promptContent = wrapper.find(EuiEmptyPrompt).text(); + + expect(promptContent).toContain('No queries were performed during this time period.'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx new file mode 100644 index 0000000000000..41690dfe26e71 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx @@ -0,0 +1,76 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; + +import { Query } from '../../types'; +import { + TERM_COLUMN_PROPS, + TAGS_COLUMN, + COUNT_COLUMN_PROPS, + ACTIONS_COLUMN, +} from './shared_columns'; + +interface Props { + items: Query[]; + hasClicks?: boolean; +} +type Columns = Array<EuiBasicTableColumn<Query>>; + +export const AnalyticsTable: React.FC<Props> = ({ items, hasClicks }) => { + const TERM_COLUMN = { + field: 'key', + ...TERM_COLUMN_PROPS, + }; + + const COUNT_COLUMNS = [ + { + field: 'searches.doc_count', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.queriesColumn', + { defaultMessage: 'Queries' } + ), + ...COUNT_COLUMN_PROPS, + }, + ]; + if (hasClicks) { + COUNT_COLUMNS.push({ + field: 'clicks.doc_count', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.clicksColumn', { + defaultMessage: 'Clicks', + }), + ...COUNT_COLUMN_PROPS, + }); + } + + return ( + <EuiBasicTable + columns={[TERM_COLUMN, TAGS_COLUMN, ...COUNT_COLUMNS, ACTIONS_COLUMN] as Columns} + items={items} + responsive + hasActions + noItemsMessage={ + <EuiEmptyPrompt + iconType="visLine" + title={ + <h4> + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noQueriesTitle', + { defaultMessage: 'No queries' } + )} + </h4> + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noQueriesDescription', + { defaultMessage: 'No queries were performed during this time period.' } + )} + /> + } + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/index.ts new file mode 100644 index 0000000000000..99363c00caaf7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AnalyticsTable } from './analytics_table'; +export { RecentQueriesTable } from './recent_queries_table'; +export { QueryClicksTable } from './query_clicks_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx new file mode 100644 index 0000000000000..5909ceec4555c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { shallow } from 'enzyme'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; + +import { InlineTagsList } from './inline_tags_list'; + +describe('InlineTagsList', () => { + it('renders', () => { + const wrapper = shallow(<InlineTagsList tags={['test']} />); + + expect(wrapper.find(EuiBadge)).toHaveLength(1); + expect(wrapper.find(EuiBadge).prop('children')).toEqual('test'); + }); + + it('renders >2 badges in a tooltip list', () => { + const wrapper = shallow(<InlineTagsList tags={['1', '2', '3', '4', '5']} />); + + expect(wrapper.find(EuiBadge)).toHaveLength(3); + expect(wrapper.find(EuiToolTip)).toHaveLength(1); + + expect(wrapper.find(EuiBadge).at(0).prop('children')).toEqual('1'); + expect(wrapper.find(EuiBadge).at(1).prop('children')).toEqual('2'); + expect(wrapper.find(EuiBadge).at(2).prop('children')).toEqual('and 3 more'); + expect(wrapper.find(EuiToolTip).prop('content')).toEqual('3, 4, 5'); + }); + + it('does not render with no tags', () => { + const wrapper = shallow(<InlineTagsList tags={[]} />); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx new file mode 100644 index 0000000000000..853f04ee1aa77 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx @@ -0,0 +1,44 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiBadgeGroup, EuiBadge, EuiToolTip } from '@elastic/eui'; + +import { Query } from '../../types'; + +interface Props { + tags?: Query['tags']; +} +export const InlineTagsList: React.FC<Props> = ({ tags }) => { + if (!tags?.length) return null; + + const displayedTags = tags.slice(0, 2); + const tooltipTags = tags.slice(2); + + return ( + <EuiBadgeGroup> + {displayedTags.map((tag: string) => ( + <EuiBadge color="hollow" key={tag}> + {tag} + </EuiBadge> + ))} + {tooltipTags.length > 0 && ( + <EuiToolTip position="bottom" content={tooltipTags.join(', ')}> + <EuiBadge> + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.moreTagsBadge', + { + defaultMessage: 'and {moreTagsCount} more', + values: { moreTagsCount: tooltipTags.length }, + } + )} + </EuiBadge> + </EuiToolTip> + )} + </EuiBadgeGroup> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx new file mode 100644 index 0000000000000..9db9c140d7f50 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.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 { mountWithIntl } from '../../../../../__mocks__'; +import '../../../../__mocks__/engine_logic.mock'; + +import React from 'react'; +import { EuiBasicTable, EuiLink, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; + +import { QueryClicksTable } from './'; + +describe('QueryClicksTable', () => { + const items = [ + { + key: 'some-document', + document: { + engine: 'some-engine', + id: 'some-document', + }, + tags: ['tagA'], + doc_count: 10, + }, + { + key: 'another-document', + document: { + engine: 'another-engine', + id: 'another-document', + }, + tags: ['tagB'], + doc_count: 5, + }, + { + key: 'deleted-document', + tags: [], + doc_count: 1, + }, + ]; + + it('renders', () => { + const wrapper = mountWithIntl(<QueryClicksTable items={items} />); + const tableContent = wrapper.find(EuiBasicTable).text(); + + expect(tableContent).toContain('Documents'); + expect(tableContent).toContain('some-document'); + expect(tableContent).toContain('another-document'); + expect(tableContent).toContain('deleted-document'); + + expect(wrapper.find(EuiLink).first().prop('href')).toEqual( + '/app/enterprise_search/engines/some-engine/documents/some-document' + ); + expect(wrapper.find(EuiLink).last().prop('href')).toEqual( + '/app/enterprise_search/engines/another-engine/documents/another-document' + ); + // deleted-document should not have a link + + expect(tableContent).toContain('Analytics tags'); + expect(tableContent).toContain('tagA'); + expect(tableContent).toContain('tagB'); + expect(wrapper.find(EuiBadge)).toHaveLength(2); + + expect(tableContent).toContain('Clicks'); + expect(tableContent).toContain('10'); + expect(tableContent).toContain('5'); + expect(tableContent).toContain('1'); + }); + + it('renders an empty prompt if no items are passed', () => { + const wrapper = mountWithIntl(<QueryClicksTable items={[]} />); + const promptContent = wrapper.find(EuiEmptyPrompt).text(); + + expect(promptContent).toContain('No clicks'); + expect(promptContent).toContain('No documents have been clicked from this query.'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx new file mode 100644 index 0000000000000..e032e42eca3a6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.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 from 'react'; + +import { i18n } from '@kbn/i18n'; +import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; +import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../../../routes'; +import { generateEnginePath } from '../../../engine'; +import { DOCUMENTS_TITLE } from '../../../documents'; + +import { QueryClick } from '../../types'; +import { FIRST_COLUMN_PROPS, TAGS_COLUMN, COUNT_COLUMN_PROPS } from './shared_columns'; + +interface Props { + items: QueryClick[]; +} +type Columns = Array<EuiBasicTableColumn<QueryClick>>; + +export const QueryClicksTable: React.FC<Props> = ({ items }) => { + const DOCUMENT_COLUMN = { + ...FIRST_COLUMN_PROPS, + field: 'document', + name: DOCUMENTS_TITLE, + render: (document: QueryClick['document'], query: QueryClick) => { + return document ? ( + <EuiLinkTo + to={generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, { + engineName: document.engine, + documentId: document.id, + })} + > + {document.id} + </EuiLinkTo> + ) : ( + query.key + ); + }, + }; + + const CLICKS_COLUMN = { + ...COUNT_COLUMN_PROPS, + field: 'doc_count', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.clicksColumn', { + defaultMessage: 'Clicks', + }), + }; + + return ( + <EuiBasicTable + columns={[DOCUMENT_COLUMN, TAGS_COLUMN, CLICKS_COLUMN] as Columns} + items={items} + responsive + noItemsMessage={ + <EuiEmptyPrompt + iconType="visLine" + title={ + <h4> + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noClicksTitle', + { defaultMessage: 'No clicks' } + )} + </h4> + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noClicksDescription', + { defaultMessage: 'No documents have been clicked from this query.' } + )} + /> + } + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx new file mode 100644 index 0000000000000..261d0f75c1cee --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__'; +import '../../../../__mocks__/engine_logic.mock'; + +import React from 'react'; +import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; + +import { RecentQueriesTable } from './'; + +describe('RecentQueriesTable', () => { + const { navigateToUrl } = mockKibanaValues; + + const items = [ + { + query_string: 'some search', + timestamp: '1970-01-03T12:00:00Z', + tags: ['tagA'], + document_ids: ['documentA', 'documentB'], + }, + { + query_string: 'another search', + timestamp: '1970-01-02T12:00:00Z', + tags: ['tagB'], + document_ids: ['documentC'], + }, + { + query_string: '', + timestamp: '1970-01-01T12:00:00Z', + tags: ['tagA', 'tagB'], + document_ids: ['documentA', 'documentB', 'documentC'], + }, + ]; + + it('renders', () => { + const wrapper = mountWithIntl(<RecentQueriesTable items={items} />); + const tableContent = wrapper.find(EuiBasicTable).text(); + + expect(tableContent).toContain('Search term'); + expect(tableContent).toContain('some search'); + expect(tableContent).toContain('another search'); + expect(tableContent).toContain('""'); + + expect(tableContent).toContain('Time'); + expect(tableContent).toContain('1/3/1970'); + expect(tableContent).toContain('1/2/1970'); + expect(tableContent).toContain('1/1/1970'); + + expect(tableContent).toContain('Analytics tags'); + expect(tableContent).toContain('tagA'); + expect(tableContent).toContain('tagB'); + expect(wrapper.find(EuiBadge)).toHaveLength(4); + + expect(tableContent).toContain('Results'); + expect(tableContent).toContain('2'); + expect(tableContent).toContain('1'); + expect(tableContent).toContain('3'); + }); + + it('renders an action column', () => { + const wrapper = mountWithIntl(<RecentQueriesTable items={items} />); + const viewQuery = wrapper.find('[data-test-subj="AnalyticsTableViewQueryButton"]').first(); + const editQuery = wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first(); + + viewQuery.simulate('click'); + expect(navigateToUrl).toHaveBeenCalledWith( + '/engines/some-engine/analytics/query_detail/some%20search' + ); + + editQuery.simulate('click'); + // TODO + }); + + it('renders an empty prompt if no items are passed', () => { + const wrapper = mountWithIntl(<RecentQueriesTable items={[]} />); + const promptContent = wrapper.find(EuiEmptyPrompt).text(); + + expect(promptContent).toContain('No recent queries'); + expect(promptContent).toContain('Queries will appear here as they are received.'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx new file mode 100644 index 0000000000000..b0dc8254c084b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx @@ -0,0 +1,82 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedDate, FormattedTime } from '@kbn/i18n/react'; +import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; + +import { RecentQuery } from '../../types'; +import { + TERM_COLUMN_PROPS, + TAGS_COLUMN, + COUNT_COLUMN_PROPS, + ACTIONS_COLUMN, +} from './shared_columns'; + +interface Props { + items: RecentQuery[]; +} +type Columns = Array<EuiBasicTableColumn<RecentQuery>>; + +export const RecentQueriesTable: React.FC<Props> = ({ items }) => { + const TERM_COLUMN = { + ...TERM_COLUMN_PROPS, + field: 'query_string', + }; + + const TIME_COLUMN = { + field: 'timestamp', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.timeColumn', { + defaultMessage: 'Time', + }), + render: (timestamp: RecentQuery['timestamp']) => { + const date = new Date(timestamp); + return ( + <> + <FormattedDate value={date} /> <FormattedTime value={date} /> + </> + ); + }, + width: '175px', + }; + + const RESULTS_COLUMN = { + ...COUNT_COLUMN_PROPS, + field: 'document_ids', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.resultsColumn', { + defaultMessage: 'Results', + }), + render: (documents: RecentQuery['document_ids']) => documents.length, + }; + + return ( + <EuiBasicTable + columns={[TERM_COLUMN, TIME_COLUMN, TAGS_COLUMN, RESULTS_COLUMN, ACTIONS_COLUMN] as Columns} + items={items} + responsive + hasActions + noItemsMessage={ + <EuiEmptyPrompt + iconType="visLine" + title={ + <h4> + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noRecentQueriesTitle', + { defaultMessage: 'No recent queries' } + )} + </h4> + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noRecentQueriesDescription', + { defaultMessage: 'Queries will appear here as they are received.' } + )} + /> + } + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx new file mode 100644 index 0000000000000..16743405e0b5e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx @@ -0,0 +1,99 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../../routes'; +import { generateEnginePath } from '../../../engine'; + +import { Query, RecentQuery } from '../../types'; +import { InlineTagsList } from './inline_tags_list'; + +/** + * Shared columns / column properties between separate analytics tables + */ + +export const FIRST_COLUMN_PROPS = { + truncateText: true, + width: '25%', + mobileOptions: { + enlarge: true, + width: '100%', + }, +}; + +export const TERM_COLUMN_PROPS = { + // Field key changes per-table + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.termColumn', { + defaultMessage: 'Search term', + }), + render: (query: Query['key']) => { + if (!query) query = '""'; + return ( + <EuiLinkTo to={generateEnginePath(ENGINE_ANALYTICS_QUERY_DETAIL_PATH, { query })}> + {query} + </EuiLinkTo> + ); + }, + ...FIRST_COLUMN_PROPS, +}; + +export const ACTIONS_COLUMN = { + width: '120px', + actions: [ + { + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.viewAction', { + defaultMessage: 'View', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.viewTooltip', + { defaultMessage: 'View query analytics' } + ), + type: 'icon', + icon: 'popout', + color: 'primary', + onClick: (item: Query | RecentQuery) => { + const { navigateToUrl } = KibanaLogic.values; + + const query = (item as Query).key || (item as RecentQuery).query_string; + navigateToUrl(generateEnginePath(ENGINE_ANALYTICS_QUERY_DETAIL_PATH, { query })); + }, + 'data-test-subj': 'AnalyticsTableViewQueryButton', + }, + { + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.editAction', { + defaultMessage: 'Edit', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.table.editTooltip', + { defaultMessage: 'Edit query analytics' } + ), + type: 'icon', + icon: 'pencil', + onClick: () => { + // TODO: CurationsLogic + }, + 'data-test-subj': 'AnalyticsTableEditQueryButton', + }, + ], +}; + +export const TAGS_COLUMN = { + field: 'tags', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.tagsColumn', { + defaultMessage: 'Analytics tags', + }), + truncateText: true, + render: (tags: Query['tags']) => <InlineTagsList tags={tags} />, +}; + +export const COUNT_COLUMN_PROPS = { + dataType: 'number', + width: '100px', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts index ae9c9ca450638..ddad726b04c26 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts @@ -7,4 +7,7 @@ export { AnalyticsCards } from './analytics_cards'; export { AnalyticsChart } from './analytics_chart'; export { AnalyticsHeader } from './analytics_header'; +export { AnalyticsSection } from './analytics_section'; +export { AnalyticsSearch } from './analytics_search'; +export { AnalyticsTable, RecentQueriesTable, QueryClicksTable } from './analytics_tables'; export { AnalyticsUnavailable } from './analytics_unavailable'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/types.ts index a3977a0c07a80..8bee8fd4407b7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/types.ts @@ -4,27 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Query { - doc_count: number; +export interface Query { key: string; - clicks?: { doc_count: number }; - searches?: { doc_count: number }; tags?: string[]; + searches?: { doc_count: number }; + clicks?: { doc_count: number }; } -interface QueryClick extends Query { +export interface QueryClick extends Query { document?: { id: string; engine: string; - tags?: string[]; }; } -interface RecentQuery { - document_ids: string[]; +export interface RecentQuery { query_string: string; - tags: string[]; timestamp: string; + tags: string[]; + document_ids: string[]; } /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx index 06bf77d35372f..e5bff981cb000 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx @@ -5,12 +5,19 @@ */ import { setMockValues } from '../../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow } from 'enzyme'; -import { AnalyticsCards, AnalyticsChart } from '../components'; -import { Analytics } from './'; +import { + AnalyticsCards, + AnalyticsChart, + AnalyticsSection, + AnalyticsTable, + RecentQueriesTable, +} from '../components'; +import { Analytics, ViewAllButton } from './analytics'; describe('Analytics overview', () => { it('renders', () => { @@ -22,10 +29,27 @@ describe('Analytics overview', () => { queriesNoResultsPerDay: [1, 2, 3], clicksPerDay: [0, 1, 5], startDate: '1970-01-01', + topQueries: [], + topQueriesNoResults: [], + topQueriesNoClicks: [], + topQueriesWithClicks: [], + recentQueries: [], }); const wrapper = shallow(<Analytics />); expect(wrapper.find(AnalyticsCards)).toHaveLength(1); expect(wrapper.find(AnalyticsChart)).toHaveLength(1); + expect(wrapper.find(AnalyticsSection)).toHaveLength(3); + expect(wrapper.find(AnalyticsTable)).toHaveLength(4); + expect(wrapper.find(RecentQueriesTable)).toHaveLength(1); + }); + + describe('ViewAllButton', () => { + it('renders', () => { + const to = '/analytics/top_queries'; + const wrapper = shallow(<ViewAllButton to={to} />); + + expect(wrapper.prop('to')).toEqual(to); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx index d3c3bff5a2947..e6a3e1ca5809b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx @@ -7,15 +7,32 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { EuiButtonTo } from '../../../../shared/react_router_helpers'; +import { + ENGINE_ANALYTICS_TOP_QUERIES_PATH, + ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH, + ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH, + ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH, + ENGINE_ANALYTICS_RECENT_QUERIES_PATH, +} from '../../../routes'; +import { generateEnginePath } from '../../engine'; import { ANALYTICS_TITLE, TOTAL_QUERIES, TOTAL_QUERIES_NO_RESULTS, TOTAL_CLICKS, + TOP_QUERIES, + TOP_QUERIES_NO_RESULTS, + TOP_QUERIES_WITH_CLICKS, + TOP_QUERIES_NO_CLICKS, + RECENT_QUERIES, } from '../constants'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSection, AnalyticsTable, RecentQueriesTable } from '../components'; import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../'; export const Analytics: React.FC = () => { @@ -27,6 +44,11 @@ export const Analytics: React.FC = () => { queriesNoResultsPerDay, clicksPerDay, startDate, + topQueries, + topQueriesNoResults, + topQueriesWithClicks, + topQueriesNoClicks, + recentQueries, } = useValues(AnalyticsLogic); return ( @@ -72,7 +94,77 @@ export const Analytics: React.FC = () => { /> <EuiSpacer /> - <p>TODO: Analytics overview</p> + <AnalyticsSection + title={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.queryTablesTitle', + { defaultMessage: 'Query analytics' } + )} + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.queryTablesDescription', + { + defaultMessage: + 'Gain insight into the most frequent queries, and which queries returned no results.', + } + )} + > + <EuiTitle size="s"> + <h3>{TOP_QUERIES}</h3> + </EuiTitle> + <AnalyticsTable items={topQueries.slice(0, 10)} hasClicks /> + <ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_TOP_QUERIES_PATH)} /> + <EuiSpacer /> + <EuiTitle size="s"> + <h3>{TOP_QUERIES_NO_RESULTS}</h3> + </EuiTitle> + <AnalyticsTable items={topQueriesNoResults.slice(0, 10)} /> + <ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH)} /> + </AnalyticsSection> + <EuiSpacer size="xl" /> + + <AnalyticsSection + title={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.clickTablesTitle', + { defaultMessage: 'Click analytics' } + )} + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.clickTablesDescription', + { + defaultMessage: 'Discover which queries generated the most and least amount of clicks.', + } + )} + > + <EuiTitle size="s"> + <h3>{TOP_QUERIES_WITH_CLICKS}</h3> + </EuiTitle> + <AnalyticsTable items={topQueriesWithClicks.slice(0, 10)} hasClicks /> + <ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH)} /> + <EuiSpacer /> + <EuiTitle size="s"> + <h3>{TOP_QUERIES_NO_CLICKS}</h3> + </EuiTitle> + <AnalyticsTable items={topQueriesNoClicks.slice(0, 10)} /> + <ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH)} /> + </AnalyticsSection> + <EuiSpacer size="xl" /> + + <AnalyticsSection + title={RECENT_QUERIES} + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.recentQueriesDescription', + { defaultMessage: 'A view into queries happening right now.' } + )} + > + <RecentQueriesTable items={recentQueries.slice(0, 10)} /> + <ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_RECENT_QUERIES_PATH)} /> + </AnalyticsSection> </AnalyticsLayout> ); }; + +export const ViewAllButton: React.FC<{ to: string }> = ({ to }) => ( + <EuiButtonTo to={to} size="s" fullWidth> + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.viewAllButtonLabel', { + defaultMessage: 'View all', + })} + </EuiButtonTo> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx index 99485340f6b88..7705d342ecdce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx @@ -13,7 +13,7 @@ import { shallow } from 'enzyme'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { AnalyticsCards, AnalyticsChart } from '../components'; +import { AnalyticsCards, AnalyticsChart, QueryClicksTable } from '../components'; import { QueryDetail } from './'; describe('QueryDetail', () => { @@ -41,5 +41,6 @@ describe('QueryDetail', () => { expect(wrapper.find(AnalyticsCards)).toHaveLength(1); expect(wrapper.find(AnalyticsChart)).toHaveLength(1); + expect(wrapper.find(QueryClicksTable)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx index 53c1dc8b845b1..d5d864f35f681 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx @@ -15,6 +15,7 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_c import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSection, QueryClicksTable } from '../components'; import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../'; const QUERY_DETAIL_TITLE = i18n.translate( @@ -28,7 +29,9 @@ interface Props { export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => { const { query } = useParams() as { query: string }; - const { totalQueriesForQuery, queriesPerDayForQuery, startDate } = useValues(AnalyticsLogic); + const { totalQueriesForQuery, queriesPerDayForQuery, startDate, topClicksForQuery } = useValues( + AnalyticsLogic + ); return ( <AnalyticsLayout isQueryView title={`"${query}"`}> @@ -63,7 +66,18 @@ export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => { /> <EuiSpacer /> - <p>TODO: Query detail page</p> + <AnalyticsSection + title={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.tableTitle', + { defaultMessage: 'Top clicks' } + )} + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.tableDescription', + { defaultMessage: 'The documents with the most clicks resulting from this query.' } + )} + > + <QueryClicksTable items={topClicksForQuery} /> + </AnalyticsSection> </AnalyticsLayout> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx index f25b044e8a56f..efd2de9223c98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { setMockValues } from '../../../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; +import { RecentQueriesTable } from '../components'; import { RecentQueries } from './'; describe('RecentQueries', () => { it('renders', () => { + setMockValues({ recentQueries: [] }); const wrapper = shallow(<RecentQueries />); - expect(wrapper.isEmptyRender()).toBe(false); // TODO + expect(wrapper.find(RecentQueriesTable)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx index 3510a2a0e8221..708863ba0e5c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx @@ -5,14 +5,20 @@ */ import React from 'react'; +import { useValues } from 'kea'; import { RECENT_QUERIES } from '../constants'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSearch, RecentQueriesTable } from '../components'; +import { AnalyticsLogic } from '../'; export const RecentQueries: React.FC = () => { + const { recentQueries } = useValues(AnalyticsLogic); + return ( <AnalyticsLayout isAnalyticsView title={RECENT_QUERIES}> - <p>TODO: Recent queries</p> + <AnalyticsSearch /> + <RecentQueriesTable items={recentQueries} /> </AnalyticsLayout> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx index 9747609aaf066..754a349c2fe94 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { setMockValues } from '../../../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; +import { AnalyticsTable } from '../components'; import { TopQueries } from './'; describe('TopQueries', () => { it('renders', () => { + setMockValues({ topQueries: [] }); const wrapper = shallow(<TopQueries />); - expect(wrapper.isEmptyRender()).toBe(false); // TODO + expect(wrapper.find(AnalyticsTable)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx index 3f2867871765c..0814ba16e39dc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx @@ -5,14 +5,20 @@ */ import React from 'react'; +import { useValues } from 'kea'; import { TOP_QUERIES } from '../constants'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSearch, AnalyticsTable } from '../components'; +import { AnalyticsLogic } from '../'; export const TopQueries: React.FC = () => { + const { topQueries } = useValues(AnalyticsLogic); + return ( <AnalyticsLayout isAnalyticsView title={TOP_QUERIES}> - <p>TODO: Top queries</p> + <AnalyticsSearch /> + <AnalyticsTable items={topQueries} hasClicks /> </AnalyticsLayout> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx index bc55753acf152..f1eb3a2f69a98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { setMockValues } from '../../../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; +import { AnalyticsTable } from '../components'; import { TopQueriesNoClicks } from './'; describe('TopQueriesNoClicks', () => { it('renders', () => { + setMockValues({ topQueriesNoClicks: [] }); const wrapper = shallow(<TopQueriesNoClicks />); - expect(wrapper.isEmptyRender()).toBe(false); // TODO + expect(wrapper.find(AnalyticsTable)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx index dc14c4a83bff3..283a790b61571 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx @@ -5,14 +5,20 @@ */ import React from 'react'; +import { useValues } from 'kea'; import { TOP_QUERIES_NO_CLICKS } from '../constants'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSearch, AnalyticsTable } from '../components'; +import { AnalyticsLogic } from '../'; export const TopQueriesNoClicks: React.FC = () => { + const { topQueriesNoClicks } = useValues(AnalyticsLogic); + return ( <AnalyticsLayout isAnalyticsView title={TOP_QUERIES_NO_CLICKS}> - <p>TODO: Top queries with no clicks</p> + <AnalyticsSearch /> + <AnalyticsTable items={topQueriesNoClicks} hasClicks /> </AnalyticsLayout> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx index 72c718f374714..8e404e34b5f3e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { setMockValues } from '../../../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; +import { AnalyticsTable } from '../components'; import { TopQueriesNoResults } from './'; describe('TopQueriesNoResults', () => { it('renders', () => { + setMockValues({ topQueriesNoResults: [] }); const wrapper = shallow(<TopQueriesNoResults />); - expect(wrapper.isEmptyRender()).toBe(false); // TODO + expect(wrapper.find(AnalyticsTable)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx index da8595b43859f..8a54d529b2dd0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx @@ -5,14 +5,20 @@ */ import React from 'react'; +import { useValues } from 'kea'; import { TOP_QUERIES_NO_RESULTS } from '../constants'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSearch, AnalyticsTable } from '../components'; +import { AnalyticsLogic } from '../'; export const TopQueriesNoResults: React.FC = () => { + const { topQueriesNoResults } = useValues(AnalyticsLogic); + return ( <AnalyticsLayout isAnalyticsView title={TOP_QUERIES_NO_RESULTS}> - <p>TODO: Top queries with no results</p> + <AnalyticsSearch /> + <AnalyticsTable items={topQueriesNoResults} hasClicks /> </AnalyticsLayout> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx index 74e31e77974ee..714da0d8e45dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { setMockValues } from '../../../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; +import { AnalyticsTable } from '../components'; import { TopQueriesWithClicks } from './'; describe('TopQueriesWithClicks', () => { it('renders', () => { + setMockValues({ topQueriesWithClicks: [] }); const wrapper = shallow(<TopQueriesWithClicks />); - expect(wrapper.isEmptyRender()).toBe(false); // TODO + expect(wrapper.find(AnalyticsTable)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx index dc6e837be61d8..73ad9e2e973d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx @@ -5,14 +5,20 @@ */ import React from 'react'; +import { useValues } from 'kea'; import { TOP_QUERIES_WITH_CLICKS } from '../constants'; import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSearch, AnalyticsTable } from '../components'; +import { AnalyticsLogic } from '../'; export const TopQueriesWithClicks: React.FC = () => { + const { topQueriesWithClicks } = useValues(AnalyticsLogic); + return ( <AnalyticsLayout isAnalyticsView title={TOP_QUERIES_WITH_CLICKS}> - <p>TODO: Top queries with clicks</p> + <AnalyticsSearch /> + <AnalyticsTable items={topQueriesWithClicks} hasClicks /> </AnalyticsLayout> ); }; From f53bc9825be973ed445d2040f4877cdeaabc8a6e Mon Sep 17 00:00:00 2001 From: Melissa Alvarez <melissa.alvarez@elastic.co> Date: Fri, 29 Jan 2021 14:48:55 -0500 Subject: [PATCH 136/163] [ML] Data Frame Analytics creation: improve existing job check (#89627) * use jobsExist endpoint instead of preloaded job list * remove unused translation * memoize jobCheck so cancel call works correctly --- .../create_analytics_advanced_editor.tsx | 41 ++++++++++++++++++- .../details_step/details_step_form.tsx | 32 ++++++++++++++- .../use_create_analytics_form/reducer.ts | 10 +---- .../use_create_analytics_form.ts | 28 +------------ .../ml_api_service/data_frame_analytics.ts | 13 ++++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 85 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index a35a314bec985..0be9e00b70f93 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useEffect, useRef } from 'react'; - +import React, { FC, Fragment, useEffect, useMemo, useRef } from 'react'; +import { debounce } from 'lodash'; import { EuiCallOut, EuiCodeEditor, @@ -22,6 +22,9 @@ import { XJsonMode } from '../../../../../../../shared_imports'; const xJsonMode = new XJsonMode(); +import { useNotifications } from '../../../../../contexts/kibana'; +import { ml } from '../../../../../services/ml_api_service'; +import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { CreateStep } from '../create_step'; import { ANALYTICS_STEPS } from '../../page'; @@ -42,11 +45,33 @@ export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = (prop } = state.form; const forceInput = useRef<HTMLInputElement | null>(null); + const { toasts } = useNotifications(); const onChange = (str: string) => { setAdvancedEditorRawString(str); }; + const debouncedJobIdCheck = useMemo( + () => + debounce(async () => { + try { + const { results } = await ml.dataFrameAnalytics.jobsExists([jobId], true); + setFormState({ jobIdExists: results[jobId] }); + } catch (e) { + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditor.errorCheckingJobIdExists', + { + defaultMessage: 'The following error occurred checking if job id exists: {error}', + values: { error: extractErrorMessage(e) }, + } + ) + ); + } + }, 400), + [jobId] + ); + // Temp effect to close the context menu popover on Clone button click useEffect(() => { if (forceInput.current === null) { @@ -57,6 +82,18 @@ export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = (prop forceInput.current.dispatchEvent(evt); }, []); + useEffect(() => { + if (jobIdValid === true) { + debouncedJobIdCheck(); + } else if (typeof jobId === 'string' && jobId.trim() === '' && jobIdExists === true) { + setFormState({ jobIdExists: false }); + } + + return () => { + debouncedJobIdCheck.cancel(); + }; + }, [jobId]); + return ( <EuiForm className="mlDataFrameAnalyticsCreateForm"> <EuiFormRow 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 448dcd8b2e1ba..872580f47d0b7 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useRef, useEffect, useState } from 'react'; +import React, { FC, Fragment, useRef, useEffect, useMemo, useState } from 'react'; import { debounce } from 'lodash'; import { EuiFieldText, @@ -94,6 +94,36 @@ export const DetailsStepForm: FC<CreateAnalyticsStepProps> = ({ } }, 400); + const debouncedJobIdCheck = useMemo( + () => + debounce(async () => { + try { + const { results } = await ml.dataFrameAnalytics.jobsExists([jobId], true); + setFormState({ jobIdExists: results[jobId] }); + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ml.dataframe.analytics.create.errorCheckingJobIdExists', { + defaultMessage: 'The following error occurred checking if job id exists: {error}', + values: { error: extractErrorMessage(e) }, + }) + ); + } + }, 400), + [jobId] + ); + + useEffect(() => { + if (jobIdValid === true) { + debouncedJobIdCheck(); + } else if (typeof jobId === 'string' && jobId.trim() === '' && jobIdExists === true) { + setFormState({ jobIdExists: false }); + } + + return () => { + debouncedJobIdCheck.cancel(); + }; + }, [jobId]); + useEffect(() => { if (destinationIndexNameValid === true) { debouncedIndexCheck(); 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 a277ae6e6a66e..998460d75f6f0 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 @@ -499,7 +499,6 @@ export function reducer(state: State, action: Action): State { } if (action.payload.jobId !== undefined) { - newFormState.jobIdExists = state.jobIds.some((id) => newFormState.jobId === id); newFormState.jobIdEmpty = newFormState.jobId === ''; newFormState.jobIdValid = isJobIdValid(newFormState.jobId); newFormState.jobIdInvalidMaxLength = !!maxLengthValidator(JOB_ID_MAX_LENGTH)( @@ -542,12 +541,6 @@ export function reducer(state: State, action: Action): State { case ACTION.SET_JOB_CONFIG: return validateAdvancedEditor({ ...state, jobConfig: action.payload }); - case ACTION.SET_JOB_IDS: { - const newState = { ...state, jobIds: action.jobIds }; - newState.form.jobIdExists = newState.jobIds.some((id) => newState.form.jobId === id); - return newState; - } - case ACTION.SWITCH_TO_ADVANCED_EDITOR: const jobConfig = getJobConfigFromFormState(state.form); const shouldDisableSwitchToForm = isAdvancedConfig(jobConfig); @@ -562,7 +555,7 @@ export function reducer(state: State, action: Action): State { }); case ACTION.SWITCH_TO_FORM: - const { jobConfig: config, jobIds } = state; + const { jobConfig: config } = state; const { jobId } = state.form; // @ts-ignore const formState = getFormStateFromJobConfig(config, false); @@ -571,7 +564,6 @@ export function reducer(state: State, action: Action): State { 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); 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 0b88f52e555c0..f5bfd3075f26b 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 @@ -14,11 +14,7 @@ import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; import { DuplicateIndexPatternError } from '../../../../../../../../../../src/plugins/data/public'; -import { - useRefreshAnalyticsList, - DataFrameAnalyticsId, - DataFrameAnalyticsConfig, -} from '../../../../common'; +import { useRefreshAnalyticsList, DataFrameAnalyticsConfig } from '../../../../common'; import { extractCloningConfig, isAdvancedConfig } from '../../components/action_clone'; import { ActionDispatchers, ACTION } from './actions'; @@ -80,9 +76,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SET_IS_JOB_STARTED, isJobStarted }); }; - const setJobIds = (jobIds: DataFrameAnalyticsId[]) => - dispatch({ type: ACTION.SET_JOB_IDS, jobIds }); - const resetRequestMessages = () => dispatch({ type: ACTION.RESET_REQUEST_MESSAGES }); const resetForm = () => dispatch({ type: ACTION.RESET_FORM }); @@ -180,25 +173,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { }; const prepareFormValidation = async () => { - // re-fetch existing analytics job IDs and indices for form validation - try { - setJobIds( - (await ml.dataFrameAnalytics.getDataFrameAnalytics()).data_frame_analytics.map( - (job: DataFrameAnalyticsConfig) => job.id - ) - ); - } catch (e) { - addRequestMessage({ - error: extractErrorMessage(e), - message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList', - { - defaultMessage: 'An error occurred getting the existing data frame analytics job IDs:', - } - ), - }); - } - try { // Set the existing index pattern titles. const indexPatternsMap: SourceIndexMap = {}; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 298dcad4ce488..7b246e557d7a5 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -45,6 +45,11 @@ interface DeleteDataFrameAnalyticsWithIndexResponse { destIndexDeleted: DeleteDataFrameAnalyticsWithIndexStatus; destIndexPatternDeleted: DeleteDataFrameAnalyticsWithIndexStatus; } +interface JobsExistsResponse { + results: { + [jobId: string]: boolean; + }; +} export const dataFrameAnalytics = { getDataFrameAnalytics(analyticsId?: string) { @@ -98,6 +103,14 @@ export const dataFrameAnalytics = { query: { treatAsRoot, type }, }); }, + jobsExists(analyticsIds: string[], allSpaces: boolean = false) { + const body = JSON.stringify({ analyticsIds, allSpaces }); + return http<JobsExistsResponse>({ + path: `${basePath()}/data_frame/analytics/jobs_exist`, + method: 'POST', + body, + }); + }, evaluateDataFrameAnalytics(evaluateConfig: any) { const body = JSON.stringify(evaluateConfig); return http<any>({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 28ef79beb72cf..d0634d6cd87a2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12595,7 +12595,6 @@ "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。", "xpack.ml.dataframe.analytics.create.errorCheckingIndexExists": "既存のインデックス名の取得中に次のエラーが発生しました:{error}", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "データフレーム分析ジョブの作成中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "既存のデータフレーム分析ジョブIDの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "データフレーム分析ジョブの開始中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.etaInputAriaLabel": "縮小が重みに適用されました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 052a00b1aefa4..4ca6d11aa8940 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12624,7 +12624,6 @@ "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。", "xpack.ml.dataframe.analytics.create.errorCheckingIndexExists": "获取现有索引名称时发生以下错误:{error}", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "创建数据帧分析作业时发生错误:", - "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "获取现有数据帧分析作业 ID 时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "启动数据帧分析作业时发生错误:", "xpack.ml.dataframe.analytics.create.etaInputAriaLabel": "缩小量已应用于权重", From 5a33872e07a6a7e59f08cdbe28798a2c99cb1dae Mon Sep 17 00:00:00 2001 From: Brian Seeders <brian.seeders@elastic.co> Date: Fri, 29 Jan 2021 15:09:33 -0500 Subject: [PATCH 137/163] [CI] Sleep before starting ciGroup tasks to smooth out CPU spikes from ES starting up (#89751) --- vars/kibanaPipeline.groovy | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 93cb7a719bbe8..3e72c9e059af8 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -130,6 +130,8 @@ def functionalTestProcess(String name, String script) { def ossCiGroupProcess(ciGroup) { return functionalTestProcess("ciGroup" + ciGroup) { + sleep((ciGroup-1)*30) // smooth out CPU spikes from ES startup + withEnv([ "CI_GROUP=${ciGroup}", "JOB=kibana-ciGroup${ciGroup}", @@ -143,6 +145,7 @@ def ossCiGroupProcess(ciGroup) { def xpackCiGroupProcess(ciGroup) { return functionalTestProcess("xpack-ciGroup" + ciGroup) { + sleep((ciGroup-1)*30) // smooth out CPU spikes from ES startup withEnv([ "CI_GROUP=${ciGroup}", "JOB=xpack-kibana-ciGroup${ciGroup}", @@ -454,6 +457,7 @@ def allCiTasks() { } def pipelineLibraryTests() { + return whenChanged(['vars/', '.ci/pipeline-library/']) { workers.base(size: 'flyweight', bootstrapped: false, ramDisk: false) { dir('.ci/pipeline-library') { From 4e18fd8a5170a2b0649aab848f75f4405cc4ceb9 Mon Sep 17 00:00:00 2001 From: Dominique Clarke <doclarke71@gmail.com> Date: Fri, 29 Jan 2021 15:15:49 -0500 Subject: [PATCH 138/163] uptime adjust useBarCharts logic (#89628) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../waterfall/components/use_bar_charts.test.tsx | 7 +++++-- .../synthetics/waterfall/components/use_bar_charts.ts | 9 ++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx index 28b74c5affbdf..b3d20a6acd3e3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx @@ -59,10 +59,13 @@ describe('useBarChartsHooks', () => { const firstChartItems = result.current[0]; const lastChartItems = result.current[4]; - // first chart items last item should be x 199, since we only display 150 items + // first chart items last item should be x 149, since we only display 150 items expect(firstChartItems[firstChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS - 1); - // since here are 5 charts, last chart first item should be x 800 + // first chart will only contain x values from 0 - 149; + expect(firstChartItems.find((item) => item.x > 149)).toBe(undefined); + + // since here are 5 charts, last chart first item should be x 600 expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4); expect(lastChartItems[lastChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS * 5 - 1); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts index 3345b30f5239f..7beb0be28902b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts @@ -17,17 +17,16 @@ export const useBarCharts = ({ data = [] }: UseBarHookProps) => { useEffect(() => { if (data.length > 0) { - let chartIndex = 1; + let chartIndex = 0; - const firstCanvasItems = data.filter((item) => item.x <= CANVAS_MAX_ITEMS); - - const chartsN: Array<IWaterfallContext['data']> = [firstCanvasItems]; + const chartsN: Array<IWaterfallContext['data']> = []; data.forEach((item) => { // Subtract 1 to account for x value starting from 0 if (item.x === CANVAS_MAX_ITEMS * chartIndex && !chartsN[item.x / CANVAS_MAX_ITEMS]) { - chartsN.push([]); + chartsN.push([item]); chartIndex++; + return; } chartsN[chartIndex - 1].push(item); }); From e866db7de011d8a3171ae85b73642c703b205274 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin <yakhin.v@gmail.com> Date: Fri, 29 Jan 2021 16:31:06 -0400 Subject: [PATCH 139/163] Migrate security page (#89720) * Add server routes for Workplace Search Security page * Initial copy/paste of component tree Also update lodash imports and fix default exports * Update paths * Remove conditional and passed in flash messages This is no longer needed with the Kibana syntax. Flash messages are set globally and only render when present. * Replace removed ConfirmModal In Kibana, we use the Eui components directly * Remove legacy AppView and sidenav * Clear flash messages globally * Update server routes * Replace Rails http with kibana http * Add setSourceRestriction action to app_logic It is used in security_logic * Add missing typings * Add route and update nav * Use internal tools for determining license * Remove Prompt as it doesn't work in Kibana There is an error that recommends using AppMountParameters.onAppLeave instead, but it doesn't cover the case where a user navigates within the app. We'll revisit this problem later. * Add i18n Also refactor PrivateSourcesTable to use static i18n strings. Before we were using 'remote' and 'standard' as both enums and parts of copy, i.e. "Enable {sourceType} private sources". But with i18n we can no longer do this. So I made a refactoring to separate these concerns. Now 'remote' and 'standard' are only used as enums. What i18n string to show is defined based on isRemote variable. * Add components unit tests * Add logic unit tests * Remove redundant imports * Use nextTick instead of awaiting for promises * Update logic tests to use new mockHelpers --- .../workplace_search/app_logic.ts | 6 + .../components/layout/nav.tsx | 4 +- .../workplace_search/constants.ts | 117 +++++++++++ .../applications/workplace_search/index.tsx | 7 + .../components/private_sources_table.test.tsx | 54 +++++ .../components/private_sources_table.tsx | 182 ++++++++++++++++ .../workplace_search/views/security/index.ts | 7 + .../views/security/security.test.tsx | 112 ++++++++++ .../views/security/security.tsx | 196 ++++++++++++++++++ .../views/security/security_logic.test.ts | 169 +++++++++++++++ .../views/security/security_logic.ts | 181 ++++++++++++++++ .../server/routes/workplace_search/index.ts | 2 + .../routes/workplace_search/security.test.ts | 108 ++++++++++ .../routes/workplace_search/security.ts | 78 +++++++ 14 files changed, 1220 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/security.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index f5f534807fabf..2ce7eed236840 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -21,6 +21,7 @@ interface AppValues extends WorkplaceSearchInitialData { interface AppActions { initializeAppData(props: InitialAppData): InitialAppData; setContext(isOrganization: boolean): boolean; + setSourceRestriction(canCreatePersonalSources: boolean): boolean; } const emptyOrg = {} as Organization; @@ -34,6 +35,7 @@ export const AppLogic = kea<MakeLogicType<AppValues, AppActions>>({ isFederatedAuth, }), setContext: (isOrganization) => isOrganization, + setSourceRestriction: (canCreatePersonalSources: boolean) => canCreatePersonalSources, }, reducers: { hasInitialized: [ @@ -64,6 +66,10 @@ export const AppLogic = kea<MakeLogicType<AppValues, AppActions>>({ emptyAccount, { initializeAppData: (_, { workplaceSearch }) => workplaceSearch?.account || emptyAccount, + setSourceRestriction: (state, canCreatePersonalSources) => ({ + ...state, + canCreatePersonalSources, + }), }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index 8a83e9aad5fd9..7357e84f27a41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -45,9 +45,7 @@ export const WorkplaceSearchNav: React.FC<Props> = ({ <SideNavLink isExternal to={getWorkplaceSearchUrl(`#${ROLE_MAPPINGS_PATH}`)}> {NAV.ROLE_MAPPINGS} </SideNavLink> - <SideNavLink isExternal to={getWorkplaceSearchUrl(`#${SECURITY_PATH}`)}> - {NAV.SECURITY} - </SideNavLink> + <SideNavLink to={SECURITY_PATH}>{NAV.SECURITY}</SideNavLink> <SideNavLink subNav={settingsSubNav} to={ORG_SETTINGS_PATH}> {NAV.SETTINGS} </SideNavLink> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index e72e28aa47d9b..17fbbf517f347 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -289,6 +289,87 @@ export const DOCUMENTATION_LINK_TITLE = i18n.translate( } ); +export const PRIVATE_SOURCES_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.privateSources.description', + { + defaultMessage: + 'Private sources are connected by users in your organization to create a personalized search experience.', + } +); + +export const PRIVATE_SOURCES_TOGGLE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.privateSourcesToggle.description', + { + defaultMessage: 'Enable private sources for your organization', + } +); + +export const REMOTE_SOURCES_TOGGLE_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesToggle.text', + { + defaultMessage: 'Enable remote private sources', + } +); + +export const REMOTE_SOURCES_TABLE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesTable.description', + { + defaultMessage: + 'Remote sources synchronize and store a limited amount of data on disk, with a low impact on storage resources.', + } +); + +export const REMOTE_SOURCES_EMPTY_TABLE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesEmptyTable.title', + { + defaultMessage: 'No remote private sources configured yet', + } +); + +export const STANDARD_SOURCES_TOGGLE_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.standardSourcesToggle.text', + { + defaultMessage: 'Enable standard private sources', + } +); + +export const STANDARD_SOURCES_TABLE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.standardSourcesTable.description', + { + defaultMessage: + 'Standard sources synchronize and store all searchable data on disk, with a directly correlated impact on storage resources.', + } +); + +export const STANDARD_SOURCES_EMPTY_TABLE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.standardSourcesEmptyTable.title', + { + defaultMessage: 'No standard private sources configured yet', + } +); + +export const SECURITY_UNSAVED_CHANGES_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.unsavedChanges.message', + { + defaultMessage: + 'Your private sources settings have not been saved. Are you sure you want to leave?', + } +); + +export const PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.privateSourcesUpdateConfirmation.text', + { + defaultMessage: 'Updates to private source configuration will take effect immediately.', + } +); + +export const SOURCE_RESTRICTIONS_SUCCESS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.sourceRestrictionsSuccess.message', + { + defaultMessage: 'Successfully updated source restrictions.', + } +); + export const PUBLIC_KEY_LABEL = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.publicKey.label', { @@ -382,6 +463,20 @@ export const SAVE_CHANGES_BUTTON = i18n.translate( } ); +export const SAVE_SETTINGS_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.saveSettings.button', + { + defaultMessage: 'Save settings', + } +); + +export const KEEP_EDITING_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.keepEditing.button', + { + defaultMessage: 'Keep editing', + } +); + export const NAME_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.name.label', { defaultMessage: 'Name', }); @@ -493,6 +588,10 @@ export const UPDATE_BUTTON = i18n.translate( } ); +export const RESET_BUTTON = i18n.translate('xpack.enterpriseSearch.workplaceSearch.reset.button', { + defaultMessage: 'Reset', +}); + export const CONFIGURE_BUTTON = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.configure.button', { @@ -522,6 +621,10 @@ export const PRIVATE_PLATINUM_LICENSE_CALLOUT = i18n.translate( } ); +export const SOURCE = i18n.translate('xpack.enterpriseSearch.workplaceSearch.source.text', { + defaultMessage: 'Source', +}); + export const PRIVATE_SOURCE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.privateSource.text', { @@ -529,6 +632,20 @@ export const PRIVATE_SOURCE = i18n.translate( } ); +export const PRIVATE_SOURCES = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.privateSources.text', + { + defaultMessage: 'Private Sources', + } +); + +export const CONFIRM_CHANGES_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.confirmChanges.text', + { + defaultMessage: 'Confirm changes', + } +); + export const CONNECTORS_HEADER_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.connectors.header.title', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index d10de7a770171..ec1b8cfcba958 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -22,6 +22,7 @@ import { SOURCES_PATH, PERSONAL_SOURCES_PATH, ORG_SETTINGS_PATH, + SECURITY_PATH, } from './routes'; import { SetupGuide } from './views/setup_guide'; @@ -29,6 +30,7 @@ import { ErrorState } from './views/error_state'; import { NotFound } from '../shared/not_found'; import { Overview } from './views/overview'; import { GroupsRouter } from './views/groups'; +import { Security } from './views/security'; import { SourcesRouter } from './views/content_sources'; import { SettingsRouter } from './views/settings'; @@ -102,6 +104,11 @@ export const WorkplaceSearchConfigured: React.FC<InitialAppData> = (props) => { <GroupsRouter /> </Layout> </Route> + <Route path={SECURITY_PATH}> + <Layout navigation={<WorkplaceSearchNav />} restrictWidth readOnlyMode={readOnlyMode}> + <Security /> + </Layout> + </Route> <Route path={ORG_SETTINGS_PATH}> <Layout navigation={<WorkplaceSearchNav settingsSubNav={<SettingsSubNav />} />} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx new file mode 100644 index 0000000000000..4db5c60d5800d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx @@ -0,0 +1,54 @@ +/* + * 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 { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiSwitch } from '@elastic/eui'; + +import { PrivateSourcesTable } from './private_sources_table'; + +describe('PrivateSourcesTable', () => { + beforeEach(() => { + setMockValues({ hasPlatinumLicense: true, isEnabled: true }); + }); + + const props = { + sourceSection: { isEnabled: true, contentSources: [] }, + updateSource: jest.fn(), + updateEnabled: jest.fn(), + }; + + it('renders', () => { + const wrapper = shallow(<PrivateSourcesTable {...props} sourceType="standard" />); + + expect(wrapper.find(EuiSwitch)).toHaveLength(1); + }); + + it('handles switches clicks', () => { + const wrapper = shallow( + <PrivateSourcesTable + {...props} + sourceSection={{ + isEnabled: false, + contentSources: [{ id: 'gmail', isEnabled: true, name: 'Gmail' }], + }} + sourceType="remote" + /> + ); + + const sectionSwitch = wrapper.find(EuiSwitch).first(); + const sourceSwitch = wrapper.find(EuiSwitch).last(); + + const event = { target: { value: true } }; + sectionSwitch.prop('onChange')(event as any); + sourceSwitch.prop('onChange')(event as any); + + expect(props.updateEnabled).toHaveBeenCalled(); + expect(props.updateSource).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx new file mode 100644 index 0000000000000..c767dfaba86f9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx @@ -0,0 +1,182 @@ +/* + * 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 classNames from 'classnames'; +import { useValues } from 'kea'; + +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiText, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { LicensingLogic } from '../../../../shared/licensing'; +import { SecurityLogic, PrivateSourceSection } from '../security_logic'; +import { + REMOTE_SOURCES_TOGGLE_TEXT, + REMOTE_SOURCES_TABLE_DESCRIPTION, + REMOTE_SOURCES_EMPTY_TABLE_TITLE, + STANDARD_SOURCES_TOGGLE_TEXT, + STANDARD_SOURCES_TABLE_DESCRIPTION, + STANDARD_SOURCES_EMPTY_TABLE_TITLE, + SOURCE, +} from '../../../constants'; + +interface PrivateSourcesTableProps { + sourceType: 'remote' | 'standard'; + sourceSection: PrivateSourceSection; + updateSource(sourceId: string, isEnabled: boolean): void; + updateEnabled(isEnabled: boolean): void; +} + +const REMOTE_SOURCES_EMPTY_TABLE_DESCRIPTION = ( + <FormattedMessage + id="xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesEmptyTable.description" + defaultMessage="Once configured, remote private sources are {enabledStrong}, and users can immediately connect the source from their Personal Dashboard." + values={{ + enabledStrong: ( + <strong> + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesEmptyTable.enabledStrong', + { defaultMessage: 'enabled by default' } + )} + </strong> + ), + }} + /> +); + +const STANDARD_SOURCES_EMPTY_TABLE_DESCRIPTION = ( + <FormattedMessage + id="xpack.enterpriseSearch.workplaceSearch.security.standardSourcesEmptyTable.description" + defaultMessage="Once configured, standard private sources are {notEnabledStrong}, and must be activated before users are allowed to connect the source from their Personal Dashboard." + values={{ + notEnabledStrong: ( + <strong> + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.security.standardSourcesEmptyTable.notEnabledStrong', + { defaultMessage: 'not enabled by default' } + )} + </strong> + ), + }} + /> +); + +export const PrivateSourcesTable: React.FC<PrivateSourcesTableProps> = ({ + sourceType, + sourceSection: { isEnabled: sectionEnabled, contentSources }, + updateSource, + updateEnabled, +}) => { + const { hasPlatinumLicense } = useValues(LicensingLogic); + const { isEnabled } = useValues(SecurityLogic); + + const isRemote = sourceType === 'remote'; + const hasSources = contentSources.length > 0; + const panelDisabled = !isEnabled || !hasPlatinumLicense; + const sectionDisabled = !sectionEnabled; + + const panelClass = classNames('euiPanel--outline euiPanel--noShadow', { + 'euiPanel--disabled': panelDisabled, + }); + + const tableClass = classNames({ 'euiTable--disabled': sectionDisabled }); + + const emptyState = ( + <> + <EuiSpacer /> + <EuiPanel className="euiPanel--inset euiPanel--noShadow euiPanel--outline"> + <EuiText textAlign="center" color="subdued" size="s"> + <strong> + {isRemote ? REMOTE_SOURCES_EMPTY_TABLE_TITLE : STANDARD_SOURCES_EMPTY_TABLE_TITLE} + </strong> + </EuiText> + <EuiText textAlign="center" color="subdued" size="s"> + {isRemote + ? REMOTE_SOURCES_EMPTY_TABLE_DESCRIPTION + : STANDARD_SOURCES_EMPTY_TABLE_DESCRIPTION} + </EuiText> + </EuiPanel> + </> + ); + + const sectionHeading = ( + <EuiFlexGroup alignItems="flexStart" justifyContent="flexStart" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiSpacer size="xs" /> + <EuiSwitch + checked={sectionEnabled} + onChange={(e) => updateEnabled(e.target.checked)} + disabled={!isEnabled || !hasPlatinumLicense} + showLabel={false} + label={`${sourceType} Sources Toggle`} + data-test-subj={`${sourceType}EnabledToggle`} + compressed + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiText size="s"> + <h4>{isRemote ? REMOTE_SOURCES_TOGGLE_TEXT : STANDARD_SOURCES_TOGGLE_TEXT}</h4> + </EuiText> + <EuiText color="subdued" size="s"> + {isRemote ? REMOTE_SOURCES_TABLE_DESCRIPTION : STANDARD_SOURCES_TABLE_DESCRIPTION} + </EuiText> + {!hasSources && emptyState} + </EuiFlexItem> + </EuiFlexGroup> + ); + + const sourcesTable = ( + <> + <EuiSpacer /> + <EuiTable className={tableClass}> + <EuiTableHeader> + <EuiTableHeaderCell>{SOURCE}</EuiTableHeaderCell> + <EuiTableHeaderCell /> + </EuiTableHeader> + <EuiTableBody> + {contentSources.map((source, i) => ( + <EuiTableRow key={i}> + <EuiTableRowCell>{source.name}</EuiTableRowCell> + <EuiTableRowCell> + <EuiSwitch + checked={!!source.isEnabled} + disabled={sectionDisabled} + onChange={(e) => updateSource(source.id, e.target.checked)} + showLabel={false} + label={`${source.name} Toggle`} + data-test-subj={`${sourceType}SourceToggle`} + compressed + /> + </EuiTableRowCell> + </EuiTableRow> + ))} + </EuiTableBody> + </EuiTable> + </> + ); + + return ( + <EuiPanel className={panelClass}> + {sectionHeading} + {hasSources && sourcesTable} + </EuiPanel> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/index.ts new file mode 100644 index 0000000000000..a2db1bbc15a15 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/index.ts @@ -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 { Security } from './security'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx new file mode 100644 index 0000000000000..bca0d5edc32d6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues, setMockActions } from '../../../__mocks__'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiSwitch, EuiConfirmModal } from '@elastic/eui'; +import { Loading } from '../../../shared/loading'; + +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { Security } from './security'; + +describe('Security', () => { + const initializeSourceRestrictions = jest.fn(); + const updatePrivateSourcesEnabled = jest.fn(); + const updateRemoteEnabled = jest.fn(); + const updateRemoteSource = jest.fn(); + const updateStandardEnabled = jest.fn(); + const updateStandardSource = jest.fn(); + const saveSourceRestrictions = jest.fn(); + const resetState = jest.fn(); + + const mockValues = { + isEnabled: true, + remote: { isEnabled: true, contentSources: [] }, + standard: { isEnabled: true, contentSources: [] }, + dataLoading: false, + unsavedChanges: false, + hasPlatinumLicense: true, + }; + + beforeEach(() => { + setMockValues(mockValues); + setMockActions({ + initializeSourceRestrictions, + updatePrivateSourcesEnabled, + updateRemoteEnabled, + updateRemoteSource, + updateStandardEnabled, + updateStandardSource, + saveSourceRestrictions, + resetState, + }); + }); + + it('renders on Basic license', () => { + setMockValues({ ...mockValues, hasPlatinumLicense: false }); + const wrapper = shallow(<Security />); + + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + expect(wrapper.find(EuiSwitch).prop('disabled')).toEqual(true); + }); + + it('renders on Platinum license', () => { + const wrapper = shallow(<Security />); + + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + expect(wrapper.find(EuiSwitch).prop('disabled')).toEqual(false); + }); + + it('returns Loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(<Security />); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('handles window.onbeforeunload change', () => { + setMockValues({ ...mockValues, unsavedChanges: true }); + shallow(<Security />); + + expect(window.onbeforeunload!({} as any)).toEqual( + 'Your private sources settings have not been saved. Are you sure you want to leave?' + ); + }); + + it('handles window.onbeforeunload unmount', () => { + setMockValues({ ...mockValues, unsavedChanges: true }); + shallow(<Security />); + + unmountHandler(); + + expect(window.onbeforeunload).toEqual(null); + }); + + it('handles switch click', () => { + const wrapper = shallow(<Security />); + + const privateSourcesSwitch = wrapper.find(EuiSwitch); + const event = { target: { checked: true } }; + privateSourcesSwitch.prop('onChange')(event as any); + + expect(updatePrivateSourcesEnabled).toHaveBeenCalled(); + }); + + it('handles confirmModal submission', () => { + setMockValues({ ...mockValues, unsavedChanges: true }); + const wrapper = shallow(<Security />); + + const header = wrapper.find(ViewContentHeader).dive(); + header.find('[data-test-subj="SaveSettingsButton"]').prop('onClick')!({} as any); + const modal = wrapper.find(EuiConfirmModal); + modal.prop('onConfirm')!({} as any); + + expect(saveSourceRestrictions).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx new file mode 100644 index 0000000000000..41df1a1acc515 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; + +import classNames from 'classnames'; +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiText, + EuiSpacer, + EuiPanel, + EuiConfirmModal, + EuiOverlayMask, +} from '@elastic/eui'; + +import { LicensingLogic } from '../../../shared/licensing'; +import { FlashMessages } from '../../../shared/flash_messages'; +import { LicenseCallout } from '../../components/shared/license_callout'; +import { Loading } from '../../../shared/loading'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SecurityLogic } from './security_logic'; + +import { PrivateSourcesTable } from './components/private_sources_table'; + +import { + SECURITY_UNSAVED_CHANGES_MESSAGE, + RESET_BUTTON, + SAVE_SETTINGS_BUTTON, + SAVE_CHANGES_BUTTON, + KEEP_EDITING_BUTTON, + PRIVATE_SOURCES, + PRIVATE_SOURCES_DESCRIPTION, + PRIVATE_SOURCES_TOGGLE_DESCRIPTION, + PRIVATE_PLATINUM_LICENSE_CALLOUT, + CONFIRM_CHANGES_TEXT, + PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT, +} from '../../constants'; + +export const Security: React.FC = () => { + const [confirmModalVisible, setConfirmModalVisibility] = useState(false); + + const hideConfirmModal = () => setConfirmModalVisibility(false); + const showConfirmModal = () => setConfirmModalVisibility(true); + + const { hasPlatinumLicense } = useValues(LicensingLogic); + + const { + initializeSourceRestrictions, + updatePrivateSourcesEnabled, + updateRemoteEnabled, + updateRemoteSource, + updateStandardEnabled, + updateStandardSource, + saveSourceRestrictions, + resetState, + } = useActions(SecurityLogic); + + const { isEnabled, remote, standard, dataLoading, unsavedChanges } = useValues(SecurityLogic); + + useEffect(() => { + initializeSourceRestrictions(); + }, []); + + useEffect(() => { + window.onbeforeunload = unsavedChanges ? () => SECURITY_UNSAVED_CHANGES_MESSAGE : null; + return () => { + window.onbeforeunload = null; + }; + }, [unsavedChanges]); + + if (dataLoading) return <Loading />; + + const panelClass = classNames('euiPanel--noShadow', { + 'euiPanel--disabled': !hasPlatinumLicense, + }); + + const savePrivateSources = () => { + saveSourceRestrictions(); + hideConfirmModal(); + }; + + const headerActions = ( + <EuiFlexGroup alignItems="center" justifyContent="flexStart" gutterSize="m"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty disabled={!unsavedChanges} onClick={resetState}> + {RESET_BUTTON} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem> + <EuiButton + disabled={!hasPlatinumLicense || !unsavedChanges} + onClick={showConfirmModal} + fill + data-test-subj="SaveSettingsButton" + > + {SAVE_SETTINGS_BUTTON} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ); + + const header = ( + <> + <ViewContentHeader + title={PRIVATE_SOURCES} + alignItems="flexStart" + description={PRIVATE_SOURCES_DESCRIPTION} + action={headerActions} + /> + <EuiSpacer /> + </> + ); + + const allSourcesToggle = ( + <EuiPanel paddingSize="none" className={panelClass}> + <EuiFlexGroup alignItems="center" justifyContent="flexStart" gutterSize="m"> + <EuiFlexItem grow={false}> + <EuiSwitch + checked={isEnabled} + onChange={(e) => updatePrivateSourcesEnabled(e.target.checked)} + disabled={!hasPlatinumLicense} + showLabel={false} + label="Private Sources Toggle" + data-test-subj="PrivateSourcesToggle" + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiText size="s"> + <h4>{PRIVATE_SOURCES_TOGGLE_DESCRIPTION}</h4> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + ); + + const platinumLicenseCallout = ( + <> + <EuiSpacer size="s" /> + <LicenseCallout message={PRIVATE_PLATINUM_LICENSE_CALLOUT} /> + </> + ); + + const sourceTables = ( + <> + <EuiSpacer size="xl" /> + <PrivateSourcesTable + sourceType="remote" + sourceSection={remote} + updateEnabled={updateRemoteEnabled} + updateSource={updateRemoteSource} + /> + <EuiSpacer size="xxl" /> + <PrivateSourcesTable + sourceType="standard" + sourceSection={standard} + updateEnabled={updateStandardEnabled} + updateSource={updateStandardSource} + /> + </> + ); + + const confirmModal = ( + <EuiOverlayMask> + <EuiConfirmModal + title={CONFIRM_CHANGES_TEXT} + onConfirm={savePrivateSources} + onCancel={hideConfirmModal} + buttonColor="primary" + cancelButtonText={KEEP_EDITING_BUTTON} + confirmButtonText={SAVE_CHANGES_BUTTON} + > + {PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT} + </EuiConfirmModal> + </EuiOverlayMask> + ); + + return ( + <> + <FlashMessages /> + {header} + {allSourcesToggle} + {!hasPlatinumLicense && platinumLicenseCallout} + {sourceTables} + {confirmModalVisible && confirmModal} + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts new file mode 100644 index 0000000000000..abb1308081f0c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts @@ -0,0 +1,169 @@ +/* + * 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 { LogicMounter } from '../../../__mocks__/kea.mock'; +import { mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; +import { SecurityLogic } from './security_logic'; +import { nextTick } from '@kbn/test/jest'; + +describe('SecurityLogic', () => { + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + const { mount } = new LogicMounter(SecurityLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + const defaultValues = { + dataLoading: true, + cachedServerState: {}, + isEnabled: false, + remote: {}, + standard: {}, + unsavedChanges: true, + }; + + const serverProps = { + isEnabled: true, + remote: { + isEnabled: true, + contentSources: [{ id: 'gmail', name: 'Gmail', isEnabled: true }], + }, + standard: { + isEnabled: true, + contentSources: [{ id: 'one_drive', name: 'OneDrive', isEnabled: true }], + }, + }; + + it('has expected default values', () => { + expect(SecurityLogic.values).toEqual(defaultValues); + }); + + describe('actions', () => { + it('setServerProps', () => { + SecurityLogic.actions.setServerProps(serverProps); + + expect(SecurityLogic.values.isEnabled).toEqual(true); + }); + + it('setSourceRestrictionsUpdated', () => { + SecurityLogic.actions.setSourceRestrictionsUpdated(serverProps); + + expect(SecurityLogic.values.isEnabled).toEqual(true); + }); + + it('updatePrivateSourcesEnabled', () => { + SecurityLogic.actions.updatePrivateSourcesEnabled(false); + + expect(SecurityLogic.values.isEnabled).toEqual(false); + }); + + it('updateRemoteEnabled', () => { + SecurityLogic.actions.updateRemoteEnabled(false); + + expect(SecurityLogic.values.remote.isEnabled).toEqual(false); + }); + + it('updateStandardEnabled', () => { + SecurityLogic.actions.updateStandardEnabled(false); + + expect(SecurityLogic.values.standard.isEnabled).toEqual(false); + }); + + it('updateRemoteSource', () => { + SecurityLogic.actions.setServerProps(serverProps); + SecurityLogic.actions.updateRemoteSource('gmail', false); + + expect(SecurityLogic.values.remote.contentSources[0].isEnabled).toEqual(false); + }); + + it('updateStandardSource', () => { + SecurityLogic.actions.setServerProps(serverProps); + SecurityLogic.actions.updateStandardSource('one_drive', false); + + expect(SecurityLogic.values.standard.contentSources[0].isEnabled).toEqual(false); + }); + }); + + describe('selectors', () => { + describe('unsavedChanges', () => { + it('returns true while loading', () => { + expect(SecurityLogic.values.unsavedChanges).toEqual(true); + }); + + it('returns false after loading', () => { + SecurityLogic.actions.setServerProps(serverProps); + + expect(SecurityLogic.values.unsavedChanges).toEqual(false); + }); + }); + }); + + describe('listeners', () => { + describe('initializeSourceRestrictions', () => { + it('calls API and sets values', async () => { + const setServerPropsSpy = jest.spyOn(SecurityLogic.actions, 'setServerProps'); + http.get.mockReturnValue(Promise.resolve(serverProps)); + SecurityLogic.actions.initializeSourceRestrictions(); + + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/org/security/source_restrictions' + ); + await nextTick(); + expect(setServerPropsSpy).toHaveBeenCalledWith(serverProps); + }); + + it('handles error', async () => { + http.get.mockReturnValue(Promise.reject('this is an error')); + + SecurityLogic.actions.initializeSourceRestrictions(); + try { + await nextTick(); + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveSourceRestrictions', () => { + it('calls API and sets values', async () => { + http.patch.mockReturnValue(Promise.resolve(serverProps)); + SecurityLogic.actions.setSourceRestrictionsUpdated(serverProps); + SecurityLogic.actions.saveSourceRestrictions(); + + expect(http.patch).toHaveBeenCalledWith( + '/api/workplace_search/org/security/source_restrictions', + { + body: JSON.stringify(serverProps), + } + ); + }); + + it('handles error', async () => { + http.patch.mockReturnValue(Promise.reject('this is an error')); + + SecurityLogic.actions.saveSourceRestrictions(); + try { + await nextTick(); + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('resetState', () => { + it('calls API and sets values', async () => { + SecurityLogic.actions.setServerProps(serverProps); + SecurityLogic.actions.updatePrivateSourcesEnabled(false); + SecurityLogic.actions.resetState(); + + expect(SecurityLogic.values.isEnabled).toEqual(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts new file mode 100644 index 0000000000000..df843b330d411 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts @@ -0,0 +1,181 @@ +/* + * 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 { isEqual } from 'lodash'; + +import { kea, MakeLogicType } from 'kea'; + +import { + clearFlashMessages, + setSuccessMessage, + flashAPIErrors, +} from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { AppLogic } from '../../app_logic'; + +import { SOURCE_RESTRICTIONS_SUCCESS_MESSAGE } from '../../constants'; + +export interface PrivateSource { + id: string; + name: string; + isEnabled: boolean; +} + +export interface PrivateSourceSection { + isEnabled: boolean; + contentSources: PrivateSource[]; +} + +export interface SecurityServerProps { + isEnabled: boolean; + remote: PrivateSourceSection; + standard: PrivateSourceSection; +} + +interface SecurityValues extends SecurityServerProps { + dataLoading: boolean; + unsavedChanges: boolean; + cachedServerState: SecurityServerProps; +} + +interface SecurityActions { + setServerProps(serverProps: SecurityServerProps): SecurityServerProps; + setSourceRestrictionsUpdated(serverProps: SecurityServerProps): SecurityServerProps; + initializeSourceRestrictions(): void; + saveSourceRestrictions(): void; + updatePrivateSourcesEnabled(isEnabled: boolean): { isEnabled: boolean }; + updateRemoteEnabled(isEnabled: boolean): { isEnabled: boolean }; + updateRemoteSource( + sourceId: string, + isEnabled: boolean + ): { sourceId: string; isEnabled: boolean }; + updateStandardEnabled(isEnabled: boolean): { isEnabled: boolean }; + updateStandardSource( + sourceId: string, + isEnabled: boolean + ): { sourceId: string; isEnabled: boolean }; + resetState(): void; +} + +const route = '/api/workplace_search/org/security/source_restrictions'; + +export const SecurityLogic = kea<MakeLogicType<SecurityValues, SecurityActions>>({ + path: ['enterprise_search', 'workplace_search', 'security_logic'], + actions: { + setServerProps: (serverProps: SecurityServerProps) => serverProps, + setSourceRestrictionsUpdated: (serverProps: SecurityServerProps) => serverProps, + initializeSourceRestrictions: () => true, + saveSourceRestrictions: () => null, + updatePrivateSourcesEnabled: (isEnabled: boolean) => ({ isEnabled }), + updateRemoteEnabled: (isEnabled: boolean) => ({ isEnabled }), + updateRemoteSource: (sourceId: string, isEnabled: boolean) => ({ sourceId, isEnabled }), + updateStandardEnabled: (isEnabled: boolean) => ({ isEnabled }), + updateStandardSource: (sourceId: string, isEnabled: boolean) => ({ sourceId, isEnabled }), + resetState: () => null, + }, + reducers: { + dataLoading: [ + true, + { + setServerProps: () => false, + }, + ], + cachedServerState: [ + {} as SecurityServerProps, + { + setServerProps: (_, serverProps) => cloneDeep(serverProps), + setSourceRestrictionsUpdated: (_, serverProps) => cloneDeep(serverProps), + }, + ], + isEnabled: [ + false, + { + setServerProps: (_, { isEnabled }) => isEnabled, + setSourceRestrictionsUpdated: (_, { isEnabled }) => isEnabled, + updatePrivateSourcesEnabled: (_, { isEnabled }) => isEnabled, + }, + ], + remote: [ + {} as PrivateSourceSection, + { + setServerProps: (_, { remote }) => remote, + setSourceRestrictionsUpdated: (_, { remote }) => remote, + updateRemoteEnabled: (state, { isEnabled }) => ({ ...state, isEnabled }), + updateRemoteSource: (state, { sourceId, isEnabled }) => + updateSourceEnabled(state, sourceId, isEnabled), + }, + ], + standard: [ + {} as PrivateSourceSection, + { + setServerProps: (_, { standard }) => standard, + setSourceRestrictionsUpdated: (_, { standard }) => standard, + updateStandardEnabled: (state, { isEnabled }) => ({ ...state, isEnabled }), + updateStandardSource: (state, { sourceId, isEnabled }) => + updateSourceEnabled(state, sourceId, isEnabled), + }, + ], + }, + selectors: ({ selectors }) => ({ + unsavedChanges: [ + () => [ + selectors.cachedServerState, + selectors.isEnabled, + selectors.remote, + selectors.standard, + ], + (cached, isEnabled, remote, standard) => + cached.isEnabled !== isEnabled || + !isEqual(cached.remote, remote) || + !isEqual(cached.standard, standard), + ], + }), + listeners: ({ actions, values }) => ({ + initializeSourceRestrictions: async () => { + const { http } = HttpLogic.values; + + try { + const response = await http.get(route); + actions.setServerProps(response); + } catch (e) { + flashAPIErrors(e); + } + }, + saveSourceRestrictions: async () => { + const { isEnabled, remote, standard } = values; + const serverData = { isEnabled, remote, standard }; + const body = JSON.stringify(serverData); + const { http } = HttpLogic.values; + + try { + const response = await http.patch(route, { body }); + actions.setSourceRestrictionsUpdated(response); + setSuccessMessage(SOURCE_RESTRICTIONS_SUCCESS_MESSAGE); + AppLogic.actions.setSourceRestriction(isEnabled); + } catch (e) { + flashAPIErrors(e); + } + }, + resetState: () => { + actions.setServerProps(cloneDeep(values.cachedServerState)); + clearFlashMessages(); + }, + }), +}); + +const updateSourceEnabled = ( + section: PrivateSourceSection, + id: string, + isEnabled: boolean +): PrivateSourceSection => { + const updatedSection = { ...section }; + const sources = updatedSection.contentSources; + const sourceIndex = sources.findIndex((source) => source.id === id); + updatedSection.contentSources[sourceIndex] = { ...sources[sourceIndex], isEnabled }; + + return updatedSection; +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts index 99445108b315a..f2792be8e6535 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts @@ -10,10 +10,12 @@ import { registerOverviewRoute } from './overview'; import { registerGroupsRoutes } from './groups'; import { registerSourcesRoutes } from './sources'; import { registerSettingsRoutes } from './settings'; +import { registerSecurityRoutes } from './security'; export const registerWorkplaceSearchRoutes = (dependencies: RouteDependencies) => { registerOverviewRoute(dependencies); registerGroupsRoutes(dependencies); registerSourcesRoutes(dependencies); registerSettingsRoutes(dependencies); + registerSecurityRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts new file mode 100644 index 0000000000000..12f84278e9ead --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerSecurityRoute, registerSecuritySourceRestrictionsRoute } from './security'; + +describe('security routes', () => { + describe('GET /api/workplace_search/org/security', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/security', + }); + + registerSecurityRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/security', + }); + }); + }); + + describe('GET /api/workplace_search/org/security/source_restrictions', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/security/source_restrictions', + payload: 'body', + }); + + registerSecuritySourceRestrictionsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/security/source_restrictions', + }); + }); + }); + + describe('PATCH /api/workplace_search/org/security/source_restrictions', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + + mockRouter = new MockRouter({ + method: 'patch', + path: '/api/workplace_search/org/security/source_restrictions', + payload: 'body', + }); + + registerSecuritySourceRestrictionsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/security/source_restrictions', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + isEnabled: true, + remote: { + isEnabled: true, + contentSources: [{ id: 'gmail', name: 'Gmail', isEnabled: true }], + }, + standard: { + isEnabled: false, + contentSources: [{ id: 'dropbox', name: 'Dropbox', isEnabled: false }], + }, + }, + }; + mockRouter.shouldValidate(request); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.ts new file mode 100644 index 0000000000000..0aa218dfc2883 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerSecurityRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/security', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/security', + }) + ); +} + +export function registerSecuritySourceRestrictionsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/security/source_restrictions', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/security/source_restrictions', + }) + ); + + router.patch( + { + path: '/api/workplace_search/org/security/source_restrictions', + validate: { + body: schema.object({ + isEnabled: schema.boolean(), + remote: schema.object({ + isEnabled: schema.boolean(), + contentSources: schema.arrayOf( + schema.object({ + isEnabled: schema.boolean(), + id: schema.string(), + name: schema.string(), + }) + ), + }), + standard: schema.object({ + isEnabled: schema.boolean(), + contentSources: schema.arrayOf( + schema.object({ + isEnabled: schema.boolean(), + id: schema.string(), + name: schema.string(), + }) + ), + }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/security/source_restrictions', + }) + ); +} + +export const registerSecurityRoutes = (dependencies: RouteDependencies) => { + registerSecurityRoute(dependencies); + registerSecuritySourceRestrictionsRoute(dependencies); +}; From df913b47bee8ccf0e836c5866ef6b4345004813d Mon Sep 17 00:00:00 2001 From: Tim Sullivan <tsullivan@users.noreply.github.com> Date: Fri, 29 Jan 2021 14:06:14 -0700 Subject: [PATCH 140/163] Update build_chromium README (#89762) * Update build_chromium README * more edits * Update init.py --- x-pack/build_chromium/README.md | 59 +++++++++++++++++++++------------ x-pack/build_chromium/build.py | 4 +-- x-pack/build_chromium/init.py | 12 ++++--- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md index 9934d06a9d96a..39382620775ad 100644 --- a/x-pack/build_chromium/README.md +++ b/x-pack/build_chromium/README.md @@ -6,50 +6,65 @@ to accept a commit hash from the Chromium repository, and initialize the build environments and run the build on Mac, Windows, and Linux. ## Before you begin + If you wish to use a remote VM to build, you'll need access to our GCP account, which is where we have two machines provisioned for the Linux and Windows builds. Mac builds can be achieved locally, and are a great place to start to gain familiarity. +**NOTE:** Linux builds should be done in Ubuntu on x86 architecture. ARM builds +are created in x86. CentOS is not supported for building Chromium. + 1. Login to our GCP instance [here using your okta credentials](https://console.cloud.google.com/). 2. Click the "Compute Engine" tab. -3. Ensure that `chromium-build-linux` and `chromium-build-windows-12-beefy` are there. -4. If #3 fails, you'll have to spin up new instances. Generally, these need `n1-standard-8` types or 8 vCPUs/30 GB memory. -5. Ensure that there's enough room left on the disk: 100GB is required. `ncdu` is a good linux util to verify what's claming space. - -## Usage +3. Find `chromium-build-linux` or `chromium-build-windows-12-beefy` and start the instance. +4. Install [Google Cloud SDK](https://cloud.google.com/sdk) locally to ssh into the GCP instance +5. System dependencies: + - 8 CPU + - 30GB memory + - 80GB free space on disk (Try `ncdu /home` to see where space is used.) + - git + - python2 (`python` must link to `python2`) + - lsb_release + - tmux is recommended in case your ssh session is interrupted +6. Copy the entire `build_chromium` directory into a GCP storage bucket, so you can copy the scripts into the instance and run them. + +## Build Script Usage ``` +# Allow our scripts to use depot_tools commands export PATH=$HOME/chromium/depot_tools:$PATH + # Create a dedicated working directory for this directory of Python scripts. mkdir ~/chromium && cd ~/chromium + # Copy the scripts from the Kibana repo to use them conveniently in the working directory -cp -r ~/path/to/kibana/x-pack/build_chromium . -# Install the OS packages, configure the environment, download the chromium source +gsutil cp -r gs://my-bucket/build_chromium . + +# Install the OS packages, configure the environment, download the chromium source (25GB) python ./build_chromium/init.sh [arch_name] # Run the build script with the path to the chromium src directory, the git commit id -python ./build_chromium/build.py <commit_id> +python ./build_chromium/build.py <commit_id> x86 -# You can add an architecture flag for ARM +# OR You can build for ARM python ./build_chromium/build.py <commit_id> arm64 ``` +**NOTE:** The `init.py` script updates git config to make it more possible for +the Chromium repo to be cloned successfully. If checking out the Chromium fails +with "early EOF" errors, the instance could be low on memory or disk space. + ## Getting the Commit ID -Getting `<commit_id>` can be tricky. The best technique seems to be: +The `build.py` script requires a commit ID of the Chromium repo. Getting `<commit_id>` can be tricky. The best technique seems to be: 1. Create a temporary working directory and intialize yarn 2. `yarn add puppeteer # install latest puppeter` -3. Look through puppeteer's node module files to find the "chromium revision" (a custom versioning convention for Chromium). +3. Look through Puppeteer documentation and Changelogs to find information +about where the "chromium revision" is located in the Puppeteer code. The code +containing it might not be distributed in the node module. + - Example: https://github.com/puppeteer/puppeteer/blob/b549256/src/revisions.ts 4. Use `https://crrev.com` and look up the revision and find the git commit info. - -The official Chromium build process is poorly documented, and seems to have -breaking changes fairly regularly. The build pre-requisites, and the build -flags change over time, so it is likely that the scripts in this directory will -be out of date by the time we have to do another Chromium build. - -This document is an attempt to note all of the gotchas we've come across while -building, so that the next time we have to tinker here, we'll have a good -starting point. + - Example: http://crrev.com/818858 leads to the git commit e62cb7e3fc7c40548cef66cdf19d270535d9350b ## Build args @@ -115,8 +130,8 @@ The more cores the better, as the build makes effective use of each. For Linux, - Linux: - SSH in using [gcloud](https://cloud.google.com/sdk/) - - Get the ssh command in the [GCP console](https://console.cloud.google.com/) -> VM instances -> your-vm-name -> SSH -> gcloud - - Their in-browser UI is kinda sluggish, so use the commandline tool + - Get the ssh command in the [GCP console](https://console.cloud.google.com/) -> VM instances -> your-vm-name -> SSH -> "View gcloud command" + - Their in-browser UI is kinda sluggish, so use the commandline tool (Google Cloud SDK is required) - Windows: - Install Microsoft's Remote Desktop tools diff --git a/x-pack/build_chromium/build.py b/x-pack/build_chromium/build.py index 8622f4a9d4c0b..0064f48ae973f 100644 --- a/x-pack/build_chromium/build.py +++ b/x-pack/build_chromium/build.py @@ -33,10 +33,10 @@ base_version = source_version[:7].strip('.') # Set to "arm" to build for ARM on Linux -arch_name = sys.argv[2] if len(sys.argv) >= 3 else 'x64' +arch_name = sys.argv[2] if len(sys.argv) >= 3 else 'unknown' if arch_name != 'x64' and arch_name != 'arm64': - raise Exception('Unexpected architecture: ' + arch_name) + raise Exception('Unexpected architecture: ' + arch_name + '. `x64` and `arm64` are supported.') print('Building Chromium ' + source_version + ' for ' + arch_name + ' from ' + src_path) print('src path: ' + src_path) diff --git a/x-pack/build_chromium/init.py b/x-pack/build_chromium/init.py index c0dd60f1cfcb0..3a2e28a884b09 100644 --- a/x-pack/build_chromium/init.py +++ b/x-pack/build_chromium/init.py @@ -8,18 +8,19 @@ # call this once the platform-specific initialization has completed. # Set to "arm" to build for ARM on Linux -arch_name = sys.argv[1] if len(sys.argv) >= 2 else 'x64' +arch_name = sys.argv[1] if len(sys.argv) >= 2 else 'undefined' build_path = path.abspath(os.curdir) src_path = path.abspath(path.join(build_path, 'chromium', 'src')) if arch_name != 'x64' and arch_name != 'arm64': - raise Exception('Unexpected architecture: ' + arch_name) + raise Exception('Unexpected architecture: ' + arch_name + '. `x64` and `arm64` are supported.') # Configure git print('Configuring git globals...') runcmd('git config --global core.autocrlf false') runcmd('git config --global core.filemode false') runcmd('git config --global branch.autosetuprebase always') +runcmd('git config --global core.compression 0') # Grab Chromium's custom build tools, if they aren't already installed # (On Windows, they are installed before this Python script is run) @@ -35,13 +36,14 @@ runcmd('git pull origin master') os.chdir(original_dir) -configure_environment(arch_name, build_path, src_path) - # Fetch the Chromium source code chromium_dir = path.join(build_path, 'chromium') if not path.isdir(chromium_dir): mkdir(chromium_dir) os.chdir(chromium_dir) - runcmd('fetch chromium') + runcmd('fetch chromium --nohooks=1 --no-history=1') else: print('Directory exists: ' + chromium_dir + '. Skipping chromium fetch.') + +# This depends on having the chromium/src directory with the complete checkout +configure_environment(arch_name, build_path, src_path) From 3720006cf8a5c264390a59e60d9403e0a3e9906f Mon Sep 17 00:00:00 2001 From: Brian Seeders <brian.seeders@elastic.co> Date: Fri, 29 Jan 2021 17:05:27 -0500 Subject: [PATCH 141/163] [CI] Move Jest tests to separate machines (#89770) --- vars/kibanaPipeline.groovy | 28 +++++++++++++++++++++------- vars/tasks.groovy | 5 +---- vars/workers.groovy | 2 ++ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 3e72c9e059af8..3032d88c26d98 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -447,13 +447,27 @@ def withTasks(Map params = [worker: [:]], Closure closure) { } def allCiTasks() { - withTasks { - tasks.check() - tasks.lint() - tasks.test() - tasks.functionalOss() - tasks.functionalXpack() - } + parallel([ + general: { + withTasks { + tasks.check() + tasks.lint() + tasks.test() + tasks.functionalOss() + tasks.functionalXpack() + } + }, + jest: { + workers.ci(name: 'jest', size: 'c2-8', ramDisk: true) { + scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh')() + } + }, + xpackJest: { + workers.ci(name: 'xpack-jest', size: 'c2-8', ramDisk: true) { + scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh')() + } + }, + ]) } def pipelineLibraryTests() { diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 3493a95f0bdce..6c4f897691136 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -30,12 +30,9 @@ def lint() { def test() { tasks([ - // These 2 tasks require isolation because of hard-coded, conflicting ports and such, so let's use Docker here + // This task requires 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.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), - kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), ]) } diff --git a/vars/workers.groovy b/vars/workers.groovy index dd634f3c25a32..e1684f7aadb43 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -19,6 +19,8 @@ def label(size) { return 'docker && tests-xl-highmem' case 'xxl': return 'docker && tests-xxl && gobld/machineType:custom-64-270336' + case 'c2-8': + return 'docker && linux && immutable && gobld/machineType:c2-standard-8' } error "unknown size '${size}'" From 2a913e4eb192b52bc12d3f66c1dd69f07205a08e Mon Sep 17 00:00:00 2001 From: Frank Hassanabad <frank.hassanabad@elastic.co> Date: Fri, 29 Jan 2021 15:53:29 -0700 Subject: [PATCH 142/163] Skips flake tests and tests with what looks like bugs (#89777) ## Summary Skips tests that have flake or in-determinism. * The sourcer code/tests are being rewritten and then those will come back by other team members. * The timeline open dialog looks to have some click and indeterminism bugs that are being investigated. Skipping for now. --- .../cypress/integration/data_sources/sourcerer.spec.ts | 4 +++- .../cypress/integration/timelines/creation.spec.ts | 5 +++-- x-pack/plugins/security_solution/cypress/tasks/timelines.ts | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts index 8b5871a6a67db..857582aac7638 100644 --- a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts @@ -28,7 +28,9 @@ import { populateTimeline } from '../../tasks/timeline'; import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; -describe('Sourcerer', () => { +// Skipped at the moment as this has flake due to click handler issues. This has been raised with team members +// and the code is being re-worked and then these tests will be unskipped +describe.skip('Sourcerer', () => { before(() => { cleanKibana(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index 2bfd2fbf0054c..ac70a1cae148e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -47,7 +47,8 @@ import { openTimeline } from '../../tasks/timelines'; import { OVERVIEW_URL } from '../../urls/navigation'; -describe('Timelines', () => { +// Skipped at the moment as there looks to be in-deterministic bugs with the open timeline dialog. +describe.skip('Timelines', () => { beforeEach(() => { cleanKibana(); }); @@ -89,7 +90,7 @@ describe('Timelines', () => { cy.get(FAVORITE_TIMELINE).should('exist'); cy.get(TIMELINE_TITLE).should('have.text', timeline.title); - cy.get(TIMELINE_DESCRIPTION).should('have.text', timeline.description); + cy.get(TIMELINE_DESCRIPTION).should('have.text', timeline.description); // This is the flake part where it sometimes does not show/load the timelines correctly cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query} `); cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); cy.get(PIN_EVENT) diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts index a04ecb1f9ccaa..c2b5790b1ae12 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -19,7 +19,10 @@ export const exportTimeline = (timelineId: string) => { }; export const openTimeline = (id: string) => { - cy.get(TIMELINE(id), { timeout: 500 }).click(); + // This temporary wait here is to reduce flakeyness until we integrate cypress-pipe. Then please let us use cypress pipe. + // Ref: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/ + // Ref: https://github.com/NicholasBoll/cypress-pipe#readme + cy.get(TIMELINE(id)).should('be.visible').wait(1500).click(); }; export const waitForTimelinesPanelToBeLoaded = () => { From 2f80e44d3b2a1820b88b7b0c5a02922f768374ce Mon Sep 17 00:00:00 2001 From: Frank Hassanabad <frank.hassanabad@elastic.co> Date: Fri, 29 Jan 2021 19:16:19 -0700 Subject: [PATCH 143/163] [Security Solution][Detection Engine] Fixes indicator matches mapping UI where invalid list values can cause overwrites of other values (#89066) ## Summary This fixes the ReactJS keys to not use array indexes for the ReactJS keys which fixes https://github.com/elastic/kibana/issues/84893 as well as a few other bugs that I will show below. The fix for the ReactJS keys is to add a unique id version 4 `uuid.v4()` to the incoming threat_mapping and the entities. On save out to elastic I remove the id. This is considered [better practices for ReactJS keys](https://reactjs.org/docs/lists-and-keys.html) Down the road we might augment the arrays to have that id information but for now I add them when we get the data and then remove them as we save the data. This PR also: * Fixes tech debt around the hooks to remove the disabling of the `react-hooks/exhaustive-deps` in a few areas * Fixes one React Hook misnamed that would not have triggered React linter rules (_useRuleAsyn) * Adds 23 new Cypress e2e tests * Adds a new pattern of dealing with on button clicks for the Cypress tests that are make it less flakey ```ts cy.get(`button[title="${indexField}"]`) .should('be.visible') .then(([e]) => e.click()); ``` * Adds several new utilities to Cypress for testing rows for indicator matches and other Cypress utils to improve velocity and ergonomics ```ts fillIndicatorMatchRow getDefineContinueButton getIndicatorInvalidationText getIndicatorIndexComboField getIndicatorDeleteButton getIndicatorOrButton getIndicatorAndButton ``` ## Bug 1 Deleting row 1 can cause row 2 to be cleared out or only partial data to stick around. Before: ![im_bug_1](https://user-images.githubusercontent.com/1151048/105916137-c57b1d80-5fed-11eb-95b7-ad25b71cf4b8.gif) After: ![im_fix_1_1](https://user-images.githubusercontent.com/1151048/105917509-9fef1380-5fef-11eb-98eb-025c226f79fe.gif) ## Bug 2 Deleting row 2 in the middle of 3 rows did not shift the value up correctly Before: ![im_bug_2](https://user-images.githubusercontent.com/1151048/105917584-c01ed280-5fef-11eb-8c5b-fefb36f81008.gif) After: ![im_fix_2](https://user-images.githubusercontent.com/1151048/105917650-e0e72800-5fef-11eb-9fd3-020d52e4e3b1.gif) ## Bug 3 When using OR with values it does not shift up correctly similar to AND Before: ![im_bug_3](https://user-images.githubusercontent.com/1151048/105917691-f2303480-5fef-11eb-9368-b11d23159606.gif) After: ![im_fix_3](https://user-images.githubusercontent.com/1151048/105917714-f9574280-5fef-11eb-9be4-1f56c207525a.gif) ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../indicator_match_rule.spec.ts | 412 ++++++++++++++---- .../cypress/screens/create_new_rule.ts | 16 + .../cypress/tasks/create_new_rule.ts | 152 ++++++- .../threat_match/entry_item.test.tsx | 9 +- .../components/threat_match/entry_item.tsx | 12 +- .../components/threat_match/helpers.test.tsx | 15 +- .../components/threat_match/helpers.tsx | 33 +- .../common/components/threat_match/index.tsx | 76 ++-- .../threat_match/list_item.test.tsx | 9 - .../components/threat_match/list_item.tsx | 4 +- .../components/threat_match/reducer.test.ts | 8 + .../common/components/threat_match/types.ts | 1 + .../utils/add_remove_id_to_item.test.ts | 76 ++++ .../common/utils/add_remove_id_to_item.ts | 49 +++ .../alerts/use_privilege_user.tsx | 7 +- .../detection_engine/alerts/use_query.tsx | 4 +- .../alerts/use_signal_index.tsx | 3 +- .../detection_engine/rules/transforms.ts | 98 +++++ .../rules/use_create_rule.tsx | 10 +- .../rules/use_pre_packaged_rules.tsx | 10 +- .../detection_engine/rules/use_rule.tsx | 18 +- .../detection_engine/rules/use_rule_async.tsx | 12 +- .../rules/use_rule_status.tsx | 6 +- .../detection_engine/rules/use_tags.tsx | 7 +- .../rules/use_update_rule.tsx | 11 +- 25 files changed, 857 insertions(+), 201 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.ts create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 37123dedfd661..2c9dc14aa05b2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -5,7 +5,7 @@ */ import { formatMitreAttackDescription } from '../../helpers/rules'; -import { newThreatIndicatorRule } from '../../objects/rule'; +import { indexPatterns, newThreatIndicatorRule } from '../../objects/rule'; import { ALERT_RULE_METHOD, @@ -70,7 +70,24 @@ import { createAndActivateRule, fillAboutRuleAndContinue, fillDefineIndicatorMatchRuleAndContinue, + fillIndexAndIndicatorIndexPattern, + fillIndicatorMatchRow, fillScheduleRuleAndContinue, + getCustomIndicatorQueryInput, + getCustomQueryInput, + getCustomQueryInvalidationText, + getDefineContinueButton, + getIndexPatternClearButton, + getIndexPatternInvalidationText, + getIndicatorAndButton, + getIndicatorAtLeastOneInvalidationText, + getIndicatorDeleteButton, + getIndicatorIndex, + getIndicatorIndexComboField, + getIndicatorIndicatorIndex, + getIndicatorInvalidationText, + getIndicatorMappingComboField, + getIndicatorOrButton, selectIndicatorMatchType, waitForAlertsToPopulate, waitForTheRuleToBeExecuted, @@ -92,14 +109,6 @@ describe('Detection rules, Indicator Match', () => { cleanKibana(); esArchiverLoad('threat_indicator'); esArchiverLoad('threat_data'); - }); - - afterEach(() => { - esArchiverUnload('threat_indicator'); - esArchiverUnload('threat_data'); - }); - - it('Creates and activates a new Indicator Match rule', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); @@ -107,89 +116,330 @@ describe('Detection rules, Indicator Match', () => { waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); goToCreateNewRule(); selectIndicatorMatchType(); - fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); - fillAboutRuleAndContinue(newThreatIndicatorRule); - fillScheduleRuleAndContinue(newThreatIndicatorRule); - createAndActivateRule(); + }); + + afterEach(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('threat_data'); + }); - cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); + describe('Creating new indicator match rules', () => { + describe('Index patterns', () => { + it('Contains a predefined index pattern', () => { + getIndicatorIndex().should('have.text', indexPatterns.join('')); + }); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => { + getIndicatorIndicatorIndex().type(`${newThreatIndicatorRule.indicatorIndexPattern}{enter}`); + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('not.exist'); + }); - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + it('Shows invalidation text when you try to continue without filling it out', () => { + getIndexPatternClearButton().click(); + getIndicatorIndicatorIndex().type(`${newThreatIndicatorRule.indicatorIndexPattern}{enter}`); + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('exist'); + }); }); - filterByCustomRules(); + describe('Indicator index patterns', () => { + it('Contains empty index pattern', () => { + getIndicatorIndicatorIndex().should('have.text', ''); + }); + + it('Does NOT show invalidation text on initial page load', () => { + getIndexPatternInvalidationText().should('not.exist'); + }); - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', 1); + it('Shows invalidation text if you try to continue without filling it out', () => { + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('exist'); + }); }); - cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name); - cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore); - cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity); - cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); - - goToRuleDetails(); - - cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description); - cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should((details) => { - expect(removeExternalLinkText(details.text())).equal(expectedUrls); - }); - getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { - expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); - }); - getDetails(TAGS_DETAILS).should('have.text', expectedTags); + + describe('custom query input', () => { + it('Has a default set of *:*', () => { + getCustomQueryInput().should('have.text', '*:*'); + }); + + it('Shows invalidation text if text is removed', () => { + getCustomQueryInput().type('{selectall}{del}'); + getCustomQueryInvalidationText().should('exist'); + }); }); - cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); - - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should( - 'have.text', - newThreatIndicatorRule.index!.join('') - ); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); - getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); - getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); - getDetails(INDICATOR_INDEX_PATTERNS).should( - 'have.text', - newThreatIndicatorRule.indicatorIndexPattern.join('') - ); - getDetails(INDICATOR_MAPPING).should( - 'have.text', - `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` - ); - getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); + + describe('custom indicator query input', () => { + it('Has a default set of *:*', () => { + getCustomIndicatorQueryInput().should('have.text', '*:*'); + }); + + it('Shows invalidation text if text is removed', () => { + getCustomIndicatorQueryInput().type('{selectall}{del}'); + getCustomQueryInvalidationText().should('exist'); + }); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should( - 'have.text', - `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}` - ); - getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( - 'have.text', - `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}` - ); + describe('Indicator mapping', () => { + beforeEach(() => { + fillIndexAndIndicatorIndexPattern( + newThreatIndicatorRule.index, + newThreatIndicatorRule.indicatorIndexPattern + ); + }); + + it('Does NOT show invalidation text on initial page load', () => { + getIndicatorInvalidationText().should('not.exist'); + }); + + it('Shows invalidation text when you try to press continue without filling anything out', () => { + getDefineContinueButton().click(); + getIndicatorAtLeastOneInvalidationText().should('exist'); + }); + + it('Shows invalidation text when the "AND" button is pressed and both the mappings are blank', () => { + getIndicatorAndButton().click(); + getIndicatorInvalidationText().should('exist'); + }); + + it('Shows invalidation text when the "OR" button is pressed and both the mappings are blank', () => { + getIndicatorOrButton().click(); + getIndicatorInvalidationText().should('exist'); + }); + + it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('not.exist'); + }); + + it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('exist'); + }); + + it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'non-existent-value', + validColumns: 'indexField', + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('exist'); + }); + + it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'agent.name', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('have.text', 'agent.name'); + getIndicatorMappingComboField().should( + 'have.text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); + }); + + it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'non-existent-value', + validColumns: 'indexField', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'second-non-existent-value', + validColumns: 'indexField', + }); + getIndicatorDeleteButton().click(); + getIndicatorMappingComboField().should('have.text', 'second-non-existent-value'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); + }); + + it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'second-non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('have.text', 'second-non-existent-value'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); + }); + + it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('text', 'Search'); + getIndicatorMappingComboField().should('text', 'Search'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); + }); + + it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'non-existent-value', + indicatorIndexField: 'non-existent-value', + validColumns: 'none', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 3, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton(2).click(); + getIndicatorIndexComboField(1).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField(1).should('text', newThreatIndicatorRule.indicatorIndexField); + getIndicatorIndexComboField(2).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField(2).should('text', newThreatIndicatorRule.indicatorIndexField); + getIndicatorIndexComboField(3).should('not.exist'); + getIndicatorMappingComboField(3).should('not.exist'); + }); + + it('Can add two OR rows and delete the second row. The first row has invalid data and the second row has valid data. The first row is deleted and the second row shifts up correctly.', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value-one', + indicatorIndexField: 'non-existent-value-two', + validColumns: 'none', + }); + getIndicatorOrButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField().should('text', newThreatIndicatorRule.indicatorIndexField); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); + }); }); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); - cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); - cy.get(ALERT_RULE_SEVERITY) - .first() - .should('have.text', newThreatIndicatorRule.severity.toLowerCase()); - cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); + it('Creates and activates a new Indicator Match rule', () => { + fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); + fillAboutRuleAndContinue(newThreatIndicatorRule); + fillScheduleRuleAndContinue(newThreatIndicatorRule); + createAndActivateRule(); + + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + 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).should('have.text', newThreatIndicatorRule.name); + cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore); + cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); + + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should( + 'have.text', + newThreatIndicatorRule.index!.join('') + ); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + getDetails(INDICATOR_INDEX_PATTERNS).should( + 'have.text', + newThreatIndicatorRule.indicatorIndexPattern.join('') + ); + getDetails(INDICATOR_MAPPING).should( + 'have.text', + `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` + ); + getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); + }); + + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}` + ); + }); + + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); + cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); + cy.get(ALERT_RULE_SEVERITY) + .first() + .should('have.text', newThreatIndicatorRule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); + }); }); }); 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 66681e77b7eb9..2a59dd33399c5 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 @@ -38,6 +38,22 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; export const THREAT_MATCH_QUERY_INPUT = '[data-test-subj="detectionEngineStepDefineThreatRuleQueryBar"] [data-test-subj="queryInput"]'; +export const THREAT_MATCH_AND_BUTTON = '[data-test-subj="andButton"]'; + +export const THREAT_ITEM_ENTRY_DELETE_BUTTON = '[data-test-subj="itemEntryDeleteButton"]'; + +export const THREAT_MATCH_OR_BUTTON = '[data-test-subj="orButton"]'; + +export const THREAT_COMBO_BOX_INPUT = '[data-test-subj="fieldAutocompleteComboBox"]'; + +export const INVALID_MATCH_CONTENT = 'All matches require both a field and threat index field.'; + +export const AT_LEAST_ONE_VALID_MATCH = 'At least one indicator match is required.'; + +export const AT_LEAST_ONE_INDEX_PATTERN = 'A minimum of one index pattern is required.'; + +export const CUSTOM_QUERY_REQUIRED = 'A custom query is required.'; + export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]'; 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 7836960b1a694..5143dc27e7d7a 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 @@ -63,13 +63,20 @@ import { EQL_QUERY_PREVIEW_HISTOGRAM, EQL_QUERY_VALIDATION_SPINNER, COMBO_BOX_CLEAR_BTN, - COMBO_BOX_RESULT, MITRE_ATTACK_TACTIC_DROPDOWN, MITRE_ATTACK_TECHNIQUE_DROPDOWN, MITRE_ATTACK_SUBTECHNIQUE_DROPDOWN, MITRE_ATTACK_ADD_TACTIC_BUTTON, MITRE_ATTACK_ADD_SUBTECHNIQUE_BUTTON, MITRE_ATTACK_ADD_TECHNIQUE_BUTTON, + THREAT_COMBO_BOX_INPUT, + THREAT_ITEM_ENTRY_DELETE_BUTTON, + THREAT_MATCH_AND_BUTTON, + INVALID_MATCH_CONTENT, + THREAT_MATCH_OR_BUTTON, + AT_LEAST_ONE_VALID_MATCH, + AT_LEAST_ONE_INDEX_PATTERN, + CUSTOM_QUERY_REQUIRED, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -144,7 +151,7 @@ export const fillAboutRuleAndContinue = ( rule: CustomRule | MachineLearningRule | ThresholdRule | ThreatIndicatorRule ) => { fillAboutRule(rule); - cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); + getAboutContinueButton().should('exist').click({ force: true }); }; export const fillAboutRuleWithOverrideAndContinue = (rule: OverrideRule) => { @@ -222,7 +229,7 @@ export const fillAboutRuleWithOverrideAndContinue = (rule: OverrideRule) => { cy.get(COMBO_BOX_INPUT).type(`${rule.timestampOverride}{enter}`); }); - cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); + getAboutContinueButton().should('exist').click({ force: true }); }; export const fillDefineCustomRuleWithImportedQueryAndContinue = ( @@ -282,19 +289,132 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { cy.get(EQL_QUERY_INPUT).should('not.exist'); }; +/** + * Fills in the indicator match rows for tests by giving it an optional rowNumber, + * a indexField, a indicatorIndexField, and an optional validRows which indicates + * which row is valid or not. + * + * There are special tricks below with Eui combo box: + * cy.get(`button[title="${indexField}"]`) + * .should('be.visible') + * .then(([e]) => e.click()); + * + * To first ensure the button is there before clicking on the button. There are + * race conditions where if the Eui drop down button from the combo box is not + * visible then the click handler is not there either, and when we click on it + * that will cause the item to _not_ be selected. Using a {enter} with the combo + * box also does not select things from EuiCombo boxes either, so I have to click + * the actual contents of the EuiCombo box to select things. + */ +export const fillIndicatorMatchRow = ({ + rowNumber, + indexField, + indicatorIndexField, + validColumns, +}: { + rowNumber?: number; // default is 1 + indexField: string; + indicatorIndexField: string; + validColumns?: 'indexField' | 'indicatorField' | 'both' | 'none'; // default is both are valid entries +}) => { + const computedRowNumber = rowNumber == null ? 1 : rowNumber; + const computedValueRows = validColumns == null ? 'both' : validColumns; + const OFFSET = 2; + cy.get(COMBO_BOX_INPUT) + .eq(computedRowNumber * OFFSET + 1) + .type(indexField); + if (computedValueRows === 'indexField' || computedValueRows === 'both') { + cy.get(`button[title="${indexField}"]`) + .should('be.visible') + .then(([e]) => e.click()); + } + cy.get(COMBO_BOX_INPUT) + .eq(computedRowNumber * OFFSET + 2) + .type(indicatorIndexField); + + if (computedValueRows === 'indicatorField' || computedValueRows === 'both') { + cy.get(`button[title="${indicatorIndexField}"]`) + .should('be.visible') + .then(([e]) => e.click()); + } +}; + +/** + * Fills in both the index pattern and the indicator match index pattern. + * @param indexPattern The index pattern. + * @param indicatorIndex The indicator index pattern. + */ +export const fillIndexAndIndicatorIndexPattern = ( + indexPattern?: string[], + indicatorIndex?: string[] +) => { + getIndexPatternClearButton().click(); + getIndicatorIndex().type(`${indexPattern}{enter}`); + getIndicatorIndicatorIndex().type(`${indicatorIndex}{enter}`); +}; + +/** Returns the indicator index drop down field. Pass in row number, default is 1 */ +export const getIndicatorIndexComboField = (row = 1) => + cy.get(THREAT_COMBO_BOX_INPUT).eq(row * 2 - 2); + +/** Returns the indicator mapping drop down field. Pass in row number, default is 1 */ +export const getIndicatorMappingComboField = (row = 1) => + cy.get(THREAT_COMBO_BOX_INPUT).eq(row * 2 - 1); + +/** Returns the indicator matches DELETE button for the mapping. Pass in row number, default is 1 */ +export const getIndicatorDeleteButton = (row = 1) => + cy.get(THREAT_ITEM_ENTRY_DELETE_BUTTON).eq(row - 1); + +/** Returns the indicator matches AND button for the mapping */ +export const getIndicatorAndButton = () => cy.get(THREAT_MATCH_AND_BUTTON); + +/** Returns the indicator matches OR button for the mapping */ +export const getIndicatorOrButton = () => cy.get(THREAT_MATCH_OR_BUTTON); + +/** Returns the invalid match content. */ +export const getIndicatorInvalidationText = () => cy.contains(INVALID_MATCH_CONTENT); + +/** Returns that at least one valid match is required content */ +export const getIndicatorAtLeastOneInvalidationText = () => cy.contains(AT_LEAST_ONE_VALID_MATCH); + +/** Returns that at least one index pattern is required content */ +export const getIndexPatternInvalidationText = () => cy.contains(AT_LEAST_ONE_INDEX_PATTERN); + +/** Returns the continue button on the step of about */ +export const getAboutContinueButton = () => cy.get(ABOUT_CONTINUE_BTN); + +/** Returns the continue button on the step of define */ +export const getDefineContinueButton = () => cy.get(DEFINE_CONTINUE_BUTTON); + +/** Returns the indicator index pattern */ +export const getIndicatorIndex = () => cy.get(COMBO_BOX_INPUT).eq(0); + +/** Returns the indicator's indicator index */ +export const getIndicatorIndicatorIndex = () => cy.get(COMBO_BOX_INPUT).eq(2); + +/** Returns the index pattern's clear button */ +export const getIndexPatternClearButton = () => cy.get(COMBO_BOX_CLEAR_BTN); + +/** Returns the custom query input */ +export const getCustomQueryInput = () => cy.get(CUSTOM_QUERY_INPUT).eq(0); + +/** Returns the custom query input */ +export const getCustomIndicatorQueryInput = () => cy.get(CUSTOM_QUERY_INPUT).eq(1); + +/** Returns custom query required content */ +export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQUIRED); + +/** + * Fills in the define indicator match rules and then presses the continue button + * @param rule The rule to use to fill in everything + */ export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRule) => { - const INDEX_PATTERNS = 0; - const INDICATOR_INDEX_PATTERN = 2; - const INDICATOR_MAPPING = 3; - const INDICATOR_INDEX_FIELD = 4; - - cy.get(COMBO_BOX_CLEAR_BTN).click(); - cy.get(COMBO_BOX_INPUT).eq(INDEX_PATTERNS).type(`${rule.index}{enter}`); - cy.get(COMBO_BOX_INPUT).eq(INDICATOR_INDEX_PATTERN).type(`${rule.indicatorIndexPattern}{enter}`); - cy.get(COMBO_BOX_INPUT).eq(INDICATOR_MAPPING).type(`${rule.indicatorMapping}{enter}`); - cy.get(COMBO_BOX_RESULT).first().click(); - cy.get(COMBO_BOX_INPUT).eq(INDICATOR_INDEX_FIELD).type(`${rule.indicatorIndexField}{enter}`); - cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + fillIndexAndIndicatorIndexPattern(rule.index, rule.indicatorIndexPattern); + fillIndicatorMatchRow({ + indexField: rule.indicatorMapping, + indicatorIndexField: rule.indicatorIndexField, + }); + getDefineContinueButton().should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; @@ -304,7 +424,7 @@ export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRu cy.get(ANOMALY_THRESHOLD_INPUT).type(`{selectall}${machineLearningRule.anomalyScoreThreshold}`, { force: true, }); - cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + getDefineContinueButton().should('exist').click({ force: true }); cy.get(MACHINE_LEARNING_DROPDOWN).should('not.exist'); }; diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.test.tsx index 36033c358766d..ce6ca7ebc22dd 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.test.tsx @@ -22,6 +22,7 @@ describe('EntryItem', () => { const wrapper = mount( <EntryItem entry={{ + id: '123', field: undefined, value: undefined, type: 'mapping', @@ -54,6 +55,7 @@ describe('EntryItem', () => { const wrapper = mount( <EntryItem entry={{ + id: '123', field: getField('ip'), type: 'mapping', value: getField('ip'), @@ -84,6 +86,7 @@ describe('EntryItem', () => { expect(mockOnChange).toHaveBeenCalledWith( { + id: '123', field: 'machine.os', type: 'mapping', value: 'ip', @@ -97,6 +100,7 @@ describe('EntryItem', () => { const wrapper = mount( <EntryItem entry={{ + id: '123', field: getField('ip'), type: 'mapping', value: getField('ip'), @@ -125,6 +129,9 @@ describe('EntryItem', () => { onChange: (a: EuiComboBoxOptionOption[]) => void; }).onChange([{ label: 'is not' }]); - expect(mockOnChange).toHaveBeenCalledWith({ field: 'ip', type: 'mapping', value: '' }, 0); + expect(mockOnChange).toHaveBeenCalledWith( + { id: '123', field: 'ip', type: 'mapping', value: '' }, + 0 + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx index c99e63ff4eda0..51b724bff2e5d 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx @@ -75,7 +75,11 @@ export const EntryItem: React.FC<EntryItemProps> = ({ </EuiFormRow> ); } else { - return comboBox; + return ( + <EuiFormRow label={''} data-test-subj="entryItemFieldInputFormRow"> + {comboBox} + </EuiFormRow> + ); } }, [handleFieldChange, indexPattern, entry, showLabel]); @@ -101,7 +105,11 @@ export const EntryItem: React.FC<EntryItemProps> = ({ </EuiFormRow> ); } else { - return comboBox; + return ( + <EuiFormRow label={''} data-test-subj="threatFieldInputFormRow"> + {comboBox} + </EuiFormRow> + ); } }, [handleThreatFieldChange, threatIndexPatterns, entry, showLabel]); diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx index b4f97808b54c4..b3a74c7697715 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx @@ -21,6 +21,10 @@ import { } from './helpers'; import { ThreatMapEntry } from '../../../../common/detection_engine/schemas/types'; +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('123'), +})); + const getMockIndexPattern = (): IndexPattern => ({ id: '1234', @@ -29,6 +33,7 @@ const getMockIndexPattern = (): IndexPattern => } as IndexPattern); const getMockEntry = (): FormattedEntry => ({ + id: '123', field: getField('ip'), value: getField('ip'), type: 'mapping', @@ -42,6 +47,7 @@ describe('Helpers', () => { afterEach(() => { moment.tz.setDefault('Browser'); + jest.clearAllMocks(); }); describe('#getFormattedEntry', () => { @@ -70,6 +76,7 @@ describe('Helpers', () => { const output = getFormattedEntry(payloadIndexPattern, payloadIndexPattern, payloadItem, 0); const expected: FormattedEntry = { entryIndex: 0, + id: '123', field: { name: 'machine.os.raw.text', type: 'string', @@ -94,6 +101,7 @@ describe('Helpers', () => { const output = getFormattedEntries(payloadIndexPattern, payloadIndexPattern, payloadItems); const expected: FormattedEntry[] = [ { + id: '123', entryIndex: 0, field: undefined, value: undefined, @@ -109,6 +117,7 @@ describe('Helpers', () => { const output = getFormattedEntries(payloadIndexPattern, payloadIndexPattern, payloadItems); const expected: FormattedEntry[] = [ { + id: '123', entryIndex: 0, field: { name: 'machine.os', @@ -134,6 +143,7 @@ describe('Helpers', () => { const output = getFormattedEntries(payloadIndexPattern, threatIndexPattern, payloadItems); const expected: FormattedEntry[] = [ { + id: '123', entryIndex: 0, field: { name: 'machine.os', @@ -170,6 +180,7 @@ describe('Helpers', () => { const output = getFormattedEntries(payloadIndexPattern, payloadIndexPattern, payloadItems); const expected: FormattedEntry[] = [ { + id: '123', field: { name: 'machine.os', type: 'string', @@ -194,6 +205,7 @@ describe('Helpers', () => { entryIndex: 0, }, { + id: '123', field: { name: 'ip', type: 'ip', @@ -249,9 +261,10 @@ describe('Helpers', () => { const payloadItem = getMockEntry(); const payloadIFieldType = getField('ip'); const output = getEntryOnFieldChange(payloadItem, payloadIFieldType); - const expected: { updatedEntry: Entry; index: number } = { + const expected: { updatedEntry: Entry & { id: string }; index: number } = { index: 0, updatedEntry: { + id: '123', field: 'ip', type: 'mapping', value: 'ip', diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx index 349dae76301d4..90a996c06e492 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import uuid from 'uuid'; import { ThreatMap, threatMap, @@ -12,6 +13,7 @@ import { import { IndexPattern, IFieldType } from '../../../../../../../src/plugins/data/common'; import { Entry, FormattedEntry, ThreatMapEntries, EmptyEntry } from './types'; +import { addIdToItem } from '../../utils/add_remove_id_to_item'; /** * Formats the entry into one that is easily usable for the UI. @@ -24,7 +26,8 @@ export const getFormattedEntry = ( indexPattern: IndexPattern, threatIndexPatterns: IndexPattern, item: Entry, - itemIndex: number + itemIndex: number, + uuidGen: () => string = uuid.v4 ): FormattedEntry => { const { fields } = indexPattern; const { fields: threatFields } = threatIndexPatterns; @@ -34,7 +37,9 @@ export const getFormattedEntry = ( const [threatFoundField] = threatFields.filter( ({ name }) => threatField != null && threatField === name ); + const maybeId: typeof item & { id?: string } = item; return { + id: maybeId.id ?? uuidGen(), field: foundField, type: 'mapping', value: threatFoundField, @@ -90,10 +95,11 @@ export const getEntryOnFieldChange = ( const { entryIndex } = item; return { updatedEntry: { + id: item.id, field: newField != null ? newField.name : '', type: 'mapping', value: item.value != null ? item.value.name : '', - }, + } as Entry, // Cast to Entry since id is only used as a react key prop and can be ignored elsewhere index: entryIndex, }; }; @@ -112,30 +118,33 @@ export const getEntryOnThreatFieldChange = ( const { entryIndex } = item; return { updatedEntry: { + id: item.id, field: item.field != null ? item.field.name : '', type: 'mapping', value: newField != null ? newField.name : '', - }, + } as Entry, // Cast to Entry since id is only used as a react key prop and can be ignored elsewhere index: entryIndex, }; }; -export const getDefaultEmptyEntry = (): EmptyEntry => ({ - field: '', - type: 'mapping', - value: '', -}); +export const getDefaultEmptyEntry = (): EmptyEntry => { + return addIdToItem({ + field: '', + type: 'mapping', + value: '', + }); +}; export const getNewItem = (): ThreatMap => { - return { + return addIdToItem({ entries: [ - { + addIdToItem({ field: '', type: 'mapping', value: '', - }, + }), ], - }; + }); }; export const filterItems = (items: ThreatMapEntries[]): ThreatMapping => { diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx index d3936e10bd877..8aa4af21b03cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx @@ -158,43 +158,45 @@ export const ThreatMatchComponent = ({ }, []); return ( <EuiFlexGroup gutterSize="s" direction="column"> - {entries.map((entryListItem, index) => ( - <EuiFlexItem grow={1} key={`${index}`}> - <EuiFlexGroup gutterSize="s" direction="column"> - {index !== 0 && - (andLogicIncluded ? ( - <EuiFlexItem grow={false}> - <EuiFlexGroup gutterSize="none" direction="row"> - <MyInvisibleAndBadge grow={false}> - <MyAndBadge includeAntennas type="and" /> - </MyInvisibleAndBadge> - <EuiFlexItem grow={false}> - <MyAndBadge type="or" /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - ) : ( - <EuiFlexItem grow={false}> - <MyAndBadge type="or" /> - </EuiFlexItem> - ))} - <EuiFlexItem grow={false}> - <ListItemComponent - key={`${index}`} - listItem={entryListItem} - listId={`${index}`} - indexPattern={indexPatterns} - threatIndexPatterns={threatIndexPatterns} - listItemIndex={index} - andLogicIncluded={andLogicIncluded} - isOnlyItem={entries.length === 1} - onDeleteEntryItem={handleDeleteEntryItem} - onChangeEntryItem={handleEntryItemChange} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - ))} + {entries.map((entryListItem, index) => { + const key = (entryListItem as typeof entryListItem & { id?: string }).id ?? `${index}`; + return ( + <EuiFlexItem grow={1} key={key}> + <EuiFlexGroup gutterSize="s" direction="column"> + {index !== 0 && + (andLogicIncluded ? ( + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="none" direction="row"> + <MyInvisibleAndBadge grow={false}> + <MyAndBadge includeAntennas type="and" /> + </MyInvisibleAndBadge> + <EuiFlexItem grow={false}> + <MyAndBadge type="or" /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + ) : ( + <EuiFlexItem grow={false}> + <MyAndBadge type="or" /> + </EuiFlexItem> + ))} + <EuiFlexItem grow={false}> + <ListItemComponent + key={key} + listItem={entryListItem} + indexPattern={indexPatterns} + threatIndexPatterns={threatIndexPatterns} + listItemIndex={index} + andLogicIncluded={andLogicIncluded} + isOnlyItem={entries.length === 1} + onDeleteEntryItem={handleDeleteEntryItem} + onChangeEntryItem={handleEntryItemChange} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + ); + })} <MyButtonsContainer data-test-subj={'andOrOperatorButtons'}> <EuiFlexGroup gutterSize="s"> diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx index 90492bc46e2b0..66af24025656e 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx @@ -68,7 +68,6 @@ describe('ListItemComponent', () => { <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}> <ListItemComponent listItem={doublePayload()} - listId={'123'} listItemIndex={0} indexPattern={ { @@ -102,7 +101,6 @@ describe('ListItemComponent', () => { <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}> <ListItemComponent listItem={doublePayload()} - listId={'123'} listItemIndex={1} indexPattern={ { @@ -134,7 +132,6 @@ describe('ListItemComponent', () => { <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}> <ListItemComponent listItem={singlePayload()} - listId={'123'} listItemIndex={1} indexPattern={ { @@ -168,7 +165,6 @@ describe('ListItemComponent', () => { <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}> <ListItemComponent listItem={singlePayload()} - listId={'123'} listItemIndex={1} indexPattern={ { @@ -210,7 +206,6 @@ describe('ListItemComponent', () => { const wrapper = mount( <ListItemComponent listItem={item} - listId={'123'} listItemIndex={0} indexPattern={ { @@ -242,7 +237,6 @@ describe('ListItemComponent', () => { const wrapper = mount( <ListItemComponent listItem={singlePayload()} - listId={'123'} listItemIndex={0} indexPattern={ { @@ -274,7 +268,6 @@ describe('ListItemComponent', () => { const wrapper = mount( <ListItemComponent listItem={singlePayload()} - listId={'123'} listItemIndex={1} indexPattern={ { @@ -308,7 +301,6 @@ describe('ListItemComponent', () => { const wrapper = mount( <ListItemComponent listItem={doublePayload()} - listId={'123'} listItemIndex={0} indexPattern={ { @@ -341,7 +333,6 @@ describe('ListItemComponent', () => { const wrapper = mount( <ListItemComponent listItem={doublePayload()} - listId={'123'} listItemIndex={0} indexPattern={ { diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.tsx index 5fa2997193bd9..d1ec40c627cb8 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.tsx @@ -22,7 +22,6 @@ const MyOverflowContainer = styled(EuiFlexItem)` interface ListItemProps { listItem: ThreatMapEntries; - listId: string; listItemIndex: number; indexPattern: IndexPattern; threatIndexPatterns: IndexPattern; @@ -35,7 +34,6 @@ interface ListItemProps { export const ListItemComponent = React.memo<ListItemProps>( ({ listItem, - listId, listItemIndex, indexPattern, threatIndexPatterns, @@ -88,7 +86,7 @@ export const ListItemComponent = React.memo<ListItemProps>( <MyOverflowContainer grow={6}> <EuiFlexGroup gutterSize="s" direction="column"> {entries.map((item, index) => ( - <EuiFlexItem key={`${listId}-${index}`} grow={1}> + <EuiFlexItem key={item.id} grow={1}> <EuiFlexGroup gutterSize="xs" alignItems="center" direction="row"> <MyOverflowContainer grow={1}> <EntryItem diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/threat_match/reducer.test.ts index 6b2a443ec45a5..db56d1e34b641 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/reducer.test.ts @@ -9,6 +9,10 @@ import { State, reducer } from './reducer'; import { getDefaultEmptyEntry } from './helpers'; import { ThreatMapEntry } from '../../../../common/detection_engine/schemas/types'; +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('123'), +})); + const initialState: State = { andLogicIncluded: false, entries: [], @@ -22,6 +26,10 @@ const getEntry = (): ThreatMapEntry => ({ }); describe('reducer', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + describe('#setEntries', () => { test('should return "andLogicIncluded" ', () => { const update = reducer()(initialState, { diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts b/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts index 0cbd885db2d54..f3af5faaed25c 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts @@ -7,6 +7,7 @@ import { ThreatMap, ThreatMapEntry } from '../../../../common/detection_engine/s import { IFieldType } from '../../../../../../../src/plugins/data/common'; export interface FormattedEntry { + id: string; field: IFieldType | undefined; type: 'mapping'; value: IFieldType | undefined; diff --git a/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts b/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts new file mode 100644 index 0000000000000..fa067a53f2573 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { addIdToItem, removeIdFromItem } from './add_remove_id_to_item'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('123'), +})); + +describe('add_remove_id_to_item', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('addIdToItem', () => { + test('it adds an id to an empty item', () => { + expect(addIdToItem({})).toEqual({ id: '123' }); + }); + + test('it adds a complex object', () => { + expect( + addIdToItem({ + field: '', + type: 'mapping', + value: '', + }) + ).toEqual({ + id: '123', + field: '', + type: 'mapping', + value: '', + }); + }); + + test('it adds an id to an existing item', () => { + expect(addIdToItem({ test: '456' })).toEqual({ id: '123', test: '456' }); + }); + + test('it does not change the id if it already exists', () => { + expect(addIdToItem({ id: '456' })).toEqual({ id: '456' }); + }); + + test('it returns the same reference if it has an id already', () => { + const obj = { id: '456' }; + expect(addIdToItem(obj)).toBe(obj); + }); + + test('it returns a new reference if it adds an id to an item', () => { + const obj = { test: '456' }; + expect(addIdToItem(obj)).not.toBe(obj); + }); + }); + + describe('removeIdFromItem', () => { + test('it removes an id from an item', () => { + expect(removeIdFromItem({ id: '456' })).toEqual({}); + }); + + test('it returns a new reference if it removes an id from an item', () => { + const obj = { id: '123', test: '456' }; + expect(removeIdFromItem(obj)).not.toBe(obj); + }); + + test('it does not effect an item without an id', () => { + expect(removeIdFromItem({ test: '456' })).toEqual({ test: '456' }); + }); + + test('it returns the same reference if it does not have an id already', () => { + const obj = { test: '456' }; + expect(removeIdFromItem(obj)).toBe(obj); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.ts b/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.ts new file mode 100644 index 0000000000000..a74cf8680fa48 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.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 uuid from 'uuid'; + +/** + * This is useful for when you have arrays without an ID and need to add one for + * ReactJS keys. I break the types slightly by introducing an id to an arbitrary item + * but then cast it back to the regular type T. + * Usage of this could be considered tech debt as I am adding an ID when the backend + * could be doing the same thing but it depends on how you want to model your data and + * if you view modeling your data with id's to please ReactJS a good or bad thing. + * @param item The item to add an id to. + */ +type NotArray<T> = T extends unknown[] ? never : T; +export const addIdToItem = <T>(item: NotArray<T>): T => { + const maybeId: typeof item & { id?: string } = item; + if (maybeId.id != null) { + return item; + } else { + return { ...item, id: uuid.v4() }; + } +}; + +/** + * This is to reverse the id you added to your arrays for ReactJS keys. + * @param item The item to remove the id from. + */ +export const removeIdFromItem = <T>( + item: NotArray<T> +): + | T + | Pick< + T & { + id?: string | undefined; + }, + Exclude<keyof T, 'id'> + > => { + const maybeId: typeof item & { id?: string } = item; + if (maybeId.id != null) { + const { id, ...noId } = maybeId; + return noId; + } else { + return item; + } +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx index b72dd3b2f84dd..191c3955caa9b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx @@ -50,7 +50,7 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { const abortCtrl = new AbortController(); setLoading(true); - async function fetchData() { + const fetchData = async () => { try { const privilege = await getUserPrivilege({ signal: abortCtrl.signal, @@ -89,15 +89,14 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { if (isSubscribed) { setLoading(false); } - } + }; fetchData(); return () => { isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatchToaster]); return { loading, ...privilegeUser }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index 3bef1d8edd048..9022e3a32163c 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -46,7 +46,7 @@ export const useQueryAlerts = <Hit, Aggs>( let isSubscribed = true; const abortCtrl = new AbortController(); - async function fetchData() { + const fetchData = async () => { try { setLoading(true); const alertResponse = await fetchQueryAlerts<Hit, Aggs>({ @@ -77,7 +77,7 @@ export const useQueryAlerts = <Hit, Aggs>( if (isSubscribed) { setLoading(false); } - } + }; fetchData(); return () => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index 5ebdb38b8dd5c..bfdc1d1ceee21 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -106,8 +106,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatchToaster]); return { loading, ...signalIndex }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts new file mode 100644 index 0000000000000..7821bb23a7ca3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.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 { flow } from 'fp-ts/lib/function'; +import { addIdToItem, removeIdFromItem } from '../../../../common/utils/add_remove_id_to_item'; +import { + CreateRulesSchema, + UpdateRulesSchema, +} from '../../../../../common/detection_engine/schemas/request'; +import { Rule } from './types'; + +// These are a collection of transforms that are UI specific and useful for UI concerns +// that are inserted between the API and the actual user interface. In some ways these +// might be viewed as technical debt or to compensate for the differences and preferences +// of how ReactJS might prefer data vs. how we want to model data. Each function should have +// a description giving context around the transform. + +/** + * Transforms the output of rules to compensate for technical debt or UI concerns such as + * ReactJS preferences for having ids within arrays if the data is not modeled that way. + * + * If you add a new transform of the output called "myNewTransform" do it + * in the form of: + * flow(removeIdFromThreatMatchArray, myNewTransform)(rule) + * + * @param rule The rule to transform the output of + * @returns The rule transformed from the output + */ +export const transformOutput = ( + rule: CreateRulesSchema | UpdateRulesSchema +): CreateRulesSchema | UpdateRulesSchema => flow(removeIdFromThreatMatchArray)(rule); + +/** + * Transforms the output of rules to compensate for technical debt or UI concerns such as + * ReactJS preferences for having ids within arrays if the data is not modeled that way. + * + * If you add a new transform of the input called "myNewTransform" do it + * in the form of: + * flow(addIdToThreatMatchArray, myNewTransform)(rule) + * + * @param rule The rule to transform the output of + * @returns The rule transformed from the output + */ +export const transformInput = (rule: Rule): Rule => flow(addIdToThreatMatchArray)(rule); + +/** + * This adds an id to the incoming threat match arrays as ReactJS prefers to have + * an id added to them for use as a stable id. Later if we decide to change the data + * model to have id's within the array then this code should be removed. If not, then + * this code should stay as an adapter for ReactJS. + * + * This does break the type system slightly as we are lying a bit to the type system as we return + * the same rule as we have previously but are augmenting the arrays with an id which TypeScript + * doesn't mind us doing here. However, downstream you will notice that you have an id when the type + * does not indicate it. In that case just cast this temporarily if you're using the id. If you're not, + * you can ignore the id and just use the normal TypeScript with ReactJS. + * + * @param rule The rule to add an id to the threat matches. + * @returns rule The rule but with id added to the threat array and entries + */ +export const addIdToThreatMatchArray = (rule: Rule): Rule => { + if (rule.type === 'threat_match' && rule.threat_mapping != null) { + const threatMapWithId = rule.threat_mapping.map((mapping) => { + const newEntries = mapping.entries.map((entry) => addIdToItem(entry)); + return addIdToItem({ entries: newEntries }); + }); + return { ...rule, threat_mapping: threatMapWithId }; + } else { + return rule; + } +}; + +/** + * This removes an id from the threat match arrays as ReactJS prefers to have + * an id added to them for use as a stable id. Later if we decide to change the data + * model to have id's within the array then this code should be removed. If not, then + * this code should stay as an adapter for ReactJS. + * + * @param rule The rule to remove an id from the threat matches. + * @returns rule The rule but with id removed from the threat array and entries + */ +export const removeIdFromThreatMatchArray = ( + rule: CreateRulesSchema | UpdateRulesSchema +): CreateRulesSchema | UpdateRulesSchema => { + if (rule.type === 'threat_match' && rule.threat_mapping != null) { + const threatMapWithoutId = rule.threat_mapping.map((mapping) => { + const newEntries = mapping.entries.map((entry) => removeIdFromItem(entry)); + const newMapping = removeIdFromItem(mapping); + return { ...newMapping, entries: newEntries }; + }); + return { ...rule, threat_mapping: threatMapWithoutId }; + } else { + return rule; + } +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index 2bbd27994fc77..fe8e0fd8ceb97 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -11,6 +11,7 @@ import { CreateRulesSchema } from '../../../../../common/detection_engine/schema import { createRule } from './api'; import * as i18n from './translations'; +import { transformOutput } from './transforms'; interface CreateRuleReturn { isLoading: boolean; @@ -29,11 +30,11 @@ export const useCreateRule = (): ReturnCreateRule => { let isSubscribed = true; const abortCtrl = new AbortController(); setIsSaved(false); - async function saveRule() { + const saveRule = async () => { if (rule != null) { try { setIsLoading(true); - await createRule({ rule, signal: abortCtrl.signal }); + await createRule({ rule: transformOutput(rule), signal: abortCtrl.signal }); if (isSubscribed) { setIsSaved(true); } @@ -46,15 +47,14 @@ export const useCreateRule = (): ReturnCreateRule => { setIsLoading(false); } } - } + }; saveRule(); return () => { isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rule]); + }, [rule, dispatchToaster]); return [{ isLoading, isSaved }, setRule]; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index d83d4e0caa977..bdbe13af40151 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -262,8 +262,14 @@ export const usePrePackagedRules = ({ isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [canUserCRUD, hasIndexWrite, isAuthenticated, hasEncryptionKey, isSignalIndexExists]); + }, [ + canUserCRUD, + hasIndexWrite, + isAuthenticated, + hasEncryptionKey, + isSignalIndexExists, + dispatchToaster, + ]); const prePackagedRuleStatus = useMemo( () => diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx index 706c2645a4ddd..3b84558d344e7 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from 'react'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { fetchRuleById } from './api'; +import { transformInput } from './transforms'; import * as i18n from './translations'; import { Rule } from './types'; @@ -28,13 +29,15 @@ export const useRule = (id: string | undefined): ReturnRule => { let isSubscribed = true; const abortCtrl = new AbortController(); - async function fetchData(idToFetch: string) { + const fetchData = async (idToFetch: string) => { try { setLoading(true); - const ruleResponse = await fetchRuleById({ - id: idToFetch, - signal: abortCtrl.signal, - }); + const ruleResponse = transformInput( + await fetchRuleById({ + id: idToFetch, + signal: abortCtrl.signal, + }) + ); if (isSubscribed) { setRule(ruleResponse); } @@ -47,7 +50,7 @@ export const useRule = (id: string | undefined): ReturnRule => { if (isSubscribed) { setLoading(false); } - } + }; if (id != null) { fetchData(id); } @@ -55,8 +58,7 @@ export const useRule = (id: string | undefined): ReturnRule => { isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id]); + }, [id, dispatchToaster]); return [loading, rule]; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx index fbca46097dcd9..48bfe71b4722b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx @@ -6,12 +6,14 @@ import { useEffect, useCallback } from 'react'; +import { flow } from 'fp-ts/lib/function'; import { useAsync, withOptionalSignal } from '../../../../shared_imports'; import { useHttp } from '../../../../common/lib/kibana'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { pureFetchRuleById } from './api'; import { Rule } from './types'; import * as i18n from './translations'; +import { transformInput } from './transforms'; export interface UseRuleAsync { error: unknown; @@ -20,11 +22,15 @@ export interface UseRuleAsync { rule: Rule | null; } -const _fetchRule = withOptionalSignal(pureFetchRuleById); -const _useRuleAsync = () => useAsync(_fetchRule); +const _fetchRule = flow(withOptionalSignal(pureFetchRuleById), async (rule: Promise<Rule>) => + transformInput(await rule) +); + +/** This does not use "_useRuleAsyncInternal" as that would deactivate the useHooks linter rule, so instead it has the word "Internal" post-pended */ +const useRuleAsyncInternal = () => useAsync(_fetchRule); export const useRuleAsync = (ruleId: string): UseRuleAsync => { - const { start, loading, result, error } = _useRuleAsync(); + const { start, loading, result, error } = useRuleAsyncInternal(); const http = useHttp(); const { addError } = useAppToasts(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx index ddf50e9edae51..2bec8f9a2d0a2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx @@ -64,8 +64,7 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus = isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id]); + }, [id, dispatchToaster]); return [loading, ruleStatus, fetchRuleStatus.current]; }; @@ -122,8 +121,7 @@ export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rules]); + }, [rules, dispatchToaster]); return { loading, rulesStatuses }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx index 038f974e1394e..bab419813e1aa 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx @@ -26,7 +26,7 @@ export const useTags = (): ReturnTags => { let isSubscribed = true; const abortCtrl = new AbortController(); - async function fetchData() { + const fetchData = async () => { setLoading(true); try { const fetchTagsResult = await fetchTags({ @@ -44,7 +44,7 @@ export const useTags = (): ReturnTags => { if (isSubscribed) { setLoading(false); } - } + }; fetchData(); reFetchTags.current = fetchData; @@ -53,8 +53,7 @@ export const useTags = (): ReturnTags => { isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatchToaster]); return [loading, tags, reFetchTags.current]; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx index a437974e93ba3..729336b697e4d 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx @@ -9,6 +9,8 @@ import { useEffect, useState, Dispatch } from 'react'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { UpdateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; +import { transformOutput } from './transforms'; + import { updateRule } from './api'; import * as i18n from './translations'; @@ -29,11 +31,11 @@ export const useUpdateRule = (): ReturnUpdateRule => { let isSubscribed = true; const abortCtrl = new AbortController(); setIsSaved(false); - async function saveRule() { + const saveRule = async () => { if (rule != null) { try { setIsLoading(true); - await updateRule({ rule, signal: abortCtrl.signal }); + await updateRule({ rule: transformOutput(rule), signal: abortCtrl.signal }); if (isSubscribed) { setIsSaved(true); } @@ -46,15 +48,14 @@ export const useUpdateRule = (): ReturnUpdateRule => { setIsLoading(false); } } - } + }; saveRule(); return () => { isSubscribed = false; abortCtrl.abort(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rule]); + }, [rule, dispatchToaster]); return [{ isLoading, isSaved }, setRule]; }; From 05b7107ff2274987b4c37889813cd4e685eca184 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar <dario.gieselaar@elastic.co> Date: Sat, 30 Jan 2021 10:49:59 +0100 Subject: [PATCH 144/163] Add APM API tests dir to CODEOWNERS (#89573) --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3343544d57fad..9e31bd31b4037 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -66,6 +66,7 @@ # APM /x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui +/x-pack/test/apm_api_integration/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @elastic/kibana-core @vigneshshanmugam /packages/kbn-apm-config-loader/ @elastic/kibana-core @vigneshshanmugam @@ -80,6 +81,7 @@ /x-pack/plugins/apm/server/lib/rum_client @elastic/uptime /x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime /x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime +/x-pack/test/apm_api_integration/tests/csm/ @elastic/uptime # Beats /x-pack/plugins/beats_management/ @elastic/beats From 52f54030c356447f6896e603b60350be97389fd2 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" <devin.hurley@elastic.co> Date: Sat, 30 Jan 2021 08:25:45 -0500 Subject: [PATCH 145/163] [Security Solution] [Detections] rename gap column and delete "last lookback date" column from monitoring table (#89801) --- .../detection_engine/rules/all/columns.tsx | 27 ++++++++++--------- .../detection_engine/rules/translations.ts | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index 0d585b4463815..86f24594fc57e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -356,19 +356,20 @@ export const getMonitoringColumns = ( truncateText: true, width: '14%', }, - { - field: 'current_status.last_look_back_date', - name: i18n.COLUMN_LAST_LOOKBACK_DATE, - render: (value: RuleStatus['current_status']['last_look_back_date']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - <FormattedDate value={value} fieldName={'last look back date'} /> - ); - }, - truncateText: true, - width: '16%', - }, + // hiding this field until after 7.11 release + // { + // field: 'current_status.last_look_back_date', + // name: i18n.COLUMN_LAST_LOOKBACK_DATE, + // render: (value: RuleStatus['current_status']['last_look_back_date']) => { + // return value == null ? ( + // getEmptyTagValue() + // ) : ( + // <FormattedDate value={value} fieldName={'last look back date'} /> + // ); + // }, + // truncateText: true, + // width: '16%', + // }, { field: 'current_status.status_date', name: i18n.COLUMN_LAST_COMPLETE_RUN, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 2d993c7be08b0..f7066cd42e4c1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -353,7 +353,7 @@ export const COLUMN_QUERY_TIMES = i18n.translate( export const COLUMN_GAP = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.columns.gap', { - defaultMessage: 'Gap (if any)', + defaultMessage: 'Last Gap (if any)', } ); From 841ab704b8e50986730a32e68f9afc3ac28b92cd Mon Sep 17 00:00:00 2001 From: Liza Katz <lizka.k@gmail.com> Date: Sun, 31 Jan 2021 12:16:46 +0200 Subject: [PATCH 146/163] [Search Sessions] Improve search session errors (#88613) * Detect ESError correctly Fix bfetch error (was recognized as unknown error) Make sure handleSearchError always returns an error object. * fix tests and improve types * type * normalize search error response format for search and bsearch * type * Added es search exception examples * Normalize and validate errors thrown from oss es_search_strategy Validate abort * Added tests for search service error handling * Update msearch tests to test for errors * Moved bsearch route to routes folder Adjusted bsearch response format Added verification of error's root cause * Align painless error object * eslint * Add to seach interceptor tests * add json to tsconfig * docs * updated xpack search strategy tests * oops * license header * Add test for xpack painless error format * doc * Fix bsearch test potential flakiness * code review * fix * code review 2 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...lic.searchinterceptor.handlesearcherror.md | 4 +- ...public.searchtimeouterror._constructor_.md | 4 +- .../test_data/illegal_argument_exception.json | 14 ++ .../test_data/index_not_found_exception.json | 21 ++ .../test_data/json_e_o_f_exception.json | 14 ++ .../search/test_data/parsing_exception.json | 17 ++ .../resource_not_found_exception.json | 13 + .../search_phase_execution_exception.json | 52 ++++ .../test_data/x_content_parse_exception.json | 17 ++ src/plugins/data/public/public.api.md | 7 +- .../public/search/errors/es_error.test.tsx | 19 +- .../data/public/search/errors/es_error.tsx | 8 +- .../search/errors/painless_error.test.tsx | 42 ++++ .../public/search/errors/painless_error.tsx | 10 +- .../public/search/errors/timeout_error.tsx | 2 +- .../data/public/search/errors/types.ts | 72 +++--- .../data/public/search/errors/utils.ts | 16 +- .../public/search/search_interceptor.test.ts | 74 +++--- .../data/public/search/search_interceptor.ts | 23 +- .../es_search/es_search_strategy.test.ts | 161 ++++++++++-- .../search/es_search/es_search_strategy.ts | 31 ++- .../data/server/search/routes/bsearch.ts | 65 +++++ .../data/server/search/routes/call_msearch.ts | 36 +-- .../data/server/search/routes/msearch.test.ts | 58 ++++- .../data/server/search/routes/search.test.ts | 99 ++++++-- .../data/server/search/search_service.ts | 55 +---- src/plugins/data/tsconfig.json | 2 +- .../kibana_utils/common/errors/index.ts | 1 + .../kibana_utils/common/errors/types.ts | 12 + src/plugins/kibana_utils/server/index.ts | 2 +- .../server/report_server_error.ts | 29 ++- test/api_integration/apis/search/bsearch.ts | 172 +++++++++++++ test/api_integration/apis/search/index.ts | 1 + .../apis/search/painless_err_req.ts | 44 ++++ test/api_integration/apis/search/search.ts | 81 ++++++- .../apis/search/verify_error.ts | 27 +++ .../search_phase_execution_exception.json | 229 ++++++++++++++++++ .../public/search/search_interceptor.test.ts | 41 +++- .../server/search/es_search_strategy.test.ts | 101 ++++++++ .../server/search/es_search_strategy.ts | 79 ++++-- x-pack/plugins/data_enhanced/tsconfig.json | 3 +- .../api_integration/apis/search/search.ts | 36 ++- 42 files changed, 1499 insertions(+), 295 deletions(-) create mode 100644 src/plugins/data/common/search/test_data/illegal_argument_exception.json create mode 100644 src/plugins/data/common/search/test_data/index_not_found_exception.json create mode 100644 src/plugins/data/common/search/test_data/json_e_o_f_exception.json create mode 100644 src/plugins/data/common/search/test_data/parsing_exception.json create mode 100644 src/plugins/data/common/search/test_data/resource_not_found_exception.json create mode 100644 src/plugins/data/common/search/test_data/search_phase_execution_exception.json create mode 100644 src/plugins/data/common/search/test_data/x_content_parse_exception.json create mode 100644 src/plugins/data/public/search/errors/painless_error.test.tsx create mode 100644 src/plugins/data/server/search/routes/bsearch.ts create mode 100644 src/plugins/kibana_utils/common/errors/types.ts create mode 100644 test/api_integration/apis/search/bsearch.ts create mode 100644 test/api_integration/apis/search/painless_err_req.ts create mode 100644 test/api_integration/apis/search/verify_error.ts create mode 100644 x-pack/plugins/data_enhanced/common/search/test_data/search_phase_execution_exception.json diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md index b5ac4a4e53887..5f8966f0227ac 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md @@ -7,14 +7,14 @@ <b>Signature:</b> ```typescript -protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; +protected handleSearchError(e: KibanaServerError | AbortError, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| e | <code>any</code> | | +| e | <code>KibanaServerError | AbortError</code> | | | timeoutSignal | <code>AbortSignal</code> | | | options | <code>ISearchOptions</code> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md index 1c6370c7d0356..b4eecca665e82 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md @@ -9,13 +9,13 @@ Constructs a new instance of the `SearchTimeoutError` class <b>Signature:</b> ```typescript -constructor(err: Error, mode: TimeoutErrorMode); +constructor(err: Record<string, any>, mode: TimeoutErrorMode); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| err | <code>Error</code> | | +| err | <code>Record<string, any></code> | | | mode | <code>TimeoutErrorMode</code> | | diff --git a/src/plugins/data/common/search/test_data/illegal_argument_exception.json b/src/plugins/data/common/search/test_data/illegal_argument_exception.json new file mode 100644 index 0000000000000..ae48468abc209 --- /dev/null +++ b/src/plugins/data/common/search/test_data/illegal_argument_exception.json @@ -0,0 +1,14 @@ +{ + "error" : { + "root_cause" : [ + { + "type" : "illegal_argument_exception", + "reason" : "failed to parse setting [timeout] with value [1] as a time value: unit is missing or unrecognized" + } + ], + "type" : "illegal_argument_exception", + "reason" : "failed to parse setting [timeout] with value [1] as a time value: unit is missing or unrecognized" + }, + "status" : 400 + } + \ No newline at end of file diff --git a/src/plugins/data/common/search/test_data/index_not_found_exception.json b/src/plugins/data/common/search/test_data/index_not_found_exception.json new file mode 100644 index 0000000000000..dc892d95ae397 --- /dev/null +++ b/src/plugins/data/common/search/test_data/index_not_found_exception.json @@ -0,0 +1,21 @@ +{ + "error" : { + "root_cause" : [ + { + "type" : "index_not_found_exception", + "reason" : "no such index [poop]", + "resource.type" : "index_or_alias", + "resource.id" : "poop", + "index_uuid" : "_na_", + "index" : "poop" + } + ], + "type" : "index_not_found_exception", + "reason" : "no such index [poop]", + "resource.type" : "index_or_alias", + "resource.id" : "poop", + "index_uuid" : "_na_", + "index" : "poop" + }, + "status" : 404 +} diff --git a/src/plugins/data/common/search/test_data/json_e_o_f_exception.json b/src/plugins/data/common/search/test_data/json_e_o_f_exception.json new file mode 100644 index 0000000000000..88134e1c6ea03 --- /dev/null +++ b/src/plugins/data/common/search/test_data/json_e_o_f_exception.json @@ -0,0 +1,14 @@ +{ + "error" : { + "root_cause" : [ + { + "type" : "json_e_o_f_exception", + "reason" : "Unexpected end-of-input: expected close marker for Object (start marker at [Source: (org.elasticsearch.common.io.stream.InputStreamStreamInput); line: 1, column: 1])\n at [Source: (org.elasticsearch.common.io.stream.InputStreamStreamInput); line: 1, column: 2]" + } + ], + "type" : "json_e_o_f_exception", + "reason" : "Unexpected end-of-input: expected close marker for Object (start marker at [Source: (org.elasticsearch.common.io.stream.InputStreamStreamInput); line: 1, column: 1])\n at [Source: (org.elasticsearch.common.io.stream.InputStreamStreamInput); line: 1, column: 2]" + }, + "status" : 400 + } + \ No newline at end of file diff --git a/src/plugins/data/common/search/test_data/parsing_exception.json b/src/plugins/data/common/search/test_data/parsing_exception.json new file mode 100644 index 0000000000000..725a847aa0e3f --- /dev/null +++ b/src/plugins/data/common/search/test_data/parsing_exception.json @@ -0,0 +1,17 @@ +{ + "error" : { + "root_cause" : [ + { + "type" : "parsing_exception", + "reason" : "[terms] query does not support [ohno]", + "line" : 4, + "col" : 17 + } + ], + "type" : "parsing_exception", + "reason" : "[terms] query does not support [ohno]", + "line" : 4, + "col" : 17 + }, + "status" : 400 +} diff --git a/src/plugins/data/common/search/test_data/resource_not_found_exception.json b/src/plugins/data/common/search/test_data/resource_not_found_exception.json new file mode 100644 index 0000000000000..7f2a3b2e6e143 --- /dev/null +++ b/src/plugins/data/common/search/test_data/resource_not_found_exception.json @@ -0,0 +1,13 @@ +{ + "error" : { + "root_cause" : [ + { + "type" : "resource_not_found_exception", + "reason" : "FlZlSXp6dkd3UXdHZjhsalVtVHBnYkEdYjNIWDhDOTZRN3ExemdmVkx4RXNQQToxMjc2ODk=" + } + ], + "type" : "resource_not_found_exception", + "reason" : "FlZlSXp6dkd3UXdHZjhsalVtVHBnYkEdYjNIWDhDOTZRN3ExemdmVkx4RXNQQToxMjc2ODk=" + }, + "status" : 404 +} diff --git a/src/plugins/data/common/search/test_data/search_phase_execution_exception.json b/src/plugins/data/common/search/test_data/search_phase_execution_exception.json new file mode 100644 index 0000000000000..ff6879f2b8960 --- /dev/null +++ b/src/plugins/data/common/search/test_data/search_phase_execution_exception.json @@ -0,0 +1,52 @@ +{ + "error" : { + "root_cause" : [ + { + "type" : "script_exception", + "reason" : "compile error", + "script_stack" : [ + "invalid", + "^---- HERE" + ], + "script" : "invalid", + "lang" : "painless", + "position" : { + "offset" : 0, + "start" : 0, + "end" : 7 + } + } + ], + "type" : "search_phase_execution_exception", + "reason" : "all shards failed", + "phase" : "query", + "grouped" : true, + "failed_shards" : [ + { + "shard" : 0, + "index" : ".kibana_11", + "node" : "b3HX8C96Q7q1zgfVLxEsPA", + "reason" : { + "type" : "script_exception", + "reason" : "compile error", + "script_stack" : [ + "invalid", + "^---- HERE" + ], + "script" : "invalid", + "lang" : "painless", + "position" : { + "offset" : 0, + "start" : 0, + "end" : 7 + }, + "caused_by" : { + "type" : "illegal_argument_exception", + "reason" : "cannot resolve symbol [invalid]" + } + } + } + ] + }, + "status" : 400 +} diff --git a/src/plugins/data/common/search/test_data/x_content_parse_exception.json b/src/plugins/data/common/search/test_data/x_content_parse_exception.json new file mode 100644 index 0000000000000..cd6e1cb2c5977 --- /dev/null +++ b/src/plugins/data/common/search/test_data/x_content_parse_exception.json @@ -0,0 +1,17 @@ +{ + "error" : { + "root_cause" : [ + { + "type" : "x_content_parse_exception", + "reason" : "[5:13] [script] failed to parse object" + } + ], + "type" : "x_content_parse_exception", + "reason" : "[5:13] [script] failed to parse object", + "caused_by" : { + "type" : "json_parse_exception", + "reason" : "Unexpected character (''' (code 39)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\n at [Source: (org.elasticsearch.common.bytes.AbstractBytesReference$BytesReferenceStreamInput); line: 5, column: 24]" + } + }, + "status" : 400 +} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5b1462e5d506b..f533af2db9672 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2282,8 +2282,11 @@ export class SearchInterceptor { protected readonly deps: SearchInterceptorDeps; // (undocumented) protected getTimeoutMode(): TimeoutErrorMode; + // Warning: (ae-forgotten-export) The symbol "KibanaServerError" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "AbortError" needs to be exported by the entry point index.d.ts + // // (undocumented) - protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; + protected handleSearchError(e: KibanaServerError | AbortError, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; // @internal protected pendingCount$: BehaviorSubject<number>; // @internal (undocumented) @@ -2453,7 +2456,7 @@ export interface SearchSourceFields { // // @public export class SearchTimeoutError extends KbnError { - constructor(err: Error, mode: TimeoutErrorMode); + constructor(err: Record<string, any>, mode: TimeoutErrorMode); // (undocumented) getErrorMessage(application: ApplicationStart): JSX.Element; // (undocumented) diff --git a/src/plugins/data/public/search/errors/es_error.test.tsx b/src/plugins/data/public/search/errors/es_error.test.tsx index adb422c1d18e7..6a4cb9c494b4f 100644 --- a/src/plugins/data/public/search/errors/es_error.test.tsx +++ b/src/plugins/data/public/search/errors/es_error.test.tsx @@ -7,23 +7,22 @@ */ import { EsError } from './es_error'; -import { IEsError } from './types'; describe('EsError', () => { it('contains the same body as the wrapped error', () => { const error = { - body: { - attributes: { - error: { - type: 'top_level_exception_type', - reason: 'top-level reason', - }, + statusCode: 500, + message: 'nope', + attributes: { + error: { + type: 'top_level_exception_type', + reason: 'top-level reason', }, }, - } as IEsError; + } as any; const esError = new EsError(error); - expect(typeof esError.body).toEqual('object'); - expect(esError.body).toEqual(error.body); + expect(typeof esError.attributes).toEqual('object'); + expect(esError.attributes).toEqual(error.attributes); }); }); diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx index fff06d2e1bfb6..d241eecfd8d5d 100644 --- a/src/plugins/data/public/search/errors/es_error.tsx +++ b/src/plugins/data/public/search/errors/es_error.tsx @@ -11,19 +11,19 @@ import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { ApplicationStart } from 'kibana/public'; import { KbnError } from '../../../../kibana_utils/common'; import { IEsError } from './types'; -import { getRootCause, getTopLevelCause } from './utils'; +import { getRootCause } from './utils'; export class EsError extends KbnError { - readonly body: IEsError['body']; + readonly attributes: IEsError['attributes']; constructor(protected readonly err: IEsError) { super('EsError'); - this.body = err.body; + this.attributes = err.attributes; } public getErrorMessage(application: ApplicationStart) { const rootCause = getRootCause(this.err)?.reason; - const topLevelCause = getTopLevelCause(this.err)?.reason; + const topLevelCause = this.attributes?.reason; const cause = rootCause ?? topLevelCause; return ( diff --git a/src/plugins/data/public/search/errors/painless_error.test.tsx b/src/plugins/data/public/search/errors/painless_error.test.tsx new file mode 100644 index 0000000000000..929f25e234a60 --- /dev/null +++ b/src/plugins/data/public/search/errors/painless_error.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { coreMock } from '../../../../../core/public/mocks'; +const startMock = coreMock.createStart(); + +import { mount } from 'enzyme'; +import { PainlessError } from './painless_error'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import * as searchPhaseException from '../../../common/search/test_data/search_phase_execution_exception.json'; + +describe('PainlessError', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Should show reason and code', () => { + const e = new PainlessError({ + statusCode: 400, + message: 'search_phase_execution_exception', + attributes: searchPhaseException.error, + }); + const component = mount(e.getErrorMessage(startMock.application)); + + const scriptElem = findTestSubject(component, 'painlessScript').getDOMNode(); + + const failedShards = e.attributes?.failed_shards![0]; + const script = failedShards!.reason.script; + expect(scriptElem.textContent).toBe(`Error executing Painless script: '${script}'`); + + const stackTraceElem = findTestSubject(component, 'painlessStackTrace').getDOMNode(); + const stackTrace = failedShards!.reason.script_stack!.join('\n'); + expect(stackTraceElem.textContent).toBe(stackTrace); + + expect(component.find('EuiButton').length).toBe(1); + }); +}); diff --git a/src/plugins/data/public/search/errors/painless_error.tsx b/src/plugins/data/public/search/errors/painless_error.tsx index 8a4248e48185b..6d11f3a16b09e 100644 --- a/src/plugins/data/public/search/errors/painless_error.tsx +++ b/src/plugins/data/public/search/errors/painless_error.tsx @@ -33,10 +33,12 @@ export class PainlessError extends EsError { return ( <> - {i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', { - defaultMessage: "Error executing Painless script: '{script}'.", - values: { script: rootCause?.script }, - })} + <EuiText data-test-subj="painlessScript"> + {i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', { + defaultMessage: "Error executing Painless script: '{script}'", + values: { script: rootCause?.script }, + })} + </EuiText> <EuiSpacer size="s" /> <EuiSpacer size="s" /> {painlessStack ? ( diff --git a/src/plugins/data/public/search/errors/timeout_error.tsx b/src/plugins/data/public/search/errors/timeout_error.tsx index ee2703b888bf1..6b9ce1b422481 100644 --- a/src/plugins/data/public/search/errors/timeout_error.tsx +++ b/src/plugins/data/public/search/errors/timeout_error.tsx @@ -24,7 +24,7 @@ export enum TimeoutErrorMode { */ export class SearchTimeoutError extends KbnError { public mode: TimeoutErrorMode; - constructor(err: Error, mode: TimeoutErrorMode) { + constructor(err: Record<string, any>, mode: TimeoutErrorMode) { super(`Request timeout: ${JSON.stringify(err?.message)}`); this.mode = mode; } diff --git a/src/plugins/data/public/search/errors/types.ts b/src/plugins/data/public/search/errors/types.ts index d62cb311bf6a4..5806ef8676b9b 100644 --- a/src/plugins/data/public/search/errors/types.ts +++ b/src/plugins/data/public/search/errors/types.ts @@ -6,57 +6,47 @@ * Public License, v 1. */ +import { KibanaServerError } from '../../../../kibana_utils/common'; + export interface FailedShard { shard: number; index: string; node: string; - reason: { + reason: Reason; +} + +export interface Reason { + type: string; + reason: string; + script_stack?: string[]; + position?: { + offset: number; + start: number; + end: number; + }; + lang?: string; + script?: string; + caused_by?: { type: string; reason: string; - script_stack: string[]; - script: string; - lang: string; - position: { - offset: number; - start: number; - end: number; - }; - caused_by: { - type: string; - reason: string; - }; }; } -export interface IEsError { - body: { - statusCode: number; - error: string; - message: string; - attributes?: { - error?: { - root_cause?: [ - { - lang: string; - script: string; - } - ]; - type: string; - reason: string; - failed_shards: FailedShard[]; - caused_by: { - type: string; - reason: string; - phase: string; - grouped: boolean; - failed_shards: FailedShard[]; - script_stack: string[]; - }; - }; - }; - }; +export interface IEsErrorAttributes { + type: string; + reason: string; + root_cause?: Reason[]; + failed_shards?: FailedShard[]; } +export type IEsError = KibanaServerError<IEsErrorAttributes>; + +/** + * Checks if a given errors originated from Elasticsearch. + * Those params are assigned to the attributes property of an error. + * + * @param e + */ export function isEsError(e: any): e is IEsError { - return !!e.body?.attributes; + return !!e.attributes; } diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts index d140e713f9440..7d303543a0c57 100644 --- a/src/plugins/data/public/search/errors/utils.ts +++ b/src/plugins/data/public/search/errors/utils.ts @@ -6,19 +6,15 @@ * Public License, v 1. */ -import { IEsError } from './types'; +import { FailedShard } from './types'; +import { KibanaServerError } from '../../../../kibana_utils/common'; -export function getFailedShards(err: IEsError) { - const failedShards = - err.body?.attributes?.error?.failed_shards || - err.body?.attributes?.error?.caused_by?.failed_shards; +export function getFailedShards(err: KibanaServerError<any>): FailedShard | undefined { + const errorInfo = err.attributes; + const failedShards = errorInfo?.failed_shards || errorInfo?.caused_by?.failed_shards; return failedShards ? failedShards[0] : undefined; } -export function getTopLevelCause(err: IEsError) { - return err.body?.attributes?.error; -} - -export function getRootCause(err: IEsError) { +export function getRootCause(err: KibanaServerError) { return getFailedShards(err)?.reason; } diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index 5ae01eccdd920..bfd73951c31c4 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -12,12 +12,15 @@ import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../../kibana_utils/public'; -import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors'; +import { SearchTimeoutError, PainlessError, TimeoutErrorMode, EsError } from './errors'; import { searchServiceMock } from './mocks'; import { ISearchStart, ISessionService } from '.'; import { bfetchPluginMock } from '../../../bfetch/public/mocks'; import { BfetchPublicSetup } from 'src/plugins/bfetch/public'; +import * as searchPhaseException from '../../common/search/test_data/search_phase_execution_exception.json'; +import * as resourceNotFoundException from '../../common/search/test_data/resource_not_found_exception.json'; + let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys<CoreSetup>; let bfetchSetup: jest.Mocked<BfetchPublicSetup>; @@ -64,15 +67,9 @@ describe('SearchInterceptor', () => { test('Renders a PainlessError', async () => { searchInterceptor.showError( new PainlessError({ - body: { - attributes: { - error: { - failed_shards: { - reason: 'bananas', - }, - }, - }, - } as any, + statusCode: 400, + message: 'search_phase_execution_exception', + attributes: searchPhaseException.error, }) ); expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); @@ -161,10 +158,8 @@ describe('SearchInterceptor', () => { describe('Should handle Timeout errors', () => { test('Should throw SearchTimeoutError on server timeout AND show toast', async () => { const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, + statusCode: 500, + message: 'Request timed out', }; fetchMock.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { @@ -177,10 +172,8 @@ describe('SearchInterceptor', () => { test('Timeout error should show multiple times if not in a session', async () => { const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, + statusCode: 500, + message: 'Request timed out', }; fetchMock.mockRejectedValue(mockResponse); const mockRequest: IEsSearchRequest = { @@ -198,10 +191,8 @@ describe('SearchInterceptor', () => { test('Timeout error should show once per each session', async () => { const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, + statusCode: 500, + message: 'Request timed out', }; fetchMock.mockRejectedValue(mockResponse); const mockRequest: IEsSearchRequest = { @@ -219,10 +210,8 @@ describe('SearchInterceptor', () => { test('Timeout error should show once in a single session', async () => { const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, + statusCode: 500, + message: 'Request timed out', }; fetchMock.mockRejectedValue(mockResponse); const mockRequest: IEsSearchRequest = { @@ -240,22 +229,9 @@ describe('SearchInterceptor', () => { test('Should throw Painless error on server error with OSS format', async () => { const mockResponse: any = { - result: 500, - body: { - attributes: { - error: { - failed_shards: [ - { - reason: { - lang: 'painless', - script_stack: ['a', 'b'], - reason: 'banana', - }, - }, - ], - }, - }, - }, + statusCode: 400, + message: 'search_phase_execution_exception', + attributes: searchPhaseException.error, }; fetchMock.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { @@ -265,6 +241,20 @@ describe('SearchInterceptor', () => { await expect(response.toPromise()).rejects.toThrow(PainlessError); }); + test('Should throw ES error on ES server error', async () => { + const mockResponse: any = { + statusCode: 400, + message: 'resource_not_found_exception', + attributes: resourceNotFoundException.error, + }; + fetchMock.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow(EsError); + }); + test('Observable should fail if user aborts (test merged signal)', async () => { const abortController = new AbortController(); fetchMock.mockImplementationOnce((options: any) => { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index f6ca9ef1a993d..6dfc8faea769e 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { get, memoize } from 'lodash'; +import { memoize } from 'lodash'; import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { PublicMethodsOf } from '@kbn/utility-types'; @@ -25,7 +25,11 @@ import { getHttpError, } from './errors'; import { toMountPoint } from '../../../kibana_react/public'; -import { AbortError, getCombinedAbortSignal } from '../../../kibana_utils/public'; +import { + AbortError, + getCombinedAbortSignal, + KibanaServerError, +} from '../../../kibana_utils/public'; import { ISessionService } from './session'; export interface SearchInterceptorDeps { @@ -87,8 +91,12 @@ export class SearchInterceptor { * @returns `Error` a search service specific error or the original error, if a specific error can't be recognized. * @internal */ - protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error { - if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') { + protected handleSearchError( + e: KibanaServerError | AbortError, + timeoutSignal: AbortSignal, + options?: ISearchOptions + ): Error { + if (timeoutSignal.aborted || e.message === 'Request timed out') { // Handle a client or a server side timeout const err = new SearchTimeoutError(e, this.getTimeoutMode()); @@ -96,7 +104,7 @@ export class SearchInterceptor { // The timeout error is shown any time a request times out, or once per session, if the request is part of a session. this.showTimeoutError(err, options?.sessionId); return err; - } else if (options?.abortSignal?.aborted) { + } else if (e instanceof AbortError) { // In the case an application initiated abort, throw the existing AbortError. return e; } else if (isEsError(e)) { @@ -106,12 +114,13 @@ export class SearchInterceptor { return new EsError(e); } } else { - return e; + return e instanceof Error ? e : new Error(e.message); } } /** * @internal + * @throws `AbortError` | `ErrorLike` */ protected runSearch( request: IKibanaSearchRequest, @@ -234,7 +243,7 @@ export class SearchInterceptor { }); this.pendingCount$.next(this.pendingCount$.getValue() + 1); return from(this.runSearch(request, { ...options, abortSignal: combinedSignal })).pipe( - catchError((e: Error) => { + catchError((e: Error | AbortError) => { return throwError(this.handleSearchError(e, timeoutSignal, options)); }), finalize(() => { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 8e66729825e39..eeef46381732e 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -6,37 +6,56 @@ * Public License, v 1. */ +import { + elasticsearchClientMock, + MockedTransportRequestPromise, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../core/server/elasticsearch/client/mocks'; import { pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; import { esSearchStrategyProvider } from './es_search_strategy'; import { SearchStrategyDependencies } from '../types'; +import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json'; +import { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { KbnServerError } from '../../../../kibana_utils/server'; + describe('ES search strategy', () => { + const successBody = { + _shards: { + total: 10, + failed: 1, + skipped: 2, + successful: 7, + }, + }; + let mockedApiCaller: MockedTransportRequestPromise<any>; + let mockApiCaller: jest.Mock<() => MockedTransportRequestPromise<any>>; const mockLogger: any = { debug: () => {}, }; - const mockApiCaller = jest.fn().mockResolvedValue({ - body: { - _shards: { - total: 10, - failed: 1, - skipped: 2, - successful: 7, - }, - }, - }); - const mockDeps = ({ - uiSettingsClient: { - get: () => {}, - }, - esClient: { asCurrentUser: { search: mockApiCaller } }, - } as unknown) as SearchStrategyDependencies; + function getMockedDeps(err?: Record<string, any>) { + mockApiCaller = jest.fn().mockImplementation(() => { + if (err) { + mockedApiCaller = elasticsearchClientMock.createErrorTransportRequestPromise(err); + } else { + mockedApiCaller = elasticsearchClientMock.createSuccessTransportRequestPromise( + successBody, + { statusCode: 200 } + ); + } + return mockedApiCaller; + }); - const mockConfig$ = pluginInitializerContextConfigMock<any>({}).legacy.globalConfig$; + return ({ + uiSettingsClient: { + get: () => {}, + }, + esClient: { asCurrentUser: { search: mockApiCaller } }, + } as unknown) as SearchStrategyDependencies; + } - beforeEach(() => { - mockApiCaller.mockClear(); - }); + const mockConfig$ = pluginInitializerContextConfigMock<any>({}).legacy.globalConfig$; it('returns a strategy with `search`', async () => { const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); @@ -48,7 +67,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*' }; await esSearchStrategyProvider(mockConfig$, mockLogger) - .search({ params }, {}, mockDeps) + .search({ params }, {}, getMockedDeps()) .subscribe(() => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toEqual({ @@ -64,7 +83,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; await esSearchStrategyProvider(mockConfig$, mockLogger) - .search({ params }, {}, mockDeps) + .search({ params }, {}, getMockedDeps()) .subscribe(() => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toEqual({ @@ -82,13 +101,109 @@ describe('ES search strategy', () => { params: { index: 'logstash-*' }, }, {}, - mockDeps + getMockedDeps() ) .subscribe((data) => { expect(data.isRunning).toBe(false); expect(data.isPartial).toBe(false); expect(data).toHaveProperty('loaded'); expect(data).toHaveProperty('rawResponse'); + expect(mockedApiCaller.abort).not.toBeCalled(); done(); })); + + it('can be aborted', async () => { + const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; + + const abortController = new AbortController(); + abortController.abort(); + + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, { abortSignal: abortController.signal }, getMockedDeps()) + .toPromise(); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toEqual({ + ...params, + track_total_hits: true, + }); + expect(mockedApiCaller.abort).toBeCalled(); + }); + + it('throws normalized error if ResponseError is thrown', async (done) => { + const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; + const errResponse = new ResponseError({ + body: indexNotFoundException, + statusCode: 404, + headers: {}, + warnings: [], + meta: {} as any, + }); + + try { + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, getMockedDeps(errResponse)) + .toPromise(); + } catch (e) { + expect(mockApiCaller).toBeCalled(); + expect(e).toBeInstanceOf(KbnServerError); + expect(e.statusCode).toBe(404); + expect(e.message).toBe(errResponse.message); + expect(e.errBody).toBe(indexNotFoundException); + done(); + } + }); + + it('throws normalized error if ElasticsearchClientError is thrown', async (done) => { + const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; + const errResponse = new ElasticsearchClientError('This is a general ESClient error'); + + try { + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, getMockedDeps(errResponse)) + .toPromise(); + } catch (e) { + expect(mockApiCaller).toBeCalled(); + expect(e).toBeInstanceOf(KbnServerError); + expect(e.statusCode).toBe(500); + expect(e.message).toBe(errResponse.message); + expect(e.errBody).toBe(undefined); + done(); + } + }); + + it('throws normalized error if ESClient throws unknown error', async (done) => { + const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; + const errResponse = new Error('ESClient error'); + + try { + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, getMockedDeps(errResponse)) + .toPromise(); + } catch (e) { + expect(mockApiCaller).toBeCalled(); + expect(e).toBeInstanceOf(KbnServerError); + expect(e.statusCode).toBe(500); + expect(e.message).toBe(errResponse.message); + expect(e.errBody).toBe(undefined); + done(); + } + }); + + it('throws KbnServerError for unknown index type', async (done) => { + const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; + + try { + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ indexType: 'banana', params }, {}, getMockedDeps()) + .toPromise(); + } catch (e) { + expect(mockApiCaller).not.toBeCalled(); + expect(e).toBeInstanceOf(KbnServerError); + expect(e.message).toBe('Unsupported index pattern type banana'); + expect(e.statusCode).toBe(400); + expect(e.errBody).toBe(undefined); + done(); + } + }); }); 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 a11bbe11f3f95..c176a50627b92 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 @@ -15,13 +15,20 @@ import type { SearchUsage } from '../collectors'; import { getDefaultSearchParams, getShardTimeout, shimAbortSignal } from './request_utils'; import { toKibanaSearchResponse } from './response_utils'; import { searchUsageObserver } from '../collectors/usage'; -import { KbnServerError } from '../../../../kibana_utils/server'; +import { getKbnServerError, KbnServerError } from '../../../../kibana_utils/server'; export const esSearchStrategyProvider = ( config$: Observable<SharedGlobalConfig>, logger: Logger, usage?: SearchUsage ): ISearchStrategy => ({ + /** + * @param request + * @param options + * @param deps + * @throws `KbnServerError` + * @returns `Observable<IEsSearchResponse<any>>` + */ search: (request, { abortSignal }, { esClient, uiSettingsClient }) => { // Only default index pattern type is supported here. // See data_enhanced for other type support. @@ -30,15 +37,19 @@ export const esSearchStrategyProvider = ( } const search = async () => { - const config = await config$.pipe(first()).toPromise(); - const params = { - ...(await getDefaultSearchParams(uiSettingsClient)), - ...getShardTimeout(config), - ...request.params, - }; - const promise = esClient.asCurrentUser.search<SearchResponse<unknown>>(params); - const { body } = await shimAbortSignal(promise, abortSignal); - return toKibanaSearchResponse(body); + try { + const config = await config$.pipe(first()).toPromise(); + const params = { + ...(await getDefaultSearchParams(uiSettingsClient)), + ...getShardTimeout(config), + ...request.params, + }; + const promise = esClient.asCurrentUser.search<SearchResponse<unknown>>(params); + const { body } = await shimAbortSignal(promise, abortSignal); + return toKibanaSearchResponse(body); + } catch (e) { + throw getKbnServerError(e); + } }; return from(search()).pipe(tap(searchUsageObserver(logger, usage))); diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts new file mode 100644 index 0000000000000..e30b7bdaa8402 --- /dev/null +++ b/src/plugins/data/server/search/routes/bsearch.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { catchError, first, map } from 'rxjs/operators'; +import { CoreStart, KibanaRequest } from 'src/core/server'; +import { BfetchServerSetup } from 'src/plugins/bfetch/server'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchClient, + ISearchOptions, +} from '../../../common/search'; +import { shimHitsTotal } from './shim_hits_total'; + +type GetScopedProider = (coreStart: CoreStart) => (request: KibanaRequest) => ISearchClient; + +export function registerBsearchRoute( + bfetch: BfetchServerSetup, + coreStartPromise: Promise<[CoreStart, {}, {}]>, + getScopedProvider: GetScopedProider +): void { + bfetch.addBatchProcessingRoute< + { request: IKibanaSearchRequest; options?: ISearchOptions }, + IKibanaSearchResponse + >('/internal/bsearch', (request) => { + return { + /** + * @param requestOptions + * @throws `KibanaServerError` + */ + onBatchItem: async ({ request: requestData, options }) => { + const coreStart = await coreStartPromise; + const search = getScopedProvider(coreStart[0])(request); + return search + .search(requestData, options) + .pipe( + first(), + map((response) => { + return { + ...response, + ...{ + rawResponse: shimHitsTotal(response.rawResponse), + }, + }; + }), + catchError((err) => { + // Re-throw as object, to get attributes passed to the client + // eslint-disable-next-line no-throw-literal + throw { + message: err.message, + statusCode: err.statusCode, + attributes: err.errBody?.error, + }; + }) + ) + .toPromise(); + }, + }; + }); +} diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts index 6578774f65a3c..fc30e2f29c3ef 100644 --- a/src/plugins/data/server/search/routes/call_msearch.ts +++ b/src/plugins/data/server/search/routes/call_msearch.ts @@ -8,12 +8,12 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { ApiResponse } from '@elastic/elasticsearch'; import { SearchResponse } from 'elasticsearch'; import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src/core/server'; import type { MsearchRequestBody, MsearchResponse } from '../../../common/search/search_source'; import { shimHitsTotal } from './shim_hits_total'; +import { getKbnServerError } from '../../../../kibana_utils/server'; import { getShardTimeout, getDefaultSearchParams, shimAbortSignal } from '..'; /** @internal */ @@ -48,6 +48,9 @@ interface CallMsearchDependencies { * @internal */ export function getCallMsearch(dependencies: CallMsearchDependencies) { + /** + * @throws KbnServerError + */ return async (params: { body: MsearchRequestBody; signal?: AbortSignal; @@ -61,28 +64,29 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) { // trackTotalHits is not supported by msearch const { track_total_hits: _, ...defaultParams } = await getDefaultSearchParams(uiSettings); - const body = convertRequestBody(params.body, timeout); - - const promise = shimAbortSignal( - esClient.asCurrentUser.msearch( + try { + const promise = esClient.asCurrentUser.msearch( { - body, + body: convertRequestBody(params.body, timeout), }, { querystring: defaultParams, } - ), - params.signal - ); - const response = (await promise) as ApiResponse<{ responses: Array<SearchResponse<any>> }>; + ); + const response = await shimAbortSignal(promise, params.signal); - return { - body: { - ...response, + return { body: { - responses: response.body.responses?.map((r: SearchResponse<any>) => shimHitsTotal(r)), + ...response, + body: { + responses: response.body.responses?.map((r: SearchResponse<unknown>) => + shimHitsTotal(r) + ), + }, }, - }, - }; + }; + } catch (e) { + throw getKbnServerError(e); + } }; } diff --git a/src/plugins/data/server/search/routes/msearch.test.ts b/src/plugins/data/server/search/routes/msearch.test.ts index 02f200d5435dd..a847931a49123 100644 --- a/src/plugins/data/server/search/routes/msearch.test.ts +++ b/src/plugins/data/server/search/routes/msearch.test.ts @@ -24,6 +24,8 @@ import { convertRequestBody } from './call_msearch'; import { registerMsearchRoute } from './msearch'; import { DataPluginStart } from '../../plugin'; import { dataPluginMock } from '../../mocks'; +import * as jsonEofException from '../../../common/search/test_data/json_e_o_f_exception.json'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; describe('msearch route', () => { let mockDataStart: MockedKeys<DataPluginStart>; @@ -76,15 +78,52 @@ describe('msearch route', () => { }); }); - it('handler throws an error if the search throws an error', async () => { - const response = { - message: 'oh no', - body: { - error: 'oops', + it('handler returns an error response if the search throws an error', async () => { + const rejectedValue = Promise.reject( + new ResponseError({ + body: jsonEofException, + statusCode: 400, + meta: {} as any, + headers: [], + warnings: [], + }) + ); + const mockClient = { + msearch: jest.fn().mockReturnValue(rejectedValue), + }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + uiSettings: { client: { get: jest.fn() } }, }, }; + const mockBody = { searches: [{ header: {}, body: {} }] }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerMsearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ }); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.msearch).toBeCalledTimes(1); + expect(mockResponse.customError).toBeCalled(); + + const error: any = mockResponse.customError.mock.calls[0][0]; + expect(error.statusCode).toBe(400); + expect(error.body.message).toBe('json_e_o_f_exception'); + expect(error.body.attributes).toBe(jsonEofException.error); + }); + + it('handler returns an error response if the search throws a general error', async () => { + const rejectedValue = Promise.reject(new Error('What happened?')); const mockClient = { - msearch: jest.fn().mockReturnValue(Promise.reject(response)), + msearch: jest.fn().mockReturnValue(rejectedValue), }; const mockContext = { core: { @@ -106,11 +145,12 @@ describe('msearch route', () => { const handler = mockRouter.post.mock.calls[0][1]; await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); - expect(mockClient.msearch).toBeCalled(); + expect(mockClient.msearch).toBeCalledTimes(1); expect(mockResponse.customError).toBeCalled(); const error: any = mockResponse.customError.mock.calls[0][0]; - expect(error.body.message).toBe('oh no'); - expect(error.body.attributes.error).toBe('oops'); + expect(error.statusCode).toBe(500); + expect(error.body.message).toBe('What happened?'); + expect(error.body.attributes).toBe(undefined); }); }); diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts index f47a42cf9d82b..2cde6d19e4c18 100644 --- a/src/plugins/data/server/search/routes/search.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -12,11 +12,27 @@ import { CoreSetup, RequestHandlerContext } from 'src/core/server'; import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { registerSearchRoute } from './search'; import { DataPluginStart } from '../../plugin'; +import * as searchPhaseException from '../../../common/search/test_data/search_phase_execution_exception.json'; +import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json'; +import { KbnServerError } from '../../../../kibana_utils/server'; describe('Search service', () => { let mockCoreSetup: MockedKeys<CoreSetup<{}, DataPluginStart>>; + function mockEsError(message: string, statusCode: number, attributes?: Record<string, any>) { + return new KbnServerError(message, statusCode, attributes); + } + + async function runMockSearch(mockContext: any, mockRequest: any, mockResponse: any) { + registerSearchRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + } + beforeEach(() => { + jest.clearAllMocks(); mockCoreSetup = coreMock.createSetup(); }); @@ -54,11 +70,7 @@ describe('Search service', () => { }); const mockResponse = httpServerMock.createResponseFactory(); - registerSearchRoute(mockCoreSetup.http.createRouter()); - - const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; - const handler = mockRouter.post.mock.calls[0][1]; - await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + await runMockSearch(mockContext, mockRequest, mockResponse); expect(mockContext.search.search).toBeCalled(); expect(mockContext.search.search.mock.calls[0][0]).toStrictEqual(mockBody); @@ -68,14 +80,9 @@ describe('Search service', () => { }); }); - it('handler throws an error if the search throws an error', async () => { + it('handler returns an error response if the search throws a painless error', async () => { const rejectedValue = from( - Promise.reject({ - message: 'oh no', - body: { - error: 'oops', - }, - }) + Promise.reject(mockEsError('search_phase_execution_exception', 400, searchPhaseException)) ); const mockContext = { @@ -84,25 +91,69 @@ describe('Search service', () => { }, }; - const mockBody = { id: undefined, params: {} }; - const mockParams = { strategy: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ - body: mockBody, - params: mockParams, + body: { id: undefined, params: {} }, + params: { strategy: 'foo' }, }); const mockResponse = httpServerMock.createResponseFactory(); - registerSearchRoute(mockCoreSetup.http.createRouter()); + await runMockSearch(mockContext, mockRequest, mockResponse); - const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; - const handler = mockRouter.post.mock.calls[0][1]; - await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + // verify error + expect(mockResponse.customError).toBeCalled(); + const error: any = mockResponse.customError.mock.calls[0][0]; + expect(error.statusCode).toBe(400); + expect(error.body.message).toBe('search_phase_execution_exception'); + expect(error.body.attributes).toBe(searchPhaseException.error); + }); + + it('handler returns an error response if the search throws an index not found error', async () => { + const rejectedValue = from( + Promise.reject(mockEsError('index_not_found_exception', 404, indexNotFoundException)) + ); + + const mockContext = { + search: { + search: jest.fn().mockReturnValue(rejectedValue), + }, + }; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { id: undefined, params: {} }, + params: { strategy: 'foo' }, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + await runMockSearch(mockContext, mockRequest, mockResponse); + + expect(mockResponse.customError).toBeCalled(); + const error: any = mockResponse.customError.mock.calls[0][0]; + expect(error.statusCode).toBe(404); + expect(error.body.message).toBe('index_not_found_exception'); + expect(error.body.attributes).toBe(indexNotFoundException.error); + }); + + it('handler returns an error response if the search throws a general error', async () => { + const rejectedValue = from(Promise.reject(new Error('This is odd'))); + + const mockContext = { + search: { + search: jest.fn().mockReturnValue(rejectedValue), + }, + }; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { id: undefined, params: {} }, + params: { strategy: 'foo' }, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + await runMockSearch(mockContext, mockRequest, mockResponse); - expect(mockContext.search.search).toBeCalled(); - expect(mockContext.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.customError).toBeCalled(); const error: any = mockResponse.customError.mock.calls[0][0]; - expect(error.body.message).toBe('oh no'); - expect(error.body.attributes.error).toBe('oops'); + expect(error.statusCode).toBe(500); + expect(error.body.message).toBe('This is odd'); + expect(error.body.attributes).toBe(undefined); }); }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index f1a6fc09ee21f..63593bbe84a08 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { pick } from 'lodash'; import { CoreSetup, @@ -18,7 +18,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; -import { catchError, first, map } from 'rxjs/operators'; +import { first } from 'rxjs/operators'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import type { @@ -64,6 +64,7 @@ import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; import { SessionService, IScopedSessionService, ISessionService } from './session'; import { KbnServerError } from '../../../kibana_utils/server'; +import { registerBsearchRoute } from './routes/bsearch'; type StrategyMap = Record<string, ISearchStrategy<any, any>>; @@ -137,43 +138,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> { ) ); - bfetch.addBatchProcessingRoute< - { request: IKibanaSearchResponse; options?: ISearchOptions }, - any - >('/internal/bsearch', (request) => { - const search = this.asScopedProvider(this.coreStart!)(request); - - return { - onBatchItem: async ({ request: requestData, options }) => { - return search - .search(requestData, options) - .pipe( - first(), - map((response) => { - return { - ...response, - ...{ - rawResponse: shimHitsTotal(response.rawResponse), - }, - }; - }), - catchError((err) => { - // eslint-disable-next-line no-throw-literal - throw { - statusCode: err.statusCode || 500, - body: { - message: err.message, - attributes: { - error: err.body?.error || err.message, - }, - }, - }; - }) - ) - .toPromise(); - }, - }; - }); + registerBsearchRoute(bfetch, core.getStartServices(), this.asScopedProvider); core.savedObjects.registerType(searchTelemetry); if (usageCollection) { @@ -285,10 +250,14 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> { options: ISearchOptions, deps: SearchStrategyDependencies ) => { - const strategy = this.getSearchStrategy<SearchStrategyRequest, SearchStrategyResponse>( - options.strategy - ); - return session.search(strategy, request, options, deps); + try { + const strategy = this.getSearchStrategy<SearchStrategyRequest, SearchStrategyResponse>( + options.strategy + ); + return session.search(strategy, request, options, deps); + } catch (e) { + return throwError(e); + } }; private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index 81bcb3b02e100..21560b1328840 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -7,7 +7,7 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts"], + "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts", "common/**/*.json"], "references": [ { "path": "../../core/tsconfig.json" }, { "path": "../bfetch/tsconfig.json" }, diff --git a/src/plugins/kibana_utils/common/errors/index.ts b/src/plugins/kibana_utils/common/errors/index.ts index 354cf1d504b28..f859e0728269a 100644 --- a/src/plugins/kibana_utils/common/errors/index.ts +++ b/src/plugins/kibana_utils/common/errors/index.ts @@ -7,3 +7,4 @@ */ export * from './errors'; +export * from './types'; diff --git a/src/plugins/kibana_utils/common/errors/types.ts b/src/plugins/kibana_utils/common/errors/types.ts new file mode 100644 index 0000000000000..89e83586dc115 --- /dev/null +++ b/src/plugins/kibana_utils/common/errors/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ +export interface KibanaServerError<T = unknown> { + statusCode: number; + message: string; + attributes?: T; +} diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts index f95ffe5c3d7b6..821118ea4640d 100644 --- a/src/plugins/kibana_utils/server/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -18,4 +18,4 @@ export { url, } from '../common'; -export { KbnServerError, reportServerError } from './report_server_error'; +export { KbnServerError, reportServerError, getKbnServerError } from './report_server_error'; diff --git a/src/plugins/kibana_utils/server/report_server_error.ts b/src/plugins/kibana_utils/server/report_server_error.ts index 664f34ca7ad51..01e80cfc7184d 100644 --- a/src/plugins/kibana_utils/server/report_server_error.ts +++ b/src/plugins/kibana_utils/server/report_server_error.ts @@ -6,23 +6,42 @@ * Public License, v 1. */ +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { KibanaResponseFactory } from 'kibana/server'; import { KbnError } from '../common'; export class KbnServerError extends KbnError { - constructor(message: string, public readonly statusCode: number) { + public errBody?: Record<string, any>; + constructor(message: string, public readonly statusCode: number, errBody?: Record<string, any>) { super(message); + this.errBody = errBody; } } -export function reportServerError(res: KibanaResponseFactory, err: any) { +/** + * Formats any error thrown into a standardized `KbnServerError`. + * @param e `Error` or `ElasticsearchClientError` + * @returns `KbnServerError` + */ +export function getKbnServerError(e: Error) { + return new KbnServerError( + e.message ?? 'Unknown error', + e instanceof ResponseError ? e.statusCode : 500, + e instanceof ResponseError ? e.body : undefined + ); +} + +/** + * + * @param res Formats a `KbnServerError` into a server error response + * @param err + */ +export function reportServerError(res: KibanaResponseFactory, err: KbnServerError) { return res.customError({ statusCode: err.statusCode ?? 500, body: { message: err.message, - attributes: { - error: err.body?.error || err.message, - }, + attributes: err.errBody?.error, }, }); } diff --git a/test/api_integration/apis/search/bsearch.ts b/test/api_integration/apis/search/bsearch.ts new file mode 100644 index 0000000000000..504680d28bf83 --- /dev/null +++ b/test/api_integration/apis/search/bsearch.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import expect from '@kbn/expect'; +import request from 'superagent'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { painlessErrReq } from './painless_err_req'; +import { verifyErrorResponse } from './verify_error'; + +function parseBfetchResponse(resp: request.Response): Array<Record<string, any>> { + return resp.text + .trim() + .split('\n') + .map((item) => JSON.parse(item)); +} + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('bsearch', () => { + describe('post', () => { + it('should return 200 a single response', async () => { + const resp = await supertest.post(`/internal/bsearch`).send({ + batch: [ + { + request: { + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }, + }, + ], + }); + + const jsonBody = JSON.parse(resp.text); + + expect(resp.status).to.be(200); + expect(jsonBody.id).to.be(0); + expect(jsonBody.result.isPartial).to.be(false); + expect(jsonBody.result.isRunning).to.be(false); + expect(jsonBody.result).to.have.property('rawResponse'); + }); + + it('should return a batch of successful resposes', async () => { + const resp = await supertest.post(`/internal/bsearch`).send({ + batch: [ + { + request: { + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }, + }, + { + request: { + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }, + }, + ], + }); + + expect(resp.status).to.be(200); + const parsedResponse = parseBfetchResponse(resp); + expect(parsedResponse).to.have.length(2); + parsedResponse.forEach((responseJson) => { + expect(responseJson.result.isPartial).to.be(false); + expect(responseJson.result.isRunning).to.be(false); + expect(responseJson.result).to.have.property('rawResponse'); + }); + }); + + it('should return error for not found strategy', async () => { + const resp = await supertest.post(`/internal/bsearch`).send({ + batch: [ + { + request: { + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'wtf', + }, + }, + ], + }); + + expect(resp.status).to.be(200); + parseBfetchResponse(resp).forEach((responseJson, i) => { + expect(responseJson.id).to.be(i); + verifyErrorResponse(responseJson.error, 404, 'Search strategy wtf not found'); + }); + }); + + it('should return 400 when index type is provided in OSS', async () => { + const resp = await supertest.post(`/internal/bsearch`).send({ + batch: [ + { + request: { + indexType: 'baad', + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }, + }, + ], + }); + + expect(resp.status).to.be(200); + parseBfetchResponse(resp).forEach((responseJson, i) => { + expect(responseJson.id).to.be(i); + verifyErrorResponse(responseJson.error, 400, 'Unsupported index pattern type baad'); + }); + }); + + describe('painless', () => { + before(async () => { + await esArchiver.loadIfNeeded( + '../../../functional/fixtures/es_archiver/logstash_functional' + ); + }); + + after(async () => { + await esArchiver.unload('../../../functional/fixtures/es_archiver/logstash_functional'); + }); + it('should return 400 for Painless error', async () => { + const resp = await supertest.post(`/internal/bsearch`).send({ + batch: [ + { + request: painlessErrReq, + }, + ], + }); + + expect(resp.status).to.be(200); + parseBfetchResponse(resp).forEach((responseJson, i) => { + expect(responseJson.id).to.be(i); + verifyErrorResponse(responseJson.error, 400, 'search_phase_execution_exception', true); + }); + }); + }); + }); + }); +} diff --git a/test/api_integration/apis/search/index.ts b/test/api_integration/apis/search/index.ts index 2f21825d6902f..6e90bf0f22c51 100644 --- a/test/api_integration/apis/search/index.ts +++ b/test/api_integration/apis/search/index.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('search', () => { loadTestFile(require.resolve('./search')); + loadTestFile(require.resolve('./bsearch')); loadTestFile(require.resolve('./msearch')); }); } diff --git a/test/api_integration/apis/search/painless_err_req.ts b/test/api_integration/apis/search/painless_err_req.ts new file mode 100644 index 0000000000000..6fbf6565d7a9e --- /dev/null +++ b/test/api_integration/apis/search/painless_err_req.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export const painlessErrReq = { + params: { + index: 'log*', + body: { + size: 500, + fields: ['*'], + script_fields: { + invalid_scripted_field: { + script: { + source: 'invalid', + lang: 'painless', + }, + }, + }, + stored_fields: ['*'], + query: { + bool: { + filter: [ + { + match_all: {}, + }, + { + range: { + '@timestamp': { + gte: '2015-01-19T12:27:55.047Z', + lte: '2021-01-19T12:27:55.047Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + }, +}; diff --git a/test/api_integration/apis/search/search.ts b/test/api_integration/apis/search/search.ts index fc13189a40753..155705f81fa8a 100644 --- a/test/api_integration/apis/search/search.ts +++ b/test/api_integration/apis/search/search.ts @@ -8,11 +8,21 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { painlessErrReq } from './painless_err_req'; +import { verifyErrorResponse } from './verify_error'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('search', () => { + before(async () => { + await esArchiver.loadIfNeeded('../../../functional/fixtures/es_archiver/logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('../../../functional/fixtures/es_archiver/logstash_functional'); + }); describe('post', () => { it('should return 200 when correctly formatted searches are provided', async () => { const resp = await supertest @@ -28,13 +38,37 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); + expect(resp.status).to.be(200); expect(resp.body.isPartial).to.be(false); expect(resp.body.isRunning).to.be(false); expect(resp.body).to.have.property('rawResponse'); }); - it('should return 404 when if no strategy is provided', async () => - await supertest + it('should return 200 if terminated early', async () => { + const resp = await supertest + .post(`/internal/search/es`) + .send({ + params: { + terminateAfter: 1, + index: 'log*', + size: 1000, + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(200); + + expect(resp.status).to.be(200); + expect(resp.body.isPartial).to.be(false); + expect(resp.body.isRunning).to.be(false); + expect(resp.body.rawResponse.terminated_early).to.be(true); + }); + + it('should return 404 when if no strategy is provided', async () => { + const resp = await supertest .post(`/internal/search`) .send({ body: { @@ -43,7 +77,10 @@ export default function ({ getService }: FtrProviderContext) { }, }, }) - .expect(404)); + .expect(404); + + verifyErrorResponse(resp.body, 404); + }); it('should return 404 when if unknown strategy is provided', async () => { const resp = await supertest @@ -56,6 +93,8 @@ export default function ({ getService }: FtrProviderContext) { }, }) .expect(404); + + verifyErrorResponse(resp.body, 404); expect(resp.body.message).to.contain('banana not found'); }); @@ -74,11 +113,33 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(400); + verifyErrorResponse(resp.body, 400); + expect(resp.body.message).to.contain('Unsupported index pattern'); }); + it('should return 400 with illegal ES argument', async () => { + const resp = await supertest + .post(`/internal/search/es`) + .send({ + params: { + timeout: 1, // This should be a time range string! + index: 'log*', + size: 1000, + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); + }); + it('should return 400 with a bad body', async () => { - await supertest + const resp = await supertest .post(`/internal/search/es`) .send({ params: { @@ -89,16 +150,26 @@ export default function ({ getService }: FtrProviderContext) { }, }) .expect(400); + + verifyErrorResponse(resp.body, 400, 'parsing_exception', true); + }); + + it('should return 400 for a painless error', async () => { + const resp = await supertest.post(`/internal/search/es`).send(painlessErrReq).expect(400); + + verifyErrorResponse(resp.body, 400, 'search_phase_execution_exception', true); }); }); describe('delete', () => { it('should return 404 when no search id provided', async () => { - await supertest.delete(`/internal/search/es`).send().expect(404); + const resp = await supertest.delete(`/internal/search/es`).send().expect(404); + verifyErrorResponse(resp.body, 404); }); it('should return 400 when trying a delete on a non supporting strategy', async () => { const resp = await supertest.delete(`/internal/search/es/123`).send().expect(400); + verifyErrorResponse(resp.body, 400); expect(resp.body.message).to.contain("Search strategy es doesn't support cancellations"); }); }); diff --git a/test/api_integration/apis/search/verify_error.ts b/test/api_integration/apis/search/verify_error.ts new file mode 100644 index 0000000000000..a5754ff47973e --- /dev/null +++ b/test/api_integration/apis/search/verify_error.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import expect from '@kbn/expect'; + +export const verifyErrorResponse = ( + r: any, + expectedCode: number, + message?: string, + shouldHaveAttrs?: boolean +) => { + expect(r.statusCode).to.be(expectedCode); + if (message) { + expect(r.message).to.be(message); + } + if (shouldHaveAttrs) { + expect(r).to.have.property('attributes'); + expect(r.attributes).to.have.property('root_cause'); + } else { + expect(r).not.to.have.property('attributes'); + } +}; diff --git a/x-pack/plugins/data_enhanced/common/search/test_data/search_phase_execution_exception.json b/x-pack/plugins/data_enhanced/common/search/test_data/search_phase_execution_exception.json new file mode 100644 index 0000000000000..b79a396445e3d --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/test_data/search_phase_execution_exception.json @@ -0,0 +1,229 @@ +{ + "error": { + "root_cause": [ + { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + } + }, + { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + } + }, + { + "type": "parse_exception", + "reason": "failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]: [failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]]" + }, + { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + } + }, + { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + } + }, + { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + } + } + ], + "type": "search_phase_execution_exception", + "reason": "all shards failed", + "phase": "query", + "grouped": true, + "failed_shards": [ + { + "shard": 0, + "index": ".apm-agent-configuration", + "node": "DEfMVCg5R12TRG4CYIxUgQ", + "reason": { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + }, + "caused_by": { + "type": "illegal_argument_exception", + "reason": "cannot resolve symbol [invalid]" + } + } + }, + { + "shard": 0, + "index": ".apm-custom-link", + "node": "DEfMVCg5R12TRG4CYIxUgQ", + "reason": { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + }, + "caused_by": { + "type": "illegal_argument_exception", + "reason": "cannot resolve symbol [invalid]" + } + } + }, + { + "shard": 0, + "index": ".kibana-event-log-8.0.0-000001", + "node": "DEfMVCg5R12TRG4CYIxUgQ", + "reason": { + "type": "parse_exception", + "reason": "failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]: [failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]]", + "caused_by": { + "type": "illegal_argument_exception", + "reason": "failed to parse date field [2021-01-19T12:2755.047Z] with format [strict_date_optional_time]", + "caused_by": { + "type": "date_time_parse_exception", + "reason": "Text '2021-01-19T12:2755.047Z' could not be parsed, unparsed text found at index 16" + } + } + } + }, + { + "shard": 0, + "index": ".kibana_1", + "node": "DEfMVCg5R12TRG4CYIxUgQ", + "reason": { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + }, + "caused_by": { + "type": "illegal_argument_exception", + "reason": "cannot resolve symbol [invalid]" + } + } + }, + { + "shard": 0, + "index": ".kibana_task_manager_1", + "node": "DEfMVCg5R12TRG4CYIxUgQ", + "reason": { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + }, + "caused_by": { + "type": "illegal_argument_exception", + "reason": "cannot resolve symbol [invalid]" + } + } + }, + { + "shard": 0, + "index": ".security-7", + "node": "DEfMVCg5R12TRG4CYIxUgQ", + "reason": { + "type": "script_exception", + "reason": "compile error", + "script_stack": [ + "invalid", + "^---- HERE" + ], + "script": "invalid", + "lang": "painless", + "position": { + "offset": 0, + "start": 0, + "end": 7 + }, + "caused_by": { + "type": "illegal_argument_exception", + "reason": "cannot resolve symbol [invalid]" + } + } + } + ] + }, + "status": 400 +} \ No newline at end of file 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 1a6fc724e2cf2..22b0f3272ff7d 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 @@ -9,10 +9,16 @@ import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { AbortError } from '../../../../../src/plugins/kibana_utils/public'; -import { ISessionService, SearchTimeoutError, SearchSessionState } from 'src/plugins/data/public'; +import { + ISessionService, + SearchTimeoutError, + SearchSessionState, + PainlessError, +} from 'src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { bfetchPluginMock } from '../../../../../src/plugins/bfetch/public/mocks'; import { BehaviorSubject } from 'rxjs'; +import * as xpackResourceNotFoundException from '../../common/search/test_data/search_phase_execution_exception.json'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -99,6 +105,33 @@ describe('EnhancedSearchInterceptor', () => { }); }); + describe('errors', () => { + test('Should throw Painless error on server error with OSS format', async () => { + const mockResponse: any = { + statusCode: 400, + message: 'search_phase_execution_exception', + attributes: xpackResourceNotFoundException.error, + }; + fetchMock.mockRejectedValueOnce(mockResponse); + const response = searchInterceptor.search({ + params: {}, + }); + await expect(response.toPromise()).rejects.toThrow(PainlessError); + }); + + test('Renders a PainlessError', async () => { + searchInterceptor.showError( + new PainlessError({ + statusCode: 400, + message: 'search_phase_execution_exception', + attributes: xpackResourceNotFoundException.error, + }) + ); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + }); + describe('search', () => { test('should resolve immediately if first call returns full result', async () => { const responses = [ @@ -342,7 +375,8 @@ describe('EnhancedSearchInterceptor', () => { { time: 10, value: { - error: 'oh no', + statusCode: 500, + message: 'oh no', id: 1, }, isError: true, @@ -364,7 +398,8 @@ describe('EnhancedSearchInterceptor', () => { await timeTravel(10); expect(error).toHaveBeenCalled(); - expect(error.mock.calls[0][0]).toBe(responses[1].value); + expect(error.mock.calls[0][0]).toBeInstanceOf(Error); + expect((error.mock.calls[0][0] as Error).message).toBe('oh no'); expect(fetchMock).toHaveBeenCalledTimes(2); expect(mockCoreSetup.http.delete).toHaveBeenCalledTimes(1); }); 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 3230895da7705..b2ddd0310f8f5 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 @@ -7,6 +7,10 @@ import { enhancedEsSearchStrategyProvider } from './es_search_strategy'; import { BehaviorSubject } from 'rxjs'; import { SearchStrategyDependencies } from '../../../../../src/plugins/data/server/search'; +import { KbnServerError } from '../../../../../src/plugins/kibana_utils/server'; +import { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; +import * as indexNotFoundException from '../../../../../src/plugins/data/common/search/test_data/index_not_found_exception.json'; +import * as xContentParseException from '../../../../../src/plugins/data/common/search/test_data/x_content_parse_exception.json'; const mockAsyncResponse = { body: { @@ -145,6 +149,54 @@ describe('ES search strategy', () => { expect(request).toHaveProperty('wait_for_completion_timeout'); expect(request).toHaveProperty('keep_alive'); }); + + it('throws normalized error if ResponseError is thrown', async () => { + const errResponse = new ResponseError({ + body: indexNotFoundException, + statusCode: 404, + headers: {}, + warnings: [], + meta: {} as any, + }); + + mockSubmitCaller.mockRejectedValue(errResponse); + + const params = { index: 'logstash-*', body: { query: {} } }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + let err: KbnServerError | undefined; + try { + await esSearch.search({ params }, {}, mockDeps).toPromise(); + } catch (e) { + err = e; + } + expect(mockSubmitCaller).toBeCalled(); + expect(err).toBeInstanceOf(KbnServerError); + expect(err?.statusCode).toBe(404); + expect(err?.message).toBe(errResponse.message); + expect(err?.errBody).toBe(indexNotFoundException); + }); + + it('throws normalized error if Error is thrown', async () => { + const errResponse = new Error('not good'); + + mockSubmitCaller.mockRejectedValue(errResponse); + + const params = { index: 'logstash-*', body: { query: {} } }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + let err: KbnServerError | undefined; + try { + await esSearch.search({ params }, {}, mockDeps).toPromise(); + } catch (e) { + err = e; + } + expect(mockSubmitCaller).toBeCalled(); + expect(err).toBeInstanceOf(KbnServerError); + expect(err?.statusCode).toBe(500); + expect(err?.message).toBe(errResponse.message); + expect(err?.errBody).toBe(undefined); + }); }); describe('cancel', () => { @@ -160,6 +212,33 @@ describe('ES search strategy', () => { const request = mockDeleteCaller.mock.calls[0][0]; expect(request).toEqual({ id }); }); + + it('throws normalized error on ResponseError', async () => { + const errResponse = new ResponseError({ + body: xContentParseException, + statusCode: 400, + headers: {}, + warnings: [], + meta: {} as any, + }); + mockDeleteCaller.mockRejectedValue(errResponse); + + const id = 'some_id'; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + let err: KbnServerError | undefined; + try { + await esSearch.cancel!(id, {}, mockDeps); + } catch (e) { + err = e; + } + + expect(mockDeleteCaller).toBeCalled(); + expect(err).toBeInstanceOf(KbnServerError); + expect(err?.statusCode).toBe(400); + expect(err?.message).toBe(errResponse.message); + expect(err?.errBody).toBe(xContentParseException); + }); }); describe('extend', () => { @@ -176,5 +255,27 @@ describe('ES search strategy', () => { const request = mockGetCaller.mock.calls[0][0]; expect(request).toEqual({ id, keep_alive: keepAlive }); }); + + it('throws normalized error on ElasticsearchClientError', async () => { + const errResponse = new ElasticsearchClientError('something is wrong with EsClient'); + mockGetCaller.mockRejectedValue(errResponse); + + const id = 'some_other_id'; + const keepAlive = '1d'; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + let err: KbnServerError | undefined; + try { + await esSearch.extend!(id, keepAlive, {}, mockDeps); + } catch (e) { + err = e; + } + + expect(mockGetCaller).toBeCalled(); + expect(err).toBeInstanceOf(KbnServerError); + expect(err?.statusCode).toBe(500); + expect(err?.message).toBe(errResponse.message); + expect(err?.errBody).toBe(undefined); + }); }); }); 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 54ed59b30952a..694d9807b5a4d 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 @@ -6,7 +6,7 @@ import type { Observable } from 'rxjs'; import type { IScopedClusterClient, Logger, SharedGlobalConfig } from 'kibana/server'; -import { first, tap } from 'rxjs/operators'; +import { catchError, first, tap } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; import { from } from 'rxjs'; import type { @@ -33,7 +33,7 @@ import { } from './request_utils'; import { toAsyncKibanaSearchResponse } from './response_utils'; import { AsyncSearchResponse } from './types'; -import { KbnServerError } from '../../../../../src/plugins/kibana_utils/server'; +import { getKbnServerError, KbnServerError } from '../../../../../src/plugins/kibana_utils/server'; export const enhancedEsSearchStrategyProvider = ( config$: Observable<SharedGlobalConfig>, @@ -41,7 +41,11 @@ export const enhancedEsSearchStrategyProvider = ( usage?: SearchUsage ): ISearchStrategy<IEsSearchRequest> => { async function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) { - await esClient.asCurrentUser.asyncSearch.delete({ id }); + try { + await esClient.asCurrentUser.asyncSearch.delete({ id }); + } catch (e) { + throw getKbnServerError(e); + } } function asyncSearch( @@ -70,7 +74,10 @@ export const enhancedEsSearchStrategyProvider = ( return pollSearch(search, cancel, options).pipe( tap((response) => (id = response.id)), - tap(searchUsageObserver(logger, usage)) + tap(searchUsageObserver(logger, usage)), + catchError((e) => { + throw getKbnServerError(e); + }) ); } @@ -90,40 +97,72 @@ export const enhancedEsSearchStrategyProvider = ( ...params, }; - const promise = esClient.asCurrentUser.transport.request({ - method, - path, - body, - querystring, - }); + try { + const promise = esClient.asCurrentUser.transport.request({ + method, + path, + body, + querystring, + }); - const esResponse = await shimAbortSignal(promise, options?.abortSignal); - const response = esResponse.body as SearchResponse<any>; - return { - rawResponse: response, - ...getTotalLoaded(response), - }; + const esResponse = await shimAbortSignal(promise, options?.abortSignal); + const response = esResponse.body as SearchResponse<any>; + return { + rawResponse: response, + ...getTotalLoaded(response), + }; + } catch (e) { + throw getKbnServerError(e); + } } return { + /** + * @param request + * @param options + * @param deps `SearchStrategyDependencies` + * @returns `Observable<IEsSearchResponse<any>>` + * @throws `KbnServerError` + */ search: (request, options: IAsyncSearchOptions, deps) => { logger.debug(`search ${JSON.stringify(request.params) || request.id}`); + if (request.indexType && request.indexType !== 'rollup') { + throw new KbnServerError('Unknown indexType', 400); + } if (request.indexType === undefined) { return asyncSearch(request, options, deps); - } else if (request.indexType === 'rollup') { - return from(rollupSearch(request, options, deps)); } else { - throw new KbnServerError('Unknown indexType', 400); + return from(rollupSearch(request, options, deps)); } }, + /** + * @param id async search ID to cancel, as returned from _async_search API + * @param options + * @param deps `SearchStrategyDependencies` + * @returns `Promise<void>` + * @throws `KbnServerError` + */ cancel: async (id, options, { esClient }) => { logger.debug(`cancel ${id}`); await cancelAsyncSearch(id, esClient); }, + /** + * + * @param id async search ID to extend, as returned from _async_search API + * @param keepAlive + * @param options + * @param deps `SearchStrategyDependencies` + * @returns `Promise<void>` + * @throws `KbnServerError` + */ extend: async (id, keepAlive, options, { esClient }) => { logger.debug(`extend ${id} by ${keepAlive}`); - await esClient.asCurrentUser.asyncSearch.get({ id, keep_alive: keepAlive }); + try { + await esClient.asCurrentUser.asyncSearch.get({ id, keep_alive: keepAlive }); + } catch (e) { + throw getKbnServerError(e); + } }, }; }; diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json index c4b09276880d9..29bfd71cb32b4 100644 --- a/x-pack/plugins/data_enhanced/tsconfig.json +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -14,7 +14,8 @@ "config.ts", "../../../typings/**/*", // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 - "public/autocomplete/providers/kql_query_suggestion/__fixtures__/*.json" + "public/autocomplete/providers/kql_query_suggestion/__fixtures__/*.json", + "common/search/test_data/*.json" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/test/api_integration/apis/search/search.ts b/x-pack/test/api_integration/apis/search/search.ts index 0c08b834a2778..2115976bcced1 100644 --- a/x-pack/test/api_integration/apis/search/search.ts +++ b/x-pack/test/api_integration/apis/search/search.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { verifyErrorResponse } from '../../../../../test/api_integration/apis/search/verify_error'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -90,6 +91,23 @@ export default function ({ getService }: FtrProviderContext) { expect(resp2.body.isRunning).to.be(false); }); + it('should fail without kbn-xref header', async () => { + const resp = await supertest + .post(`/internal/search/ese`) + .send({ + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'Request must contain a kbn-xsrf header.'); + }); + it('should return 400 when unknown index type is provided', async () => { const resp = await supertest .post(`/internal/search/ese`) @@ -106,7 +124,7 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(400); - expect(resp.body.message).to.contain('Unknown indexType'); + verifyErrorResponse(resp.body, 400, 'Unknown indexType'); }); it('should return 400 if invalid id is provided', async () => { @@ -124,7 +142,7 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(400); - expect(resp.body.message).to.contain('illegal_argument_exception'); + verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); }); it('should return 404 if unkown id is provided', async () => { @@ -143,12 +161,11 @@ export default function ({ getService }: FtrProviderContext) { }, }) .expect(404); - - expect(resp.body.message).to.contain('resource_not_found_exception'); + verifyErrorResponse(resp.body, 404, 'resource_not_found_exception', true); }); it('should return 400 with a bad body', async () => { - await supertest + const resp = await supertest .post(`/internal/search/ese`) .set('kbn-xsrf', 'foo') .send({ @@ -160,6 +177,8 @@ export default function ({ getService }: FtrProviderContext) { }, }) .expect(400); + + verifyErrorResponse(resp.body, 400, 'parsing_exception', true); }); }); @@ -186,8 +205,7 @@ export default function ({ getService }: FtrProviderContext) { }, }) .expect(400); - - expect(resp.body.message).to.contain('illegal_argument_exception'); + verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); }); it('should return 400 if rollup search is without non-existent index', async () => { @@ -207,7 +225,7 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(400); - expect(resp.body.message).to.contain('illegal_argument_exception'); + verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); }); it('should rollup search', async () => { @@ -241,7 +259,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send() .expect(400); - expect(resp.body.message).to.contain('illegal_argument_exception'); + verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); }); it('should delete a search', async () => { From af337ce4edb6f09b69ab0513785c664be3e82f12 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall <clint.hall@elastic.co> Date: Sun, 31 Jan 2021 08:37:58 -0600 Subject: [PATCH 147/163] [Presentation Team] Migrate to Typescript Project References (#86019) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/input_control_vis/tsconfig.json | 21 ++++++++ tsconfig.json | 1 + tsconfig.refs.json | 11 ++-- .../server/demodata/get_demo_rows.ts | 2 + .../renderers/error/index.tsx | 2 +- .../filters/dropdown_filter/index.tsx | 2 +- .../canvas_plugin_src/renderers/table.tsx | 2 +- .../export/export/export_app.component.tsx | 2 +- .../apps/home/home_app/home_app.component.tsx | 2 +- .../workpad/workpad_app/workpad_telemetry.tsx | 2 +- .../asset_manager/asset.component.tsx | 2 +- .../asset_manager/asset_manager.component.tsx | 2 +- .../confirm_modal/confirm_modal.tsx | 2 +- .../page_preview/page_preview.component.tsx | 2 +- .../components/toolbar/toolbar.component.tsx | 2 +- .../workpad_config.component.tsx | 2 +- .../refresh_control.component.tsx | 2 +- .../canvas/public/functions/filters.ts | 2 +- x-pack/plugins/canvas/public/functions/pie.ts | 2 +- .../canvas/public/functions/plot/index.ts | 2 +- .../canvas/public/functions/timelion.ts | 2 +- x-pack/plugins/canvas/public/functions/to.ts | 2 +- .../lib/template_from_react_component.tsx | 2 +- .../canvas/server/sample_data/index.ts | 4 +- .../shareable_runtime/context/actions.ts | 2 +- .../canvas/shareable_runtime/test/index.ts | 3 ++ x-pack/plugins/canvas/tsconfig.json | 52 ++++++++++++++++++ x-pack/plugins/canvas/types/state.ts | 2 +- .../server/routes/lib/get_document_payload.ts | 2 +- x-pack/plugins/reporting/tsconfig.json | 31 +++++++++++ x-pack/tsconfig.json | 54 ++++++++++--------- x-pack/tsconfig.refs.json | 42 ++++++++------- 32 files changed, 190 insertions(+), 75 deletions(-) create mode 100644 src/plugins/input_control_vis/tsconfig.json create mode 100644 x-pack/plugins/canvas/tsconfig.json create mode 100644 x-pack/plugins/reporting/tsconfig.json diff --git a/src/plugins/input_control_vis/tsconfig.json b/src/plugins/input_control_vis/tsconfig.json new file mode 100644 index 0000000000000..bef7bc394a6cc --- /dev/null +++ b/src/plugins/input_control_vis/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../data/tsconfig.json"}, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/tsconfig.json b/tsconfig.json index 2647ac9a9d75e..d8fb2804242bc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ "src/plugins/es_ui_shared/**/*", "src/plugins/expressions/**/*", "src/plugins/home/**/*", + "src/plugins/input_control_vis/**/*", "src/plugins/inspector/**/*", "src/plugins/kibana_legacy/**/*", "src/plugins/kibana_react/**/*", diff --git a/tsconfig.refs.json b/tsconfig.refs.json index fa1b533a3dd38..9a65b385b7820 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -2,12 +2,12 @@ "include": [], "references": [ { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "./src/plugins/advanced_settings/tsconfig.json" }, { "path": "./src/plugins/apm_oss/tsconfig.json" }, { "path": "./src/plugins/bfetch/tsconfig.json" }, { "path": "./src/plugins/charts/tsconfig.json" }, { "path": "./src/plugins/console/tsconfig.json" }, + { "path": "./src/plugins/dashboard/tsconfig.json" }, { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/discover/tsconfig.json" }, @@ -15,8 +15,6 @@ { "path": "./src/plugins/es_ui_shared/tsconfig.json" }, { "path": "./src/plugins/expressions/tsconfig.json" }, { "path": "./src/plugins/home/tsconfig.json" }, - { "path": "./src/plugins/dashboard/tsconfig.json" }, - { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, @@ -26,16 +24,17 @@ { "path": "./src/plugins/maps_legacy/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/presentation_util/tsconfig.json" }, { "path": "./src/plugins/region_map/tsconfig.json" }, - { "path": "./src/plugins/saved_objects/tsconfig.json" }, { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, - { "path": "./src/plugins/presentation_util/tsconfig.json" }, + { "path": "./src/plugins/saved_objects/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/spaces_oss/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/telemetry_management_section/tsconfig.json" }, + { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/tile_map/tsconfig.json" }, { "path": "./src/plugins/timelion/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts index 58a2354b5cf38..ff5a4506ab82a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/get_demo_rows.ts @@ -5,8 +5,10 @@ */ import { cloneDeep } from 'lodash'; +// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config import ci from './ci.json'; import { DemoRows } from './demo_rows_types'; +// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config import shirts from './shirts.json'; import { getFunctionErrors } from '../../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx index a9296bd9a1241..238b2edc3bd6d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx @@ -12,7 +12,7 @@ import { Popover } from '../../../public/components/popover'; import { RendererStrings } from '../../../i18n'; import { RendererFactory } from '../../../types'; -interface Config { +export interface Config { error: Error; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index bfc36932a8a07..6c1dd086c8667 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -15,7 +15,7 @@ import { RendererStrings } from '../../../../i18n'; const { dropdownFilter: strings } = RendererStrings; -interface Config { +export interface Config { /** The column to use within the exactly function */ column: string; /** diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/table.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/table.tsx index ada159e07f6ae..4933b1b4ba51d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/table.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/table.tsx @@ -12,7 +12,7 @@ import { RendererFactory, Style, Datatable } from '../../types'; const { dropdownFilter: strings } = RendererStrings; -interface TableArguments { +export interface TableArguments { font?: Style; paginate: boolean; perPage: number; diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx b/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx index 03121e749d0dc..f26408b1200f1 100644 --- a/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx +++ b/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx @@ -13,7 +13,7 @@ import { WorkpadPage } from '../../../components/workpad_page'; import { Link } from '../../../components/link'; import { CanvasWorkpad } from '../../../../types'; -interface Props { +export interface Props { workpad: CanvasWorkpad; selectedPageIndex: number; initializeWorkpad: () => void; diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx index 3c2e989cc8e51..7fbdc24c112a1 100644 --- a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx +++ b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx @@ -11,7 +11,7 @@ import { WorkpadManager } from '../../../components/workpad_manager'; // @ts-expect-error untyped local import { setDocTitle } from '../../../lib/doc_title'; -interface Props { +export interface Props { onLoad: () => void; } diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx index 981334ff8d9f2..3697d5dad2dae 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx @@ -46,7 +46,7 @@ interface ResolvedArgs { [keys: string]: any; } -interface ElementsLoadedTelemetryProps extends PropsFromRedux { +export interface ElementsLoadedTelemetryProps extends PropsFromRedux { workpad: Workpad; } 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 ed000741bc542..d94802bf2a772 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 @@ -28,7 +28,7 @@ import { ComponentStrings } from '../../../i18n'; const { Asset: strings } = ComponentStrings; -interface Props { +export interface Props { /** The asset to be rendered */ asset: AssetType; /** The function to execute when the user clicks 'Create' */ diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx index 98f3d8b48829d..6c1b546b49aa1 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx @@ -33,7 +33,7 @@ import { ComponentStrings } from '../../../i18n'; const { AssetManager: strings } = ComponentStrings; -interface Props { +export interface Props { /** The assets to display within the modal */ assets: AssetType[]; /** Function to invoke when the modal is closed */ diff --git a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx index 31a75acbba4ec..9d0a5e0a9f51d 100644 --- a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx +++ b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx @@ -8,7 +8,7 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { FunctionComponent } from 'react'; -interface Props { +export interface Props { isOpen: boolean; title?: string; message: string; diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx index fd1dc869d60ec..da1fe8473e36d 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx +++ b/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx @@ -10,7 +10,7 @@ import { DomPreview } from '../dom_preview'; import { PageControls } from './page_controls'; import { CanvasPage } from '../../../types'; -interface Props { +export interface Props { isWriteable: boolean; page: Pick<CanvasPage, 'id' | 'style'>; height: number; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index 7151e72a44780..d33ba57050d4b 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -31,7 +31,7 @@ const { Toolbar: strings } = ComponentStrings; type TrayType = 'pageManager' | 'expression'; -interface Props { +export interface Props { isWriteable: boolean; selectedElement?: CanvasElement; selectedPageNumber: number; diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx index a7424882f1072..4068272bbaf11 100644 --- a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx @@ -30,7 +30,7 @@ import { ComponentStrings } from '../../../i18n'; const { WorkpadConfig: strings } = ComponentStrings; -interface Props { +export interface Props { size: { height: number; width: number; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx index d651e649128f9..023d87c7c3565 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx @@ -12,7 +12,7 @@ import { ToolTipShortcut } from '../../tool_tip_shortcut'; import { ComponentStrings } from '../../../../i18n'; const { WorkpadHeaderRefreshControlSettings: strings } = ComponentStrings; -interface Props { +export interface Props { doRefresh: MouseEventHandler<HTMLButtonElement>; inFlight: boolean; } diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index fdb5d69d35515..70120ccad6f54 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -15,7 +15,7 @@ import { ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; import { InitializeArguments } from '.'; -interface Arguments { +export interface Arguments { group: string[]; ungrouped: boolean; } diff --git a/x-pack/plugins/canvas/public/functions/pie.ts b/x-pack/plugins/canvas/public/functions/pie.ts index ab3f1b932dc3c..e7cf153b9cd0f 100644 --- a/x-pack/plugins/canvas/public/functions/pie.ts +++ b/x-pack/plugins/canvas/public/functions/pie.ts @@ -61,7 +61,7 @@ export interface Pie { options: PieOptions; } -interface Arguments { +export interface Arguments { palette: PaletteOutput; seriesStyle: SeriesStyle[]; radius: number | 'auto'; diff --git a/x-pack/plugins/canvas/public/functions/plot/index.ts b/x-pack/plugins/canvas/public/functions/plot/index.ts index a4661dc3401df..79aa11cfa2d80 100644 --- a/x-pack/plugins/canvas/public/functions/plot/index.ts +++ b/x-pack/plugins/canvas/public/functions/plot/index.ts @@ -17,7 +17,7 @@ import { getTickHash } from './get_tick_hash'; import { getFunctionHelp } from '../../../i18n'; import { AxisConfig, PointSeries, Render, SeriesStyle, Legend } from '../../../types'; -interface Arguments { +export interface Arguments { seriesStyle: SeriesStyle[]; defaultStyle: SeriesStyle; palette: PaletteOutput; diff --git a/x-pack/plugins/canvas/public/functions/timelion.ts b/x-pack/plugins/canvas/public/functions/timelion.ts index 947972fa310c9..3018540e5bf8e 100644 --- a/x-pack/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/plugins/canvas/public/functions/timelion.ts @@ -15,7 +15,7 @@ import { Datatable, ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; import { InitializeArguments } from './'; -interface Arguments { +export interface Arguments { query: string; interval: string; from: string; diff --git a/x-pack/plugins/canvas/public/functions/to.ts b/x-pack/plugins/canvas/public/functions/to.ts index 36b2d3f9f04c6..c8ac4f714e5c4 100644 --- a/x-pack/plugins/canvas/public/functions/to.ts +++ b/x-pack/plugins/canvas/public/functions/to.ts @@ -10,7 +10,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; import { getFunctionHelp, getFunctionErrors } from '../../i18n'; import { InitializeArguments } from '.'; -interface Arguments { +export interface Arguments { type: string[]; } diff --git a/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx b/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx index f4e715b1bbc49..95225cf13ff3b 100644 --- a/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx +++ b/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx @@ -11,7 +11,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { ErrorBoundary } from '../components/enhance/error_boundary'; import { ArgumentHandlers } from '../../types/arguments'; -interface Props { +export interface Props { renderError: Function; } diff --git a/x-pack/plugins/canvas/server/sample_data/index.ts b/x-pack/plugins/canvas/server/sample_data/index.ts index 212d9f5132831..9c9ecb718fd5f 100644 --- a/x-pack/plugins/canvas/server/sample_data/index.ts +++ b/x-pack/plugins/canvas/server/sample_data/index.ts @@ -3,9 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config import ecommerceSavedObjects from './ecommerce_saved_objects.json'; +// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config import flightsSavedObjects from './flights_saved_objects.json'; +// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config import webLogsSavedObjects from './web_logs_saved_objects.json'; import { loadSampleData } from './load_sample_data'; diff --git a/x-pack/plugins/canvas/shareable_runtime/context/actions.ts b/x-pack/plugins/canvas/shareable_runtime/context/actions.ts index 8c88afbadfd9e..a36435688505d 100644 --- a/x-pack/plugins/canvas/shareable_runtime/context/actions.ts +++ b/x-pack/plugins/canvas/shareable_runtime/context/actions.ts @@ -17,7 +17,7 @@ export enum CanvasShareableActions { SET_TOOLBAR_AUTOHIDE = 'SET_TOOLBAR_AUTOHIDE', } -interface FluxAction<T, P> { +export interface FluxAction<T, P> { type: T; payload: P; } diff --git a/x-pack/plugins/canvas/shareable_runtime/test/index.ts b/x-pack/plugins/canvas/shareable_runtime/test/index.ts index 288dd0dc3a5be..f0d2ebcc20128 100644 --- a/x-pack/plugins/canvas/shareable_runtime/test/index.ts +++ b/x-pack/plugins/canvas/shareable_runtime/test/index.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config import hello from './workpads/hello.json'; +// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config import austin from './workpads/austin.json'; +// @ts-ignore this file is too large for TypeScript, so it is excluded from our project config import test from './workpads/test.json'; export * from './utils'; diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json new file mode 100644 index 0000000000000..3e3986082e207 --- /dev/null +++ b/x-pack/plugins/canvas/tsconfig.json @@ -0,0 +1,52 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "../../../typings/**/*", + "__fixtures__/**/*", + "canvas_plugin_src/**/*", + "common/**/*", + "i18n/**/*", + "public/**/*", + "server/**/*", + "shareable_runtime/**/*", + "storybook/**/*", + "tasks/mocks/*", + "types/**/*", + "**/*.json", + ], + "exclude": [ + // these files are too large and upset tsc, so we exclude them + "server/sample_data/*.json", + "canvas_plugin_src/functions/server/demodata/*.json", + "shareable_runtime/test/workpads/*.json", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/bfetch/tsconfig.json"}, + { "path": "../../../src/plugins/charts/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/discover/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/inspector/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_legacy/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../lens/tsconfig.json" }, + { "path": "../maps/tsconfig.json" }, + { "path": "../reporting/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/canvas/types/state.ts b/x-pack/plugins/canvas/types/state.ts index 03bb931dc9b26..33f913563daac 100644 --- a/x-pack/plugins/canvas/types/state.ts +++ b/x-pack/plugins/canvas/types/state.ts @@ -52,7 +52,7 @@ type ExpressionType = | Style | Range; -interface ExpressionRenderable { +export interface ExpressionRenderable { state: 'ready' | 'pending'; value: Render<ExpressionType> | null; error: null; diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index b154978d041f4..7706aa9d650c7 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -13,7 +13,7 @@ import { ReportDocument } from '../../lib/store'; import { TaskRunResult } from '../../lib/tasks'; import { ExportTypeDefinition } from '../../types'; -interface ErrorFromPayload { +export interface ErrorFromPayload { message: string; } diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json new file mode 100644 index 0000000000000..88e8d343f4700 --- /dev/null +++ b/x-pack/plugins/reporting/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/discover/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + ] +} diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 4b161e3559849..1be6b5cf84cda 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -7,6 +7,7 @@ "plugins/apm/e2e/cypress/**/*", "plugins/apm/ftr_e2e/**/*", "plugins/apm/scripts/**/*", + "plugins/canvas/**/*", "plugins/console_extensions/**/*", "plugins/data_enhanced/**/*", "plugins/discover_enhanced/**/*", @@ -23,6 +24,7 @@ "plugins/maps/**/*", "plugins/maps_file_upload/**/*", "plugins/maps_legacy_licensing/**/*", + "plugins/reporting/**/*", "plugins/searchprofiler/**/*", "plugins/security_solution/cypress/**/*", "plugins/task_manager/**/*", @@ -49,15 +51,13 @@ }, "references": [ { "path": "../src/core/tsconfig.json" }, - { "path": "../src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "../src/plugins/management/tsconfig.json" }, { "path": "../src/plugins/bfetch/tsconfig.json" }, { "path": "../src/plugins/charts/tsconfig.json" }, { "path": "../src/plugins/console/tsconfig.json" }, { "path": "../src/plugins/dashboard/tsconfig.json" }, - { "path": "../src/plugins/discover/tsconfig.json" }, { "path": "../src/plugins/data/tsconfig.json" }, { "path": "../src/plugins/dev_tools/tsconfig.json" }, + { "path": "../src/plugins/discover/tsconfig.json" }, { "path": "../src/plugins/embeddable/tsconfig.json" }, { "path": "../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../src/plugins/expressions/tsconfig.json" }, @@ -67,53 +67,55 @@ { "path": "../src/plugins/kibana_react/tsconfig.json" }, { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../src/plugins/management/tsconfig.json" }, { "path": "../src/plugins/navigation/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, - { "path": "../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../src/plugins/presentation_util/tsconfig.json" }, { "path": "../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, - { "path": "../src/plugins/presentation_util/tsconfig.json" }, + { "path": "../src/plugins/saved_objects/tsconfig.json" }, { "path": "../src/plugins/security_oss/tsconfig.json" }, { "path": "../src/plugins/share/tsconfig.json" }, - { "path": "../src/plugins/telemetry/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../src/plugins/telemetry_management_section/tsconfig.json" }, + { "path": "../src/plugins/telemetry/tsconfig.json" }, { "path": "../src/plugins/ui_actions/tsconfig.json" }, { "path": "../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, + { "path": "./plugins/actions/tsconfig.json"}, + { "path": "./plugins/alerts/tsconfig.json"}, + { "path": "./plugins/beats_management/tsconfig.json" }, + { "path": "./plugins/canvas/tsconfig.json" }, + { "path": "./plugins/cloud/tsconfig.json" }, { "path": "./plugins/console_extensions/tsconfig.json" }, { "path": "./plugins/data_enhanced/tsconfig.json" }, { "path": "./plugins/discover_enhanced/tsconfig.json" }, - { "path": "./plugins/global_search/tsconfig.json" }, - { "path": "./plugins/global_search_providers/tsconfig.json" }, + { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "./plugins/encrypted_saved_objects/tsconfig.json" }, { "path": "./plugins/enterprise_search/tsconfig.json" }, + { "path": "./plugins/event_log/tsconfig.json" }, { "path": "./plugins/features/tsconfig.json" }, + { "path": "./plugins/global_search_bar/tsconfig.json" }, + { "path": "./plugins/global_search_providers/tsconfig.json" }, + { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, - { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, - { "path": "./plugins/event_log/tsconfig.json" }, - { "path": "./plugins/licensing/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, - { "path": "./plugins/maps/tsconfig.json" }, + { "path": "./plugins/license_management/tsconfig.json" }, + { "path": "./plugins/licensing/tsconfig.json" }, { "path": "./plugins/maps_file_upload/tsconfig.json" }, { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, + { "path": "./plugins/maps/tsconfig.json" }, + { "path": "./plugins/painless_lab/tsconfig.json" }, + { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, { "path": "./plugins/searchprofiler/tsconfig.json" }, + { "path": "./plugins/security/tsconfig.json" }, + { "path": "./plugins/spaces/tsconfig.json" }, + { "path": "./plugins/stack_alerts/tsconfig.json"}, { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "./plugins/translations/tsconfig.json" }, - { "path": "./plugins/spaces/tsconfig.json" }, - { "path": "./plugins/security/tsconfig.json" }, - { "path": "./plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "./plugins/beats_management/tsconfig.json" }, - { "path": "./plugins/cloud/tsconfig.json" }, - { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./plugins/global_search_bar/tsconfig.json" }, - { "path": "./plugins/actions/tsconfig.json"}, - { "path": "./plugins/alerts/tsconfig.json"}, { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, - { "path": "./plugins/stack_alerts/tsconfig.json"}, - { "path": "./plugins/license_management/tsconfig.json" }, - { "path": "./plugins/painless_lab/tsconfig.json" }, + { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "./plugins/watcher/tsconfig.json" }, ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index f5b35c9429a1c..ed209cd241586 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -3,38 +3,40 @@ "references": [ { "path": "./plugins/actions/tsconfig.json"}, { "path": "./plugins/alerts/tsconfig.json"}, - { "path": "./plugins/dashboard_enhanced/tsconfig.json" }, - { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/lens/tsconfig.json" }, + { "path": "./plugins/beats_management/tsconfig.json" }, + { "path": "./plugins/canvas/tsconfig.json" }, + { "path": "./plugins/cloud/tsconfig.json" }, { "path": "./plugins/console_extensions/tsconfig.json" }, - { "path": "./plugins/discover_enhanced/tsconfig.json" }, + { "path": "./plugins/dashboard_enhanced/tsconfig.json" }, { "path": "./plugins/data_enhanced/tsconfig.json" }, - { "path": "./plugins/global_search/tsconfig.json" }, - { "path": "./plugins/global_search_providers/tsconfig.json" }, + { "path": "./plugins/discover_enhanced/tsconfig.json" }, + { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "./plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "./plugins/enterprise_search/tsconfig.json" }, { "path": "./plugins/event_log/tsconfig.json"}, { "path": "./plugins/features/tsconfig.json" }, + { "path": "./plugins/global_search_bar/tsconfig.json" }, + { "path": "./plugins/global_search_providers/tsconfig.json" }, + { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, - { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, - { "path": "./plugins/enterprise_search/tsconfig.json" }, - { "path": "./plugins/maps/tsconfig.json" }, + { "path": "./plugins/lens/tsconfig.json" }, + { "path": "./plugins/license_management/tsconfig.json" }, + { "path": "./plugins/licensing/tsconfig.json" }, { "path": "./plugins/maps_file_upload/tsconfig.json" }, { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, + { "path": "./plugins/maps/tsconfig.json" }, + { "path": "./plugins/painless_lab/tsconfig.json" }, + { "path": "./plugins/reporting/tsconfig.json" }, + { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, { "path": "./plugins/searchprofiler/tsconfig.json" }, + { "path": "./plugins/security/tsconfig.json" }, + { "path": "./plugins/spaces/tsconfig.json" }, + { "path": "./plugins/stack_alerts/tsconfig.json"}, { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "./plugins/translations/tsconfig.json" }, { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, - { "path": "./plugins/spaces/tsconfig.json" }, - { "path": "./plugins/security/tsconfig.json" }, - { "path": "./plugins/stack_alerts/tsconfig.json"}, - { "path": "./plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "./plugins/beats_management/tsconfig.json" }, - { "path": "./plugins/cloud/tsconfig.json" }, - { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./plugins/global_search_bar/tsconfig.json" }, - { "path": "./plugins/license_management/tsconfig.json" }, - { "path": "./plugins/painless_lab/tsconfig.json" }, + { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "./plugins/watcher/tsconfig.json" } ] } From 4f43096c64c4b27205ecd8fd3aecfd1426da6892 Mon Sep 17 00:00:00 2001 From: Nicolas Ruflin <spam@ruflin.com> Date: Mon, 1 Feb 2021 10:28:49 +0100 Subject: [PATCH 148/163] [Fleet] Remove comments around experimental registry (#89830) The experimental registry was used for the 7.8 release but since then was not touched anymore. Because of this it should not show up in the code anymore even if it is commented out. --- .../plugins/fleet/server/services/epm/registry/registry_url.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts index efc25cc2efb5d..4f17a2b88670a 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts @@ -11,12 +11,10 @@ import { appContextService, licenseService } from '../../'; const PRODUCTION_REGISTRY_URL_CDN = 'https://epr.elastic.co'; // const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co'; -// const EXPERIMENTAL_REGISTRY_URL_CDN = 'https://epr-experimental.elastic.co/'; const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co'; // const PRODUCTION_REGISTRY_URL_NO_CDN = 'https://epr.ea-web.elastic.dev'; // const STAGING_REGISTRY_URL_NO_CDN = 'https://epr-staging.ea-web.elastic.dev'; -// const EXPERIMENTAL_REGISTRY_URL_NO_CDN = 'https://epr-experimental.ea-web.elastic.dev/'; // const SNAPSHOT_REGISTRY_URL_NO_CDN = 'https://epr-snapshot.ea-web.elastic.dev'; const getDefaultRegistryUrl = (): string => { From c2f53a96ebb6e4a5a9a8e4dcbbcde33aa4d1f20d Mon Sep 17 00:00:00 2001 From: Anton Dosov <anton.dosov@elastic.co> Date: Mon, 1 Feb 2021 10:40:38 +0100 Subject: [PATCH 149/163] [Search Sessions][Dashboard] Clear search session when navigating from dashboard route (#89749) --- src/plugins/dashboard/public/application/dashboard_app.tsx | 7 +++++++ .../apps/dashboard/async_search/send_to_background.ts | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 7ea181715717b..6955365ebca3f 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -265,6 +265,13 @@ export function DashboardApp({ }; }, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]); + // clear search session when leaving dashboard route + useEffect(() => { + return () => { + data.search.session.clear(); + }; + }, [data.search.session]); + return ( <div className="app-container dshAppContainer"> {savedDashboard && dashboardStateManager && dashboardContainer && viewMode && ( diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts index 7e878e763bfc1..3e417551c3cb9 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts @@ -96,6 +96,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // should leave session state untouched await PageObjects.dashboard.switchToEditMode(); await searchSessions.expectState('restored'); + + // navigating to a listing page clears the session + await PageObjects.dashboard.gotoDashboardLandingPage(); + await searchSessions.missingOrFail(); }); }); } From f0717a0a79d8cb1c772a9039aa7796691aa78ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= <casper@elastic.co> Date: Mon, 1 Feb 2021 10:54:08 +0100 Subject: [PATCH 150/163] [Observability] `ActionMenu` style fixes (#89547) * [Observability] Reduced space between title and subtitle * [Observability] Reduce margin between sections * [Observability] Reduce list item font size * [Observability] Remove spacer * [APM] Changes button style and label * [Logs] Changes the actions button label and style * [Logs] Fixes the overlap of actions button and close * Updated test and snapshot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../CustomLinkMenuSection/index.tsx | 1 - .../TransactionActionMenu.test.tsx | 12 ++++++------ .../TransactionActionMenu/TransactionActionMenu.tsx | 8 ++++---- .../TransactionActionMenu.test.tsx.snap | 8 ++++---- .../log_entry_flyout/log_entry_actions_menu.tsx | 8 ++++---- .../logging/log_entry_flyout/log_entry_flyout.tsx | 2 +- .../public/components/shared/action_menu/index.tsx | 6 +++--- 7 files changed, 22 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx index ae22718af8b57..43f566a93a89d 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx @@ -107,7 +107,6 @@ export function CustomLinkMenuSection({ </EuiFlexItem> </EuiFlexGroup> - <EuiSpacer size="s" /> <SectionSubtitle> {i18n.translate( 'xpack.apm.transactionActionMenu.customLink.subtitle', diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx index 48c863b460482..3141dc7a5f3c6 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx @@ -52,7 +52,7 @@ const renderTransaction = async (transaction: Record<string, any>) => { } ); - fireEvent.click(rendered.getByText('Actions')); + fireEvent.click(rendered.getByText('Investigate')); return rendered; }; @@ -289,7 +289,7 @@ describe('TransactionActionMenu component', () => { }); const component = renderTransactionActionMenuWithLicense(license); act(() => { - fireEvent.click(component.getByText('Actions')); + fireEvent.click(component.getByText('Investigate')); }); expectTextsNotInDocument(component, ['Custom Links']); }); @@ -313,7 +313,7 @@ describe('TransactionActionMenu component', () => { { wrapper: Wrapper } ); act(() => { - fireEvent.click(component.getByText('Actions')); + fireEvent.click(component.getByText('Investigate')); }); expectTextsNotInDocument(component, ['Custom Links']); }); @@ -330,7 +330,7 @@ describe('TransactionActionMenu component', () => { }); const component = renderTransactionActionMenuWithLicense(license); act(() => { - fireEvent.click(component.getByText('Actions')); + fireEvent.click(component.getByText('Investigate')); }); expectTextsInDocument(component, ['Custom Links']); }); @@ -347,7 +347,7 @@ describe('TransactionActionMenu component', () => { }); const component = renderTransactionActionMenuWithLicense(license); act(() => { - fireEvent.click(component.getByText('Actions')); + fireEvent.click(component.getByText('Investigate')); }); expectTextsInDocument(component, ['Custom Links']); }); @@ -364,7 +364,7 @@ describe('TransactionActionMenu component', () => { }); const component = renderTransactionActionMenuWithLicense(license); act(() => { - fireEvent.click(component.getByText('Actions')); + fireEvent.click(component.getByText('Investigate')); }); expectTextsInDocument(component, ['Custom Links']); act(() => { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 312513db80886..22fa25f93b212 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; @@ -30,11 +30,11 @@ interface Props { function ActionMenuButton({ onClick }: { onClick: () => void }) { return ( - <EuiButtonEmpty iconType="arrowDown" iconSide="right" onClick={onClick}> + <EuiButton iconType="arrowDown" iconSide="right" onClick={onClick}> {i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', { - defaultMessage: 'Actions', + defaultMessage: 'Investigate', })} - </EuiButtonEmpty> + </EuiButton> ); } diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap index fa6db645d28a8..ea33fb3c3df08 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap @@ -10,20 +10,20 @@ exports[`TransactionActionMenu component matches the snapshot 1`] = ` class="euiPopover__anchor" > <button - class="euiButtonEmpty euiButtonEmpty--primary" + class="euiButton euiButton--primary" type="button" > <span - class="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content" + class="euiButtonContent euiButtonContent--iconRight euiButton__content" > <span class="euiButtonContent__icon" data-euiicon-type="arrowDown" /> <span - class="euiButtonEmpty__text" + class="euiButton__text" > - Actions + Investigate </span> </span> </button> diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index aa3b4532e878e..9fef939733432 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; import { useVisibilityState } from '../../../utils/use_visibility_state'; @@ -67,7 +67,7 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ <EuiPopover anchorPosition="downRight" button={ - <EuiButtonEmpty + <EuiButton data-test-subj="logEntryActionsMenuButton" disabled={!hasMenuItems} iconSide="right" @@ -76,9 +76,9 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ > <FormattedMessage id="xpack.infra.logEntryActionsMenu.buttonLabel" - defaultMessage="Actions" + defaultMessage="Investigate" /> - </EuiButtonEmpty> + </EuiButton> } closePopover={hide} id="logEntryActionsMenu" diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx index 5684d4068f3be..7d8ca95f9b93b 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx @@ -88,7 +88,7 @@ export const LogEntryFlyout = ({ </> ) : null} </EuiFlexItem> - <EuiFlexItem grow={false}> + <EuiFlexItem style={{ padding: 8 }} grow={false}> {logEntry ? <LogEntryActionsMenu logEntry={logEntry} /> : null} </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx index 4819a0760d88a..af61f618a89b2 100644 --- a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx @@ -26,7 +26,7 @@ export function SectionTitle({ children }: { children?: ReactNode }) { <EuiText size={'s'} grow={false}> <h5>{children}</h5> </EuiText> - <EuiSpacer size={'s'} /> + <EuiSpacer size={'xs'} /> </> ); } @@ -55,7 +55,7 @@ export function SectionSpacer() { } export const Section = styled.div` - margin-bottom: 24px; + margin-bottom: 16px; &:last-of-type { margin-bottom: 0; } @@ -63,7 +63,7 @@ export const Section = styled.div` export type SectionLinkProps = EuiListGroupItemProps; export function SectionLink(props: SectionLinkProps) { - return <EuiListGroupItem style={{ padding: 0 }} size={'s'} {...props} />; + return <EuiListGroupItem style={{ padding: 0 }} size={'xs'} {...props} />; } export function ActionMenuDivider() { From 84d49f11238c76c806b360a28f1d579dde38ab16 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet <pierre.gayvallet@elastic.co> Date: Mon, 1 Feb 2021 11:03:44 +0100 Subject: [PATCH 151/163] [SOM] display invalid references in the relationship flyout (#88814) * return invalid relations and display them in SOM * add FTR test --- .../saved_objects_management/common/index.ts | 9 +- .../saved_objects_management/common/types.ts | 16 +- .../public/lib/get_relationships.test.ts | 9 +- .../public/lib/get_relationships.ts | 6 +- .../__snapshots__/relationships.test.tsx.snap | 1097 ++++++++++------- .../components/relationships.test.tsx | 265 ++-- .../components/relationships.tsx | 179 ++- .../saved_objects_management/public/types.ts | 9 +- .../server/lib/find_relationships.test.ts | 227 +++- .../server/lib/find_relationships.ts | 73 +- .../server/routes/relationships.ts | 4 +- .../saved_objects_management/server/types.ts | 9 +- .../saved_objects_management/relationships.ts | 106 +- .../saved_objects/relationships/data.json | 190 +++ .../saved_objects/relationships/data.json.gz | Bin 1385 -> 0 bytes .../saved_objects/relationships/mappings.json | 16 +- .../apps/saved_objects_management/index.ts | 1 + .../show_relationships.ts | 52 + .../show_relationships/data.json | 36 + .../show_relationships/mappings.json | 473 +++++++ .../management/saved_objects_page.ts | 16 + 21 files changed, 2058 insertions(+), 735 deletions(-) create mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json delete mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz create mode 100644 test/functional/apps/saved_objects_management/show_relationships.ts create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json diff --git a/src/plugins/saved_objects_management/common/index.ts b/src/plugins/saved_objects_management/common/index.ts index a8395e602979c..8850899e38958 100644 --- a/src/plugins/saved_objects_management/common/index.ts +++ b/src/plugins/saved_objects_management/common/index.ts @@ -6,4 +6,11 @@ * Public License, v 1. */ -export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types'; +export { + SavedObjectWithMetadata, + SavedObjectMetadata, + SavedObjectRelation, + SavedObjectRelationKind, + SavedObjectInvalidRelation, + SavedObjectGetRelationshipsResponse, +} from './types'; diff --git a/src/plugins/saved_objects_management/common/types.ts b/src/plugins/saved_objects_management/common/types.ts index 8618cf4332acf..e100dfc6b23e6 100644 --- a/src/plugins/saved_objects_management/common/types.ts +++ b/src/plugins/saved_objects_management/common/types.ts @@ -28,12 +28,26 @@ export type SavedObjectWithMetadata<T = unknown> = SavedObject<T> & { meta: SavedObjectMetadata; }; +export type SavedObjectRelationKind = 'child' | 'parent'; + /** * Represents a relation between two {@link SavedObject | saved object} */ export interface SavedObjectRelation { id: string; type: string; - relationship: 'child' | 'parent'; + relationship: SavedObjectRelationKind; meta: SavedObjectMetadata; } + +export interface SavedObjectInvalidRelation { + id: string; + type: string; + relationship: SavedObjectRelationKind; + error: string; +} + +export interface SavedObjectGetRelationshipsResponse { + relations: SavedObjectRelation[]; + invalidRelations: SavedObjectInvalidRelation[]; +} diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts index b609fac67dac1..4454907f530fe 100644 --- a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts +++ b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts @@ -6,6 +6,7 @@ * Public License, v 1. */ +import { SavedObjectGetRelationshipsResponse } from '../types'; import { httpServiceMock } from '../../../../core/public/mocks'; import { getRelationships } from './get_relationships'; @@ -22,13 +23,17 @@ describe('getRelationships', () => { }); it('should handle successful responses', async () => { - httpMock.get.mockResolvedValue([1, 2]); + const serverResponse: SavedObjectGetRelationshipsResponse = { + relations: [], + invalidRelations: [], + }; + httpMock.get.mockResolvedValue(serverResponse); const response = await getRelationships(httpMock, 'dashboard', '1', [ 'search', 'index-pattern', ]); - expect(response).toEqual([1, 2]); + expect(response).toEqual(serverResponse); }); it('should handle errors', async () => { diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.ts index 0eb97e1052fa4..69aeb6fbf580b 100644 --- a/src/plugins/saved_objects_management/public/lib/get_relationships.ts +++ b/src/plugins/saved_objects_management/public/lib/get_relationships.ts @@ -8,19 +8,19 @@ import { HttpStart } from 'src/core/public'; import { get } from 'lodash'; -import { SavedObjectRelation } from '../types'; +import { SavedObjectGetRelationshipsResponse } from '../types'; export async function getRelationships( http: HttpStart, type: string, id: string, savedObjectTypes: string[] -): Promise<SavedObjectRelation[]> { +): Promise<SavedObjectGetRelationshipsResponse> { const url = `/api/kibana/management/saved_objects/relationships/${encodeURIComponent( type )}/${encodeURIComponent(id)}`; try { - return await http.get<SavedObjectRelation[]>(url, { + return await http.get<SavedObjectGetRelationshipsResponse>(url, { query: { savedObjectTypes, }, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap index 15e5cb89b622c..c39263f304249 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap @@ -28,133 +28,131 @@ exports[`Relationships should render dashboards normally 1`] = ` </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> - <div> - <EuiCallOut> - <p> - Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children. - </p> - </EuiCallOut> - <EuiSpacer /> - <EuiInMemoryTable - columns={ - Array [ - Object { - "align": "center", - "description": "Type of the saved object", - "field": "type", - "name": "Type", - "render": [Function], - "sortable": false, - "width": "50px", + <EuiCallOut> + <p> + Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children. + </p> + </EuiCallOut> + <EuiSpacer /> + <EuiInMemoryTable + columns={ + Array [ + Object { + "align": "center", + "description": "Type of the saved object", + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + "width": "50px", + }, + Object { + "data-test-subj": "directRelationship", + "dataType": "string", + "field": "relationship", + "name": "Direct relationship", + "render": [Function], + "sortable": false, + "width": "125px", + }, + Object { + "dataType": "string", + "description": "Title of the saved object", + "field": "meta.title", + "name": "Title", + "render": [Function], + "sortable": false, + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "data-test-subj": "relationshipsTableAction-inspect", + "description": "Inspect this saved object", + "icon": "inspect", + "name": "Inspect", + "onClick": [Function], + "type": "icon", + }, + ], + "name": "Actions", + }, + ] + } + items={ + Array [ + Object { + "id": "1", + "meta": Object { + "editUrl": "/management/kibana/objects/savedVisualizations/1", + "icon": "visualizeApp", + "inAppUrl": Object { + "path": "/app/visualize#/edit/1", + "uiCapabilitiesPath": "visualize.show", + }, + "title": "My Visualization Title 1", }, + "relationship": "child", + "type": "visualization", + }, + Object { + "id": "2", + "meta": Object { + "editUrl": "/management/kibana/objects/savedVisualizations/2", + "icon": "visualizeApp", + "inAppUrl": Object { + "path": "/app/visualize#/edit/2", + "uiCapabilitiesPath": "visualize.show", + }, + "title": "My Visualization Title 2", + }, + "relationship": "child", + "type": "visualization", + }, + ] + } + pagination={true} + responsive={true} + rowProps={[Function]} + search={ + Object { + "filters": Array [ Object { - "data-test-subj": "directRelationship", - "dataType": "string", "field": "relationship", + "multiSelect": "or", "name": "Direct relationship", - "render": [Function], - "sortable": false, - "width": "125px", - }, - Object { - "dataType": "string", - "description": "Title of the saved object", - "field": "meta.title", - "name": "Title", - "render": [Function], - "sortable": false, - }, - Object { - "actions": Array [ + "options": Array [ Object { - "available": [Function], - "data-test-subj": "relationshipsTableAction-inspect", - "description": "Inspect this saved object", - "icon": "inspect", - "name": "Inspect", - "onClick": [Function], - "type": "icon", + "name": "parent", + "value": "parent", + "view": "Parent", }, - ], - "name": "Actions", - }, - ] - } - items={ - Array [ - Object { - "id": "1", - "meta": Object { - "editUrl": "/management/kibana/objects/savedVisualizations/1", - "icon": "visualizeApp", - "inAppUrl": Object { - "path": "/app/visualize#/edit/1", - "uiCapabilitiesPath": "visualize.show", + Object { + "name": "child", + "value": "child", + "view": "Child", }, - "title": "My Visualization Title 1", - }, - "relationship": "child", - "type": "visualization", + ], + "type": "field_value_selection", }, Object { - "id": "2", - "meta": Object { - "editUrl": "/management/kibana/objects/savedVisualizations/2", - "icon": "visualizeApp", - "inAppUrl": Object { - "path": "/app/visualize#/edit/2", - "uiCapabilitiesPath": "visualize.show", + "field": "type", + "multiSelect": "or", + "name": "Type", + "options": Array [ + Object { + "name": "visualization", + "value": "visualization", + "view": "visualization", }, - "title": "My Visualization Title 2", - }, - "relationship": "child", - "type": "visualization", + ], + "type": "field_value_selection", }, - ] - } - pagination={true} - responsive={true} - rowProps={[Function]} - search={ - Object { - "filters": Array [ - Object { - "field": "relationship", - "multiSelect": "or", - "name": "Direct relationship", - "options": Array [ - Object { - "name": "parent", - "value": "parent", - "view": "Parent", - }, - Object { - "name": "child", - "value": "child", - "view": "Child", - }, - ], - "type": "field_value_selection", - }, - Object { - "field": "type", - "multiSelect": "or", - "name": "Type", - "options": Array [ - Object { - "name": "visualization", - "value": "visualization", - "view": "visualization", - }, - ], - "type": "field_value_selection", - }, - ], - } + ], } - tableLayout="fixed" - /> - </div> + } + tableLayout="fixed" + /> </EuiFlyoutBody> </EuiFlyout> `; @@ -231,138 +229,315 @@ exports[`Relationships should render index patterns normally 1`] = ` </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> - <div> - <EuiCallOut> - <p> - Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children. - </p> - </EuiCallOut> - <EuiSpacer /> - <EuiInMemoryTable - columns={ - Array [ - Object { - "align": "center", - "description": "Type of the saved object", - "field": "type", - "name": "Type", - "render": [Function], - "sortable": false, - "width": "50px", + <EuiCallOut> + <p> + Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children. + </p> + </EuiCallOut> + <EuiSpacer /> + <EuiInMemoryTable + columns={ + Array [ + Object { + "align": "center", + "description": "Type of the saved object", + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + "width": "50px", + }, + Object { + "data-test-subj": "directRelationship", + "dataType": "string", + "field": "relationship", + "name": "Direct relationship", + "render": [Function], + "sortable": false, + "width": "125px", + }, + Object { + "dataType": "string", + "description": "Title of the saved object", + "field": "meta.title", + "name": "Title", + "render": [Function], + "sortable": false, + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "data-test-subj": "relationshipsTableAction-inspect", + "description": "Inspect this saved object", + "icon": "inspect", + "name": "Inspect", + "onClick": [Function], + "type": "icon", + }, + ], + "name": "Actions", + }, + ] + } + items={ + Array [ + Object { + "id": "1", + "meta": Object { + "editUrl": "/management/kibana/objects/savedSearches/1", + "icon": "search", + "inAppUrl": Object { + "path": "/app/discover#//1", + "uiCapabilitiesPath": "discover.show", + }, + "title": "My Search Title", }, + "relationship": "parent", + "type": "search", + }, + Object { + "id": "2", + "meta": Object { + "editUrl": "/management/kibana/objects/savedVisualizations/2", + "icon": "visualizeApp", + "inAppUrl": Object { + "path": "/app/visualize#/edit/2", + "uiCapabilitiesPath": "visualize.show", + }, + "title": "My Visualization Title", + }, + "relationship": "parent", + "type": "visualization", + }, + ] + } + pagination={true} + responsive={true} + rowProps={[Function]} + search={ + Object { + "filters": Array [ Object { - "data-test-subj": "directRelationship", - "dataType": "string", "field": "relationship", + "multiSelect": "or", "name": "Direct relationship", - "render": [Function], - "sortable": false, - "width": "125px", - }, - Object { - "dataType": "string", - "description": "Title of the saved object", - "field": "meta.title", - "name": "Title", - "render": [Function], - "sortable": false, - }, - Object { - "actions": Array [ + "options": Array [ + Object { + "name": "parent", + "value": "parent", + "view": "Parent", + }, Object { - "available": [Function], - "data-test-subj": "relationshipsTableAction-inspect", - "description": "Inspect this saved object", - "icon": "inspect", - "name": "Inspect", - "onClick": [Function], - "type": "icon", + "name": "child", + "value": "child", + "view": "Child", }, ], - "name": "Actions", + "type": "field_value_selection", }, - ] - } - items={ - Array [ Object { - "id": "1", - "meta": Object { - "editUrl": "/management/kibana/objects/savedSearches/1", - "icon": "search", - "inAppUrl": Object { - "path": "/app/discover#//1", - "uiCapabilitiesPath": "discover.show", + "field": "type", + "multiSelect": "or", + "name": "Type", + "options": Array [ + Object { + "name": "search", + "value": "search", + "view": "search", }, - "title": "My Search Title", - }, - "relationship": "parent", - "type": "search", - }, - Object { - "id": "2", - "meta": Object { - "editUrl": "/management/kibana/objects/savedVisualizations/2", - "icon": "visualizeApp", - "inAppUrl": Object { - "path": "/app/visualize#/edit/2", - "uiCapabilitiesPath": "visualize.show", + Object { + "name": "visualization", + "value": "visualization", + "view": "visualization", }, - "title": "My Visualization Title", - }, - "relationship": "parent", - "type": "visualization", + ], + "type": "field_value_selection", }, - ] + ], } - pagination={true} - responsive={true} - rowProps={[Function]} - search={ + } + tableLayout="fixed" + /> + </EuiFlyoutBody> +</EuiFlyout> +`; + +exports[`Relationships should render invalid relations 1`] = ` +<EuiFlyout + onClose={[MockFunction]} +> + <EuiFlyoutHeader + hasBorder={true} + > + <EuiTitle + size="m" + > + <h2> + <EuiToolTip + content="index patterns" + delay="regular" + position="top" + > + <EuiIcon + aria-label="index patterns" + size="m" + type="indexPatternApp" + /> + </EuiToolTip> +    + MyIndexPattern* + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiCallOut + color="warning" + iconType="alert" + title="This saved object has some invalid relations." + /> + <EuiSpacer /> + <EuiInMemoryTable + columns={ + Array [ Object { - "filters": Array [ - Object { - "field": "relationship", - "multiSelect": "or", - "name": "Direct relationship", - "options": Array [ - Object { - "name": "parent", - "value": "parent", - "view": "Parent", - }, - Object { - "name": "child", - "value": "child", - "view": "Child", - }, - ], - "type": "field_value_selection", - }, + "data-test-subj": "relationshipsObjectType", + "description": "Type of the saved object", + "field": "type", + "name": "Type", + "sortable": false, + "width": "150px", + }, + Object { + "data-test-subj": "relationshipsObjectId", + "description": "Id of the saved object", + "field": "id", + "name": "Id", + "sortable": false, + "width": "150px", + }, + Object { + "data-test-subj": "directRelationship", + "dataType": "string", + "field": "relationship", + "name": "Direct relationship", + "render": [Function], + "sortable": false, + "width": "125px", + }, + Object { + "data-test-subj": "relationshipsError", + "description": "Error encountered with the relation", + "field": "error", + "name": "Error", + "sortable": false, + }, + ] + } + items={ + Array [ + Object { + "error": "Saved object [dashboard/1] not found", + "id": "1", + "relationship": "child", + "type": "dashboard", + }, + ] + } + pagination={true} + responsive={true} + rowProps={[Function]} + tableLayout="fixed" + /> + <EuiSpacer /> + <EuiCallOut> + <p> + Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children. + </p> + </EuiCallOut> + <EuiSpacer /> + <EuiInMemoryTable + columns={ + Array [ + Object { + "align": "center", + "description": "Type of the saved object", + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + "width": "50px", + }, + Object { + "data-test-subj": "directRelationship", + "dataType": "string", + "field": "relationship", + "name": "Direct relationship", + "render": [Function], + "sortable": false, + "width": "125px", + }, + Object { + "dataType": "string", + "description": "Title of the saved object", + "field": "meta.title", + "name": "Title", + "render": [Function], + "sortable": false, + }, + Object { + "actions": Array [ Object { - "field": "type", - "multiSelect": "or", - "name": "Type", - "options": Array [ - Object { - "name": "search", - "value": "search", - "view": "search", - }, - Object { - "name": "visualization", - "value": "visualization", - "view": "visualization", - }, - ], - "type": "field_value_selection", + "available": [Function], + "data-test-subj": "relationshipsTableAction-inspect", + "description": "Inspect this saved object", + "icon": "inspect", + "name": "Inspect", + "onClick": [Function], + "type": "icon", }, ], - } + "name": "Actions", + }, + ] + } + items={Array []} + pagination={true} + responsive={true} + rowProps={[Function]} + search={ + Object { + "filters": Array [ + Object { + "field": "relationship", + "multiSelect": "or", + "name": "Direct relationship", + "options": Array [ + Object { + "name": "parent", + "value": "parent", + "view": "Parent", + }, + Object { + "name": "child", + "value": "child", + "view": "Child", + }, + ], + "type": "field_value_selection", + }, + Object { + "field": "type", + "multiSelect": "or", + "name": "Type", + "options": Array [], + "type": "field_value_selection", + }, + ], } - tableLayout="fixed" - /> - </div> + } + tableLayout="fixed" + /> </EuiFlyoutBody> </EuiFlyout> `; @@ -395,138 +570,136 @@ exports[`Relationships should render searches normally 1`] = ` </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> - <div> - <EuiCallOut> - <p> - Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children. - </p> - </EuiCallOut> - <EuiSpacer /> - <EuiInMemoryTable - columns={ - Array [ - Object { - "align": "center", - "description": "Type of the saved object", - "field": "type", - "name": "Type", - "render": [Function], - "sortable": false, - "width": "50px", + <EuiCallOut> + <p> + Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children. + </p> + </EuiCallOut> + <EuiSpacer /> + <EuiInMemoryTable + columns={ + Array [ + Object { + "align": "center", + "description": "Type of the saved object", + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + "width": "50px", + }, + Object { + "data-test-subj": "directRelationship", + "dataType": "string", + "field": "relationship", + "name": "Direct relationship", + "render": [Function], + "sortable": false, + "width": "125px", + }, + Object { + "dataType": "string", + "description": "Title of the saved object", + "field": "meta.title", + "name": "Title", + "render": [Function], + "sortable": false, + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "data-test-subj": "relationshipsTableAction-inspect", + "description": "Inspect this saved object", + "icon": "inspect", + "name": "Inspect", + "onClick": [Function], + "type": "icon", + }, + ], + "name": "Actions", + }, + ] + } + items={ + Array [ + Object { + "id": "1", + "meta": Object { + "editUrl": "/management/kibana/indexPatterns/patterns/1", + "icon": "indexPatternApp", + "inAppUrl": Object { + "path": "/app/management/kibana/indexPatterns/patterns/1", + "uiCapabilitiesPath": "management.kibana.indexPatterns", + }, + "title": "My Index Pattern", }, + "relationship": "child", + "type": "index-pattern", + }, + Object { + "id": "2", + "meta": Object { + "editUrl": "/management/kibana/objects/savedVisualizations/2", + "icon": "visualizeApp", + "inAppUrl": Object { + "path": "/app/visualize#/edit/2", + "uiCapabilitiesPath": "visualize.show", + }, + "title": "My Visualization Title", + }, + "relationship": "parent", + "type": "visualization", + }, + ] + } + pagination={true} + responsive={true} + rowProps={[Function]} + search={ + Object { + "filters": Array [ Object { - "data-test-subj": "directRelationship", - "dataType": "string", "field": "relationship", + "multiSelect": "or", "name": "Direct relationship", - "render": [Function], - "sortable": false, - "width": "125px", - }, - Object { - "dataType": "string", - "description": "Title of the saved object", - "field": "meta.title", - "name": "Title", - "render": [Function], - "sortable": false, - }, - Object { - "actions": Array [ + "options": Array [ Object { - "available": [Function], - "data-test-subj": "relationshipsTableAction-inspect", - "description": "Inspect this saved object", - "icon": "inspect", - "name": "Inspect", - "onClick": [Function], - "type": "icon", + "name": "parent", + "value": "parent", + "view": "Parent", + }, + Object { + "name": "child", + "value": "child", + "view": "Child", }, ], - "name": "Actions", + "type": "field_value_selection", }, - ] - } - items={ - Array [ Object { - "id": "1", - "meta": Object { - "editUrl": "/management/kibana/indexPatterns/patterns/1", - "icon": "indexPatternApp", - "inAppUrl": Object { - "path": "/app/management/kibana/indexPatterns/patterns/1", - "uiCapabilitiesPath": "management.kibana.indexPatterns", + "field": "type", + "multiSelect": "or", + "name": "Type", + "options": Array [ + Object { + "name": "index-pattern", + "value": "index-pattern", + "view": "index-pattern", }, - "title": "My Index Pattern", - }, - "relationship": "child", - "type": "index-pattern", - }, - Object { - "id": "2", - "meta": Object { - "editUrl": "/management/kibana/objects/savedVisualizations/2", - "icon": "visualizeApp", - "inAppUrl": Object { - "path": "/app/visualize#/edit/2", - "uiCapabilitiesPath": "visualize.show", + Object { + "name": "visualization", + "value": "visualization", + "view": "visualization", }, - "title": "My Visualization Title", - }, - "relationship": "parent", - "type": "visualization", + ], + "type": "field_value_selection", }, - ] - } - pagination={true} - responsive={true} - rowProps={[Function]} - search={ - Object { - "filters": Array [ - Object { - "field": "relationship", - "multiSelect": "or", - "name": "Direct relationship", - "options": Array [ - Object { - "name": "parent", - "value": "parent", - "view": "Parent", - }, - Object { - "name": "child", - "value": "child", - "view": "Child", - }, - ], - "type": "field_value_selection", - }, - Object { - "field": "type", - "multiSelect": "or", - "name": "Type", - "options": Array [ - Object { - "name": "index-pattern", - "value": "index-pattern", - "view": "index-pattern", - }, - Object { - "name": "visualization", - "value": "visualization", - "view": "visualization", - }, - ], - "type": "field_value_selection", - }, - ], - } + ], } - tableLayout="fixed" - /> - </div> + } + tableLayout="fixed" + /> </EuiFlyoutBody> </EuiFlyout> `; @@ -559,133 +732,131 @@ exports[`Relationships should render visualizations normally 1`] = ` </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> - <div> - <EuiCallOut> - <p> - Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children. - </p> - </EuiCallOut> - <EuiSpacer /> - <EuiInMemoryTable - columns={ - Array [ - Object { - "align": "center", - "description": "Type of the saved object", - "field": "type", - "name": "Type", - "render": [Function], - "sortable": false, - "width": "50px", + <EuiCallOut> + <p> + Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children. + </p> + </EuiCallOut> + <EuiSpacer /> + <EuiInMemoryTable + columns={ + Array [ + Object { + "align": "center", + "description": "Type of the saved object", + "field": "type", + "name": "Type", + "render": [Function], + "sortable": false, + "width": "50px", + }, + Object { + "data-test-subj": "directRelationship", + "dataType": "string", + "field": "relationship", + "name": "Direct relationship", + "render": [Function], + "sortable": false, + "width": "125px", + }, + Object { + "dataType": "string", + "description": "Title of the saved object", + "field": "meta.title", + "name": "Title", + "render": [Function], + "sortable": false, + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "data-test-subj": "relationshipsTableAction-inspect", + "description": "Inspect this saved object", + "icon": "inspect", + "name": "Inspect", + "onClick": [Function], + "type": "icon", + }, + ], + "name": "Actions", + }, + ] + } + items={ + Array [ + Object { + "id": "1", + "meta": Object { + "editUrl": "/management/kibana/objects/savedDashboards/1", + "icon": "dashboardApp", + "inAppUrl": Object { + "path": "/app/kibana#/dashboard/1", + "uiCapabilitiesPath": "dashboard.show", + }, + "title": "My Dashboard 1", }, + "relationship": "parent", + "type": "dashboard", + }, + Object { + "id": "2", + "meta": Object { + "editUrl": "/management/kibana/objects/savedDashboards/2", + "icon": "dashboardApp", + "inAppUrl": Object { + "path": "/app/kibana#/dashboard/2", + "uiCapabilitiesPath": "dashboard.show", + }, + "title": "My Dashboard 2", + }, + "relationship": "parent", + "type": "dashboard", + }, + ] + } + pagination={true} + responsive={true} + rowProps={[Function]} + search={ + Object { + "filters": Array [ Object { - "data-test-subj": "directRelationship", - "dataType": "string", "field": "relationship", + "multiSelect": "or", "name": "Direct relationship", - "render": [Function], - "sortable": false, - "width": "125px", - }, - Object { - "dataType": "string", - "description": "Title of the saved object", - "field": "meta.title", - "name": "Title", - "render": [Function], - "sortable": false, - }, - Object { - "actions": Array [ + "options": Array [ Object { - "available": [Function], - "data-test-subj": "relationshipsTableAction-inspect", - "description": "Inspect this saved object", - "icon": "inspect", - "name": "Inspect", - "onClick": [Function], - "type": "icon", + "name": "parent", + "value": "parent", + "view": "Parent", }, - ], - "name": "Actions", - }, - ] - } - items={ - Array [ - Object { - "id": "1", - "meta": Object { - "editUrl": "/management/kibana/objects/savedDashboards/1", - "icon": "dashboardApp", - "inAppUrl": Object { - "path": "/app/kibana#/dashboard/1", - "uiCapabilitiesPath": "dashboard.show", + Object { + "name": "child", + "value": "child", + "view": "Child", }, - "title": "My Dashboard 1", - }, - "relationship": "parent", - "type": "dashboard", + ], + "type": "field_value_selection", }, Object { - "id": "2", - "meta": Object { - "editUrl": "/management/kibana/objects/savedDashboards/2", - "icon": "dashboardApp", - "inAppUrl": Object { - "path": "/app/kibana#/dashboard/2", - "uiCapabilitiesPath": "dashboard.show", + "field": "type", + "multiSelect": "or", + "name": "Type", + "options": Array [ + Object { + "name": "dashboard", + "value": "dashboard", + "view": "dashboard", }, - "title": "My Dashboard 2", - }, - "relationship": "parent", - "type": "dashboard", + ], + "type": "field_value_selection", }, - ] + ], } - pagination={true} - responsive={true} - rowProps={[Function]} - search={ - Object { - "filters": Array [ - Object { - "field": "relationship", - "multiSelect": "or", - "name": "Direct relationship", - "options": Array [ - Object { - "name": "parent", - "value": "parent", - "view": "Parent", - }, - Object { - "name": "child", - "value": "child", - "view": "Child", - }, - ], - "type": "field_value_selection", - }, - Object { - "field": "type", - "multiSelect": "or", - "name": "Type", - "options": Array [ - Object { - "name": "dashboard", - "value": "dashboard", - "view": "dashboard", - }, - ], - "type": "field_value_selection", - }, - ], - } - } - tableLayout="fixed" - /> - </div> + } + tableLayout="fixed" + /> </EuiFlyoutBody> </EuiFlyout> `; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx index 72a4b0f2788fa..e590520193bba 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx @@ -25,36 +25,39 @@ describe('Relationships', () => { goInspectObject: () => {}, canGoInApp: () => true, basePath: httpServiceMock.createSetupContract().basePath, - getRelationships: jest.fn().mockImplementation(() => [ - { - type: 'search', - id: '1', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedSearches/1', - icon: 'search', - inAppUrl: { - path: '/app/discover#//1', - uiCapabilitiesPath: 'discover.show', + getRelationships: jest.fn().mockImplementation(() => ({ + relations: [ + { + type: 'search', + id: '1', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedSearches/1', + icon: 'search', + inAppUrl: { + path: '/app/discover#//1', + uiCapabilitiesPath: 'discover.show', + }, + title: 'My Search Title', }, - title: 'My Search Title', }, - }, - { - type: 'visualization', - id: '2', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedVisualizations/2', - icon: 'visualizeApp', - inAppUrl: { - path: '/app/visualize#/edit/2', - uiCapabilitiesPath: 'visualize.show', + { + type: 'visualization', + id: '2', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedVisualizations/2', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/visualize#/edit/2', + uiCapabilitiesPath: 'visualize.show', + }, + title: 'My Visualization Title', }, - title: 'My Visualization Title', }, - }, - ]), + ], + invalidRelations: [], + })), savedObject: { id: '1', type: 'index-pattern', @@ -92,36 +95,39 @@ describe('Relationships', () => { goInspectObject: () => {}, canGoInApp: () => true, basePath: httpServiceMock.createSetupContract().basePath, - getRelationships: jest.fn().mockImplementation(() => [ - { - type: 'index-pattern', - id: '1', - relationship: 'child', - meta: { - editUrl: '/management/kibana/indexPatterns/patterns/1', - icon: 'indexPatternApp', - inAppUrl: { - path: '/app/management/kibana/indexPatterns/patterns/1', - uiCapabilitiesPath: 'management.kibana.indexPatterns', + getRelationships: jest.fn().mockImplementation(() => ({ + relations: [ + { + type: 'index-pattern', + id: '1', + relationship: 'child', + meta: { + editUrl: '/management/kibana/indexPatterns/patterns/1', + icon: 'indexPatternApp', + inAppUrl: { + path: '/app/management/kibana/indexPatterns/patterns/1', + uiCapabilitiesPath: 'management.kibana.indexPatterns', + }, + title: 'My Index Pattern', }, - title: 'My Index Pattern', }, - }, - { - type: 'visualization', - id: '2', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedVisualizations/2', - icon: 'visualizeApp', - inAppUrl: { - path: '/app/visualize#/edit/2', - uiCapabilitiesPath: 'visualize.show', + { + type: 'visualization', + id: '2', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedVisualizations/2', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/visualize#/edit/2', + uiCapabilitiesPath: 'visualize.show', + }, + title: 'My Visualization Title', }, - title: 'My Visualization Title', }, - }, - ]), + ], + invalidRelations: [], + })), savedObject: { id: '1', type: 'search', @@ -159,36 +165,39 @@ describe('Relationships', () => { goInspectObject: () => {}, canGoInApp: () => true, basePath: httpServiceMock.createSetupContract().basePath, - getRelationships: jest.fn().mockImplementation(() => [ - { - type: 'dashboard', - id: '1', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedDashboards/1', - icon: 'dashboardApp', - inAppUrl: { - path: '/app/kibana#/dashboard/1', - uiCapabilitiesPath: 'dashboard.show', + getRelationships: jest.fn().mockImplementation(() => ({ + relations: [ + { + type: 'dashboard', + id: '1', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedDashboards/1', + icon: 'dashboardApp', + inAppUrl: { + path: '/app/kibana#/dashboard/1', + uiCapabilitiesPath: 'dashboard.show', + }, + title: 'My Dashboard 1', }, - title: 'My Dashboard 1', }, - }, - { - type: 'dashboard', - id: '2', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedDashboards/2', - icon: 'dashboardApp', - inAppUrl: { - path: '/app/kibana#/dashboard/2', - uiCapabilitiesPath: 'dashboard.show', + { + type: 'dashboard', + id: '2', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedDashboards/2', + icon: 'dashboardApp', + inAppUrl: { + path: '/app/kibana#/dashboard/2', + uiCapabilitiesPath: 'dashboard.show', + }, + title: 'My Dashboard 2', }, - title: 'My Dashboard 2', }, - }, - ]), + ], + invalidRelations: [], + })), savedObject: { id: '1', type: 'visualization', @@ -226,36 +235,39 @@ describe('Relationships', () => { goInspectObject: () => {}, canGoInApp: () => true, basePath: httpServiceMock.createSetupContract().basePath, - getRelationships: jest.fn().mockImplementation(() => [ - { - type: 'visualization', - id: '1', - relationship: 'child', - meta: { - editUrl: '/management/kibana/objects/savedVisualizations/1', - icon: 'visualizeApp', - inAppUrl: { - path: '/app/visualize#/edit/1', - uiCapabilitiesPath: 'visualize.show', + getRelationships: jest.fn().mockImplementation(() => ({ + relations: [ + { + type: 'visualization', + id: '1', + relationship: 'child', + meta: { + editUrl: '/management/kibana/objects/savedVisualizations/1', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/visualize#/edit/1', + uiCapabilitiesPath: 'visualize.show', + }, + title: 'My Visualization Title 1', }, - title: 'My Visualization Title 1', }, - }, - { - type: 'visualization', - id: '2', - relationship: 'child', - meta: { - editUrl: '/management/kibana/objects/savedVisualizations/2', - icon: 'visualizeApp', - inAppUrl: { - path: '/app/visualize#/edit/2', - uiCapabilitiesPath: 'visualize.show', + { + type: 'visualization', + id: '2', + relationship: 'child', + meta: { + editUrl: '/management/kibana/objects/savedVisualizations/2', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/visualize#/edit/2', + uiCapabilitiesPath: 'visualize.show', + }, + title: 'My Visualization Title 2', }, - title: 'My Visualization Title 2', }, - }, - ]), + ], + invalidRelations: [], + })), savedObject: { id: '1', type: 'dashboard', @@ -324,4 +336,49 @@ describe('Relationships', () => { expect(props.getRelationships).toHaveBeenCalled(); expect(component).toMatchSnapshot(); }); + + it('should render invalid relations', async () => { + const props: RelationshipsProps = { + goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, + getRelationships: jest.fn().mockImplementation(() => ({ + relations: [], + invalidRelations: [ + { + id: '1', + type: 'dashboard', + relationship: 'child', + error: 'Saved object [dashboard/1] not found', + }, + ], + })), + savedObject: { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + meta: { + title: 'MyIndexPattern*', + icon: 'indexPatternApp', + editUrl: '#/management/kibana/indexPatterns/patterns/1', + inAppUrl: { + path: '/management/kibana/indexPatterns/patterns/1', + uiCapabilitiesPath: 'management.kibana.indexPatterns', + }, + }, + }, + close: jest.fn(), + }; + + const component = shallowWithI18nProvider(<Relationships {...props} />); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(props.getRelationships).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index 2d62699b6f1f2..aee61f7bc9c7a 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -26,11 +26,17 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { IBasePath } from 'src/core/public'; import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; -import { SavedObjectWithMetadata, SavedObjectRelation } from '../../../types'; +import { + SavedObjectWithMetadata, + SavedObjectRelationKind, + SavedObjectRelation, + SavedObjectInvalidRelation, + SavedObjectGetRelationshipsResponse, +} from '../../../types'; export interface RelationshipsProps { basePath: IBasePath; - getRelationships: (type: string, id: string) => Promise<SavedObjectRelation[]>; + getRelationships: (type: string, id: string) => Promise<SavedObjectGetRelationshipsResponse>; savedObject: SavedObjectWithMetadata; close: () => void; goInspectObject: (obj: SavedObjectWithMetadata) => void; @@ -38,17 +44,47 @@ export interface RelationshipsProps { } export interface RelationshipsState { - relationships: SavedObjectRelation[]; + relations: SavedObjectRelation[]; + invalidRelations: SavedObjectInvalidRelation[]; isLoading: boolean; error?: string; } +const relationshipColumn = { + field: 'relationship', + name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnRelationshipName', { + defaultMessage: 'Direct relationship', + }), + dataType: 'string', + sortable: false, + width: '125px', + 'data-test-subj': 'directRelationship', + render: (relationship: SavedObjectRelationKind) => { + return ( + <EuiText size="s"> + {relationship === 'parent' ? ( + <FormattedMessage + id="savedObjectsManagement.objectsTable.relationships.columnRelationship.parentAsValue" + defaultMessage="Parent" + /> + ) : ( + <FormattedMessage + id="savedObjectsManagement.objectsTable.relationships.columnRelationship.childAsValue" + defaultMessage="Child" + /> + )} + </EuiText> + ); + }, +}; + export class Relationships extends Component<RelationshipsProps, RelationshipsState> { constructor(props: RelationshipsProps) { super(props); this.state = { - relationships: [], + relations: [], + invalidRelations: [], isLoading: false, error: undefined, }; @@ -70,8 +106,11 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt this.setState({ isLoading: true }); try { - const relationships = await getRelationships(savedObject.type, savedObject.id); - this.setState({ relationships, isLoading: false, error: undefined }); + const { relations, invalidRelations } = await getRelationships( + savedObject.type, + savedObject.id + ); + this.setState({ relations, invalidRelations, isLoading: false, error: undefined }); } catch (err) { this.setState({ error: err.message, isLoading: false }); } @@ -99,9 +138,83 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt ); } - renderRelationships() { - const { goInspectObject, savedObject, basePath } = this.props; - const { relationships, isLoading, error } = this.state; + renderInvalidRelationship() { + const { invalidRelations } = this.state; + if (!invalidRelations.length) { + return null; + } + + const columns = [ + { + field: 'type', + name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTypeName', { + defaultMessage: 'Type', + }), + width: '150px', + description: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnTypeDescription', + { defaultMessage: 'Type of the saved object' } + ), + sortable: false, + 'data-test-subj': 'relationshipsObjectType', + }, + { + field: 'id', + name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnIdName', { + defaultMessage: 'Id', + }), + width: '150px', + description: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnIdDescription', + { defaultMessage: 'Id of the saved object' } + ), + sortable: false, + 'data-test-subj': 'relationshipsObjectId', + }, + relationshipColumn, + { + field: 'error', + name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnErrorName', { + defaultMessage: 'Error', + }), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnErrorDescription', + { defaultMessage: 'Error encountered with the relation' } + ), + sortable: false, + 'data-test-subj': 'relationshipsError', + }, + ]; + + return ( + <> + <EuiCallOut + color="warning" + iconType="alert" + title={i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.invalidRelationShip', + { + defaultMessage: 'This saved object has some invalid relations.', + } + )} + /> + <EuiSpacer /> + <EuiInMemoryTable + items={invalidRelations} + columns={columns as any} + pagination={true} + rowProps={() => ({ + 'data-test-subj': `invalidRelationshipsTableRow`, + })} + /> + <EuiSpacer /> + </> + ); + } + + renderRelationshipsTable() { + const { goInspectObject, basePath, savedObject } = this.props; + const { relations, isLoading, error } = this.state; if (error) { return this.renderError(); @@ -137,39 +250,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt ); }, }, - { - field: 'relationship', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.relationships.columnRelationshipName', - { defaultMessage: 'Direct relationship' } - ), - dataType: 'string', - sortable: false, - width: '125px', - 'data-test-subj': 'directRelationship', - render: (relationship: string) => { - if (relationship === 'parent') { - return ( - <EuiText size="s"> - <FormattedMessage - id="savedObjectsManagement.objectsTable.relationships.columnRelationship.parentAsValue" - defaultMessage="Parent" - /> - </EuiText> - ); - } - if (relationship === 'child') { - return ( - <EuiText size="s"> - <FormattedMessage - id="savedObjectsManagement.objectsTable.relationships.columnRelationship.childAsValue" - defaultMessage="Child" - /> - </EuiText> - ); - } - }, - }, + relationshipColumn, { field: 'meta.title', name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTitleName', { @@ -224,7 +305,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt ]; const filterTypesMap = new Map( - relationships.map((relationship) => [ + relations.map((relationship) => [ relationship.type, { value: relationship.type, @@ -277,7 +358,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt }; return ( - <div> + <> <EuiCallOut> <p> {i18n.translate( @@ -296,7 +377,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt </EuiCallOut> <EuiSpacer /> <EuiInMemoryTable - items={relationships} + items={relations} columns={columns as any} pagination={true} search={search} @@ -304,7 +385,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt 'data-test-subj': `relationshipsTableRow`, })} /> - </div> + </> ); } @@ -328,8 +409,10 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt </h2> </EuiTitle> </EuiFlyoutHeader> - - <EuiFlyoutBody>{this.renderRelationships()}</EuiFlyoutBody> + <EuiFlyoutBody> + {this.renderInvalidRelationship()} + {this.renderRelationshipsTable()} + </EuiFlyoutBody> </EuiFlyout> ); } diff --git a/src/plugins/saved_objects_management/public/types.ts b/src/plugins/saved_objects_management/public/types.ts index 37f239227475d..cdfa3c43e5af2 100644 --- a/src/plugins/saved_objects_management/public/types.ts +++ b/src/plugins/saved_objects_management/public/types.ts @@ -6,4 +6,11 @@ * Public License, v 1. */ -export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common'; +export { + SavedObjectMetadata, + SavedObjectWithMetadata, + SavedObjectRelationKind, + SavedObjectRelation, + SavedObjectInvalidRelation, + SavedObjectGetRelationshipsResponse, +} from '../common'; diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts index 631faf0c23c98..416be7d7e7426 100644 --- a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts @@ -6,10 +6,35 @@ * Public License, v 1. */ +import type { SavedObject, SavedObjectError } from 'src/core/types'; +import type { SavedObjectsFindResponse } from 'src/core/server'; import { findRelationships } from './find_relationships'; import { managementMock } from '../services/management.mock'; import { savedObjectsClientMock } from '../../../../core/server/mocks'; +const createObj = (parts: Partial<SavedObject<any>>): SavedObject<any> => ({ + id: 'id', + type: 'type', + attributes: {}, + references: [], + ...parts, +}); + +const createFindResponse = (objs: SavedObject[]): SavedObjectsFindResponse => ({ + saved_objects: objs.map((obj) => ({ ...obj, score: 1 })), + total: objs.length, + per_page: 20, + page: 1, +}); + +const createError = (parts: Partial<SavedObjectError>): SavedObjectError => ({ + error: 'error', + message: 'message', + metadata: {}, + statusCode: 404, + ...parts, +}); + describe('findRelationships', () => { let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>; let managementService: ReturnType<typeof managementMock.create>; @@ -19,7 +44,7 @@ describe('findRelationships', () => { managementService = managementMock.create(); }); - it('returns the child and parent references of the object', async () => { + it('calls the savedObjectClient APIs with the correct parameters', async () => { const type = 'dashboard'; const id = 'some-id'; const references = [ @@ -36,46 +61,35 @@ describe('findRelationships', () => { ]; const referenceTypes = ['some-type', 'another-type']; - savedObjectsClient.get.mockResolvedValue({ - id, - type, - attributes: {}, - references, - }); - + savedObjectsClient.get.mockResolvedValue( + createObj({ + id, + type, + references, + }) + ); savedObjectsClient.bulkGet.mockResolvedValue({ saved_objects: [ - { + createObj({ type: 'some-type', id: 'ref-1', - attributes: {}, - references: [], - }, - { + }), + createObj({ type: 'another-type', id: 'ref-2', - attributes: {}, - references: [], - }, + }), ], }); - - savedObjectsClient.find.mockResolvedValue({ - saved_objects: [ - { + savedObjectsClient.find.mockResolvedValue( + createFindResponse([ + createObj({ type: 'parent-type', id: 'parent-id', - attributes: {}, - score: 1, - references: [], - }, - ], - total: 1, - per_page: 20, - page: 1, - }); + }), + ]) + ); - const relationships = await findRelationships({ + await findRelationships({ type, id, size: 20, @@ -101,8 +115,63 @@ describe('findRelationships', () => { perPage: 20, type: referenceTypes, }); + }); + + it('returns the child and parent references of the object', async () => { + const type = 'dashboard'; + const id = 'some-id'; + const references = [ + { + type: 'some-type', + id: 'ref-1', + name: 'ref 1', + }, + { + type: 'another-type', + id: 'ref-2', + name: 'ref 2', + }, + ]; + const referenceTypes = ['some-type', 'another-type']; + + savedObjectsClient.get.mockResolvedValue( + createObj({ + id, + type, + references, + }) + ); + savedObjectsClient.bulkGet.mockResolvedValue({ + saved_objects: [ + createObj({ + type: 'some-type', + id: 'ref-1', + }), + createObj({ + type: 'another-type', + id: 'ref-2', + }), + ], + }); + savedObjectsClient.find.mockResolvedValue( + createFindResponse([ + createObj({ + type: 'parent-type', + id: 'parent-id', + }), + ]) + ); + + const { relations, invalidRelations } = await findRelationships({ + type, + id, + size: 20, + client: savedObjectsClient, + referenceTypes, + savedObjectsManagement: managementService, + }); - expect(relationships).toEqual([ + expect(relations).toEqual([ { id: 'ref-1', relationship: 'child', @@ -122,6 +191,70 @@ describe('findRelationships', () => { meta: expect.any(Object), }, ]); + expect(invalidRelations).toHaveLength(0); + }); + + it('returns the invalid relations', async () => { + const type = 'dashboard'; + const id = 'some-id'; + const references = [ + { + type: 'some-type', + id: 'ref-1', + name: 'ref 1', + }, + { + type: 'another-type', + id: 'ref-2', + name: 'ref 2', + }, + ]; + const referenceTypes = ['some-type', 'another-type']; + + savedObjectsClient.get.mockResolvedValue( + createObj({ + id, + type, + references, + }) + ); + const ref1Error = createError({ message: 'Not found' }); + savedObjectsClient.bulkGet.mockResolvedValue({ + saved_objects: [ + createObj({ + type: 'some-type', + id: 'ref-1', + error: ref1Error, + }), + createObj({ + type: 'another-type', + id: 'ref-2', + }), + ], + }); + savedObjectsClient.find.mockResolvedValue(createFindResponse([])); + + const { relations, invalidRelations } = await findRelationships({ + type, + id, + size: 20, + client: savedObjectsClient, + referenceTypes, + savedObjectsManagement: managementService, + }); + + expect(relations).toEqual([ + { + id: 'ref-2', + relationship: 'child', + type: 'another-type', + meta: expect.any(Object), + }, + ]); + + expect(invalidRelations).toEqual([ + { type: 'some-type', id: 'ref-1', relationship: 'child', error: ref1Error.message }, + ]); }); it('uses the management service to consolidate the relationship objects', async () => { @@ -144,32 +277,24 @@ describe('findRelationships', () => { uiCapabilitiesPath: 'uiCapabilitiesPath', }); - savedObjectsClient.get.mockResolvedValue({ - id, - type, - attributes: {}, - references, - }); - + savedObjectsClient.get.mockResolvedValue( + createObj({ + id, + type, + references, + }) + ); savedObjectsClient.bulkGet.mockResolvedValue({ saved_objects: [ - { + createObj({ type: 'some-type', id: 'ref-1', - attributes: {}, - references: [], - }, + }), ], }); + savedObjectsClient.find.mockResolvedValue(createFindResponse([])); - savedObjectsClient.find.mockResolvedValue({ - saved_objects: [], - total: 0, - per_page: 20, - page: 1, - }); - - const relationships = await findRelationships({ + const { relations } = await findRelationships({ type, id, size: 20, @@ -183,7 +308,7 @@ describe('findRelationships', () => { expect(managementService.getEditUrl).toHaveBeenCalledTimes(1); expect(managementService.getInAppUrl).toHaveBeenCalledTimes(1); - expect(relationships).toEqual([ + expect(relations).toEqual([ { id: 'ref-1', relationship: 'child', diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.ts index 0ceef484196a3..bc6568e73c4e2 100644 --- a/src/plugins/saved_objects_management/server/lib/find_relationships.ts +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.ts @@ -9,7 +9,11 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { injectMetaAttributes } from './inject_meta_attributes'; import { ISavedObjectsManagement } from '../services'; -import { SavedObjectRelation, SavedObjectWithMetadata } from '../types'; +import { + SavedObjectInvalidRelation, + SavedObjectWithMetadata, + SavedObjectGetRelationshipsResponse, +} from '../types'; export async function findRelationships({ type, @@ -25,17 +29,19 @@ export async function findRelationships({ client: SavedObjectsClientContract; referenceTypes: string[]; savedObjectsManagement: ISavedObjectsManagement; -}): Promise<SavedObjectRelation[]> { +}): Promise<SavedObjectGetRelationshipsResponse> { const { references = [] } = await client.get(type, id); // Use a map to avoid duplicates, it does happen but have a different "name" in the reference - const referencedToBulkGetOpts = new Map( - references.map((ref) => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }]) - ); + const childrenReferences = [ + ...new Map( + references.map((ref) => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }]) + ).values(), + ]; const [childReferencesResponse, parentReferencesResponse] = await Promise.all([ - referencedToBulkGetOpts.size > 0 - ? client.bulkGet([...referencedToBulkGetOpts.values()]) + childrenReferences.length > 0 + ? client.bulkGet(childrenReferences) : Promise.resolve({ saved_objects: [] }), client.find({ hasReference: { type, id }, @@ -44,28 +50,37 @@ export async function findRelationships({ }), ]); - return childReferencesResponse.saved_objects - .map((obj) => injectMetaAttributes(obj, savedObjectsManagement)) - .map(extractCommonProperties) - .map( - (obj) => - ({ - ...obj, - relationship: 'child', - } as SavedObjectRelation) - ) - .concat( - parentReferencesResponse.saved_objects - .map((obj) => injectMetaAttributes(obj, savedObjectsManagement)) - .map(extractCommonProperties) - .map( - (obj) => - ({ - ...obj, - relationship: 'parent', - } as SavedObjectRelation) - ) - ); + const invalidRelations: SavedObjectInvalidRelation[] = childReferencesResponse.saved_objects + .filter((obj) => Boolean(obj.error)) + .map((obj) => ({ + id: obj.id, + type: obj.type, + relationship: 'child', + error: obj.error!.message, + })); + + const relations = [ + ...childReferencesResponse.saved_objects + .filter((obj) => !obj.error) + .map((obj) => injectMetaAttributes(obj, savedObjectsManagement)) + .map(extractCommonProperties) + .map((obj) => ({ + ...obj, + relationship: 'child' as const, + })), + ...parentReferencesResponse.saved_objects + .map((obj) => injectMetaAttributes(obj, savedObjectsManagement)) + .map(extractCommonProperties) + .map((obj) => ({ + ...obj, + relationship: 'parent' as const, + })), + ]; + + return { + relations, + invalidRelations, + }; } function extractCommonProperties(savedObject: SavedObjectWithMetadata) { diff --git a/src/plugins/saved_objects_management/server/routes/relationships.ts b/src/plugins/saved_objects_management/server/routes/relationships.ts index 3a52c973fde8d..5417ff2926120 100644 --- a/src/plugins/saved_objects_management/server/routes/relationships.ts +++ b/src/plugins/saved_objects_management/server/routes/relationships.ts @@ -38,7 +38,7 @@ export const registerRelationshipsRoute = ( ? req.query.savedObjectTypes : [req.query.savedObjectTypes]; - const relations = await findRelationships({ + const findRelationsResponse = await findRelationships({ type, id, client, @@ -48,7 +48,7 @@ export const registerRelationshipsRoute = ( }); return res.ok({ - body: relations, + body: findRelationsResponse, }); }) ); diff --git a/src/plugins/saved_objects_management/server/types.ts b/src/plugins/saved_objects_management/server/types.ts index 710bb5db7d1cb..562970d2d2dcd 100644 --- a/src/plugins/saved_objects_management/server/types.ts +++ b/src/plugins/saved_objects_management/server/types.ts @@ -12,4 +12,11 @@ export interface SavedObjectsManagementPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectsManagementPluginStart {} -export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common'; +export { + SavedObjectMetadata, + SavedObjectWithMetadata, + SavedObjectRelationKind, + SavedObjectRelation, + SavedObjectInvalidRelation, + SavedObjectGetRelationshipsResponse, +} from '../common'; diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index 185c6ded01de4..6dea461f790e8 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -14,23 +14,32 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const responseSchema = schema.arrayOf( - schema.object({ - id: schema.string(), - type: schema.string(), - relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]), - meta: schema.object({ - title: schema.string(), - icon: schema.string(), - editUrl: schema.string(), - inAppUrl: schema.object({ - path: schema.string(), - uiCapabilitiesPath: schema.string(), - }), - namespaceType: schema.string(), + const relationSchema = schema.object({ + id: schema.string(), + type: schema.string(), + relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]), + meta: schema.object({ + title: schema.string(), + icon: schema.string(), + editUrl: schema.string(), + inAppUrl: schema.object({ + path: schema.string(), + uiCapabilitiesPath: schema.string(), }), - }) - ); + namespaceType: schema.string(), + }), + }); + const invalidRelationSchema = schema.object({ + id: schema.string(), + type: schema.string(), + relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]), + error: schema.string(), + }); + + const responseSchema = schema.object({ + relations: schema.arrayOf(relationSchema), + invalidRelations: schema.arrayOf(invalidRelationSchema), + }); describe('relationships', () => { before(async () => { @@ -64,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) { .get(relationshipsUrl('search', '960372e0-3224-11e8-a572-ffca06da1357')) .expect(200); - expect(resp.body).to.eql([ + expect(resp.body.relations).to.eql([ { id: '8963ca30-3224-11e8-a572-ffca06da1357', type: 'index-pattern', @@ -108,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) { ) .expect(200); - expect(resp.body).to.eql([ + expect(resp.body.relations).to.eql([ { id: '8963ca30-3224-11e8-a572-ffca06da1357', type: 'index-pattern', @@ -145,8 +154,7 @@ export default function ({ getService }: FtrProviderContext) { ]); }); - // TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. - it.skip('should return 404 if search finds no results', async () => { + it('should return 404 if search finds no results', async () => { await supertest .get(relationshipsUrl('search', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')) .expect(404); @@ -169,7 +177,7 @@ export default function ({ getService }: FtrProviderContext) { .get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357')) .expect(200); - expect(resp.body).to.eql([ + expect(resp.body.relations).to.eql([ { id: 'add810b0-3224-11e8-a572-ffca06da1357', type: 'visualization', @@ -210,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) { .get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357', ['search'])) .expect(200); - expect(resp.body).to.eql([ + expect(resp.body.relations).to.eql([ { id: 'add810b0-3224-11e8-a572-ffca06da1357', type: 'visualization', @@ -246,8 +254,7 @@ export default function ({ getService }: FtrProviderContext) { ]); }); - // TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. - it.skip('should return 404 if dashboard finds no results', async () => { + it('should return 404 if dashboard finds no results', async () => { await supertest .get(relationshipsUrl('dashboard', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')) .expect(404); @@ -270,7 +277,7 @@ export default function ({ getService }: FtrProviderContext) { .get(relationshipsUrl('visualization', 'a42c0580-3224-11e8-a572-ffca06da1357')) .expect(200); - expect(resp.body).to.eql([ + expect(resp.body.relations).to.eql([ { id: '960372e0-3224-11e8-a572-ffca06da1357', type: 'search', @@ -313,7 +320,7 @@ export default function ({ getService }: FtrProviderContext) { ) .expect(200); - expect(resp.body).to.eql([ + expect(resp.body.relations).to.eql([ { id: '960372e0-3224-11e8-a572-ffca06da1357', type: 'search', @@ -356,7 +363,7 @@ export default function ({ getService }: FtrProviderContext) { .get(relationshipsUrl('index-pattern', '8963ca30-3224-11e8-a572-ffca06da1357')) .expect(200); - expect(resp.body).to.eql([ + expect(resp.body.relations).to.eql([ { id: '960372e0-3224-11e8-a572-ffca06da1357', type: 'search', @@ -399,7 +406,7 @@ export default function ({ getService }: FtrProviderContext) { ) .expect(200); - expect(resp.body).to.eql([ + expect(resp.body.relations).to.eql([ { id: '960372e0-3224-11e8-a572-ffca06da1357', type: 'search', @@ -425,5 +432,48 @@ export default function ({ getService }: FtrProviderContext) { .expect(404); }); }); + + describe('invalid references', () => { + it('should validate the response schema', async () => { + const resp = await supertest.get(relationshipsUrl('dashboard', 'invalid-refs')).expect(200); + + expect(() => { + responseSchema.validate(resp.body); + }).not.to.throwError(); + }); + + it('should return the invalid relations', async () => { + const resp = await supertest.get(relationshipsUrl('dashboard', 'invalid-refs')).expect(200); + + expect(resp.body).to.eql({ + invalidRelations: [ + { + error: 'Saved object [visualization/invalid-vis] not found', + id: 'invalid-vis', + relationship: 'child', + type: 'visualization', + }, + ], + relations: [ + { + id: 'add810b0-3224-11e8-a572-ffca06da1357', + meta: { + editUrl: + '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'visualize.show', + }, + namespaceType: 'single', + title: 'Visualization', + }, + relationship: 'child', + type: 'visualization', + }, + ], + }); + }); + }); }); } diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json new file mode 100644 index 0000000000000..21d84c4b55e55 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json @@ -0,0 +1,190 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "timelion-sheet:190f3e90-2ec3-11e8-ba48-69fc4e41e1f6", + "source": { + "type": "timelion-sheet", + "updated_at": "2018-03-23T17:53:30.872Z", + "timelion-sheet": { + "title": "New TimeLion Sheet", + "hits": 0, + "description": "", + "timelion_sheet": [ + ".es(*)" + ], + "timelion_interval": "auto", + "timelion_chart_height": 275, + "timelion_columns": 2, + "timelion_rows": 2, + "version": 1 + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "index-pattern:8963ca30-3224-11e8-a572-ffca06da1357", + "source": { + "type": "index-pattern", + "updated_at": "2018-03-28T01:08:34.290Z", + "index-pattern": { + "title": "saved_objects*", + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "config:7.0.0-alpha1", + "source": { + "type": "config", + "updated_at": "2018-03-28T01:08:39.248Z", + "config": { + "buildNum": 8467, + "telemetry:optIn": false, + "defaultIndex": "8963ca30-3224-11e8-a572-ffca06da1357" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "search:960372e0-3224-11e8-a572-ffca06da1357", + "source": { + "type": "search", + "updated_at": "2018-03-28T01:08:55.182Z", + "search": { + "title": "OneRecord", + "description": "", + "hits": 0, + "columns": [ + "_source" + ], + "sort": [ + "_score", + "desc" + ], + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"8963ca30-3224-11e8-a572-ffca06da1357\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"id:3\",\"language\":\"lucene\"},\"filter\":[]}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:a42c0580-3224-11e8-a572-ffca06da1357", + "source": { + "type": "visualization", + "updated_at": "2018-03-28T01:09:18.936Z", + "visualization": { + "title": "VisualizationFromSavedSearch", + "visState": "{\"title\":\"VisualizationFromSavedSearch\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "description": "", + "savedSearchId": "960372e0-3224-11e8-a572-ffca06da1357", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:add810b0-3224-11e8-a572-ffca06da1357", + "source": { + "type": "visualization", + "updated_at": "2018-03-28T01:09:35.163Z", + "visualization": { + "title": "Visualization", + "visState": "{\"title\":\"Visualization\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"8963ca30-3224-11e8-a572-ffca06da1357\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357", + "source": { + "type": "dashboard", + "updated_at": "2018-03-28T01:09:50.606Z", + "dashboard": { + "title": "Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"add810b0-3224-11e8-a572-ffca06da1357\",\"embeddableConfig\":{}},{\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"2\",\"type\":\"visualization\",\"id\":\"a42c0580-3224-11e8-a572-ffca06da1357\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "dashboard:invalid-refs", + "source": { + "type": "dashboard", + "updated_at": "2018-03-28T01:09:50.606Z", + "dashboard": { + "title": "Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[]", + "optionsJSON": "{}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + } + }, + "references": [ + { + "type":"visualization", + "id": "add810b0-3224-11e8-a572-ffca06da1357", + "name": "valid-ref" + }, + { + "type":"visualization", + "id": "invalid-vis", + "name": "missing-ref" + } + ] + } + } +} diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz deleted file mode 100644 index 0834567abb66b663079894089ed4edd91f1cf0b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1385 zcmV-v1(y0BiwFP!000026V+JVZ`(Eyf6rfGXfIn48~X5vthb^?hYW2R#6}+$2LUCX zWv;U5QB=~@(Eq+8C0mrECGvtGnI97Qcs$<me)qeRw<o=xCGR(21wD)M$U0SmTax5T zvc)g>m8BGZD22gy7Lt@`B_*dyDA^hk#?yYb0+4|-wU-`D?Y;|<*LNK7`ym<n{qb}e z4PoejvmEaXWIPv9eURZw(`coS>-mNf3G{|YrRCa=-?zQK>&=}>F!BP=9{3aY&szV$ zPJNPIlZig;9PWB^RQ!yJy;<WxR9i8bp_XlkC}fdf8;SaAzp1@D@Md@5)qV|E2ax^x z?l)^Mx^COaQV9Z6piGlo@>cWFiU@hL0v4~-Deh#{s>PFhohtX;wq?QZ4%co$WMx=R zB`i*Me~Xji<YfDN#OT%jhDeMv4gBfYi->3UJ=YzUfFYxa+g~mtVvi|tywT)oz%*<= zi5GuvJAv&7-f-YfZ38b&GwpE6$Sqpr;a?ER?46mNC4+>j8?~;s3o9jSSXjZrx?yx- zoi4PmT98S>(pbwPo~IIpHa?f20#pu`B*{RDfCx-=n5d0X<Vr^3SU^l<Q!0SaPlB&M z^5~mNMz*t3oHl(?5xyOFvWN?4x|8PX5X8~$?1TsY?8KcN(hzHUWC~xwrP7Z#lCeW9 z|Ho_{?TT6|uB{g%rHH3X76+4oJ+S*E*{q23H0zX`y3@^c;0}F*ZmRtao(Xf7(DQta zQhzv}n7j=MtU-$VfN$iPqNnm|&BnAOd4g+Iq@B3+#jdnWcrYE?-o%Ax5`1Z_^Hq;V z1IITffogv{rGHJ~5|D|g)ve37%mj6-ZFKyKI@())#)W*iK{29nSmjB(1*2UX(lQw{ z)u+DdHuVK0X@tJNkePPxkJ;CA6(bgU)gQ33yMRa6{R)SWL=7VElcX-<%C%bXcMjqn zzi#VCMJIu$jU*(Ea}t-NlH?Jj_*me=k|k0ROmKBw)R$1a7;0}>mXn12Br5R%8M=`@ z@}CLbhRu!`o(7ITn0jLa!%Z{oQ2u7>C=%5$m^G`Xv^A4>dX;v)U*G*>2Ab4gu{Me} zM38k>=5_<(qRgYC8^Ma-UEr+BNOFnerr8g01%b(;?7c$HXSju=v5wVInk;MUtU_j* zCkZZ7CJ@;r!j!0}OwPF^iD5>n@1OECDm!PsE@6e8M;)dHHPzB^$<d)es-mJbZ1>?- z?M*kg6|9LCDn4e>!6g(0Le;qIoaw7Jstj+xx-H}8jtv+;9r-G&Q+TF9egr4K5YHH8 z{cqgx2rs+_6Hw|qcK9kx;9)l#d(UBl<4eC;>#aD)Dx!4Gc_P`ynCU3}3^AnU?AK;z z_gIkzW>#XJzi?{K$aw~rhyXB&0jq<HSzUv_3xKpIdG8XaVflkntIRE|bDr)7cob*a zXjT79B)MvAm0a@{eu`@izOdw^ZOJXWIrLQZNvsK}&uEaAyw{hB8^ZV#(+zQ9{elMd z;bE+I7#s8vhr%om=kP<;Rj}l#oUxzE^4P|@e`Nye$~$jjJoz6m4JFws<V4UQoY>KX zJa<^$+w06QBYQBm%~_*1(atU(9~^P?Z)F>jVs-73tAHE}MnB@)V3{9PZthSGn5rm8 z`0%4D)BEZ_+u^=w44ezge2uHHjc1+h!Q(X9?e+ojRVCGh^vkNlw_r+D<$ciabY&Ht zc8p0&nnAh82jzARs>4kCNKn^i4!O>3W>hF8;`<!w<$%S%5D~L9t7&P)C|sxj<_c2v ruMJQ0hx+~U5;CdYlODbUKZjk8C5H#>(&bg?DMtAR@76`}lotR1KQp@| diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json index c670508247b1a..6dd4d198e0f67 100644 --- a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json +++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json @@ -12,6 +12,20 @@ "mappings": { "dynamic": "strict", "properties": { + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, "config": { "dynamic": "true", "properties": { @@ -280,4 +294,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/functional/apps/saved_objects_management/index.ts b/test/functional/apps/saved_objects_management/index.ts index 9491661de73ef..5e4eaefb7e9d1 100644 --- a/test/functional/apps/saved_objects_management/index.ts +++ b/test/functional/apps/saved_objects_management/index.ts @@ -12,5 +12,6 @@ export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderC describe('saved objects management', function savedObjectsManagementAppTestSuite() { this.tags('ciGroup7'); loadTestFile(require.resolve('./edit_saved_object')); + loadTestFile(require.resolve('./show_relationships')); }); } diff --git a/test/functional/apps/saved_objects_management/show_relationships.ts b/test/functional/apps/saved_objects_management/show_relationships.ts new file mode 100644 index 0000000000000..6f3fb5a4973e2 --- /dev/null +++ b/test/functional/apps/saved_objects_management/show_relationships.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']); + + describe('saved objects relationships flyout', () => { + beforeEach(async () => { + await esArchiver.load('saved_objects_management/show_relationships'); + }); + + afterEach(async () => { + await esArchiver.unload('saved_objects_management/show_relationships'); + }); + + it('displays the invalid references', async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + + const objects = await PageObjects.savedObjects.getRowTitles(); + expect(objects.includes('Dashboard with missing refs')).to.be(true); + + await PageObjects.savedObjects.clickRelationshipsByTitle('Dashboard with missing refs'); + + const invalidRelations = await PageObjects.savedObjects.getInvalidRelations(); + + expect(invalidRelations).to.eql([ + { + error: 'Saved object [visualization/missing-vis-ref] not found', + id: 'missing-vis-ref', + relationship: 'Child', + type: 'visualization', + }, + { + error: 'Saved object [dashboard/missing-dashboard-ref] not found', + id: 'missing-dashboard-ref', + relationship: 'Child', + type: 'dashboard', + }, + ]); + }); + }); +} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json new file mode 100644 index 0000000000000..4d5b969a3c931 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json @@ -0,0 +1,36 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:dash-with-missing-refs", + "source": { + "dashboard": { + "title": "Dashboard with missing refs", + "hits": 0, + "description": "", + "panelsJSON": "[]", + "optionsJSON": "{}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + } + }, + "type": "dashboard", + "references": [ + { + "type": "visualization", + "id": "missing-vis-ref", + "name": "some missing ref" + }, + { + "type": "dashboard", + "id": "missing-dashboard-ref", + "name": "some other missing ref" + } + ], + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json new file mode 100644 index 0000000000000..d53e6c96e883e --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json @@ -0,0 +1,473 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape", + "tree": "quadtree" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } +} diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 1cdf76ad58ef0..cf162f12df9d9 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -257,6 +257,22 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv }); } + async getInvalidRelations() { + const rows = await testSubjects.findAll('invalidRelationshipsTableRow'); + return mapAsync(rows, async (row) => { + const objectType = await row.findByTestSubject('relationshipsObjectType'); + const objectId = await row.findByTestSubject('relationshipsObjectId'); + const relationship = await row.findByTestSubject('directRelationship'); + const error = await row.findByTestSubject('relationshipsError'); + return { + type: await objectType.getVisibleText(), + id: await objectId.getVisibleText(), + relationship: await relationship.getVisibleText(), + error: await error.getVisibleText(), + }; + }); + } + async getTableSummary() { const table = await testSubjects.find('savedObjectsTable'); const $ = await table.parseDomContent(); From 61a51b568481abfba41f71781d24acfd4f65c7ee Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens <jloleysens@gmail.com> Date: Mon, 1 Feb 2021 11:14:46 +0100 Subject: [PATCH 152/163] [ILM] New copy for rollover and small refactor for timeline (#89422) * refactor timeline and relative ms calculation logic for easier use outside of edit_policy section * further refactor, move child component to own file in timeline, and clean up public API for relative timing calculation * added copy to call out variation in timing (slop) introduced by rollover * use separate copy for timeline * remove unused import * fix unresolved merge * implement copy feedback * added component integration for showing/hiding hot phase icon on timeline Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_policy/edit_policy.helpers.tsx | 1 + .../edit_policy/edit_policy.test.ts | 8 + .../components/phases/hot_phase/hot_phase.tsx | 5 + .../components/timeline/components/index.ts | 7 + .../components/timeline_phase_text.tsx | 28 ++ .../edit_policy/components/timeline/index.ts | 2 +- .../timeline/timeline.container.tsx | 33 +++ .../components/timeline/timeline.scss | 4 + .../components/timeline/timeline.tsx | 252 ++++++++++-------- .../sections/edit_policy/i18n_texts.ts | 7 + ...absolute_timing_to_relative_timing.test.ts | 9 +- .../lib/absolute_timing_to_relative_timing.ts | 78 +++--- .../sections/edit_policy/lib/index.ts | 5 +- 13 files changed, 288 insertions(+), 151 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 64b654b030236..d9256ec916ec8 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -251,6 +251,7 @@ export const setup = async (arg?: { appServicesContext: Partial<AppServicesConte setWaitForSnapshotPolicy, savePolicy, timeline: { + hasRolloverIndicator: () => exists('timelineHotPhaseRolloverToolTip'), hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), hasColdPhase: () => exists('ilmTimelineColdPhase'), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index bb96e8b4df239..05793a4bed581 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -843,5 +843,13 @@ describe('<EditPolicy />', () => { expect(actions.timeline.hasColdPhase()).toBe(true); expect(actions.timeline.hasDeletePhase()).toBe(true); }); + + test('show and hide rollover indicator on timeline', async () => { + const { actions } = testBed; + expect(actions.timeline.hasRolloverIndicator()).toBe(true); + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.timeline.hasRolloverIndicator()).toBe(false); + }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index fb7c9a80acba0..02de47f8c56ef 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -16,6 +16,7 @@ import { EuiTextColor, EuiSwitch, EuiIconTip, + EuiIcon, } from '@elastic/eui'; import { useFormData, UseField, SelectField, NumericField } from '../../../../../../shared_imports'; @@ -80,6 +81,10 @@ export const HotPhase: FunctionComponent = () => { </p> </EuiTextColor> <EuiSpacer /> + <EuiIcon type="iInCircle" /> +   + {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming} + <EuiSpacer /> <UseField<boolean> path={isUsingDefaultRolloverPath}> {(field) => ( <> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts new file mode 100644 index 0000000000000..1c9d5e1abc316 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts @@ -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 { TimelinePhaseText } from './timeline_phase_text'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx new file mode 100644 index 0000000000000..a44e0f2407c52 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx @@ -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 React, { FunctionComponent, ReactNode } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +export const TimelinePhaseText: FunctionComponent<{ + phaseName: ReactNode | string; + durationInPhase?: ReactNode | string; +}> = ({ phaseName, durationInPhase }) => ( + <EuiFlexGroup justifyContent="spaceBetween" gutterSize="none"> + <EuiFlexItem> + <EuiText size="s"> + <strong>{phaseName}</strong> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {typeof durationInPhase === 'string' ? ( + <EuiText size="s">{durationInPhase}</EuiText> + ) : ( + durationInPhase + )} + </EuiFlexItem> + </EuiFlexGroup> +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts index 4664429db37d7..7bcaa6584edf0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts @@ -3,4 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { Timeline } from './timeline'; +export { Timeline } from './timeline.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx new file mode 100644 index 0000000000000..75f53fcb25091 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +import { useFormData } from '../../../../../shared_imports'; + +import { formDataToAbsoluteTimings } from '../../lib'; + +import { useConfigurationIssues } from '../../form'; + +import { FormInternal } from '../../types'; + +import { Timeline as ViewComponent } from './timeline'; + +export const Timeline: FunctionComponent = () => { + const [formData] = useFormData<FormInternal>(); + const timings = formDataToAbsoluteTimings(formData); + const { isUsingRollover } = useConfigurationIssues(); + return ( + <ViewComponent + hotPhaseMinAge={timings.hot.min_age} + warmPhaseMinAge={timings.warm?.min_age} + coldPhaseMinAge={timings.cold?.min_age} + deletePhaseMinAge={timings.delete?.min_age} + isUsingRollover={isUsingRollover} + hasDeletePhase={Boolean(formData._meta?.delete?.enabled)} + /> + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss index 452221a29a991..7d65d2cd6b212 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss @@ -84,4 +84,8 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%); background-color: $euiColorVis1; } } + + &__rolloverIcon { + display: inline-block; + } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx index 40bab9c676de2..2e2db88e1384d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import React, { FunctionComponent, useMemo } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import { - EuiText, EuiIcon, EuiIconProps, EuiFlexGroup, @@ -16,18 +15,19 @@ import { } from '@elastic/eui'; import { PhasesExceptDelete } from '../../../../../../common/types'; -import { useFormData } from '../../../../../shared_imports'; - -import { FormInternal } from '../../types'; import { - calculateRelativeTimingMs, + calculateRelativeFromAbsoluteMilliseconds, normalizeTimingsToHumanReadable, PhaseAgeInMilliseconds, + AbsoluteTimings, } from '../../lib'; import './timeline.scss'; import { InfinityIconSvg } from './infinity_icon.svg'; +import { TimelinePhaseText } from './components'; + +const exists = (v: unknown) => v != null; const InfinityIcon: FunctionComponent<Omit<EuiIconProps, 'type'>> = (props) => ( <EuiIcon type={InfinityIconSvg} {...props} /> @@ -56,6 +56,13 @@ const i18nTexts = { hotPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle', { defaultMessage: 'Hot phase', }), + rolloverTooltip: i18n.translate( + 'xpack.indexLifecycleMgmt.timeline.hotPhaseRolloverToolTipContent', + { + defaultMessage: + 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.', + } + ), warmPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle', { defaultMessage: 'Warm phase', }), @@ -88,121 +95,136 @@ const calculateWidths = (inputs: PhaseAgeInMilliseconds) => { }; }; -const TimelinePhaseText: FunctionComponent<{ - phaseName: string; - durationInPhase?: React.ReactNode | string; -}> = ({ phaseName, durationInPhase }) => ( - <EuiFlexGroup justifyContent="spaceBetween" gutterSize="none"> - <EuiFlexItem> - <EuiText size="s"> - <strong>{phaseName}</strong> - </EuiText> - </EuiFlexItem> - <EuiFlexItem grow={false}> - {typeof durationInPhase === 'string' ? ( - <EuiText size="s">{durationInPhase}</EuiText> - ) : ( - durationInPhase - )} - </EuiFlexItem> - </EuiFlexGroup> -); - -export const Timeline: FunctionComponent = () => { - const [formData] = useFormData<FormInternal>(); - - const phaseTimingInMs = useMemo(() => { - return calculateRelativeTimingMs(formData); - }, [formData]); +interface Props { + hasDeletePhase: boolean; + /** + * For now we assume the hot phase does not have a min age + */ + hotPhaseMinAge: undefined; + isUsingRollover: boolean; + warmPhaseMinAge?: string; + coldPhaseMinAge?: string; + deletePhaseMinAge?: string; +} - const humanReadableTimings = useMemo(() => normalizeTimingsToHumanReadable(phaseTimingInMs), [ - phaseTimingInMs, - ]); - - const widths = calculateWidths(phaseTimingInMs); - - const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode => - phaseTimingInMs.phases[phase] === Infinity ? ( - <InfinityIcon aria-label={humanReadableTimings[phase]} /> - ) : ( - humanReadableTimings[phase] - ); - - return ( - <EuiFlexGroup gutterSize="s" direction="column" responsive={false}> - <EuiFlexItem> - <EuiTitle size="s"> - <h2> - {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', { - defaultMessage: 'Policy Timeline', - })} - </h2> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem> - <div - className="ilmTimeline" - ref={(el) => { - if (el) { - el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot); - el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null); - el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null); - } - }} - > - <EuiFlexGroup gutterSize="none" alignItems="flexStart" responsive={false}> - <EuiFlexItem> - <div className="ilmTimeline__phasesContainer"> - {/* These are the actual color bars for the timeline */} - <div - data-test-subj="ilmTimelineHotPhase" - className="ilmTimeline__phasesContainer__phase ilmTimeline__hotPhase" - > - <div className="ilmTimeline__colorBar ilmTimeline__hotPhase__colorBar" /> - <TimelinePhaseText - phaseName={i18nTexts.hotPhase} - durationInPhase={getDurationInPhaseContent('hot')} - /> - </div> - {formData._meta?.warm.enabled && ( +/** + * Display a timeline given ILM policy phase information. This component is re-usable and memo-ized + * and should not rely directly on any application-specific context. + */ +export const Timeline: FunctionComponent<Props> = memo( + ({ hasDeletePhase, isUsingRollover, ...phasesMinAge }) => { + const absoluteTimings: AbsoluteTimings = { + hot: { min_age: phasesMinAge.hotPhaseMinAge }, + warm: phasesMinAge.warmPhaseMinAge ? { min_age: phasesMinAge.warmPhaseMinAge } : undefined, + cold: phasesMinAge.coldPhaseMinAge ? { min_age: phasesMinAge.coldPhaseMinAge } : undefined, + delete: phasesMinAge.deletePhaseMinAge + ? { min_age: phasesMinAge.deletePhaseMinAge } + : undefined, + }; + + const phaseAgeInMilliseconds = calculateRelativeFromAbsoluteMilliseconds(absoluteTimings); + const humanReadableTimings = normalizeTimingsToHumanReadable(phaseAgeInMilliseconds); + + const widths = calculateWidths(phaseAgeInMilliseconds); + + const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode => + phaseAgeInMilliseconds.phases[phase] === Infinity ? ( + <InfinityIcon aria-label={humanReadableTimings[phase]} /> + ) : ( + humanReadableTimings[phase] + ); + + return ( + <EuiFlexGroup gutterSize="s" direction="column" responsive={false}> + <EuiFlexItem> + <EuiTitle size="s"> + <h2> + {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', { + defaultMessage: 'Policy Timeline', + })} + </h2> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem> + <div + className="ilmTimeline" + ref={(el) => { + if (el) { + el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot); + el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null); + el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null); + } + }} + > + <EuiFlexGroup gutterSize="none" alignItems="flexStart" responsive={false}> + <EuiFlexItem> + <div className="ilmTimeline__phasesContainer"> + {/* These are the actual color bars for the timeline */} <div - data-test-subj="ilmTimelineWarmPhase" - className="ilmTimeline__phasesContainer__phase ilmTimeline__warmPhase" + data-test-subj="ilmTimelineHotPhase" + className="ilmTimeline__phasesContainer__phase ilmTimeline__hotPhase" > - <div className="ilmTimeline__colorBar ilmTimeline__warmPhase__colorBar" /> + <div className="ilmTimeline__colorBar ilmTimeline__hotPhase__colorBar" /> <TimelinePhaseText - phaseName={i18nTexts.warmPhase} - durationInPhase={getDurationInPhaseContent('warm')} + phaseName={ + isUsingRollover ? ( + <> + {i18nTexts.hotPhase} +   + <div + className="ilmTimeline__rolloverIcon" + data-test-subj="timelineHotPhaseRolloverToolTip" + > + <EuiIconTip type="iInCircle" content={i18nTexts.rolloverTooltip} /> + </div> + </> + ) : ( + i18nTexts.hotPhase + ) + } + durationInPhase={getDurationInPhaseContent('hot')} /> </div> - )} - {formData._meta?.cold.enabled && ( + {exists(phaseAgeInMilliseconds.phases.warm) && ( + <div + data-test-subj="ilmTimelineWarmPhase" + className="ilmTimeline__phasesContainer__phase ilmTimeline__warmPhase" + > + <div className="ilmTimeline__colorBar ilmTimeline__warmPhase__colorBar" /> + <TimelinePhaseText + phaseName={i18nTexts.warmPhase} + durationInPhase={getDurationInPhaseContent('warm')} + /> + </div> + )} + {exists(phaseAgeInMilliseconds.phases.cold) && ( + <div + data-test-subj="ilmTimelineColdPhase" + className="ilmTimeline__phasesContainer__phase ilmTimeline__coldPhase" + > + <div className="ilmTimeline__colorBar ilmTimeline__coldPhase__colorBar" /> + <TimelinePhaseText + phaseName={i18nTexts.coldPhase} + durationInPhase={getDurationInPhaseContent('cold')} + /> + </div> + )} + </div> + </EuiFlexItem> + {hasDeletePhase && ( + <EuiFlexItem grow={false}> <div - data-test-subj="ilmTimelineColdPhase" - className="ilmTimeline__phasesContainer__phase ilmTimeline__coldPhase" + data-test-subj="ilmTimelineDeletePhase" + className="ilmTimeline__deleteIconContainer" > - <div className="ilmTimeline__colorBar ilmTimeline__coldPhase__colorBar" /> - <TimelinePhaseText - phaseName={i18nTexts.coldPhase} - durationInPhase={getDurationInPhaseContent('cold')} - /> + <EuiIconTip type="trash" content={i18nTexts.deleteIcon.toolTipContent} /> </div> - )} - </div> - </EuiFlexItem> - {formData._meta?.delete.enabled && ( - <EuiFlexItem grow={false}> - <div - data-test-subj="ilmTimelineDeletePhase" - className="ilmTimeline__deleteIconContainer" - > - <EuiIconTip type="trash" content={i18nTexts.deleteIcon.toolTipContent} /> - </div> - </EuiFlexItem> - )} - </EuiFlexGroup> - </div> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; + </EuiFlexItem> + )} + </EuiFlexGroup> + </div> + </EuiFlexItem> + </EuiFlexGroup> + ); + } +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 71085a6d7a2b8..cf8c92b8333d0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -11,6 +11,13 @@ export const i18nTexts = { shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.indexFieldLabel', { defaultMessage: 'Shrink index', }), + rolloverOffsetsHotPhaseTiming: i18n.translate( + 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescription', + { + defaultMessage: + 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.', + } + ), searchableSnapshotInHotPhase: { searchableSnapshotDisallowed: { calloutTitle: i18n.translate( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts index 28910871fa33b..405de2b55a2f7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts @@ -4,13 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { flow } from 'fp-ts/function'; import { deserializer } from '../form'; import { + formDataToAbsoluteTimings, + calculateRelativeFromAbsoluteMilliseconds, absoluteTimingToRelativeTiming, - calculateRelativeTimingMs, } from './absolute_timing_to_relative_timing'; +export const calculateRelativeTimingMs = flow( + formDataToAbsoluteTimings, + calculateRelativeFromAbsoluteMilliseconds +); + describe('Conversion of absolute policy timing to relative timing', () => { describe('calculateRelativeTimingMs', () => { describe('policy that never deletes data (keep forever)', () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index 2f37608b2d7ae..a44863b2f1ce2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -14,16 +14,21 @@ * * This code converts the absolute timings to _relative_ timings of the form: 30 days in hot phase, * 40 days in warm phase then forever in cold phase. + * + * All functions exported from this file can be viewed as utilities for working with form data and + * other defined interfaces to calculate the relative amount of time data will spend in a phase. */ import moment from 'moment'; -import { flow } from 'fp-ts/lib/function'; import { i18n } from '@kbn/i18n'; +import { flow } from 'fp-ts/function'; import { splitSizeAndUnits } from '../../../lib/policies'; import { FormInternal } from '../types'; +/* -===- Private functions and types -===- */ + type MinAgePhase = 'warm' | 'cold' | 'delete'; type Phase = 'hot' | MinAgePhase; @@ -43,7 +48,34 @@ const i18nTexts = { }), }; -interface AbsoluteTimings { +const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete']; + +const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ + min_age: formData.phases?.[phase]?.min_age + ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit + : '0ms', +}); + +/** + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math + * for all date math values. ILM policies also support "micros" and "nanos". + */ +const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => { + let milliseconds: number; + const { units, size } = splitSizeAndUnits(phase.min_age); + if (units === 'micros') { + milliseconds = parseInt(size, 10) / 1e3; + } else if (units === 'nanos') { + milliseconds = parseInt(size, 10) / 1e6; + } else { + milliseconds = moment.duration(size, units as any).asMilliseconds(); + } + return milliseconds; +}; + +/* -===- Public functions and types -===- */ + +export interface AbsoluteTimings { hot: { min_age: undefined; }; @@ -67,16 +99,7 @@ export interface PhaseAgeInMilliseconds { }; } -const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete']; - -const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ - min_age: - formData.phases && formData.phases[phase]?.min_age - ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit - : '0ms', -}); - -const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => { +export const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => { const { _meta } = formData; if (!_meta) { return { hot: { min_age: undefined } }; @@ -89,28 +112,13 @@ const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => { }; }; -/** - * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math - * for all date math values. ILM policies also support "micros" and "nanos". - */ -const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => { - let milliseconds: number; - const { units, size } = splitSizeAndUnits(phase.min_age); - if (units === 'micros') { - milliseconds = parseInt(size, 10) / 1e3; - } else if (units === 'nanos') { - milliseconds = parseInt(size, 10) / 1e6; - } else { - milliseconds = moment.duration(size, units as any).asMilliseconds(); - } - return milliseconds; -}; - /** * Given a set of phase minimum age absolute timings, like hot phase 0ms and warm phase 3d, work out * the number of milliseconds data will reside in phase. */ -const calculateMilliseconds = (inputs: AbsoluteTimings): PhaseAgeInMilliseconds => { +export const calculateRelativeFromAbsoluteMilliseconds = ( + inputs: AbsoluteTimings +): PhaseAgeInMilliseconds => { return phaseOrder.reduce<PhaseAgeInMilliseconds>( (acc, phaseName, idx) => { // Delete does not have an age associated with it @@ -152,6 +160,8 @@ const calculateMilliseconds = (inputs: AbsoluteTimings): PhaseAgeInMilliseconds ); }; +export type RelativePhaseTimingInMs = ReturnType<typeof calculateRelativeFromAbsoluteMilliseconds>; + const millisecondsToDays = (milliseconds?: number): string | undefined => { if (milliseconds == null) { return; @@ -177,10 +187,12 @@ export const normalizeTimingsToHumanReadable = ({ }; }; -export const calculateRelativeTimingMs = flow(formDataToAbsoluteTimings, calculateMilliseconds); - +/** + * Given {@link FormInternal}, extract the min_age values for each phase and calculate + * human readable strings for communicating how long data will remain in a phase. + */ export const absoluteTimingToRelativeTiming = flow( formDataToAbsoluteTimings, - calculateMilliseconds, + calculateRelativeFromAbsoluteMilliseconds, normalizeTimingsToHumanReadable ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts index 9593fcc810a6f..a9372c99a72fc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts @@ -6,7 +6,10 @@ export { absoluteTimingToRelativeTiming, - calculateRelativeTimingMs, + calculateRelativeFromAbsoluteMilliseconds, normalizeTimingsToHumanReadable, + formDataToAbsoluteTimings, + AbsoluteTimings, PhaseAgeInMilliseconds, + RelativePhaseTimingInMs, } from './absolute_timing_to_relative_timing'; From 19b1f46611d05a9e494b1fe107bf103d417a0456 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli <efstratia.kalafateli@elastic.co> Date: Mon, 1 Feb 2021 12:43:06 +0200 Subject: [PATCH 153/163] Fixes flakiness on timelion suggestions (#89538) * Fixes flakiness on timelion suggestions * Improvements * Remove flakiness Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/timelion/_expression_typeahead.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/timelion/_expression_typeahead.js b/test/functional/apps/timelion/_expression_typeahead.js index 744f8de15e767..3db5cb48dd38b 100644 --- a/test/functional/apps/timelion/_expression_typeahead.js +++ b/test/functional/apps/timelion/_expression_typeahead.js @@ -75,18 +75,18 @@ export default function ({ getPageObjects }) { await PageObjects.timelion.updateExpression(',split'); await PageObjects.timelion.clickSuggestion(); const suggestions = await PageObjects.timelion.getSuggestionItemsText(); - expect(suggestions.length).to.eql(51); + expect(suggestions.length).not.to.eql(0); expect(suggestions[0].includes('@message.raw')).to.eql(true); - await PageObjects.timelion.clickSuggestion(10, 2000); + await PageObjects.timelion.clickSuggestion(10); }); it('should show field suggestions for metric argument when index pattern set', async () => { await PageObjects.timelion.updateExpression(',metric'); await PageObjects.timelion.clickSuggestion(); await PageObjects.timelion.updateExpression('avg:'); - await PageObjects.timelion.clickSuggestion(0, 2000); + await PageObjects.timelion.clickSuggestion(0); const suggestions = await PageObjects.timelion.getSuggestionItemsText(); - expect(suggestions.length).to.eql(2); + expect(suggestions.length).not.to.eql(0); expect(suggestions[0].includes('avg:bytes')).to.eql(true); }); }); From e31b6a8c91e88741238bddc90147a825444640eb Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Mon, 1 Feb 2021 11:52:57 +0100 Subject: [PATCH 154/163] [Lens] Add smoke test for lens in canvas (#88657) --- x-pack/test/functional/apps/canvas/index.js | 1 + x-pack/test/functional/apps/canvas/lens.ts | 30 ++ x-pack/test/functional/config.js | 1 + .../es_archives/canvas/lens/data.json | 190 ++++++++ .../es_archives/canvas/lens/mappings.json | 409 ++++++++++++++++++ 5 files changed, 631 insertions(+) create mode 100644 x-pack/test/functional/apps/canvas/lens.ts create mode 100644 x-pack/test/functional/es_archives/canvas/lens/data.json create mode 100644 x-pack/test/functional/es_archives/canvas/lens/mappings.json diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js index b7031cf0e55da..d5f7540f48c83 100644 --- a/x-pack/test/functional/apps/canvas/index.js +++ b/x-pack/test/functional/apps/canvas/index.js @@ -26,6 +26,7 @@ export default function canvasApp({ loadTestFile, getService }) { loadTestFile(require.resolve('./custom_elements')); loadTestFile(require.resolve('./feature_controls/canvas_security')); loadTestFile(require.resolve('./feature_controls/canvas_spaces')); + loadTestFile(require.resolve('./lens')); loadTestFile(require.resolve('./reports')); }); } diff --git a/x-pack/test/functional/apps/canvas/lens.ts b/x-pack/test/functional/apps/canvas/lens.ts new file mode 100644 index 0000000000000..e74795de6c7ea --- /dev/null +++ b/x-pack/test/functional/apps/canvas/lens.ts @@ -0,0 +1,30 @@ +/* + * 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 canvasLensTest({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['canvas', 'common', 'header', 'lens']); + const esArchiver = getService('esArchiver'); + + describe('lens in canvas', function () { + before(async () => { + await esArchiver.load('canvas/lens'); + // open canvas home + await PageObjects.common.navigateToApp('canvas'); + // load test workpad + await PageObjects.common.navigateToApp('canvas', { + hash: '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31/page/1', + }); + }); + + it('renders lens visualization', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.lens.assertMetric('Maximum of bytes', '16,788'); + }); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 1815942a06a9a..fc508f8477ebe 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -232,6 +232,7 @@ export default async function ({ readConfigFile }) { { feature: { canvas: ['all'], + visualize: ['all'], }, spaces: ['*'], }, diff --git a/x-pack/test/functional/es_archives/canvas/lens/data.json b/x-pack/test/functional/es_archives/canvas/lens/data.json new file mode 100644 index 0000000000000..dca7d31d71082 --- /dev/null +++ b/x-pack/test/functional/es_archives/canvas/lens/data.json @@ -0,0 +1,190 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "space": { + "_reserved": true, + "color": "#00bfb3", + "description": "This is your default space!", + "name": "Default" + }, + "type": "space", + "updated_at": "2018-11-06T18:20:26.703Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "canvas-workpad:workpad-1705f884-6224-47de-ba49-ca224fe6ec31", + "index": ".kibana_1", + "source": { + "canvas-workpad": { + "@created": "2018-11-19T19:17:12.646Z", + "@timestamp": "2018-11-19T19:36:28.499Z", + "assets": { + }, + "colors": [ + "#37988d", + "#c19628", + "#b83c6f", + "#3f9939", + "#1785b0", + "#ca5f35", + "#45bdb0", + "#f2bc33", + "#e74b8b", + "#4fbf48", + "#1ea6dc", + "#fd7643", + "#72cec3", + "#f5cc5d", + "#ec77a8", + "#7acf74", + "#4cbce4", + "#fd986f", + "#a1ded7", + "#f8dd91", + "#f2a4c5", + "#a6dfa2", + "#86d2ed", + "#fdba9f", + "#000000", + "#444444", + "#777777", + "#BBBBBB", + "#FFFFFF", + "rgba(255,255,255,0)" + ], + "height": 920, + "id": "workpad-1705f884-6224-47de-ba49-ca224fe6ec31", + "isWriteable": true, + "name": "Test Workpad", + "page": 0, + "pages": [ + { + "elements": [ + { + "expression": "savedLens id=\"my-lens-vis\" timerange={timerange from=\"2014-01-01\" to=\"2018-01-01\"}", + "id": "element-8f64a10a-01f3-4a71-a682-5b627cbe4d0e", + "position": { + "angle": 0, + "height": 238, + "left": 33.5, + "top": 20, + "width": 338 + } + } + ], + "id": "page-c38cd459-10fe-45f9-847b-2cbd7ec74319", + "style": { + "background": "#fff" + }, + "transition": { + } + } + ], + "width": 840 + }, + "type": "canvas-workpad", + "updated_at": "2018-11-19T19:36:28.511Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "lens:my-lens-vis", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-lens", + "title": "logstash-lens" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-lens", + "layers": { + "c61a8afb-a185-4fae-a064-fb3846f6c451": { + "columnOrder": [ + "2cd09808-3915-49f4-b3b0-82767eba23f7" + ], + "columns": { + "2cd09808-3915-49f4-b3b0-82767eba23f7": { + "dataType": "number", + "isBucketed": false, + "label": "Maximum of bytes", + "operationType": "max", + "scale": "ratio", + "sourceField": "bytes" + } + }, + "indexPatternId": "logstash-lens" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "accessor": "2cd09808-3915-49f4-b3b0-82767eba23f7", + "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451" + } + }, + "title": "Artistpreviouslyknownaslens", + "visualizationType": "lnsMetric" + }, + "references": [], + "type": "lens", + "updated_at": "2019-10-16T00:28:08.979Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": "logstash-lens", + "id": "1", + "source": { + "@timestamp": "2015-09-20T02:00:00.000Z", + "bytes": 16788 + } + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:logstash-lens", + "index": ".kibana_1", + "source": { + "index-pattern" : { + "title" : "logstash-lens", + "timeFieldName" : "@timestamp", + "fields" : "[]" + }, + "type" : "index-pattern", + "references" : [ ], + "migrationVersion" : { + "index-pattern" : "7.6.0" + }, + "updated_at" : "2020-08-19T08:39:09.998Z" + }, + "type": "_doc" + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/canvas/lens/mappings.json b/x-pack/test/functional/es_archives/canvas/lens/mappings.json new file mode 100644 index 0000000000000..811bfaaae0d2c --- /dev/null +++ b/x-pack/test/functional/es_archives/canvas/lens/mappings.json @@ -0,0 +1,409 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "dynamic": "strict", + "properties": { + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "index": false, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "type": "object" + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "disabledFeatures": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "index": "logstash-lens", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "bytes": { + "type": "float" + } + } + }, + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "0" + } + } + } +} \ No newline at end of file From 1b8c3c1dcc5c077a61d3c66511a525157374f4f7 Mon Sep 17 00:00:00 2001 From: Marta Bondyra <marta.bondyra@gmail.com> Date: Mon, 1 Feb 2021 11:54:16 +0100 Subject: [PATCH 155/163] [Lens] Refactor reorder drag and drop (#88578) --- test/functional/services/common/browser.ts | 2 +- .../__snapshots__/drag_drop.test.tsx.snap | 24 +- .../lens/public/drag_drop/drag_drop.scss | 11 +- .../lens/public/drag_drop/drag_drop.test.tsx | 443 ++++++--- .../lens/public/drag_drop/drag_drop.tsx | 892 +++++++++++------- .../lens/public/drag_drop/providers.tsx | 222 ++++- .../plugins/lens/public/drag_drop/readme.md | 4 +- .../config_panel/config_panel.test.tsx | 8 + .../config_panel/config_panel.tsx | 63 +- .../config_panel/dimension_button.tsx | 66 ++ .../draggable_dimension_button.tsx | 110 +++ .../config_panel/empty_dimension_button.tsx | 97 ++ .../config_panel/layer_panel.scss | 8 +- .../config_panel/layer_panel.test.tsx | 199 ++-- .../editor_frame/config_panel/layer_panel.tsx | 511 ++++------ .../config_panel/remove_layer_button.tsx | 60 ++ .../editor_frame/config_panel/types.ts | 26 +- .../editor_frame/data_panel_wrapper.tsx | 6 +- .../editor_frame/editor_frame.test.tsx | 24 +- .../editor_frame/editor_frame.tsx | 12 +- .../editor_frame/suggestion_helpers.ts | 4 +- .../workspace_panel/workspace_panel.test.tsx | 12 +- .../workspace_panel/workspace_panel.tsx | 16 +- .../datapanel.test.tsx | 15 +- .../indexpattern_datasource/datapanel.tsx | 27 +- .../dimension_panel/droppable.test.ts | 7 + .../dimension_panel/droppable.ts | 189 ++-- .../field_item.test.tsx | 2 + .../indexpattern_datasource/field_item.tsx | 18 +- .../indexpattern_datasource/field_list.tsx | 11 +- .../fields_accordion.test.tsx | 1 + .../fields_accordion.tsx | 105 ++- .../indexpattern_datasource/indexpattern.tsx | 4 +- .../public/indexpattern_datasource/mocks.ts | 5 + x-pack/plugins/lens/public/types.ts | 8 +- .../xy_visualization/xy_config_panel.tsx | 10 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../test/functional/page_objects/lens_page.ts | 2 +- 39 files changed, 2074 insertions(+), 1152 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_button.tsx create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 635fde6dad720..4a7e82d5b42c0 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -289,13 +289,13 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { } const origin = document.querySelector(arguments[0]); - const target = document.querySelector(arguments[1]); const dragStartEvent = createEvent('dragstart'); dispatchEvent(origin, dragStartEvent); setTimeout(() => { const dropEvent = createEvent('drop'); + const target = document.querySelector(arguments[1]); dispatchEvent(target, dropEvent, dragStartEvent.dataTransfer); const dragEndEvent = createEvent('dragend'); dispatchEvent(origin, dragEndEvent, dropEvent.dataTransfer); diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap index dc53f3a2bc2a7..6423a9f6190a7 100644 --- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap +++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap @@ -13,10 +13,8 @@ exports[`DragDrop items that have droppable=false get special styling when anoth <button className="lnsDragDrop lnsDragDrop-isDroppable lnsDragDrop-isNotDroppable" data-test-subj="lnsDragDrop" - onDragEnd={[Function]} onDragLeave={[Function]} onDragOver={[Function]} - onDragStart={[Function]} onDrop={[Function]} > Hello! @@ -24,11 +22,19 @@ exports[`DragDrop items that have droppable=false get special styling when anoth `; exports[`DragDrop renders if nothing is being dragged 1`] = ` -<button - class="lnsDragDrop lnsDragDrop-isDraggable" - data-test-subj="lnsDragDrop" - draggable="true" -> - Hello! -</button> +<div> + <button + aria-describedby="lnsDragDrop-keyboardInstructions" + aria-label="dragging" + class="euiScreenReaderOnly--showOnFocus lnsDragDrop__keyboardHandler" + data-test-subj="lnsDragDrop-keyboardHandler" + /> + <button + class="lnsDragDrop lnsDragDrop-isDraggable" + data-test-subj="lnsDragDrop" + draggable="true" + > + Hello! + </button> +</div> `; diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index ded0b4552a4e5..d0a4019055d57 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -52,7 +52,7 @@ } } -.lnsDragDrop__reorderableContainer { +.lnsDragDrop__container { position: relative; } @@ -63,11 +63,18 @@ height: calc(100% + #{$lnsLayerPanelDimensionMargin}); } -.lnsDragDrop-isReorderable { +.lnsDragDrop-translatableDrop { + transform: translateY(0); transition: transform $euiAnimSpeedFast ease-in-out; pointer-events: none; } +.lnsDragDrop-translatableDrag { + transform: translateY(0); + transition: transform $euiAnimSpeedFast ease-in-out; + position: relative; +} + // Draggable item when it is moving .lnsDragDrop-isHidden { opacity: 0; diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index 07b489d29ad06..9e1583b0c6e81 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -6,11 +6,33 @@ import React from 'react'; import { render, mount } from 'enzyme'; -import { DragDrop, ReorderableDragDrop, DropToHandler, DropHandler } from './drag_drop'; -import { ChildDragDropProvider, ReorderProvider } from './providers'; +import { DragDrop, DropHandler } from './drag_drop'; +import { + ChildDragDropProvider, + DragContextState, + ReorderProvider, + DragDropIdentifier, + ActiveDropTarget, +} from './providers'; +import { act } from 'react-dom/test-utils'; jest.useFakeTimers(); +const defaultContext = { + dragging: undefined, + setDragging: jest.fn(), + setActiveDropTarget: () => {}, + activeDropTarget: undefined, + keyboardMode: false, + setKeyboardMode: () => {}, + setA11yMessage: jest.fn(), +}; + +const dataTransfer = { + setData: jest.fn(), + getData: jest.fn(), +}; + describe('DragDrop', () => { const value = { id: '1', label: 'hello' }; test('renders if nothing is being dragged', () => { @@ -26,7 +48,7 @@ describe('DragDrop', () => { test('dragover calls preventDefault if droppable is true', () => { const preventDefault = jest.fn(); const component = mount( - <DragDrop droppable> + <DragDrop droppable value={value}> <button>Hello!</button> </DragDrop> ); @@ -39,7 +61,7 @@ describe('DragDrop', () => { test('dragover does not call preventDefault if droppable is false', () => { const preventDefault = jest.fn(); const component = mount( - <DragDrop> + <DragDrop value={value}> <button>Hello!</button> </DragDrop> ); @@ -51,13 +73,9 @@ describe('DragDrop', () => { test('dragstart sets dragging in the context', async () => { const setDragging = jest.fn(); - const dataTransfer = { - setData: jest.fn(), - getData: jest.fn(), - }; const component = mount( - <ChildDragDropProvider dragging={value} setDragging={setDragging}> + <ChildDragDropProvider {...defaultContext} dragging={value} setDragging={setDragging}> <DragDrop value={value} draggable={true} label="drag label"> <button>Hello!</button> </DragDrop> @@ -79,7 +97,11 @@ describe('DragDrop', () => { const onDrop = jest.fn(); const component = mount( - <ChildDragDropProvider dragging={{ id: '2', label: 'hi' }} setDragging={setDragging}> + <ChildDragDropProvider + {...defaultContext} + dragging={{ id: '2', label: 'hi' }} + setDragging={setDragging} + > <DragDrop onDrop={onDrop} droppable={true} value={value}> <button>Hello!</button> </DragDrop> @@ -93,7 +115,7 @@ describe('DragDrop', () => { expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); expect(setDragging).toBeCalledWith(undefined); - expect(onDrop).toBeCalledWith({ id: '2', label: 'hi' }); + expect(onDrop).toBeCalledWith({ id: '2', label: 'hi' }, { id: '1', label: 'hello' }); }); test('drop function is not called on droppable=false', async () => { @@ -103,7 +125,7 @@ describe('DragDrop', () => { const onDrop = jest.fn(); const component = mount( - <ChildDragDropProvider dragging={{ id: 'hi' }} setDragging={setDragging}> + <ChildDragDropProvider {...defaultContext} dragging={{ id: 'hi' }} setDragging={setDragging}> <DragDrop onDrop={onDrop} droppable={false} value={value}> <button>Hello!</button> </DragDrop> @@ -127,6 +149,7 @@ describe('DragDrop', () => { throw x; }} droppable + value={value} > <button>Hello!</button> </DragDrop> @@ -137,11 +160,11 @@ describe('DragDrop', () => { test('items that have droppable=false get special styling when another item is dragged', () => { const component = mount( - <ChildDragDropProvider dragging={value} setDragging={() => {}}> + <ChildDragDropProvider {...defaultContext} dragging={value}> <DragDrop value={value} draggable={true} label="a"> <button>Hello!</button> </DragDrop> - <DragDrop onDrop={(x: unknown) => {}} droppable={false}> + <DragDrop onDrop={(x: unknown) => {}} droppable={false} value={{ id: '2' }}> <button>Hello!</button> </DragDrop> </ChildDragDropProvider> @@ -153,17 +176,25 @@ describe('DragDrop', () => { test('additional styles are reflected in the className until drop', () => { let dragging: { id: '1' } | undefined; const getAdditionalClasses = jest.fn().mockReturnValue('additional'); + let activeDropTarget; + const component = mount( <ChildDragDropProvider + {...defaultContext} dragging={dragging} setDragging={() => { dragging = { id: '1' }; }} + setActiveDropTarget={(val) => { + activeDropTarget = { activeDropTarget: val }; + }} + activeDropTarget={activeDropTarget} > <DragDrop value={{ label: 'ignored', id: '3' }} draggable={true} label="a"> <button>Hello!</button> </DragDrop> <DragDrop + value={value} onDrop={(x: unknown) => {}} droppable getAdditionalClassesOnEnter={getAdditionalClasses} @@ -173,10 +204,6 @@ describe('DragDrop', () => { </ChildDragDropProvider> ); - const dataTransfer = { - setData: jest.fn(), - getData: jest.fn(), - }; component .find('[data-test-subj="lnsDragDrop"]') .first() @@ -184,40 +211,91 @@ describe('DragDrop', () => { jest.runAllTimers(); component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); - expect(component.find('.additional')).toHaveLength(1); - - component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave'); + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop'); expect(component.find('.additional')).toHaveLength(0); + }); + + test('additional enter styles are reflected in the className until dragleave', () => { + let dragging: { id: '1' } | undefined; + const getAdditionalClasses = jest.fn().mockReturnValue('additional'); + const setActiveDropTarget = jest.fn(); + + const component = mount( + <ChildDragDropProvider + setA11yMessage={jest.fn()} + dragging={dragging} + setDragging={() => { + dragging = { id: '1' }; + }} + setActiveDropTarget={setActiveDropTarget} + activeDropTarget={ + ({ activeDropTarget: value } as unknown) as DragContextState['activeDropTarget'] + } + keyboardMode={false} + setKeyboardMode={(keyboardMode) => true} + > + <DragDrop value={{ label: 'ignored', id: '3' }} draggable={true} label="a"> + <button>Hello!</button> + </DragDrop> + <DragDrop + value={value} + onDrop={(x: unknown) => {}} + droppable + getAdditionalClassesOnEnter={getAdditionalClasses} + > + <button>Hello!</button> + </DragDrop> + </ChildDragDropProvider> + ); + + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + jest.runAllTimers(); component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); - component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop'); - expect(component.find('.additional')).toHaveLength(0); + expect(component.find('.additional')).toHaveLength(1); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave'); + expect(setActiveDropTarget).toBeCalledWith(undefined); }); describe('reordering', () => { const mountComponent = ( - dragging: { id: '1' } | undefined, - onDrop: DropHandler = jest.fn(), - dropTo: DropToHandler = jest.fn() - ) => - mount( - <ChildDragDropProvider - dragging={{ id: '1' }} - setDragging={() => { - dragging = { id: '1' }; - }} - > + dragContext: Partial<DragContextState> | undefined, + onDrop: DropHandler = jest.fn() + ) => { + let dragging = dragContext?.dragging; + let keyboardMode = !!dragContext?.keyboardMode; + let activeDropTarget = dragContext?.activeDropTarget; + const baseContext = { + dragging, + setDragging: (val?: DragDropIdentifier) => { + dragging = val; + }, + keyboardMode, + setKeyboardMode: jest.fn((mode) => { + keyboardMode = mode; + }), + setActiveDropTarget: (target?: DragDropIdentifier) => { + activeDropTarget = { activeDropTarget: target } as ActiveDropTarget; + }, + activeDropTarget, + setA11yMessage: jest.fn(), + }; + return mount( + <ChildDragDropProvider {...baseContext} {...dragContext}> <ReorderProvider id="groupId"> <DragDrop label="1" draggable - droppable + droppable={false} dragType="reorder" dropType="reorder" - itemsInGroup={['1', '2', '3']} + reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]} value={{ id: '1' }} onDrop={onDrop} - dropTo={dropTo} > <span>1</span> </DragDrop> @@ -227,12 +305,11 @@ describe('DragDrop', () => { droppable dragType="reorder" dropType="reorder" - itemsInGroup={['1', '2', '3']} + reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]} value={{ id: '2', }} onDrop={onDrop} - dropTo={dropTo} > <span>2</span> </DragDrop> @@ -242,132 +319,270 @@ describe('DragDrop', () => { droppable dragType="reorder" dropType="reorder" - itemsInGroup={['1', '2', '3']} + reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]} value={{ id: '3', }} onDrop={onDrop} - dropTo={dropTo} > <span>3</span> </DragDrop> </ReorderProvider> </ChildDragDropProvider> ); - test(`ReorderableDragDrop component doesn't appear for groups of 1 or less`, () => { - let dragging; - const component = mount( - <ChildDragDropProvider - dragging={dragging} - setDragging={() => { - dragging = { id: '1' }; - }} - > - <ReorderProvider id="groupId"> - <DragDrop - label="1" - draggable - droppable - dragType="reorder" - dropType="reorder" - itemsInGroup={['1']} - value={{ id: '1' }} - onDrop={jest.fn()} - dropTo={jest.fn()} - > - <div /> - </DragDrop> - </ReorderProvider> - </ChildDragDropProvider> - ); - expect(component.find(ReorderableDragDrop)).toHaveLength(0); - }); - test(`Reorderable component renders properly`, () => { + }; + test(`Inactive reorderable group renders properly`, () => { const component = mountComponent(undefined, jest.fn()); - expect(component.find(ReorderableDragDrop)).toHaveLength(3); + expect(component.find('.lnsDragDrop-reorderable')).toHaveLength(3); }); - test(`Elements between dragged and drop get extra class to show the reorder effect when dragging`, () => { - const component = mountComponent({ id: '1' }, jest.fn()); - const dataTransfer = { - setData: jest.fn(), - getData: jest.fn(), - }; - component - .find(ReorderableDragDrop) - .first() - .find('[data-test-subj="lnsDragDrop"]') - .simulate('dragstart', { dataTransfer }); - jest.runAllTimers(); - component.find('[data-test-subj="lnsDragDrop-reorderableDrop"]').at(2).simulate('dragover'); + test(`Reorderable group with lifted element renders properly`, () => { + const setDragging = jest.fn(); + const setA11yMessage = jest.fn(); + const component = mountComponent( + { dragging: { id: '1' }, setA11yMessage, setDragging }, + jest.fn() + ); + act(() => { + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + jest.runAllTimers(); + }); + + expect(setDragging).toBeCalledWith({ id: '1' }); + expect(setA11yMessage).toBeCalledWith('You have lifted an item 1 in position 1'); + expect( + component + .find('[data-test-subj="lnsDragDrop-reorderableGroup"]') + .hasClass('lnsDragDrop-isActiveGroup') + ).toEqual(true); + }); + + test(`Reordered elements get extra styles to show the reorder effect when dragging`, () => { + const component = mountComponent({ dragging: { id: '1' } }, jest.fn()); + + act(() => { + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + jest.runAllTimers(); + }); + + component + .find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]') + .at(1) + .simulate('dragover'); expect( component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style') - ).toEqual({}); + ).toEqual(undefined); expect( - component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(1).prop('style') + component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(0).prop('style') ).toEqual({ - transform: 'translateY(-40px)', + transform: 'translateY(-8px)', }); expect( - component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(2).prop('style') + component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style') ).toEqual({ - transform: 'translateY(-40px)', + transform: 'translateY(-8px)', }); - component.find('[data-test-subj="lnsDragDrop-reorderableDrop"]').at(2).simulate('dragleave'); + component + .find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]') + .at(1) + .simulate('dragleave'); expect( - component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(1).prop('style') - ).toEqual({}); + component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style') + ).toEqual(undefined); expect( - component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(2).prop('style') - ).toEqual({}); + component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style') + ).toEqual(undefined); }); + test(`Dropping an item runs onDrop function`, () => { + const setDragging = jest.fn(); + const setA11yMessage = jest.fn(); const preventDefault = jest.fn(); const stopPropagation = jest.fn(); const onDrop = jest.fn(); - const component = mountComponent({ id: '1' }, onDrop); + const component = mountComponent( + { dragging: { id: '1' }, setA11yMessage, setDragging }, + onDrop + ); component - .find('[data-test-subj="lnsDragDrop-reorderableDrop"]') + .find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]') .at(1) .simulate('drop', { preventDefault, stopPropagation }); + jest.runAllTimers(); + + expect(setA11yMessage).toBeCalledWith( + 'You have dropped the item. You have moved the item from position 1 to positon 3' + ); expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); - expect(onDrop).toBeCalledWith({ id: '1' }); + expect(onDrop).toBeCalledWith({ id: '1' }, { id: '3' }); }); - test(`Keyboard navigation: user can reorder an element`, () => { + + test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { const onDrop = jest.fn(); - const dropTo = jest.fn(); - const component = mountComponent({ id: '1' }, onDrop, dropTo); + const component = mountComponent( + { + dragging: { id: '1' }, + activeDropTarget: { activeDropTarget: { id: '3' } } as ActiveDropTarget, + keyboardMode: true, + }, + onDrop + ); const keyboardHandler = component - .find(ReorderableDragDrop) - .at(1) - .find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .simulate('focus'); + + act(() => { + keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); + keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); + keyboardHandler.simulate('keydown', { key: 'Enter' }); + }); + expect(onDrop).toBeCalledWith({ id: '1' }, { id: '3' }); + }); + + test(`Keyboard Navigation: Reordered elements get extra styles to show the reorder effect`, () => { + const setA11yMessage = jest.fn(); + const component = mountComponent( + { dragging: { id: '1' }, keyboardMode: true, setA11yMessage }, + jest.fn() + ); + const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); - expect(dropTo).toBeCalledWith('3'); - keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); - expect(dropTo).toBeCalledWith('1'); + expect( + component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style') + ).toEqual({ + transform: 'translateY(+8px)', + }); + expect( + component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(0).prop('style') + ).toEqual({ + transform: 'translateY(-40px)', + }); + expect( + component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style') + ).toEqual(undefined); + expect(setA11yMessage).toBeCalledWith( + 'You have moved the item 1 from position 1 to position 2' + ); + + component + .find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]') + .at(1) + .simulate('dragleave'); + expect( + component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style') + ).toEqual(undefined); + expect( + component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style') + ).toEqual(undefined); }); + test(`Keyboard Navigation: User cannot move an element outside of the group`, () => { const onDrop = jest.fn(); - const dropTo = jest.fn(); - const component = mountComponent({ id: '1' }, onDrop, dropTo); - const keyboardHandler = component - .find(ReorderableDragDrop) - .first() - .find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + const setActiveDropTarget = jest.fn(); + const setA11yMessage = jest.fn(); + const component = mountComponent( + { dragging: { id: '1' }, keyboardMode: true, setActiveDropTarget, setA11yMessage }, + onDrop + ); + const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); - expect(dropTo).not.toHaveBeenCalled(); + expect(setActiveDropTarget).not.toHaveBeenCalled(); + keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); - expect(dropTo).toBeCalledWith('2'); + + expect(setActiveDropTarget).toBeCalledWith({ id: '2' }); + expect(setA11yMessage).toBeCalledWith( + 'You have moved the item 1 from position 1 to position 2' + ); + }); + + test(`Keyboard Navigation: User cannot drop element to itself`, () => { + const setActiveDropTarget = jest.fn(); + const setA11yMessage = jest.fn(); + const component = mount( + <ChildDragDropProvider + {...defaultContext} + keyboardMode={true} + activeDropTarget={{ + activeDropTarget: { id: '2' }, + }} + dragging={{ id: '1' }} + setActiveDropTarget={setActiveDropTarget} + setA11yMessage={setA11yMessage} + > + <ReorderProvider id="groupId"> + <DragDrop + label="1" + draggable + droppable={false} + dragType="reorder" + dropType="reorder" + reorderableGroup={[{ id: '1' }, { id: '2' }]} + value={{ id: '1' }} + > + <span>1</span> + </DragDrop> + <DragDrop + label="2" + draggable + droppable + dragType="reorder" + dropType="reorder" + reorderableGroup={[{ id: '1' }, { id: '2' }]} + value={{ id: '2' }} + > + <span>2</span> + </DragDrop> + </ReorderProvider> + </ChildDragDropProvider> + ); + const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); + expect(setActiveDropTarget).toBeCalledWith({ id: '1' }); + expect(setA11yMessage).toBeCalledWith('You have moved back the item 1 to position 1'); + }); + + test(`Keyboard Navigation: Doesn't call onDrop when movement is cancelled`, () => { + const setA11yMessage = jest.fn(); + const onDrop = jest.fn(); + + const component = mountComponent({ dragging: { id: '1' }, setA11yMessage }, onDrop); + const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'Escape' }); + + jest.runAllTimers(); + + expect(onDrop).not.toHaveBeenCalled(); + expect(setA11yMessage).toBeCalledWith( + 'Movement cancelled. The item has returned to its starting position 1' + ); + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); + keyboardHandler.simulate('blur'); + + expect(onDrop).not.toHaveBeenCalled(); + expect(setA11yMessage).toBeCalledWith( + 'Movement cancelled. The item has returned to its starting position 1' + ); }); }); }); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 32facbf8e84a8..2dbcfab8d5738 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -5,11 +5,17 @@ */ import './drag_drop.scss'; -import React, { useState, useContext, useEffect } from 'react'; +import React, { useContext, useEffect, memo } from 'react'; import classNames from 'classnames'; import { keys, EuiScreenReaderOnly } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { DragContext, DragContextState, ReorderContext, ReorderState } from './providers'; +import { + DragDropIdentifier, + DragContext, + DragContextState, + ReorderContext, + ReorderState, + reorderAnnouncements, +} from './providers'; import { trackUiEvent } from '../lens_ui_telemetry'; export type DroppableEvent = React.DragEvent<HTMLElement>; @@ -17,12 +23,7 @@ export type DroppableEvent = React.DragEvent<HTMLElement>; /** * A function that handles a drop event. */ -export type DropHandler = (item: unknown) => void; - -/** - * A function that handles a dropTo event. - */ -export type DropToHandler = (dropTargetId: string) => void; +export type DropHandler = (dropped: DragDropIdentifier, dropTarget: DragDropIdentifier) => void; /** * The base props to the DragDrop component. @@ -32,24 +33,20 @@ interface BaseProps { * The CSS class(es) for the root element. */ className?: string; - /** - * The event handler that fires when this item - * is dropped to the one with passed id - * + * The label for accessibility */ - dropTo?: DropToHandler; + label?: string; + /** * The event handler that fires when an item * is dropped onto this DragDrop component. */ onDrop?: DropHandler; /** - * The value associated with this item, if it is draggable. - * If this component is dragged, this will be the value of - * "dragging" in the root drag/drop context. + * The value associated with this item. */ - value?: DragContextState['dragging']; + value: DragDropIdentifier; /** * Optional comparison function to check whether a value is the dragged one @@ -60,7 +57,10 @@ interface BaseProps { * The React element which will be passed the draggable handlers */ children: React.ReactElement; - + /** + * Indicates whether or not this component is draggable. + */ + draggable?: boolean; /** * Indicates whether or not the currently dragged item * can be dropped onto this component. @@ -75,12 +75,12 @@ interface BaseProps { /** * The optional test subject associated with this DOM element. */ - 'data-test-subj'?: string; + dataTestSubj?: string; /** * items belonging to the same group that can be reordered */ - itemsInGroup?: string[]; + reorderableGroup?: DragDropIdentifier[]; /** * Indicates to the user whether the currently dragged item @@ -93,34 +93,46 @@ interface BaseProps { * replace something that is existing or add a new one */ dropType?: 'add' | 'replace' | 'reorder'; + + /** + * temporary flag to exclude the draggable elements that don't have keyboard nav yet. To be removed along with the feature development + */ + noKeyboardSupportYet?: boolean; } /** * The props for a draggable instance of that component. */ -interface DraggableProps extends BaseProps { - /** - * Indicates whether or not this component is draggable. - */ - draggable: true; +interface DragInnerProps extends BaseProps { /** * The label, which should be attached to the drag event, and which will e.g. * be used if the element will be dropped into a text field. */ - label: string; + label?: string; + isDragging: boolean; + keyboardMode: boolean; + setKeyboardMode: DragContextState['setKeyboardMode']; + setDragging: DragContextState['setDragging']; + setActiveDropTarget: DragContextState['setActiveDropTarget']; + setA11yMessage: DragContextState['setA11yMessage']; + activeDropTarget: DragContextState['activeDropTarget']; + onDragStart?: ( + target?: + | DroppableEvent['currentTarget'] + | React.KeyboardEvent<HTMLButtonElement>['currentTarget'] + ) => void; + onDragEnd?: () => void; + extraKeyboardHandler?: (e: React.KeyboardEvent<HTMLButtonElement>) => void; } /** * The props for a non-draggable instance of that component. */ -interface NonDraggableProps extends BaseProps { - /** - * Indicates whether or not this component is draggable. - */ - draggable?: false; -} +interface DropInnerProps extends BaseProps, DragContextState { + isDragging: boolean; -type Props = DraggableProps | NonDraggableProps; + isNotDroppable: boolean; +} /** * A draggable / droppable item. Items can be both draggable and droppable at @@ -129,40 +141,189 @@ type Props = DraggableProps | NonDraggableProps; * @param props */ -export const DragDrop = (props: Props) => { - const { dragging, setDragging } = useContext(DragContext); - const { value, draggable, droppable, isValueEqual } = props; +const lnsLayerPanelDimensionMargin = 8; - return ( - <DragDropInner - {...props} - dragging={droppable ? dragging : undefined} - setDragging={setDragging} - isDragging={ - !!(draggable && ((isValueEqual && isValueEqual(value, dragging)) || value === dragging)) +export const DragDrop = (props: BaseProps) => { + const { + dragging, + setDragging, + keyboardMode, + setKeyboardMode, + activeDropTarget, + setActiveDropTarget, + setA11yMessage, + } = useContext(DragContext); + + const { value, draggable, droppable, reorderableGroup } = props; + + const isDragging = !!(draggable && value.id === dragging?.id); + + const dragProps = { + ...props, + isDragging, + keyboardMode: isDragging ? keyboardMode : false, // optimization to not rerender all dragging components + activeDropTarget: isDragging ? activeDropTarget : undefined, // optimization to not rerender all dragging components + setKeyboardMode, + setDragging, + setActiveDropTarget, + setA11yMessage, + }; + + const dropProps = { + ...props, + setKeyboardMode, + keyboardMode, + dragging, + setDragging, + activeDropTarget, + setActiveDropTarget, + isDragging, + setA11yMessage, + isNotDroppable: + // If the configuration has provided a droppable flag, but this particular item is not + // droppable, then it should be less prominent. Ignores items that are both + // draggable and drop targets + !!(droppable === false && dragging && value.id !== dragging.id), + }; + + if (draggable && !droppable) { + if (reorderableGroup && reorderableGroup.length > 1) { + return ( + <ReorderableDrag + {...dragProps} + draggable={draggable} + reorderableGroup={reorderableGroup} + dragging={dragging} + /> + ); + } else { + return <DragInner {...dragProps} draggable={draggable} />; + } + } + if ( + reorderableGroup && + reorderableGroup.length > 1 && + reorderableGroup?.some((i) => i.id === value.id) + ) { + return <ReorderableDrop reorderableGroup={reorderableGroup} {...dropProps} />; + } + return <DropInner {...dropProps} />; +}; + +const DragInner = memo(function DragDropInner({ + dataTestSubj, + className, + value, + children, + setDragging, + setKeyboardMode, + setActiveDropTarget, + label = '', + keyboardMode, + isDragging, + activeDropTarget, + onDrop, + dragType, + onDragStart, + onDragEnd, + extraKeyboardHandler, + noKeyboardSupportYet, +}: DragInnerProps) { + const dragStart = (e?: DroppableEvent | React.KeyboardEvent<HTMLButtonElement>) => { + // Setting stopPropgagation causes Chrome failures, so + // we are manually checking if we've already handled this + // in a nested child, and doing nothing if so... + if (e && 'dataTransfer' in e && e.dataTransfer.getData('text')) { + return; + } + + // We only can reach the dragStart method if the element is draggable, + // so we know we have DraggableProps if we reach this code. + if (e && 'dataTransfer' in e) { + e.dataTransfer.setData('text', label); + } + + // Chrome causes issues if you try to render from within a + // dragStart event, so we drop a setTimeout to avoid that. + + const currentTarget = e?.currentTarget; + setTimeout(() => { + setDragging(value); + if (onDragStart) { + onDragStart(currentTarget); } - isNotDroppable={ - // If the configuration has provided a droppable flag, but this particular item is not - // droppable, then it should be less prominent. Ignores items that are both - // draggable and drop targets - droppable === false && Boolean(dragging) && value !== dragging + }); + }; + + const dragEnd = (e?: DroppableEvent) => { + e?.stopPropagation(); + setDragging(undefined); + setActiveDropTarget(undefined); + setKeyboardMode(false); + if (onDragEnd) { + onDragEnd(); + } + }; + + const dropToActiveDropTarget = () => { + if (isDragging && activeDropTarget?.activeDropTarget) { + trackUiEvent('drop_total'); + if (onDrop) { + onDrop(value, activeDropTarget.activeDropTarget); } - /> + } + }; + + return ( + <div className={className}> + {!noKeyboardSupportYet && ( + <EuiScreenReaderOnly showOnFocus> + <button + aria-label={label} + aria-describedby={`lnsDragDrop-keyboardInstructions`} + className="lnsDragDrop__keyboardHandler" + data-test-subj="lnsDragDrop-keyboardHandler" + onBlur={() => { + dragEnd(); + }} + onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => { + if (e.key === keys.ENTER || e.key === keys.SPACE) { + if (activeDropTarget) { + dropToActiveDropTarget(); + } + if (isDragging) { + dragEnd(); + } else { + dragStart(e); + setKeyboardMode(true); + } + } else if (e.key === keys.ESCAPE) { + dragEnd(); + } + if (extraKeyboardHandler) { + extraKeyboardHandler(e); + } + }} + /> + </EuiScreenReaderOnly> + )} + + {React.cloneElement(children, { + 'data-test-subj': dataTestSubj || 'lnsDragDrop', + className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable', { + 'lnsDragDrop-isHidden': isDragging && dragType === 'move' && !keyboardMode, + }), + draggable: true, + onDragEnd: dragEnd, + onDragStart: dragStart, + })} + </div> ); -}; +}); -const DragDropInner = React.memo(function DragDropInner( - props: Props & - DragContextState & { - isDragging: boolean; - isNotDroppable: boolean; - } -) { - const [state, setState] = useState({ - isActive: false, - dragEnterClassNames: '', - }); +const DropInner = memo(function DropInner(props: DropInnerProps) { const { + dataTestSubj, className, onDrop, value, @@ -175,10 +336,16 @@ const DragDropInner = React.memo(function DragDropInner( isNotDroppable, dragType = 'copy', dropType = 'add', - dropTo, - itemsInGroup, + keyboardMode, + setKeyboardMode, + activeDropTarget, + setActiveDropTarget, + getAdditionalClassesOnEnter, } = props; + const activeDropTargetMatches = + activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id; + const isMoveDragging = isDragging && dragType === 'move'; const classes = classNames( @@ -186,339 +353,364 @@ const DragDropInner = React.memo(function DragDropInner( { 'lnsDragDrop-isDraggable': draggable, 'lnsDragDrop-isDragging': isDragging, - 'lnsDragDrop-isHidden': isMoveDragging, + 'lnsDragDrop-isHidden': isMoveDragging && !keyboardMode, 'lnsDragDrop-isDroppable': !draggable, 'lnsDragDrop-isDropTarget': droppable && dragType !== 'reorder', - 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive && dragType !== 'reorder', + 'lnsDragDrop-isActiveDropTarget': + droppable && activeDropTargetMatches && dragType !== 'reorder', 'lnsDragDrop-isNotDroppable': !isMoveDragging && isNotDroppable, - 'lnsDragDrop-isReplacing': droppable && state.isActive && dropType === 'replace', + 'lnsDragDrop-isReplacing': droppable && activeDropTargetMatches && dropType === 'replace', }, - state.dragEnterClassNames - ); - - const dragStart = (e: DroppableEvent) => { - // Setting stopPropgagation causes Chrome failures, so - // we are manually checking if we've already handled this - // in a nested child, and doing nothing if so... - if (e.dataTransfer.getData('text')) { - return; + getAdditionalClassesOnEnter && { + [getAdditionalClassesOnEnter()]: activeDropTargetMatches, } - - // We only can reach the dragStart method if the element is draggable, - // so we know we have DraggableProps if we reach this code. - e.dataTransfer.setData('text', (props as DraggableProps).label); - - // Chrome causes issues if you try to render from within a - // dragStart event, so we drop a setTimeout to avoid that. - setState({ ...state }); - setTimeout(() => setDragging(value)); - }; - - const dragEnd = (e: DroppableEvent) => { - e.stopPropagation(); - setDragging(undefined); - }; + ); const dragOver = (e: DroppableEvent) => { if (!droppable) { return; } - e.preventDefault(); // An optimization to prevent a bunch of React churn. - if (!state.isActive) { - setState({ - ...state, - isActive: true, - dragEnterClassNames: props.getAdditionalClassesOnEnter - ? props.getAdditionalClassesOnEnter() - : '', - }); + // todo: replace with custom function ? + if (!activeDropTargetMatches) { + setActiveDropTarget(value); } }; const dragLeave = () => { - setState({ ...state, isActive: false, dragEnterClassNames: '' }); + setActiveDropTarget(undefined); }; - const drop = (e: DroppableEvent) => { + const drop = (e: DroppableEvent | React.KeyboardEvent<HTMLButtonElement>) => { e.preventDefault(); e.stopPropagation(); - setState({ ...state, isActive: false, dragEnterClassNames: '' }); - setDragging(undefined); - - if (onDrop && droppable) { + if (onDrop && droppable && dragging) { trackUiEvent('drop_total'); - onDrop(dragging); + onDrop(dragging, value); } + setActiveDropTarget(undefined); + setDragging(undefined); + setKeyboardMode(false); }; + return ( + <> + {React.cloneElement(children, { + 'data-test-subj': dataTestSubj || 'lnsDragDrop', + className: classNames(children.props.className, classes, className), + onDragOver: dragOver, + onDragLeave: dragLeave, + onDrop: drop, + draggable, + })} + </> + ); +}); - const isReorderDragging = !!(dragging && itemsInGroup?.includes(dragging.id)); +const ReorderableDrag = memo(function ReorderableDrag( + props: DragInnerProps & { reorderableGroup: DragDropIdentifier[]; dragging?: DragDropIdentifier } +) { + const { + reorderState: { isReorderOn, reorderedItems, direction }, + setReorderState, + } = useContext(ReorderContext); - if ( - draggable && - itemsInGroup?.length && - itemsInGroup.length > 1 && - value?.id && - dropTo && - (!dragging || isReorderDragging) - ) { - const { label } = props as DraggableProps; - return ( - <ReorderableDragDrop - dropTo={dropTo} - label={label} - className={className} - dataTestSubj={props['data-test-subj'] || 'lnsDragDrop'} - draggingProps={{ - className: classNames(children.props.className, classes), - draggable, - onDragEnd: dragEnd, - onDragStart: dragStart, - isReorderDragging, - }} - dropProps={{ - onDrop: drop, - onDragOver: dragOver, - onDragLeave: dragLeave, - dragging, - droppable, - itemsInGroup, - id: value.id, - isActive: state.isActive, - }} - > - {children} - </ReorderableDragDrop> - ); - } - return React.cloneElement(children, { - 'data-test-subj': props['data-test-subj'] || 'lnsDragDrop', - className: classNames(children.props.className, classes, className), - onDragOver: dragOver, - onDragLeave: dragLeave, - onDrop: drop, - draggable, - onDragEnd: dragEnd, - onDragStart: dragStart, - }); -}); + const { + value, + setActiveDropTarget, + label = '', + keyboardMode, + isDragging, + activeDropTarget, + reorderableGroup, + onDrop, + setA11yMessage, + } = props; -const getKeyboardReorderMessageMoved = ( - itemLabel: string, - position: number, - prevPosition: number -) => - i18n.translate('xpack.lens.dragDrop.elementMoved', { - defaultMessage: `You have moved the item {itemLabel} from position {prevPosition} to position {position}`, - values: { - itemLabel, - position, - prevPosition, - }, - }); - -const getKeyboardReorderMessageLifted = (itemLabel: string, position: number) => - i18n.translate('xpack.lens.dragDrop.elementLifted', { - defaultMessage: `You have lifted an item {itemLabel} in position {position}`, - values: { - itemLabel, - position, - }, - }); + const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id); + + const isFocusInGroup = keyboardMode + ? isDragging && + (!activeDropTarget?.activeDropTarget || + reorderableGroup.some((i) => i.id === activeDropTarget?.activeDropTarget?.id)) + : isDragging; + + useEffect(() => { + setReorderState((s: ReorderState) => ({ + ...s, + isReorderOn: isFocusInGroup, + })); + }, [setReorderState, isFocusInGroup]); + + const onReorderableDragStart = ( + currentTarget?: + | DroppableEvent['currentTarget'] + | React.KeyboardEvent<HTMLButtonElement>['currentTarget'] + ) => { + if (currentTarget) { + const height = currentTarget.offsetHeight + lnsLayerPanelDimensionMargin; + setReorderState((s: ReorderState) => ({ + ...s, + draggingHeight: height, + })); + } -const lnsLayerPanelDimensionMargin = 8; + setA11yMessage(reorderAnnouncements.lifted(label, currentIndex + 1)); + }; -export const ReorderableDragDrop = ({ - draggingProps, - dropProps, - children, - label, - dropTo, - className, - dataTestSubj, -}: { - draggingProps: { - className: string; - draggable: Props['draggable']; - onDragEnd: (e: DroppableEvent) => void; - onDragStart: (e: DroppableEvent) => void; - isReorderDragging: boolean; + const onReorderableDragEnd = () => { + resetReorderState(); + setA11yMessage(reorderAnnouncements.cancelled(currentIndex + 1)); }; - dropProps: { - onDrop: (e: DroppableEvent) => void; - onDragOver: (e: DroppableEvent) => void; - onDragLeave: () => void; - dragging: DragContextState['dragging']; - droppable: DraggableProps['droppable']; - itemsInGroup: string[]; - id: string; - isActive: boolean; + + const onReorderableDrop = (dragging: DragDropIdentifier, target: DragDropIdentifier) => { + if (onDrop) { + onDrop(dragging, target); + const targetIndex = reorderableGroup.findIndex( + (i) => i.id === activeDropTarget?.activeDropTarget?.id + ); + + resetReorderState(); + setA11yMessage(reorderAnnouncements.dropped(targetIndex + 1, currentIndex + 1)); + } }; - children: React.ReactElement; - label: string; - dropTo: DropToHandler; - className?: string; - dataTestSubj: string; -}) => { - const { itemsInGroup, dragging, id, droppable } = dropProps; - const { reorderState, setReorderState } = useContext(ReorderContext); - const { isReorderOn, reorderedItems, draggingHeight, direction, groupId } = reorderState; - const currentIndex = itemsInGroup.indexOf(id); + const resetReorderState = () => + setReorderState((s: ReorderState) => ({ + ...s, + reorderedItems: [], + })); + + const extraKeyboardHandler = (e: React.KeyboardEvent<HTMLButtonElement>) => { + if (isReorderOn && keyboardMode) { + e.stopPropagation(); + e.preventDefault(); + let activeDropTargetIndex = reorderableGroup.findIndex((i) => i.id === value.id); + if (activeDropTarget?.activeDropTarget) { + const index = reorderableGroup.findIndex( + (i) => i.id === activeDropTarget.activeDropTarget?.id + ); + if (index !== -1) activeDropTargetIndex = index; + } + if (keys.ARROW_DOWN === e.key) { + if (activeDropTargetIndex < reorderableGroup.length - 1) { + setA11yMessage( + reorderAnnouncements.moved(label, activeDropTargetIndex + 2, currentIndex + 1) + ); + onReorderableDragOver(reorderableGroup[activeDropTargetIndex + 1]); + } + } else if (keys.ARROW_UP === e.key) { + if (activeDropTargetIndex > 0) { + setA11yMessage( + reorderAnnouncements.moved(label, activeDropTargetIndex, currentIndex + 1) + ); + + onReorderableDragOver(reorderableGroup[activeDropTargetIndex - 1]); + } + } + } + }; - useEffect( - () => + const onReorderableDragOver = (target: DragDropIdentifier) => { + let droppingIndex = currentIndex; + if (keyboardMode && 'id' in target) { + setActiveDropTarget(target); + droppingIndex = reorderableGroup.findIndex((i) => i.id === target.id); + } + const draggingIndex = reorderableGroup.findIndex((i) => i.id === value?.id); + if (draggingIndex === -1) { + return; + } + + if (draggingIndex === droppingIndex) { setReorderState((s: ReorderState) => ({ ...s, - isReorderOn: draggingProps.isReorderDragging, - })), - [draggingProps.isReorderDragging, setReorderState] - ); + reorderedItems: [], + })); + } + + setReorderState((s: ReorderState) => + draggingIndex < droppingIndex + ? { + ...s, + reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1), + direction: '-', + } + : { + ...s, + reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex), + direction: '+', + } + ); + }; + + const areItemsReordered = isDragging && keyboardMode && reorderedItems.length; return ( <div - className={classNames('lnsDragDrop__reorderableContainer', className)} - data-test-subj={dataTestSubj} - > - <EuiScreenReaderOnly showOnFocus> - <button - aria-label={label} - aria-describedby={`lnsDragDrop-reorderInstructions-${groupId}`} - className="lnsDragDrop__keyboardHandler" - data-test-subj="lnsDragDrop-keyboardHandler" - onBlur={() => { - setReorderState((s: ReorderState) => ({ - ...s, - isReorderOn: false, - keyboardReorderMessage: '', - })); - }} - onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => { - if (e.key === keys.ENTER || e.key === keys.SPACE) { - setReorderState((s: ReorderState) => ({ - ...s, - isReorderOn: !isReorderOn, - keyboardReorderMessage: isReorderOn - ? '' - : getKeyboardReorderMessageLifted(label, currentIndex + 1), - })); - } else if (e.key === keys.ESCAPE) { - setReorderState((s: ReorderState) => ({ - ...s, - isReorderOn: false, - keyboardReorderMessage: '', - })); + data-test-subj="lnsDragDrop-reorderableDrag" + className={ + isDragging + ? 'lnsDragDrop-reorderable lnsDragDrop-translatableDrag' + : 'lnsDragDrop-reorderable' + } + style={ + areItemsReordered + ? { + transform: `translateY(${direction === '+' ? '-' : '+'}${reorderedItems.reduce( + (acc, cur) => { + return acc + Number(cur.height || 0) + lnsLayerPanelDimensionMargin; + }, + 0 + )}px)`, } - if (isReorderOn) { - e.stopPropagation(); - e.preventDefault(); - - if (keys.ARROW_DOWN === e.key) { - if (currentIndex < itemsInGroup.length - 1) { - setReorderState((s: ReorderState) => ({ - ...s, - keyboardReorderMessage: getKeyboardReorderMessageMoved( - label, - currentIndex + 2, - currentIndex + 1 - ), - })); - dropTo(itemsInGroup[currentIndex + 1]); - } - } else if (keys.ARROW_UP === e.key) { - if (currentIndex > 0) { - setReorderState((s: ReorderState) => ({ - ...s, - keyboardReorderMessage: getKeyboardReorderMessageMoved( - label, - currentIndex, - currentIndex + 1 - ), - })); - dropTo(itemsInGroup[currentIndex - 1]); - } + : undefined + } + > + <DragInner + {...props} + extraKeyboardHandler={extraKeyboardHandler} + onDragStart={onReorderableDragStart} + onDragEnd={onReorderableDragEnd} + onDrop={onReorderableDrop} + /> + </div> + ); +}); + +const ReorderableDrop = memo(function ReorderableDrop( + props: DropInnerProps & { reorderableGroup: DragDropIdentifier[] } +) { + const { + onDrop, + value, + droppable, + dragging, + setDragging, + setKeyboardMode, + activeDropTarget, + setActiveDropTarget, + reorderableGroup, + setA11yMessage, + } = props; + + const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id); + const activeDropTargetMatches = + activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id; + + const { + reorderState: { isReorderOn, reorderedItems, draggingHeight, direction }, + setReorderState, + } = useContext(ReorderContext); + + const heightRef = React.useRef<HTMLDivElement>(null); + + const isReordered = + isReorderOn && reorderedItems.some((el) => el.id === value.id) && reorderedItems.length; + + useEffect(() => { + if (isReordered && heightRef.current?.clientHeight) { + setReorderState((s) => ({ + ...s, + reorderedItems: s.reorderedItems.map((el) => + el.id === value.id + ? { + ...el, + height: heightRef.current?.clientHeight, } - } - }} - /> - </EuiScreenReaderOnly> - {React.cloneElement(children, { - ['data-test-subj']: 'lnsDragDrop-reorderableDrag', - draggable: draggingProps.draggable, - onDragEnd: draggingProps.onDragEnd, - onDragStart: (e: DroppableEvent) => { - const height = e.currentTarget.offsetHeight + lnsLayerPanelDimensionMargin; - setReorderState((s: ReorderState) => ({ + : el + ), + })); + } + }, [isReordered, setReorderState, value.id]); + + const onReorderableDragOver = (e: DroppableEvent) => { + if (!droppable) { + return; + } + e.preventDefault(); + + // An optimization to prevent a bunch of React churn. + // todo: replace with custom function ? + if (!activeDropTargetMatches) { + setActiveDropTarget(value); + } + + const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id); + + if (!dragging || draggingIndex === -1) { + return; + } + const droppingIndex = currentIndex; + if (draggingIndex === droppingIndex) { + setReorderState((s: ReorderState) => ({ + ...s, + reorderedItems: [], + })); + } + + setReorderState((s: ReorderState) => + draggingIndex < droppingIndex + ? { ...s, - draggingHeight: height, - })); - draggingProps.onDragStart(e); - }, - className: classNames( - draggingProps.className, - { - 'lnsDragDrop-isKeyboardModeActive': isReorderOn, - }, - { - 'lnsDragDrop-isReorderable': draggingProps.isReorderDragging, + reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1), + direction: '-', } - ), - style: reorderedItems.includes(id) - ? { - transform: `translateY(${direction}${draggingHeight}px)`, - } - : {}, - })} + : { + ...s, + reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex), + direction: '+', + } + ); + }; + + const onReorderableDrop = (e: DroppableEvent) => { + e.preventDefault(); + e.stopPropagation(); + + setActiveDropTarget(undefined); + setDragging(undefined); + setKeyboardMode(false); + + if (onDrop && droppable && dragging) { + trackUiEvent('drop_total'); + + onDrop(dragging, value); + const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging.id); + // setTimeout ensures it will run after dragEnd messaging + setTimeout(() => + setA11yMessage(reorderAnnouncements.dropped(currentIndex + 1, draggingIndex + 1)) + ); + } + }; + + return ( + <div> <div - data-test-subj="lnsDragDrop-reorderableDrop" + style={ + reorderedItems.some((i) => i.id === value.id) + ? { + transform: `translateY(${direction}${draggingHeight}px)`, + } + : undefined + } + ref={heightRef} + data-test-subj="lnsDragDrop-translatableDrop" + className="lnsDragDrop-translatableDrop lnsDragDrop-reorderable" + > + <DropInner {...props} /> + </div> + + <div + data-test-subj="lnsDragDrop-reorderableDropLayer" className={classNames('lnsDragDrop', { ['lnsDragDrop__reorderableDrop']: dragging && droppable, })} - onDrop={(e) => { - dropProps.onDrop(e); - setReorderState((s: ReorderState) => ({ - ...s, - reorderedItems: [], - })); - }} - onDragOver={(e: DroppableEvent) => { - if (!droppable) { - return; - } - dropProps.onDragOver(e); - if (!dropProps.isActive) { - if (!dragging) { - return; - } - const draggingIndex = itemsInGroup.indexOf(dragging.id); - const droppingIndex = currentIndex; - if (draggingIndex === droppingIndex) { - setReorderState((s: ReorderState) => ({ - ...s, - reorderedItems: [], - })); - } - - setReorderState((s: ReorderState) => - draggingIndex < droppingIndex - ? { - ...s, - reorderedItems: itemsInGroup.slice(draggingIndex + 1, droppingIndex + 1), - direction: '-', - } - : { - ...s, - reorderedItems: itemsInGroup.slice(droppingIndex, draggingIndex), - direction: '+', - } - ); - } - }} + onDrop={onReorderableDrop} + onDragOver={onReorderableDragOver} onDragLeave={() => { - dropProps.onDragLeave(); setReorderState((s: ReorderState) => ({ ...s, reorderedItems: [], @@ -527,4 +719,4 @@ export const ReorderableDragDrop = ({ /> </div> ); -}; +}); diff --git a/x-pack/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers.tsx index 5e0fc648454ad..86ff5054520af 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers.tsx @@ -9,12 +9,13 @@ import classNames from 'classnames'; import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -export type Dragging = - | (Record<string, unknown> & { - id: string; - }) - | undefined; +export type DragDropIdentifier = Record<string, unknown> & { + id: string; +}; +export interface ActiveDropTarget { + activeDropTarget?: DragDropIdentifier; +} /** * The shape of the drag / drop context. */ @@ -22,12 +23,26 @@ export interface DragContextState { /** * The item being dragged or undefined. */ - dragging: Dragging; + dragging?: DragDropIdentifier; + /** + * keyboard mode + */ + keyboardMode: boolean; + /** + * keyboard mode + */ + setKeyboardMode: (mode: boolean) => void; /** * Set the item being dragged. */ - setDragging: (dragging: Dragging) => void; + setDragging: (dragging?: DragDropIdentifier) => void; + + activeDropTarget?: ActiveDropTarget; + + setActiveDropTarget: (newTarget?: DragDropIdentifier) => void; + + setA11yMessage: (message: string) => void; } /** @@ -38,28 +53,52 @@ export interface DragContextState { export const DragContext = React.createContext<DragContextState>({ dragging: undefined, setDragging: () => {}, + keyboardMode: false, + setKeyboardMode: () => {}, + activeDropTarget: undefined, + setActiveDropTarget: () => {}, + setA11yMessage: () => {}, }); /** * The argument to DragDropProvider. */ export interface ProviderProps { + /** + * keyboard mode + */ + keyboardMode: boolean; + /** + * keyboard mode + */ + setKeyboardMode: (mode: boolean) => void; + /** + * Set the item being dragged. + */ /** * The item being dragged. If unspecified, the provider will * behave as if it is the root provider. */ - dragging: Dragging; + dragging?: DragDropIdentifier; /** * Sets the item being dragged. If unspecified, the provider * will behave as if it is the root provider. */ - setDragging: (dragging: Dragging) => void; + setDragging: (dragging?: DragDropIdentifier) => void; + + activeDropTarget?: { + activeDropTarget?: DragDropIdentifier; + }; + + setActiveDropTarget: (newTarget?: DragDropIdentifier) => void; /** * The React children. */ children: React.ReactNode; + + setA11yMessage: (message: string) => void; } /** @@ -70,15 +109,60 @@ export interface ProviderProps { * @param props */ export function RootDragDropProvider({ children }: { children: React.ReactNode }) { - const [state, setState] = useState<{ dragging: Dragging }>({ + const [draggingState, setDraggingState] = useState<{ dragging?: DragDropIdentifier }>({ dragging: undefined, }); - const setDragging = useMemo(() => (dragging: Dragging) => setState({ dragging }), [setState]); + const [keyboardModeState, setKeyboardModeState] = useState(false); + const [a11yMessageState, setA11yMessageState] = useState(''); + const [activeDropTargetState, setActiveDropTargetState] = useState<{ + activeDropTarget?: DragDropIdentifier; + }>({ + activeDropTarget: undefined, + }); + + const setDragging = useMemo( + () => (dragging?: DragDropIdentifier) => setDraggingState({ dragging }), + [setDraggingState] + ); + + const setA11yMessage = useMemo(() => (message: string) => setA11yMessageState(message), [ + setA11yMessageState, + ]); + + const setActiveDropTarget = useMemo( + () => (activeDropTarget?: DragDropIdentifier) => + setActiveDropTargetState((s) => ({ ...s, activeDropTarget })), + [setActiveDropTargetState] + ); return ( - <ChildDragDropProvider dragging={state.dragging} setDragging={setDragging}> - {children} - </ChildDragDropProvider> + <div> + <ChildDragDropProvider + keyboardMode={keyboardModeState} + setKeyboardMode={setKeyboardModeState} + dragging={draggingState.dragging} + setA11yMessage={setA11yMessage} + setDragging={setDragging} + activeDropTarget={activeDropTargetState} + setActiveDropTarget={setActiveDropTarget} + > + {children} + </ChildDragDropProvider> + <EuiPortal> + <EuiScreenReaderOnly> + <div> + <p aria-live="assertive" aria-atomic={true}> + {a11yMessageState} + </p> + <p id={`lnsDragDrop-keyboardInstructions`}> + {i18n.translate('xpack.lens.dragDrop.keyboardInstructions', { + defaultMessage: `Press enter or space to start reordering the dimension group. When dragging, use arrow keys to reorder. Press enter or space again to finish.`, + })} + </p> + </div> + </EuiScreenReaderOnly> + </EuiPortal> + </div> ); } @@ -89,8 +173,36 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } * * @param props */ -export function ChildDragDropProvider({ dragging, setDragging, children }: ProviderProps) { - const value = useMemo(() => ({ dragging, setDragging }), [setDragging, dragging]); +export function ChildDragDropProvider({ + dragging, + setDragging, + setKeyboardMode, + keyboardMode, + activeDropTarget, + setActiveDropTarget, + setA11yMessage, + children, +}: ProviderProps) { + const value = useMemo( + () => ({ + setKeyboardMode, + keyboardMode, + dragging, + setDragging, + activeDropTarget, + setActiveDropTarget, + setA11yMessage, + }), + [ + setDragging, + dragging, + activeDropTarget, + setActiveDropTarget, + setKeyboardMode, + keyboardMode, + setA11yMessage, + ] + ); return <DragContext.Provider value={value}>{children}</DragContext.Provider>; } @@ -98,7 +210,7 @@ export interface ReorderState { /** * Ids of the elements that are translated up or down */ - reorderedItems: string[]; + reorderedItems: DragDropIdentifier[]; /** * Direction of the move of dragged element in the reordered list @@ -112,10 +224,6 @@ export interface ReorderState { * indicates that user is in keyboard mode */ isReorderOn: boolean; - /** - * aria-live message for changes in reordering - */ - keyboardReorderMessage: string; /** * reorder group needed for screen reader aria-described-by attribute */ @@ -135,7 +243,6 @@ export const ReorderContext = React.createContext<ReorderContextState>({ direction: '-', draggingHeight: 40, isReorderOn: false, - keyboardReorderMessage: '', groupId: '', }, setReorderState: () => () => {}, @@ -155,33 +262,70 @@ export function ReorderProvider({ direction: '-', draggingHeight: 40, isReorderOn: false, - keyboardReorderMessage: '', groupId: id, }); const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ setState, ]); - return ( - <div className={classNames(className, { 'lnsDragDrop-isActiveGroup': state.isReorderOn })}> + <div + data-test-subj="lnsDragDrop-reorderableGroup" + className={classNames(className, { + 'lnsDragDrop-isActiveGroup': state.isReorderOn && React.Children.count(children) > 1, + })} + > <ReorderContext.Provider value={{ reorderState: state, setReorderState }}> {children} </ReorderContext.Provider> - <EuiPortal> - <EuiScreenReaderOnly> - <div> - <p aria-live="assertive" aria-atomic={true}> - {state.keyboardReorderMessage} - </p> - <p id={`lnsDragDrop-reorderInstructions-${id}`}> - {i18n.translate('xpack.lens.dragDrop.reorderInstructions', { - defaultMessage: `Press space bar to start a drag. When dragging, use arrow keys to reorder. Press space bar again to finish.`, - })} - </p> - </div> - </EuiScreenReaderOnly> - </EuiPortal> </div> ); } + +export const reorderAnnouncements = { + moved: (itemLabel: string, position: number, prevPosition: number) => { + return prevPosition === position + ? i18n.translate('xpack.lens.dragDrop.elementMovedBack', { + defaultMessage: `You have moved back the item {itemLabel} to position {prevPosition}`, + values: { + itemLabel, + prevPosition, + }, + }) + : i18n.translate('xpack.lens.dragDrop.elementMoved', { + defaultMessage: `You have moved the item {itemLabel} from position {prevPosition} to position {position}`, + values: { + itemLabel, + position, + prevPosition, + }, + }); + }, + + lifted: (itemLabel: string, position: number) => + i18n.translate('xpack.lens.dragDrop.elementLifted', { + defaultMessage: `You have lifted an item {itemLabel} in position {position}`, + values: { + itemLabel, + position, + }, + }), + + cancelled: (position: number) => + i18n.translate('xpack.lens.dragDrop.abortMessageReorder', { + defaultMessage: + 'Movement cancelled. The item has returned to its starting position {position}', + values: { + position, + }, + }), + dropped: (position: number, prevPosition: number) => + i18n.translate('xpack.lens.dragDrop.dropMessageReorder', { + defaultMessage: + 'You have dropped the item. You have moved the item from position {prevPosition} to positon {position}', + values: { + position, + prevPosition, + }, + }), +}; diff --git a/x-pack/plugins/lens/public/drag_drop/readme.md b/x-pack/plugins/lens/public/drag_drop/readme.md index 1e812c7adac27..e48564a074986 100644 --- a/x-pack/plugins/lens/public/drag_drop/readme.md +++ b/x-pack/plugins/lens/public/drag_drop/readme.md @@ -30,7 +30,7 @@ In your child application, place a `ChildDragDropProvider` at the root of that, This enables your child application to share the same drag / drop context as the root application. -## Dragging +## DragDropIdentifier An item can be both draggable and droppable at the same time, but for simplicity's sake, we'll treat these two cases separately. @@ -88,7 +88,7 @@ The children `DragDrop` components must have props defined as in the example: droppable dragType="reorder" dropType="reorder" - itemsInGroup={fields.map((f) => f.id)} // consists ids of all reorderable elements in the group, eg. ['3', '5', '1'] + reorderableGroup={fields} // consists all reorderable elements in the group, eg. [{id:'3'}, {id:'5'}, {id:'1'}] value={{ id: f.id, }} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index 70c4fb5567226..0ebcb5bb07482 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -92,6 +92,14 @@ describe('ConfigPanel', () => { mockDatasource = createMockDatasource('ds1'); }); + // in what case is this test needed? + it('should fail to render layerPanels if the public API is out of date', () => { + const props = getDefaultProps(); + props.framePublicAPI.datasourceLayers = {}; + const component = mountWithIntl(<LayerPanels {...props} />); + expect(component.find(LayerPanel).exists()).toBe(false); + }); + describe('focus behavior when adding or removing layers', () => { it('should focus the only layer when resetting the layer', () => { const component = mountWithIntl(<LayerPanels {...getDefaultProps()} />); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index ec1a5c226d351..67c8a6b5e4abc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -134,37 +134,42 @@ export function LayerPanels( [dispatch] ); + const datasourcePublicAPIs = props.framePublicAPI.datasourceLayers; + return ( <EuiForm className="lnsConfigPanel"> - {layerIds.map((layerId, index) => ( - <LayerPanel - {...props} - setLayerRef={setLayerRef} - key={layerId} - layerId={layerId} - index={index} - visualizationState={visualizationState} - updateVisualization={setVisualizationState} - updateDatasource={updateDatasource} - updateAll={updateAll} - isOnlyLayer={layerIds.length === 1} - onRemoveLayer={() => { - dispatch({ - type: 'UPDATE_STATE', - subType: 'REMOVE_OR_CLEAR_LAYER', - updater: (state) => - removeLayer({ - activeVisualization, - layerId, - trackUiEvent, - datasourceMap, - state, - }), - }); - removeLayerRef(layerId); - }} - /> - ))} + {layerIds.map((layerId, layerIndex) => + datasourcePublicAPIs[layerId] ? ( + <LayerPanel + {...props} + activeVisualization={activeVisualization} + setLayerRef={setLayerRef} + key={layerId} + layerId={layerId} + layerIndex={layerIndex} + visualizationState={visualizationState} + updateVisualization={setVisualizationState} + updateDatasource={updateDatasource} + updateAll={updateAll} + isOnlyLayer={layerIds.length === 1} + onRemoveLayer={() => { + dispatch({ + type: 'UPDATE_STATE', + subType: 'REMOVE_OR_CLEAR_LAYER', + updater: (state) => + removeLayer({ + activeVisualization, + layerId, + trackUiEvent, + datasourceMap, + state, + }), + }); + removeLayerRef(layerId); + }} + /> + ) : null + )} {activeVisualization.appendLayer && visualizationState && ( <EuiFlexItem grow={true}> <EuiToolTip diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_button.tsx new file mode 100644 index 0000000000000..06f50d7f32440 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_button.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButtonIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ColorIndicator } from './color_indicator'; +import { PaletteIndicator } from './palette_indicator'; +import { VisualizationDimensionGroupConfig, AccessorConfig } from '../../../types'; + +const triggerLinkA11yText = (label: string) => + i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit {label} configuration', + values: { label }, + }); + +export function DimensionButton({ + group, + children, + onClick, + onRemoveClick, + accessorConfig, + label, +}: { + group: VisualizationDimensionGroupConfig; + children: React.ReactElement; + onClick: (id: string) => void; + onRemoveClick: (id: string) => void; + accessorConfig: AccessorConfig; + label: string; +}) { + return ( + <> + <EuiLink + className="lnsLayerPanel__dimensionLink" + data-test-subj="lnsLayerPanel-dimensionLink" + onClick={() => onClick(accessorConfig.columnId)} + aria-label={triggerLinkA11yText(label)} + title={triggerLinkA11yText(label)} + > + <ColorIndicator accessorConfig={accessorConfig}>{children}</ColorIndicator> + </EuiLink> + <EuiButtonIcon + className="lnsLayerPanel__dimensionRemove" + data-test-subj="indexPattern-dimension-remove" + iconType="cross" + iconSize="s" + size="s" + color="danger" + aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { + defaultMessage: 'Remove configuration from "{groupLabel}"', + values: { groupLabel: group.groupLabel }, + })} + title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { + defaultMessage: 'Remove configuration from "{groupLabel}"', + values: { groupLabel: group.groupLabel }, + })} + onClick={() => onRemoveClick(accessorConfig.columnId)} + /> + <PaletteIndicator accessorConfig={accessorConfig} /> + </> + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx new file mode 100644 index 0000000000000..8de57cb43b16f --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -0,0 +1,110 @@ +/* + * 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, { useMemo } from 'react'; +import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop'; +import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation } from '../../../types'; +import { LayerDatasourceDropProps } from './types'; + +const isFromTheSameGroup = (el1: DragDropIdentifier, el2?: DragDropIdentifier) => + el2 && isDraggedOperation(el2) && el1.groupId === el2.groupId && el1.columnId !== el2.columnId; + +const isSelf = (el1: DragDropIdentifier, el2?: DragDropIdentifier) => + isDraggedOperation(el2) && el1.columnId === el2.columnId; + +export function DraggableDimensionButton({ + layerId, + label, + accessorIndex, + groupIndex, + layerIndex, + columnId, + group, + onDrop, + children, + dragDropContext, + layerDatasourceDropProps, + layerDatasource, +}: { + dragDropContext: DragContextState; + layerId: string; + groupIndex: number; + layerIndex: number; + onDrop: (droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier) => void; + group: VisualizationDimensionGroupConfig; + label: string; + children: React.ReactElement; + layerDatasource: Datasource<unknown, unknown>; + layerDatasourceDropProps: LayerDatasourceDropProps; + accessorIndex: number; + columnId: string; +}) { + const value = useMemo(() => { + return { + columnId, + groupId: group.groupId, + layerId, + id: columnId, + }; + }, [columnId, group.groupId, layerId]); + + const { dragging } = dragDropContext; + + const isCurrentGroup = group.groupId === dragging?.groupId; + const isOperationDragged = isDraggedOperation(dragging); + const canHandleDrop = + Boolean(dragDropContext.dragging) && + layerDatasource.canHandleDrop({ + ...layerDatasourceDropProps, + columnId, + filterOperations: group.filterOperations, + }); + + const dragType = isSelf(value, dragging) + ? 'move' + : isOperationDragged && isCurrentGroup + ? 'reorder' + : 'copy'; + + const dropType = isOperationDragged ? (!isCurrentGroup ? 'replace' : 'reorder') : 'add'; + + const isCompatibleFromOtherGroup = !isCurrentGroup && canHandleDrop; + + const isDroppable = isOperationDragged + ? dragType === 'reorder' + ? isFromTheSameGroup(value, dragging) + : isCompatibleFromOtherGroup + : canHandleDrop; + + const reorderableGroup = useMemo( + () => + group.accessors.map((a) => ({ + columnId: a.columnId, + id: a.columnId, + groupId: group.groupId, + layerId, + })), + [group, layerId] + ); + + return ( + <div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}> + <DragDrop + noKeyboardSupportYet={reorderableGroup.length < 2} // to be removed when navigating outside of groups is added + draggable + dragType={dragType} + dropType={dropType} + reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined} + value={value} + label={label} + droppable={dragging && isDroppable} + onDrop={onDrop} + > + {children} + </DragDrop> + </div> + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx new file mode 100644 index 0000000000000..88e1663d0b49c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { generateId } from '../../../id_generator'; +import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop'; +import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation } from '../../../types'; +import { LayerDatasourceDropProps } from './types'; + +export function EmptyDimensionButton({ + dragDropContext, + group, + layerDatasource, + layerDatasourceDropProps, + layerId, + groupIndex, + layerIndex, + onClick, + onDrop, +}: { + dragDropContext: DragContextState; + layerId: string; + groupIndex: number; + layerIndex: number; + onClick: (id: string) => void; + onDrop: (droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier) => void; + group: VisualizationDimensionGroupConfig; + + layerDatasource: Datasource<unknown, unknown>; + layerDatasourceDropProps: LayerDatasourceDropProps; +}) { + const handleDrop = (droppedItem: DragDropIdentifier) => onDrop(droppedItem, value); + + const value = useMemo(() => { + const newId = generateId(); + return { + columnId: newId, + groupId: group.groupId, + layerId, + isNew: true, + id: newId, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [group.accessors.length, group.groupId, layerId]); + + return ( + <div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}> + <DragDrop + value={value} + onDrop={handleDrop} + droppable={ + Boolean(dragDropContext.dragging) && + // Verify that the dragged item is not coming from the same group + // since this would be a duplicate + (!isDraggedOperation(dragDropContext.dragging) || + dragDropContext.dragging.groupId !== group.groupId) && + layerDatasource.canHandleDrop({ + ...layerDatasourceDropProps, + columnId: value.columnId, + filterOperations: group.filterOperations, + }) + } + > + <div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty"> + <EuiButtonEmpty + className="lnsLayerPanel__triggerText" + color="text" + size="xs" + iconType="plusInCircleFilled" + contentProps={{ + className: 'lnsLayerPanel__triggerTextContent', + }} + aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnAriaLabel', { + defaultMessage: 'Drop a field or click to add to {groupLabel}', + values: { groupLabel: group.groupLabel }, + })} + data-test-subj="lns-empty-dimension" + onClick={() => { + onClick(value.columnId); + }} + > + <FormattedMessage + id="xpack.lens.configure.emptyConfig" + defaultMessage="Drop a field or click to add" + /> + </EuiButtonEmpty> + </div> + </DragDrop> + </div> + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index 2ed91b962ff11..ec4c2adba8fd7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -76,6 +76,7 @@ .lnsLayerPanel__dimensionContainer { margin: 0 $euiSizeS $euiSizeS; + position: relative; &:last-child { margin-bottom: 0; @@ -127,12 +128,13 @@ } } -.lnsLayerPanel__dimensionLink { +// Added .lnsLayerPanel__dimension specificity required for animation style override +.lnsLayerPanel__dimension .lnsLayerPanel__dimensionLink { width: 100%; &:focus { - background-color: transparent !important; // sass-lint:disable-line no-important - outline: none !important; // sass-lint:disable-line no-important + background-color: transparent; + animation: none !important; // sass-lint:disable-line no-important } &:focus .lnsLayerPanel__triggerTextLabel, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index cab07150b6d56..d93cbbb58835e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -12,7 +12,7 @@ import { createMockDatasource, DatasourceMock, } from '../../mocks'; -import { ChildDragDropProvider, DroppableEvent } from '../../../drag_drop'; +import { ChildDragDropProvider, DroppableEvent, DragDrop } from '../../../drag_drop'; import { EuiFormRow } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test/jest'; import { Visualization } from '../../../types'; @@ -22,9 +22,20 @@ import { generateId } from '../../../id_generator'; jest.mock('../../../id_generator'); +const defaultContext = { + dragging: undefined, + setDragging: jest.fn(), + setActiveDropTarget: () => {}, + activeDropTarget: undefined, + keyboardMode: false, + setKeyboardMode: () => {}, + setA11yMessage: jest.fn(), +}; + describe('LayerPanel', () => { let mockVisualization: jest.Mocked<Visualization>; let mockVisualization2: jest.Mocked<Visualization>; + let mockDatasource: DatasourceMock; function getDefaultProps() { @@ -34,11 +45,7 @@ describe('LayerPanel', () => { }; return { layerId: 'first', - activeVisualizationId: 'vis1', - visualizationMap: { - vis1: mockVisualization, - vis2: mockVisualization2, - }, + activeVisualization: mockVisualization, activeDatasourceId: 'ds1', datasourceMap: { ds1: mockDatasource, @@ -58,7 +65,7 @@ describe('LayerPanel', () => { onRemoveLayer: jest.fn(), dispatch: jest.fn(), core: coreMock.createStart(), - index: 0, + layerIndex: 0, setLayerRef: jest.fn(), }; } @@ -92,20 +99,6 @@ describe('LayerPanel', () => { mockDatasource = createMockDatasource('ds1'); }); - it('should fail to render if the public API is out of date', () => { - const props = getDefaultProps(); - props.framePublicAPI.datasourceLayers = {}; - const component = mountWithIntl(<LayerPanel {...props} />); - expect(component.isEmptyRender()).toBe(true); - }); - - it('should fail to render if the active visualization is missing', () => { - const component = mountWithIntl( - <LayerPanel {...getDefaultProps()} activeVisualizationId="missing" /> - ); - expect(component.isEmptyRender()).toBe(true); - }); - describe('layer reset and remove', () => { it('should show the reset button when single layer', () => { const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />); @@ -147,8 +140,7 @@ describe('LayerPanel', () => { }); const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />); - - const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); + const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); expect(group).toHaveLength(1); }); @@ -167,8 +159,7 @@ describe('LayerPanel', () => { }); const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />); - - const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); + const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); expect(group).toHaveLength(1); }); @@ -231,50 +222,6 @@ describe('LayerPanel', () => { expect(panel.props.children).toHaveLength(2); }); - it('should keep the DimensionContainer open when configuring a new dimension', () => { - /** - * The ID generation system for new dimensions has been messy before, so - * this tests that the ID used in the first render is used to keep the container - * open in future renders - */ - (generateId as jest.Mock).mockReturnValueOnce(`newid`); - (generateId as jest.Mock).mockReturnValueOnce(`bad`); - mockVisualization.getConfiguration.mockReturnValueOnce({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: [], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - }, - ], - }); - // Normally the configuration would change in response to a state update, - // but this test is updating it directly - mockVisualization.getConfiguration.mockReturnValueOnce({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: [{ columnId: 'newid' }], - filterOperations: () => true, - supportsMoreColumns: false, - dataTestSubj: 'lnsGroup', - }, - ], - }); - - const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />); - act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); - }); - component.update(); - - expect(component.find('EuiFlyoutHeader').exists()).toBe(true); - }); - it('should not update the visualization if the datasource is incomplete', () => { (generateId as jest.Mock).mockReturnValueOnce(`newid`); const updateAll = jest.fn(); @@ -338,6 +285,50 @@ describe('LayerPanel', () => { expect(updateAll).toHaveBeenCalled(); }); + it('should keep the DimensionContainer open when configuring a new dimension', () => { + /** + * The ID generation system for new dimensions has been messy before, so + * this tests that the ID used in the first render is used to keep the container + * open in future renders + */ + (generateId as jest.Mock).mockReturnValueOnce(`newid`); + (generateId as jest.Mock).mockReturnValueOnce(`bad`); + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + // Normally the configuration would change in response to a state update, + // but this test is updating it directly + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'newid' }], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />); + act(() => { + component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + }); + component.update(); + + expect(component.find('EuiFlyoutHeader').exists()).toBe(true); + }); + it('should close the DimensionContainer when the active visualization changes', () => { /** * The ID generation system for new dimensions has been messy before, so @@ -361,7 +352,7 @@ describe('LayerPanel', () => { }); // Normally the configuration would change in response to a state update, // but this test is updating it directly - mockVisualization.getConfiguration.mockReturnValueOnce({ + mockVisualization.getConfiguration.mockReturnValue({ groups: [ { groupLabel: 'A', @@ -382,7 +373,7 @@ describe('LayerPanel', () => { component.update(); expect(component.find('EuiFlyoutHeader').exists()).toBe(true); act(() => { - component.setProps({ activeVisualizationId: 'vis2' }); + component.setProps({ activeVisualization: mockVisualization2 }); }); component.update(); expect(component.find('EuiFlyoutHeader').exists()).toBe(false); @@ -452,7 +443,7 @@ describe('LayerPanel', () => { const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' }; const component = mountWithIntl( - <ChildDragDropProvider dragging={draggingField} setDragging={jest.fn()}> + <ChildDragDropProvider {...defaultContext} dragging={draggingField}> <LayerPanel {...getDefaultProps()} /> </ChildDragDropProvider> ); @@ -465,7 +456,7 @@ describe('LayerPanel', () => { }) ); - component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop'); + component.find('[data-test-subj="lnsGroup"] DragDrop').first().simulate('drop'); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ @@ -495,7 +486,7 @@ describe('LayerPanel', () => { const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' }; const component = mountWithIntl( - <ChildDragDropProvider dragging={draggingField} setDragging={jest.fn()}> + <ChildDragDropProvider {...defaultContext} dragging={draggingField}> <LayerPanel {...getDefaultProps()} /> </ChildDragDropProvider> ); @@ -505,10 +496,14 @@ describe('LayerPanel', () => { ); expect( - component.find('DragDrop[data-test-subj="lnsGroup"]').first().prop('droppable') + component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('droppable') ).toEqual(false); - component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop'); + component + .find('[data-test-subj="lnsGroup"] DragDrop') + .first() + .find('.lnsLayerPanel__dimension') + .simulate('drop'); expect(mockDatasource.onDrop).not.toHaveBeenCalled(); }); @@ -542,12 +537,11 @@ describe('LayerPanel', () => { const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' }; const component = mountWithIntl( - <ChildDragDropProvider dragging={draggingOperation} setDragging={jest.fn()}> + <ChildDragDropProvider {...defaultContext} dragging={draggingOperation}> <LayerPanel {...getDefaultProps()} /> </ChildDragDropProvider> ); - expect(mockDatasource.canHandleDrop).toHaveBeenCalledTimes(2); expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( expect.objectContaining({ dragDropContext: expect.objectContaining({ @@ -557,7 +551,7 @@ describe('LayerPanel', () => { ); // Simulate drop on the pre-populated dimension - component.find('DragDrop[data-test-subj="lnsGroupB"]').at(0).simulate('drop'); + component.find('[data-test-subj="lnsGroupB"] DragDrop').at(0).simulate('drop'); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'b', @@ -568,7 +562,7 @@ describe('LayerPanel', () => { ); // Simulate drop on the empty dimension - component.find('DragDrop[data-test-subj="lnsGroupB"]').at(1).simulate('drop'); + component.find('[data-test-subj="lnsGroupB"] DragDrop').at(1).simulate('drop'); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'newid', @@ -596,18 +590,55 @@ describe('LayerPanel', () => { const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' }; const component = mountWithIntl( - <ChildDragDropProvider dragging={draggingOperation} setDragging={jest.fn()}> + <ChildDragDropProvider {...defaultContext} dragging={draggingOperation}> + <LayerPanel {...getDefaultProps()} /> + </ChildDragDropProvider> + ); + + component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, { + layerId: 'first', + columnId: 'b', + groupId: 'a', + id: 'b', + }); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + groupId: 'a', + droppedItem: draggingOperation, + }) + ); + }); + + it('should copy when dropping on empty slot in the same group', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'a' }, { columnId: 'b' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' }; + + const component = mountWithIntl( + <ChildDragDropProvider {...defaultContext} dragging={draggingOperation}> <LayerPanel {...getDefaultProps()} /> </ChildDragDropProvider> ); - expect(mockDatasource.canHandleDrop).not.toHaveBeenCalled(); - component.find('DragDrop[data-test-subj="lnsGroup"]').at(1).prop('onDrop')!( + component.find('[data-test-subj="lnsGroup"] DragDrop').at(2).prop('onDrop')!( (draggingOperation as unknown) as DroppableEvent ); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - isReorder: true, + groupId: 'a', + droppedItem: draggingOperation, + isNew: true, }) ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 999f75686b1cb..a1b13878851ee 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -5,66 +5,35 @@ */ import './layer_panel.scss'; -import React, { useContext, useState, useEffect } from 'react'; -import { - EuiPanel, - EuiSpacer, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiFormRow, - EuiLink, -} from '@elastic/eui'; +import React, { useContext, useState, useEffect, useMemo, useCallback } from 'react'; +import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { NativeRenderer } from '../../../native_renderer'; -import { StateSetter, isDraggedOperation } from '../../../types'; -import { DragContext, DragDrop, ChildDragDropProvider, ReorderProvider } from '../../../drag_drop'; +import { StateSetter, Visualization } from '../../../types'; +import { + DragContext, + DragDropIdentifier, + ChildDragDropProvider, + ReorderProvider, +} from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; -import { generateId } from '../../../id_generator'; -import { ConfigPanelWrapperProps, ActiveDimensionState } from './types'; +import { LayerPanelProps, ActiveDimensionState } from './types'; import { DimensionContainer } from './dimension_container'; -import { ColorIndicator } from './color_indicator'; -import { PaletteIndicator } from './palette_indicator'; - -const triggerLinkA11yText = (label: string) => - i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Click to edit configuration for {label} or drag to move', - values: { label }, - }); +import { RemoveLayerButton } from './remove_layer_button'; +import { EmptyDimensionButton } from './empty_dimension_button'; +import { DimensionButton } from './dimension_button'; +import { DraggableDimensionButton } from './draggable_dimension_button'; const initialActiveDimensionState = { isNew: false, }; -function isConfiguration( - value: unknown -): value is { columnId: string; groupId: string; layerId: string } { - return ( - Boolean(value) && - typeof value === 'object' && - 'columnId' in value! && - 'groupId' in value && - 'layerId' in value - ); -} - -function isSameConfiguration(config1: unknown, config2: unknown) { - return ( - isConfiguration(config1) && - isConfiguration(config2) && - config1.columnId === config2.columnId && - config1.groupId === config2.groupId && - config1.layerId === config2.layerId - ); -} - export function LayerPanel( - props: Exclude<ConfigPanelWrapperProps, 'state' | 'setState'> & { + props: Exclude<LayerPanelProps, 'state' | 'setState'> & { + activeVisualization: Visualization; layerId: string; - index: number; + layerIndex: number; isOnlyLayer: boolean; updateVisualization: StateSetter<unknown>; updateDatasource: (datasourceId: string, newState: unknown) => void; @@ -82,26 +51,25 @@ export function LayerPanel( initialActiveDimensionState ); - const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, setLayerRef, index } = props; + const { + framePublicAPI, + layerId, + isOnlyLayer, + onRemoveLayer, + setLayerRef, + layerIndex, + activeVisualization, + updateVisualization, + updateDatasource, + } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; useEffect(() => { setActiveDimension(initialActiveDimensionState); - }, [props.activeVisualizationId]); + }, [activeVisualization.id]); - const setLayerRefMemoized = React.useCallback((el) => setLayerRef(layerId, el), [ - layerId, - setLayerRef, - ]); + const setLayerRefMemoized = useCallback((el) => setLayerRef(layerId, el), [layerId, setLayerRef]); - if ( - !datasourcePublicAPI || - !props.activeVisualizationId || - !props.visualizationMap[props.activeVisualizationId] - ) { - return null; - } - const activeVisualization = props.visualizationMap[props.activeVisualizationId]; const layerVisualizationConfigProps = { layerId, dragDropContext, @@ -110,18 +78,23 @@ export function LayerPanel( dateRange: props.framePublicAPI.dateRange, activeData: props.framePublicAPI.activeData, }; + const datasourceId = datasourcePublicAPI.datasourceId; const layerDatasourceState = props.datasourceStates[datasourceId].state; - const layerDatasource = props.datasourceMap[datasourceId]; - const layerDatasourceDropProps = { - layerId, - dragDropContext, - state: layerDatasourceState, - setState: (newState: unknown) => { - props.updateDatasource(datasourceId, newState); - }, - }; + const layerDatasourceDropProps = useMemo( + () => ({ + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + updateDatasource(datasourceId, newState); + }, + }), + [layerId, dragDropContext, layerDatasourceState, datasourceId, updateDatasource] + ); + + const layerDatasource = props.datasourceMap[datasourceId]; const layerDatasourceConfigProps = { ...layerDatasourceDropProps, @@ -135,10 +108,68 @@ export function LayerPanel( const { activeId, activeGroup } = activeDimension; const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state); + + const { setDimension, removeDimension } = activeVisualization; + const layerDatasourceOnDrop = layerDatasource.onDrop; + + const onDrop = useMemo(() => { + return (droppedItem: DragDropIdentifier, targetItem: DragDropIdentifier) => { + const { columnId, groupId, layerId: targetLayerId, isNew } = (targetItem as unknown) as { + groupId: string; + columnId: string; + layerId: string; + isNew?: boolean; + }; + + const filterOperations = + groups.find(({ groupId: gId }) => gId === targetItem.groupId)?.filterOperations || + (() => false); + + const dropResult = layerDatasourceOnDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId, + groupId, + layerId: targetLayerId, + isNew, + filterOperations, + }); + if (dropResult) { + updateVisualization( + setDimension({ + columnId, + groupId, + layerId: targetLayerId, + prevState: props.visualizationState, + }) + ); + + if (typeof dropResult === 'object') { + // When a column is moved, we delete the reference to the old + updateVisualization( + removeDimension({ + columnId: dropResult.deleted, + layerId: targetLayerId, + prevState: props.visualizationState, + }) + ); + } + } + }; + }, [ + groups, + layerDatasourceOnDrop, + props.visualizationState, + updateVisualization, + setDimension, + removeDimension, + layerDatasourceDropProps, + ]); + return ( <ChildDragDropProvider {...dragDropContext}> <section tabIndex={-1} ref={setLayerRefMemoized} className="lnsLayerPanel"> - <EuiPanel data-test-subj={`lns-layerPanel-${index}`} paddingSize="s"> + <EuiPanel data-test-subj={`lns-layerPanel-${layerIndex}`} paddingSize="s"> <EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}> <EuiFlexItem grow={false} className="lnsLayerPanel__settingsFlexItem"> <LayerSettings @@ -193,10 +224,8 @@ export function LayerPanel( <EuiSpacer size="m" /> - {groups.map((group) => { - const newId = generateId(); + {groups.map((group, groupIndex) => { const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( <EuiFormRow className={ @@ -222,261 +251,86 @@ export function LayerPanel( } > <> - <ReorderProvider - id={`${layerId}-${group.groupId}`} - className={'lnsLayerPanel__group'} - > - {group.accessors.map((accessorConfig) => { - const accessor = accessorConfig.columnId; - const { dragging } = dragDropContext; - const dragType = - isDraggedOperation(dragging) && accessor === dragging.columnId - ? 'move' - : isDraggedOperation(dragging) && group.groupId === dragging.groupId - ? 'reorder' - : 'copy'; - - const dropType = isDraggedOperation(dragging) - ? group.groupId !== dragging.groupId - ? 'replace' - : 'reorder' - : 'add'; - - const isFromCompatibleGroup = - dragging?.groupId !== group.groupId && - layerDatasource.canHandleDrop({ - ...layerDatasourceDropProps, - columnId: accessor, - filterOperations: group.filterOperations, - }); - - const isFromTheSameGroup = - isDraggedOperation(dragging) && - dragging.groupId === group.groupId && - dragging.columnId !== accessor; - - const isDroppable = isDraggedOperation(dragging) - ? dragType === 'reorder' - ? isFromTheSameGroup - : isFromCompatibleGroup - : layerDatasource.canHandleDrop({ - ...layerDatasourceDropProps, - columnId: accessor, - filterOperations: group.filterOperations, - }); + <ReorderProvider id={group.groupId} className={'lnsLayerPanel__group'}> + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; return ( - <DragDrop - key={accessor} - draggable={!activeId} - dragType={dragType} - dropType={dropType} - data-test-subj={group.dataTestSubj} - itemsInGroup={group.accessors.map((a) => - typeof a === 'string' ? a : a.columnId - )} - className={'lnsLayerPanel__dimensionContainer'} - value={{ - columnId: accessor, - groupId: group.groupId, - layerId, - id: accessor, - }} - isValueEqual={isSameConfiguration} - label={columnLabelMap[accessor]} - droppable={dragging && isDroppable} - dropTo={(dropTargetId: string) => { - layerDatasource.onDrop({ - isReorder: true, - ...layerDatasourceDropProps, - droppedItem: { - columnId: accessor, - groupId: group.groupId, - layerId, - id: accessor, - }, - columnId: dropTargetId, - filterOperations: group.filterOperations, - }); - }} - onDrop={(droppedItem) => { - const isReorder = - isDraggedOperation(droppedItem) && - droppedItem.groupId === group.groupId && - droppedItem.columnId !== accessor; - - const dropResult = layerDatasource.onDrop({ - isReorder, - ...layerDatasourceDropProps, - droppedItem, - columnId: accessor, - filterOperations: group.filterOperations, - }); - if (typeof dropResult === 'object') { - // When a column is moved, we delete the reference to the old - props.updateVisualization( - activeVisualization.removeDimension({ - layerId, - columnId: dropResult.deleted, - prevState: props.visualizationState, - }) - ); - } - }} + <DraggableDimensionButton + accessorIndex={accessorIndex} + columnId={columnId} + dragDropContext={dragDropContext} + group={group} + groupIndex={groupIndex} + key={columnId} + layerDatasourceDropProps={layerDatasourceDropProps} + label={columnLabelMap[columnId]} + layerDatasource={layerDatasource} + layerIndex={layerIndex} + layerId={layerId} + onDrop={onDrop} > <div className="lnsLayerPanel__dimension"> - <EuiLink - className="lnsLayerPanel__dimensionLink" - data-test-subj="lnsLayerPanel-dimensionLink" - onClick={() => { - if (activeId) { - setActiveDimension(initialActiveDimensionState); - } else { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: accessor, - }); - } + <DimensionButton + accessorConfig={accessorConfig} + label={columnLabelMap[accessorConfig.columnId]} + group={group} + onClick={(id: string) => { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); }} - aria-label={triggerLinkA11yText(columnLabelMap[accessor])} - title={triggerLinkA11yText(columnLabelMap[accessor])} - > - <ColorIndicator accessorConfig={accessorConfig}> - <NativeRenderer - render={layerDatasource.renderDimensionTrigger} - nativeProps={{ - ...layerDatasourceConfigProps, - columnId: accessor, - filterOperations: group.filterOperations, - }} - /> - </ColorIndicator> - </EuiLink> - <EuiButtonIcon - className="lnsLayerPanel__dimensionRemove" - data-test-subj="indexPattern-dimension-remove" - iconType="cross" - iconSize="s" - size="s" - color="danger" - aria-label={i18n.translate( - 'xpack.lens.indexPattern.removeColumnLabel', - { - defaultMessage: 'Remove configuration from "{groupLabel}"', - values: { groupLabel: group.groupLabel }, - } - )} - title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { - defaultMessage: 'Remove configuration from "{groupLabel}"', - values: { groupLabel: group.groupLabel }, - })} - onClick={() => { + onRemoveClick={(id: string) => { trackUiEvent('indexpattern_dimension_removed'); props.updateAll( datasourceId, layerDatasource.removeColumn({ layerId, - columnId: accessor, + columnId: id, prevState: layerDatasourceState, }), activeVisualization.removeDimension({ layerId, - columnId: accessor, + columnId: id, prevState: props.visualizationState, }) ); }} - /> - <PaletteIndicator accessorConfig={accessorConfig} /> + > + <NativeRenderer + render={layerDatasource.renderDimensionTrigger} + nativeProps={{ + ...layerDatasourceConfigProps, + columnId: accessorConfig.columnId, + filterOperations: group.filterOperations, + }} + /> + </DimensionButton> </div> - </DragDrop> + </DraggableDimensionButton> ); })} </ReorderProvider> {group.supportsMoreColumns ? ( - <div className={'lnsLayerPanel__dimensionContainer'}> - <DragDrop - data-test-subj={group.dataTestSubj} - droppable={ - Boolean(dragDropContext.dragging) && - // Verify that the dragged item is not coming from the same group - // since this would be a reorder - (!isDraggedOperation(dragDropContext.dragging) || - dragDropContext.dragging.groupId !== group.groupId) && - layerDatasource.canHandleDrop({ - ...layerDatasourceDropProps, - columnId: newId, - filterOperations: group.filterOperations, - }) - } - onDrop={(droppedItem) => { - const dropResult = layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: newId, - filterOperations: group.filterOperations, - }); - if (dropResult) { - props.updateVisualization( - activeVisualization.setDimension({ - layerId, - groupId: group.groupId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - - if (typeof dropResult === 'object') { - // When a column is moved, we delete the reference to the old - props.updateVisualization( - activeVisualization.removeDimension({ - layerId, - columnId: dropResult.deleted, - prevState: props.visualizationState, - }) - ); - } - } - }} - > - <div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty"> - <EuiButtonEmpty - className="lnsLayerPanel__triggerText" - color="text" - size="xs" - iconType="plusInCircleFilled" - contentProps={{ - className: 'lnsLayerPanel__triggerTextContent', - }} - aria-label={i18n.translate( - 'xpack.lens.indexPattern.removeColumnAriaLabel', - { - defaultMessage: 'Drop a field or click to add to {groupLabel}', - values: { groupLabel: group.groupLabel }, - } - )} - data-test-subj="lns-empty-dimension" - onClick={() => { - if (activeId) { - setActiveDimension(initialActiveDimensionState); - } else { - setActiveDimension({ - isNew: true, - activeGroup: group, - activeId: newId, - }); - } - }} - > - <FormattedMessage - id="xpack.lens.configure.emptyConfig" - defaultMessage="Drop a field or click to add" - /> - </EuiButtonEmpty> - </div> - </DragDrop> - </div> + <EmptyDimensionButton + dragDropContext={dragDropContext} + group={group} + groupIndex={groupIndex} + layerId={layerId} + layerIndex={layerIndex} + layerDatasource={layerDatasource} + layerDatasourceDropProps={layerDatasourceDropProps} + onClick={(id) => { + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: true, + }); + }} + onDrop={onDrop} + /> ) : null} </> </EuiFormRow> @@ -572,44 +426,11 @@ export function LayerPanel( <EuiFlexGroup justifyContent="center"> <EuiFlexItem grow={false}> - <EuiButtonEmpty - size="xs" - iconType="trash" - color="danger" - data-test-subj="lnsLayerRemove" - aria-label={ - isOnlyLayer - ? i18n.translate('xpack.lens.resetLayerAriaLabel', { - defaultMessage: 'Reset layer {index}', - values: { index: index + 1 }, - }) - : i18n.translate('xpack.lens.deleteLayerAriaLabel', { - defaultMessage: `Delete layer {index}`, - values: { index: index + 1 }, - }) - } - onClick={() => { - // If we don't blur the remove / clear button, it remains focused - // which is a strange UX in this case. e.target.blur doesn't work - // due to who knows what, but probably event re-writing. Additionally, - // activeElement does not have blur so, we need to do some casting + safeguards. - const el = (document.activeElement as unknown) as { blur: () => void }; - - if (el?.blur) { - el.blur(); - } - - onRemoveLayer(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: `Delete layer`, - })} - </EuiButtonEmpty> + <RemoveLayerButton + onRemoveLayer={onRemoveLayer} + layerIndex={layerIndex} + isOnlyLayer={isOnlyLayer} + /> </EuiFlexItem> </EuiFlexGroup> </EuiPanel> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx new file mode 100644 index 0000000000000..526e2fcefe19d --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function RemoveLayerButton({ + onRemoveLayer, + layerIndex, + isOnlyLayer, +}: { + onRemoveLayer: () => void; + layerIndex: number; + isOnlyLayer: boolean; +}) { + return ( + <EuiButtonEmpty + size="xs" + iconType="trash" + color="danger" + data-test-subj="lnsLayerRemove" + aria-label={ + isOnlyLayer + ? i18n.translate('xpack.lens.resetLayerAriaLabel', { + defaultMessage: 'Reset layer {index}', + values: { index: layerIndex + 1 }, + }) + : i18n.translate('xpack.lens.deleteLayerAriaLabel', { + defaultMessage: `Delete layer {index}`, + values: { index: layerIndex + 1 }, + }) + } + onClick={() => { + // If we don't blur the remove / clear button, it remains focused + // which is a strange UX in this case. e.target.blur doesn't work + // due to who knows what, but probably event re-writing. Additionally, + // activeElement does not have blur so, we need to do some casting + safeguards. + const el = (document.activeElement as unknown) as { blur: () => void }; + + if (el?.blur) { + el.blur(); + } + + onRemoveLayer(); + }} + > + {isOnlyLayer + ? i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }) + : i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: `Delete layer`, + })} + </EuiButtonEmpty> + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index c172c6da6848c..0a53fc741c207 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -12,7 +12,7 @@ import { DatasourceDimensionEditorProps, VisualizationDimensionGroupConfig, } from '../../../types'; - +import { DragContextState } from '../../../drag_drop'; export interface ConfigPanelWrapperProps { activeDatasourceId: string; visualizationState: unknown; @@ -31,6 +31,30 @@ export interface ConfigPanelWrapperProps { core: DatasourceDimensionEditorProps['core']; } +export interface LayerPanelProps { + activeDatasourceId: string; + visualizationState: unknown; + datasourceMap: Record<string, Datasource>; + activeVisualization: Visualization; + dispatch: (action: Action) => void; + framePublicAPI: FramePublicAPI; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; + core: DatasourceDimensionEditorProps['core']; +} + +export interface LayerDatasourceDropProps { + layerId: string; + dragDropContext: DragContextState; + state: unknown; + setState: (newState: unknown) => void; +} + export interface ActiveDimensionState { isNew: boolean; activeId?: string; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 69bdff0151f6c..c45dc82a3aeb2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; -import { DragContext, Dragging } from '../../drag_drop'; +import { DragContext, DragDropIdentifier } from '../../drag_drop'; import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types'; import { Query, Filter } from '../../../../../../src/plugins/data/public'; @@ -26,8 +26,8 @@ interface DataPanelWrapperProps { query: Query; dateRange: FramePublicAPI['dateRange']; filters: Filter[]; - dropOntoWorkspace: (field: Dragging) => void; - hasSuggestionForField: (field: Dragging) => boolean; + dropOntoWorkspace: (field: DragDropIdentifier) => void; + hasSuggestionForField: (field: DragDropIdentifier) => boolean; } export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index c0728bd030a0a..7daf1ebb17b97 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -1338,10 +1338,14 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).filter('[data-test-subj="mockVisA"]').prop('onDrop')!({ - indexPatternId: '1', - field: {}, - }); + instance.find('[data-test-subj="mockVisA"]').find(DragDrop).prop('onDrop')!( + { + indexPatternId: '1', + field: {}, + id: '1', + }, + { id: 'lnsWorkspace' } + ); }); expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( @@ -1435,10 +1439,14 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).filter('[data-test-subj="lnsWorkspace"]').prop('onDrop')!({ - indexPatternId: '1', - field: {}, - }); + instance.find(DragDrop).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!( + { + indexPatternId: '1', + field: {}, + id: '1', + }, + { id: 'lnsWorkspace' } + ); }); expect(mockVisualization3.getConfiguration).toHaveBeenCalledWith( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index b6df0caa07577..c3412c32c2184 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useReducer, useState } from 'react'; +import React, { useEffect, useReducer, useState, useCallback } from 'react'; import { CoreSetup, CoreStart } from 'kibana/public'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; @@ -16,7 +16,7 @@ import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; import { Document } from '../../persistence/saved_object_store'; -import { Dragging, RootDragDropProvider } from '../../drag_drop'; +import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { generateId } from '../../id_generator'; import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; @@ -260,7 +260,7 @@ export function EditorFrame(props: EditorFrameProps) { ); const getSuggestionForField = React.useCallback( - (field: Dragging) => { + (field: DragDropIdentifier) => { const { activeDatasourceId, datasourceStates } = state; const activeVisualizationId = state.visualization.activeId; const visualizationState = state.visualization.state; @@ -290,12 +290,12 @@ export function EditorFrame(props: EditorFrameProps) { ] ); - const hasSuggestionForField = React.useCallback( - (field: Dragging) => getSuggestionForField(field) !== undefined, + const hasSuggestionForField = useCallback( + (field: DragDropIdentifier) => getSuggestionForField(field) !== undefined, [getSuggestionForField] ); - const dropOntoWorkspace = React.useCallback( + const dropOntoWorkspace = useCallback( (field) => { const suggestion = getSuggestionForField(field); if (suggestion) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 5cdc5ce592497..95dbf8264c588 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -19,7 +19,7 @@ import { DatasourcePublicAPI, } from '../../types'; import { Action } from './state_management'; -import { Dragging } from '../../drag_drop'; +import { DragDropIdentifier } from '../../drag_drop'; export interface Suggestion { visualizationId: string; @@ -231,7 +231,7 @@ export function getTopSuggestionForField( visualizationState: unknown, datasource: Datasource, datasourceStates: Record<string, { state: unknown; isLoading: boolean }>, - field: Dragging + field: DragDropIdentifier ) { const hasData = Object.values(datasourceLayers).some( (datasourceLayer) => datasourceLayer.getTableSpec().length > 0 diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index ddb2640d50d59..2f94d8e65dce6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -784,7 +784,15 @@ describe('workspace_panel', () => { function initComponent(draggingContext = draggedField) { instance = mount( - <ChildDragDropProvider dragging={draggingContext} setDragging={() => {}}> + <ChildDragDropProvider + dragging={draggingContext} + setDragging={() => {}} + setActiveDropTarget={() => {}} + activeDropTarget={undefined} + keyboardMode={false} + setKeyboardMode={() => {}} + setA11yMessage={() => {}} + > <WorkspacePanel activeDatasourceId={'mock'} datasourceStates={{ @@ -822,7 +830,7 @@ describe('workspace_panel', () => { }); initComponent(); - instance.find(DragDrop).prop('onDrop')!(draggedField); + instance.find(DragDrop).prop('onDrop')!(draggedField, { id: 'lnsWorkspace' }); expect(mockDispatch).toHaveBeenCalledWith({ type: 'SWITCH_VISUALIZATION', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 5fc7b80a3d0ce..0c1fa932da09c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -39,7 +39,7 @@ import { isLensFilterEvent, isLensEditEvent, } from '../../../types'; -import { DragDrop, DragContext, Dragging } from '../../../drag_drop'; +import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop'; import { Suggestion, switchToSuggestion } from '../suggestion_helpers'; import { buildExpression } from '../expression_helpers'; import { debouncedComponent } from '../../../debounced_component'; @@ -75,7 +75,7 @@ export interface WorkspacePanelProps { plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; title?: string; visualizeTriggerFieldContext?: VisualizeFieldContext; - getSuggestionForField: (field: Dragging) => Suggestion | undefined; + getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined; } interface WorkspaceState { @@ -83,8 +83,10 @@ interface WorkspaceState { expandError: boolean; } +const workspaceDropValue = { id: 'lnsWorkspace' }; + // Exported for testing purposes only. -export function WorkspacePanel({ +export const WorkspacePanel = React.memo(function WorkspacePanel({ activeDatasourceId, activeVisualizationId, visualizationMap, @@ -102,7 +104,8 @@ export function WorkspacePanel({ }: WorkspacePanelProps) { const dragDropContext = useContext(DragContext); - const suggestionForDraggedField = getSuggestionForField(dragDropContext.dragging); + const suggestionForDraggedField = + dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging); const [localState, setLocalState] = useState<WorkspaceState>({ expressionBuildError: undefined, @@ -296,10 +299,11 @@ export function WorkspacePanel({ > <DragDrop className="lnsWorkspacePanel__dragDrop" - data-test-subj="lnsWorkspace" + dataTestSubj="lnsWorkspace" draggable={false} droppable={Boolean(suggestionForDraggedField)} onDrop={onDrop} + value={workspaceDropValue} > <div> {renderVisualization()} @@ -308,7 +312,7 @@ export function WorkspacePanel({ </DragDrop> </WorkspacePanelWrapper> ); -} +}); export const InnerVisualizationWrapper = ({ expression, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 8e41abf23e934..794ccd6936c90 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -278,7 +278,10 @@ describe('IndexPattern Data Panel', () => { {...defaultProps} state={state} setState={setStateSpy} - dragDropContext={{ dragging: { id: '1' }, setDragging: () => {} }} + dragDropContext={{ + ...createMockedDragDropContext(), + dragging: { id: '1' }, + }} /> ); @@ -297,7 +300,10 @@ describe('IndexPattern Data Panel', () => { indexPatterns: {}, }} setState={jest.fn()} - dragDropContext={{ dragging: { id: '1' }, setDragging: () => {} }} + dragDropContext={{ + ...createMockedDragDropContext(), + dragging: { id: '1' }, + }} changeIndexPattern={jest.fn()} /> ); @@ -329,7 +335,10 @@ describe('IndexPattern Data Panel', () => { ...defaultProps, changeIndexPattern: jest.fn(), setState, - dragDropContext: { dragging: { id: '1' }, setDragging: () => {} }, + dragDropContext: { + ...createMockedDragDropContext(), + dragging: { id: '1' }, + }, dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, state: { indexPatternRefs: [], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 4031cae548a10..c3dbcdc3e0573 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -426,6 +426,23 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ ); }, [unfilteredFieldGroups, localState.nameFilter, localState.typeFilter]); + const checkFieldExists = useCallback( + (field) => + field.type === 'document' || + fieldExists(existingFields, currentIndexPattern.title, field.name), + [existingFields, currentIndexPattern.title] + ); + + const { nameFilter, typeFilter } = localState; + + const filter = useMemo( + () => ({ + nameFilter, + typeFilter, + }), + [nameFilter, typeFilter] + ); + const fieldProps = useMemo( () => ({ core, @@ -586,17 +603,11 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ </EuiScreenReaderOnly> <EuiFlexItem> <FieldList - exists={(field) => - field.type === 'document' || - fieldExists(existingFields, currentIndexPattern.title, field.name) - } + exists={checkFieldExists} fieldProps={fieldProps} fieldGroups={fieldGroups} hasSyncedExistingFields={!!hasSyncedExistingFields} - filter={{ - nameFilter: localState.nameFilter, - typeFilter: localState.typeFilter, - }} + filter={filter} currentIndexPatternId={currentIndexPatternId} existenceFetchFailed={existenceFetchFailed} existFieldsInIndex={!!allFields.length} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 6be03a92a445e..477f14848c08e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -316,6 +316,7 @@ describe('IndexPatternDimensionEditorPanel', () => { droppedItem: dragging, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: '1', }); expect(setState).toBeCalledTimes(1); @@ -352,6 +353,7 @@ describe('IndexPatternDimensionEditorPanel', () => { droppedItem: dragging, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed, + groupId: '1', }); expect(setState).toBeCalledTimes(1); @@ -387,6 +389,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, droppedItem: dragging, filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: '1', }); expect(setState).toBeCalledTimes(1); @@ -438,6 +441,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, + groupId: '1', }); expect(setState).toBeCalledTimes(1); @@ -473,6 +477,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, droppedItem: dragging, columnId: 'col2', + groupId: '1', }); expect(setState).toBeCalledTimes(1); @@ -538,6 +543,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, droppedItem: dragging, state: testState, + groupId: '1', }); expect(setState).toBeCalledTimes(1); @@ -600,6 +606,7 @@ describe('IndexPatternDimensionEditorPanel', () => { droppedItem: dragging, state: testState, filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', }; const stateWithColumnOrder = (columnOrder: string[]) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index e4eabafc6938e..0308d5e9103bf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -8,6 +8,7 @@ import { DatasourceDimensionDropProps, DatasourceDimensionDropHandlerProps, isDraggedOperation, + DraggedOperation, } from '../../types'; import { IndexPatternColumn } from '../indexpattern'; import { insertOrReplaceColumn } from '../operations'; @@ -15,7 +16,15 @@ import { mergeLayer } from '../state_helpers'; import { hasField, isDraggedField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { getOperationSupportMatrix } from './operation_support'; +import { getOperationSupportMatrix, OperationSupportMatrix } from './operation_support'; + +type DropHandlerProps<T = DraggedOperation> = Pick< + DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>, + 'columnId' | 'setState' | 'state' | 'layerId' | 'droppedItem' +> & { + droppedItem: T; + operationSupportMatrix: OperationSupportMatrix; +}; export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPrivateState>) { const operationSupportMatrix = getOperationSupportMatrix(props); @@ -29,11 +38,11 @@ export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPr if (isDraggedField(dragging)) { const currentColumn = props.state.layers[props.layerId].columns[props.columnId]; - return ( + return Boolean( layerIndexPatternId === dragging.indexPatternId && - Boolean(hasOperationForField(dragging.field)) && - (!currentColumn || - (hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name)) + Boolean(hasOperationForField(dragging.field)) && + (!currentColumn || + (hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name)) ); } @@ -59,74 +68,80 @@ function reorderElements(items: string[], dest: string, src: string) { return result; } -export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) { - const operationSupportMatrix = getOperationSupportMatrix(props); - const { setState, state, layerId, columnId, droppedItem } = props; - - if (isDraggedOperation(droppedItem) && props.isReorder) { - const dropEl = columnId; +const onReorderDrop = ({ columnId, setState, state, layerId, droppedItem }: DropHandlerProps) => { + setState( + mergeLayer({ + state, + layerId, + newLayer: { + columnOrder: reorderElements( + state.layers[layerId].columnOrder, + columnId, + droppedItem.columnId + ), + }, + }) + ); - setState( - mergeLayer({ - state, - layerId, - newLayer: { - columnOrder: reorderElements( - state.layers[layerId].columnOrder, - dropEl, - droppedItem.columnId - ), - }, - }) - ); - - return true; + return true; +}; + +const onMoveDropToCompatibleGroup = ({ + columnId, + setState, + state, + layerId, + droppedItem, +}: DropHandlerProps) => { + const layer = state.layers[layerId]; + const op = { ...layer.columns[droppedItem.columnId] }; + const newColumns = { ...layer.columns }; + delete newColumns[droppedItem.columnId]; + newColumns[columnId] = op; + + const newColumnOrder = [...layer.columnOrder]; + const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId); + const newIndex = newColumnOrder.findIndex((c) => c === columnId); + + if (newIndex === -1) { + newColumnOrder[oldIndex] = columnId; + } else { + newColumnOrder.splice(oldIndex, 1); } + // Time to replace + setState( + mergeLayer({ + state, + layerId, + newLayer: { + columnOrder: newColumnOrder, + columns: newColumns, + }, + }) + ); + return { deleted: droppedItem.columnId }; +}; + +const onFieldDrop = ({ + columnId, + setState, + state, + layerId, + droppedItem, + operationSupportMatrix, +}: DropHandlerProps<unknown>) => { function hasOperationForField(field: IndexPatternField) { return Boolean(operationSupportMatrix.operationByField[field.name]); } - if (isDraggedOperation(droppedItem) && droppedItem.layerId === layerId) { - const layer = state.layers[layerId]; - const op = { ...layer.columns[droppedItem.columnId] }; - if (!props.filterOperations(op)) { - return false; - } - - const newColumns = { ...layer.columns }; - delete newColumns[droppedItem.columnId]; - newColumns[columnId] = op; - - const newColumnOrder = [...layer.columnOrder]; - const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId); - const newIndex = newColumnOrder.findIndex((c) => c === columnId); - - if (newIndex === -1) { - newColumnOrder[oldIndex] = columnId; - } else { - newColumnOrder.splice(oldIndex, 1); - } - - // Time to replace - setState( - mergeLayer({ - state, - layerId, - newLayer: { - columnOrder: newColumnOrder, - columns: newColumns, - }, - }) - ); - return { deleted: droppedItem.columnId }; - } - if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { // TODO: What do we do if we couldn't find a column? return false; } + // dragged field, not operation + const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name]; if (!operationsForNewField || operationsForNewField.size === 0) { @@ -159,6 +174,56 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPr const hasData = Object.values(state.layers).some(({ columns }) => columns.length); trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); setState(mergeLayer({ state, layerId, newLayer })); - return true; +}; + +export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) { + const operationSupportMatrix = getOperationSupportMatrix(props); + const { setState, state, droppedItem, columnId, layerId, groupId, isNew } = props; + + if (!isDraggedOperation(droppedItem)) { + return onFieldDrop({ + columnId, + setState, + state, + layerId, + droppedItem, + operationSupportMatrix, + }); + } + const isExistingFromSameGroup = + droppedItem.groupId === groupId && droppedItem.columnId !== columnId && !isNew; + + // reorder in the same group + if (isExistingFromSameGroup) { + return onReorderDrop({ + columnId, + setState, + state, + layerId, + droppedItem, + operationSupportMatrix, + }); + } + + // replace or move to compatible group + const isFromOtherGroup = droppedItem.groupId !== groupId && droppedItem.layerId === layerId; + + if (isFromOtherGroup) { + const layer = state.layers[layerId]; + const op = { ...layer.columns[droppedItem.columnId] }; + + if (props.filterOperations(op)) { + return onMoveDropToCompatibleGroup({ + columnId, + setState, + state, + layerId, + droppedItem, + operationSupportMatrix, + }); + } + } + + return false; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 1019b2c33e0e5..881e7a7228762 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -95,6 +95,8 @@ describe('IndexPattern Field Item', () => { }, exists: true, chartsThemeService, + groupIndex: 0, + itemIndex: 0, dropOntoWorkspace: () => {}, hasSuggestionForField: () => false, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 740b557b668b7..ff335a0da56ee 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -6,7 +6,7 @@ import './field_item.scss'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, useMemo } from 'react'; import DateMath from '@elastic/datemath'; import { EuiButtonGroup, @@ -48,7 +48,7 @@ import { import { FieldButton } from '../../../../../src/plugins/kibana_react/public'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { DraggedField } from './indexpattern'; -import { DragDrop, Dragging } from '../drag_drop'; +import { DragDrop, DragDropIdentifier } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { BucketedAggregation, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField } from './types'; @@ -69,6 +69,8 @@ export interface FieldItemProps { chartsThemeService: ChartsPluginSetup['theme']; filters: Filter[]; hideDetails?: boolean; + itemIndex: number; + groupIndex: number; dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; } @@ -106,7 +108,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { const [infoIsOpen, setOpen] = useState(false); const dropOntoWorkspaceAndClose = useCallback( - (droppedField: Dragging) => { + (droppedField: DragDropIdentifier) => { dropOntoWorkspace(droppedField); setOpen(false); }, @@ -163,10 +165,11 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { } } - const value = React.useMemo( + const value = useMemo( () => ({ field, indexPatternId: indexPattern.id, id: field.name } as DraggedField), [field, indexPattern.id] ); + const lensFieldIcon = <LensFieldIcon type={field.type as DataType} />; const lensInfoIcon = ( <EuiIconTip @@ -200,10 +203,11 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { container={document.querySelector<HTMLElement>('.application') || undefined} button={ <DragDrop + noKeyboardSupportYet + draggable label={field.displayName} value={value} - data-test-subj={`lnsFieldListPanelField-${field.name}`} - draggable + dataTestSubj={`lnsFieldListPanelField-${field.name}`} > <FieldButton className={`lnsFieldItem lnsFieldItem--${field.type} lnsFieldItem--${ @@ -215,7 +219,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { ['aria-label']: i18n.translate( 'xpack.lens.indexPattern.fieldStatsButtonAriaLabel', { - defaultMessage: '{fieldName}: {fieldType}. Hit enter for a field preview.', + defaultMessage: 'Preview {fieldName}: {fieldType}', values: { fieldName: field.displayName, fieldType: field.type, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index 7bcd44e3d25d0..d9fba160bc37b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -40,7 +40,7 @@ function getDisplayedFieldsLength( .reduce((allFieldCount, [, { fields }]) => allFieldCount + fields.length, 0); } -export function FieldList({ +export const FieldList = React.memo(function FieldList({ exists, fieldGroups, existenceFetchFailed, @@ -135,13 +135,15 @@ export function FieldList({ {Object.entries(fieldGroups) .filter(([, { showInAccordion }]) => !showInAccordion) .flatMap(([, { fields }]) => - fields.map((field) => ( + fields.map((field, index) => ( <FieldItem {...fieldProps} exists={exists(field)} field={field} hideDetails={true} key={field.name} + itemIndex={index} + groupIndex={0} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} /> @@ -151,7 +153,7 @@ export function FieldList({ <EuiSpacer size="s" /> {Object.entries(fieldGroups) .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, fieldGroup]) => ( + .map(([key, fieldGroup], index) => ( <Fragment key={key}> <FieldsAccordion dropOntoWorkspace={dropOntoWorkspace} @@ -168,6 +170,7 @@ export function FieldList({ isFiltered={fieldGroup.fieldCount !== fieldGroup.fields.length} paginatedFields={paginatedFields[key]} fieldProps={fieldProps} + groupIndex={index + 1} onToggle={(open) => { setAccordionState((s) => ({ ...s, @@ -198,4 +201,4 @@ export function FieldList({ </div> </div> ); -} +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx index e2f615217bb4a..dca3de24014bc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx @@ -72,6 +72,7 @@ describe('Fields Accordion', () => { fieldProps, renderCallout: <div id="lens-test-callout">Callout</div>, exists: () => true, + groupIndex: 0, dropOntoWorkspace: () => {}, hasSuggestionForField: () => false, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 11adf1a128c1b..11710ffa18068 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -5,7 +5,7 @@ */ import './datapanel.scss'; -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiText, @@ -50,11 +50,12 @@ export interface FieldsAccordionProps { exists: (field: IndexPatternField) => boolean; showExistenceFetchError?: boolean; hideDetails?: boolean; + groupIndex: number; dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; } -export const InnerFieldsAccordion = function InnerFieldsAccordion({ +export const FieldsAccordion = memo(function InnerFieldsAccordion({ initialIsOpen, onToggle, id, @@ -69,28 +70,72 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ exists, hideDetails, showExistenceFetchError, + groupIndex, dropOntoWorkspace, hasSuggestionForField, }: FieldsAccordionProps) { const renderField = useCallback( - (field: IndexPatternField) => ( + (field: IndexPatternField, index) => ( <FieldItem {...fieldProps} key={field.name} field={field} exists={exists(field)} hideDetails={hideDetails} + itemIndex={index} + groupIndex={groupIndex} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} /> ), - [fieldProps, exists, hideDetails, dropOntoWorkspace, hasSuggestionForField] + [fieldProps, exists, hideDetails, dropOntoWorkspace, hasSuggestionForField, groupIndex] ); - const titleClassname = classNames({ - // eslint-disable-next-line @typescript-eslint/naming-convention - lnsInnerIndexPatternDataPanel__titleTooltip: !!helpTooltip, - }); + const renderButton = useMemo(() => { + const titleClassname = classNames({ + // eslint-disable-next-line @typescript-eslint/naming-convention + lnsInnerIndexPatternDataPanel__titleTooltip: !!helpTooltip, + }); + return ( + <EuiText size="xs"> + <strong className={titleClassname}>{label}</strong> + {!!helpTooltip && ( + <EuiIconTip + aria-label={helpTooltip} + type="questionInCircle" + color="subdued" + size="s" + position="right" + content={helpTooltip} + iconProps={{ + className: 'eui-alignTop', + }} + /> + )} + </EuiText> + ); + }, [label, helpTooltip]); + + const extraAction = useMemo(() => { + return showExistenceFetchError ? ( + <EuiIconTip + aria-label={i18n.translate('xpack.lens.indexPattern.existenceErrorAriaLabel', { + defaultMessage: 'Existence fetch failed', + })} + type="alert" + color="warning" + content={i18n.translate('xpack.lens.indexPattern.existenceErrorLabel', { + defaultMessage: "Field information can't be loaded", + })} + /> + ) : hasLoaded ? ( + <EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}> + {fieldsCount} + </EuiNotificationBadge> + ) : ( + <EuiLoadingSpinner size="m" /> + ); + }, [showExistenceFetchError, hasLoaded, isFiltered, fieldsCount]); return ( <EuiAccordion @@ -98,44 +143,8 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ onToggle={onToggle} data-test-subj={id} id={id} - buttonContent={ - <EuiText size="xs"> - <strong className={titleClassname}>{label}</strong> - {!!helpTooltip && ( - <EuiIconTip - aria-label={helpTooltip} - type="questionInCircle" - color="subdued" - size="s" - position="right" - content={helpTooltip} - iconProps={{ - className: 'eui-alignTop', - }} - /> - )} - </EuiText> - } - extraAction={ - showExistenceFetchError ? ( - <EuiIconTip - aria-label={i18n.translate('xpack.lens.indexPattern.existenceErrorAriaLabel', { - defaultMessage: 'Existence fetch failed', - })} - type="alert" - color="warning" - content={i18n.translate('xpack.lens.indexPattern.existenceErrorLabel', { - defaultMessage: "Field information can't be loaded", - })} - /> - ) : hasLoaded ? ( - <EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}> - {fieldsCount} - </EuiNotificationBadge> - ) : ( - <EuiLoadingSpinner size="m" /> - ) - } + buttonContent={renderButton} + extraAction={extraAction} > <EuiSpacer size="s" /> {hasLoaded && @@ -148,6 +157,4 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ ))} </EuiAccordion> ); -}; - -export const FieldsAccordion = memo(InnerFieldsAccordion); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index e51cd36156d1b..c309212eed164 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -51,11 +51,11 @@ import { mergeLayer } from './state_helpers'; import { Datasource, StateSetter } from '../types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { deleteColumn, isReferenced } from './operations'; -import { Dragging } from '../drag_drop/providers'; +import { DragDropIdentifier } from '../drag_drop/providers'; export { OperationType, IndexPatternColumn, deleteColumn } from './operations'; -export type DraggedField = Dragging & { +export type DraggedField = DragDropIdentifier & { field: IndexPatternField; indexPatternId: string; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 4aea9e8ac67a9..67ddbe8c45ab7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -247,5 +247,10 @@ export function createMockedDragDropContext(): jest.Mocked<DragContextState> { return { dragging: undefined, setDragging: jest.fn(), + activeDropTarget: undefined, + setActiveDropTarget: jest.fn(), + keyboardMode: false, + setKeyboardMode: jest.fn(), + setA11yMessage: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 907ef3a700ce6..8f202faeb9ee8 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -16,7 +16,7 @@ import { Datatable, SerializedFieldFormat, } from '../../../../src/plugins/expressions/public'; -import { DragContextState, Dragging } from './drag_drop'; +import { DragContextState, DragDropIdentifier } from './drag_drop'; import { Document } from './persistence'; import { DateRange } from '../common'; import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; @@ -226,8 +226,8 @@ export interface DatasourceDataPanelProps<T = unknown> { query: Query; dateRange: DateRange; filters: Filter[]; - dropOntoWorkspace: (field: Dragging) => void; - hasSuggestionForField: (field: Dragging) => boolean; + dropOntoWorkspace: (field: DragDropIdentifier) => void; + hasSuggestionForField: (field: DragDropIdentifier) => boolean; } interface SharedDimensionProps { @@ -301,6 +301,8 @@ export type DatasourceDimensionDropProps<T> = SharedDimensionProps & { export type DatasourceDimensionDropHandlerProps<T> = DatasourceDimensionDropProps<T> & { droppedItem: unknown; + groupId: string; + isNew?: boolean; }; export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index b8bca09bb353c..91fa2f5921d2f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -5,7 +5,7 @@ */ import './xy_config_panel.scss'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, memo } from 'react'; import { i18n } from '@kbn/i18n'; import { Position } from '@elastic/charts'; import { debounce } from 'lodash'; @@ -179,8 +179,7 @@ function getValueLabelDisableReason({ defaultMessage: 'This setting cannot be changed on stacked or percentage bar charts', }); } - -export function XyToolbar(props: VisualizationToolbarProps<State>) { +export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProps<State>) { const { state, setState, frame } = props; const hasNonBarSeries = state?.layers.some(({ seriesType }) => @@ -485,7 +484,8 @@ export function XyToolbar(props: VisualizationToolbarProps<State>) { </EuiFlexItem> </EuiFlexGroup> ); -} +}); + const idPrefix = htmlIdGenerator()(); export function DimensionEditor( @@ -653,7 +653,7 @@ const ColorPicker = ({ } }; - const updateColorInState: EuiColorPickerProps['onChange'] = React.useMemo( + const updateColorInState: EuiColorPickerProps['onChange'] = useMemo( () => debounce((text, output) => { const newYConfigs = [...(layer.yConfig || [])]; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d0634d6cd87a2..b50a7092e108d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11188,7 +11188,6 @@ "xpack.lens.discover.visualizeFieldLegend": "Visualize フィールド", "xpack.lens.dragDrop.elementLifted": "位置 {position} のアイテム {itemLabel} を持ち上げました。", "xpack.lens.dragDrop.elementMoved": "位置 {prevPosition} から位置 {position} までアイテム {itemLabel} を移動しました", - "xpack.lens.dragDrop.reorderInstructions": "スペースバーを押すと、ドラッグを開始します。ドラッグするときには、矢印キーで並べ替えることができます。もう一度スペースバーを押すと終了します。", "xpack.lens.editLayerSettings": "レイヤー設定を編集", "xpack.lens.editLayerSettingsChartType": "レイヤー設定を編集、{chartType}", "xpack.lens.editorFrame.buildExpressionError": "グラフの準備中に予期しないエラーが発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4ca6d11aa8940..a93b2c78690c1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11217,7 +11217,6 @@ "xpack.lens.discover.visualizeFieldLegend": "可视化字段", "xpack.lens.dragDrop.elementLifted": "您已将项目 {itemLabel} 提升到位置 {position}", "xpack.lens.dragDrop.elementMoved": "您已将项目 {itemLabel} 从位置 {prevPosition} 移到位置 {position}", - "xpack.lens.dragDrop.reorderInstructions": "按空格键开始拖动。拖动时,使用方向键重新排序。再次按空格键结束操作。", "xpack.lens.editLayerSettings": "编辑图层设置", "xpack.lens.editLayerSettingsChartType": "编辑图层设置 {chartType}", "xpack.lens.editorFrame.buildExpressionError": "准备图表时发生意外错误", diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index dabead6ffbdad..17f9fb036129a 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -202,7 +202,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }) .lnsDragDrop`; const dropping = `[data-test-subj='${dimension}']:nth-of-type(${ endIndex + 1 - }) [data-test-subj='lnsDragDrop-reorderableDrop'`; + }) [data-test-subj='lnsDragDrop-reorderableDropLayer'`; await browser.html5DragAndDrop(dragging, dropping); await PageObjects.header.waitUntilLoadingHasFinished(); }, From fb19aab307fb80740b60fbd4a0861e75380e96f9 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger <walter@elastic.co> Date: Mon, 1 Feb 2021 12:30:58 +0100 Subject: [PATCH 156/163] [ML] Data Frame Analytics: Adds scatterplot matrix to regression/classification results pages. (#88353) - Adds support for scatterplot matrices to regression/classification results pages - Lazy loads the scatterplot matrix including Vega code using Suspense. The approach is taken from the Kibana Vega plugin, creating this separate bundle means you'll load the 600kb+ Vega code only on pages where actually needed and not e.g. already on the analytics job list. Note for reviews: The file scatterplot_matrix_view.tsx did not change besides the default export, it just shows up as a new file because of the refactoring to support lazy loading. - Adds support for analytics configuration that use the excludes instead of includes field list. - Adds the field used for color legends to tooltips. --- .../components/scatterplot_matrix/index.ts | 2 + .../scatterplot_matrix/scatterplot_matrix.tsx | 319 +---------------- .../scatterplot_matrix_loading.tsx | 19 ++ .../scatterplot_matrix_vega_lite_spec.test.ts | 1 + .../scatterplot_matrix_vega_lite_spec.ts | 11 +- ...trix.scss => scatterplot_matrix_view.scss} | 0 .../scatterplot_matrix_view.tsx | 323 ++++++++++++++++++ .../use_scatterplot_field_options.ts | 50 +++ .../get_scatterplot_matrix_legend_type.ts | 22 ++ .../data_frame_analytics/common/index.ts | 1 + .../common/use_results_view_config.ts | 6 + .../configuration_step_form.tsx | 16 +- .../expandable_section_splom.tsx | 13 +- .../exploration_page_wrapper.tsx | 41 ++- .../outlier_exploration.tsx | 15 +- .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- .../classification_creation.ts | 21 ++ .../outlier_detection_creation.ts | 31 ++ .../regression_creation.ts | 20 ++ .../functional/services/canvas_element.ts | 10 +- .../ml/data_frame_analytics_scatterplot.ts | 40 +++ x-pack/test/functional/services/ml/index.ts | 5 + 23 files changed, 629 insertions(+), 345 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_loading.tsx rename x-pack/plugins/ml/public/application/components/scatterplot_matrix/{scatterplot_matrix.scss => scatterplot_matrix_view.scss} (100%) create mode 100644 x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_view.tsx create mode 100644 x-pack/plugins/ml/public/application/components/scatterplot_matrix/use_scatterplot_field_options.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/get_scatterplot_matrix_legend_type.ts create mode 100644 x-pack/test/functional/services/ml/data_frame_analytics_scatterplot.ts diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/index.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/index.ts index 4f564dde8cb43..903fe5b6ed985 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/index.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/index.ts @@ -4,5 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { useScatterplotFieldOptions } from './use_scatterplot_field_options'; export { LEGEND_TYPES } from './scatterplot_matrix_vega_lite_spec'; export { ScatterplotMatrix } from './scatterplot_matrix'; +export type { ScatterplotMatrixViewProps as ScatterplotMatrixProps } from './scatterplot_matrix_view'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index b1ee9afb17788..a90fe924b91ac 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -4,316 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useEffect, useState, FC } from 'react'; +import React, { FC, Suspense } from 'react'; -// There is still an issue with Vega Lite's typings with the strict mode Kibana is using. -// @ts-ignore -import { compile } from 'vega-lite/build-es5/vega-lite'; -import { parse, View, Warn } from 'vega'; -import { Handler } from 'vega-tooltip'; +import type { ScatterplotMatrixViewProps } from './scatterplot_matrix_view'; +import { ScatterplotMatrixLoading } from './scatterplot_matrix_loading'; -import { - htmlIdGenerator, - EuiComboBox, - EuiComboBoxOptionOption, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiLoadingSpinner, - EuiSelect, - EuiSpacer, - EuiSwitch, - EuiText, -} from '@elastic/eui'; +const ScatterplotMatrixLazy = React.lazy(() => import('./scatterplot_matrix_view')); -import { i18n } from '@kbn/i18n'; - -import type { SearchResponse7 } from '../../../../common/types/es_client'; - -import { useMlApiContext } from '../../contexts/kibana'; - -import { getProcessedFields } from '../data_grid'; -import { useCurrentEuiTheme } from '../color_range_legend'; - -import { - getScatterplotMatrixVegaLiteSpec, - LegendType, - OUTLIER_SCORE_FIELD, -} from './scatterplot_matrix_vega_lite_spec'; - -import './scatterplot_matrix.scss'; - -const SCATTERPLOT_MATRIX_DEFAULT_FIELDS = 4; -const SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE = 1000; -const SCATTERPLOT_MATRIX_DEFAULT_FETCH_MIN_SIZE = 1; -const SCATTERPLOT_MATRIX_DEFAULT_FETCH_MAX_SIZE = 10000; - -const TOGGLE_ON = i18n.translate('xpack.ml.splom.toggleOn', { - defaultMessage: 'On', -}); -const TOGGLE_OFF = i18n.translate('xpack.ml.splom.toggleOff', { - defaultMessage: 'Off', -}); - -const sampleSizeOptions = [100, 1000, 10000].map((d) => ({ value: d, text: '' + d })); - -interface ScatterplotMatrixProps { - fields: string[]; - index: string; - resultsField?: string; - color?: string; - legendType?: LegendType; -} - -export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({ - fields: allFields, - index, - resultsField, - color, - legendType, -}) => { - const { esSearch } = useMlApiContext(); - - // dynamicSize is optionally used for outlier charts where the scatterplot marks - // are sized according to outlier_score - const [dynamicSize, setDynamicSize] = useState<boolean>(false); - - // used to give the use the option to customize the fields used for the matrix axes - const [fields, setFields] = useState<string[]>([]); - - useEffect(() => { - const defaultFields = - allFields.length > SCATTERPLOT_MATRIX_DEFAULT_FIELDS - ? allFields.slice(0, SCATTERPLOT_MATRIX_DEFAULT_FIELDS) - : allFields; - setFields(defaultFields); - }, [allFields]); - - // the amount of documents to be fetched - const [fetchSize, setFetchSize] = useState<number>(SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE); - // flag to add a random score to the ES query to fetch documents - const [randomizeQuery, setRandomizeQuery] = useState<boolean>(false); - - const [isLoading, setIsLoading] = useState<boolean>(false); - - // contains the fetched documents and columns to be passed on to the Vega spec. - const [splom, setSplom] = useState<{ items: any[]; columns: string[] } | undefined>(); - - // formats the array of field names for EuiComboBox - const fieldOptions = useMemo( - () => - allFields.map((d) => ({ - label: d, - })), - [allFields] - ); - - const fieldsOnChange = (newFields: EuiComboBoxOptionOption[]) => { - setFields(newFields.map((d) => d.label)); - }; - - const fetchSizeOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => { - setFetchSize( - Math.min( - Math.max(parseInt(e.target.value, 10), SCATTERPLOT_MATRIX_DEFAULT_FETCH_MIN_SIZE), - SCATTERPLOT_MATRIX_DEFAULT_FETCH_MAX_SIZE - ) - ); - }; - - const randomizeQueryOnChange = () => { - setRandomizeQuery(!randomizeQuery); - }; - - const dynamicSizeOnChange = () => { - setDynamicSize(!dynamicSize); - }; - - const { euiTheme } = useCurrentEuiTheme(); - - useEffect(() => { - async function fetchSplom(options: { didCancel: boolean }) { - setIsLoading(true); - try { - const queryFields = [ - ...fields, - ...(color !== undefined ? [color] : []), - ...(legendType !== undefined ? [] : [`${resultsField}.${OUTLIER_SCORE_FIELD}`]), - ]; - - const query = randomizeQuery - ? { - function_score: { - random_score: { seed: 10, field: '_seq_no' }, - }, - } - : { match_all: {} }; - - const resp: SearchResponse7 = await esSearch({ - index, - body: { - fields: queryFields, - _source: false, - query, - from: 0, - size: fetchSize, - }, - }); - - if (!options.didCancel) { - const items = resp.hits.hits.map((d) => - getProcessedFields(d.fields, (key: string) => - key.startsWith(`${resultsField}.feature_importance`) - ) - ); - - setSplom({ columns: fields, items }); - setIsLoading(false); - } - } catch (e) { - // TODO error handling - setIsLoading(false); - } - } - - const options = { didCancel: false }; - fetchSplom(options); - return () => { - options.didCancel = true; - }; - // stringify the fields array, otherwise the comparator will trigger on new but identical instances. - }, [fetchSize, JSON.stringify(fields), index, randomizeQuery, resultsField]); - - const htmlId = useMemo(() => htmlIdGenerator()(), []); - - useEffect(() => { - if (splom === undefined) { - return; - } - - const { items, columns } = splom; - - const values = - resultsField !== undefined - ? items - : items.map((d) => { - d[`${resultsField}.${OUTLIER_SCORE_FIELD}`] = 0; - return d; - }); - - const vegaSpec = getScatterplotMatrixVegaLiteSpec( - values, - columns, - euiTheme, - resultsField, - color, - legendType, - dynamicSize - ); - - const vgSpec = compile(vegaSpec).spec; - - const view = new View(parse(vgSpec)) - .logLevel(Warn) - .renderer('canvas') - .tooltip(new Handler().call) - .initialize(`#${htmlId}`); - - view.runAsync(); // evaluate and render the view - }, [resultsField, splom, color, legendType, dynamicSize]); - - return ( - <> - {splom === undefined ? ( - <EuiText textAlign="center"> - <EuiSpacer size="l" /> - <EuiLoadingSpinner size="l" /> - <EuiSpacer size="l" /> - </EuiText> - ) : ( - <> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label={i18n.translate('xpack.ml.splom.fieldSelectionLabel', { - defaultMessage: 'Fields', - })} - display="rowCompressed" - fullWidth - > - <EuiComboBox - compressed - fullWidth - placeholder={i18n.translate('xpack.ml.splom.fieldSelectionPlaceholder', { - defaultMessage: 'Select fields', - })} - options={fieldOptions} - selectedOptions={fields.map((d) => ({ - label: d, - }))} - onChange={fieldsOnChange} - isClearable={true} - data-test-subj="mlScatterplotMatrixFieldsComboBox" - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem style={{ width: '200px' }} grow={false}> - <EuiFormRow - label={i18n.translate('xpack.ml.splom.SampleSizeLabel', { - defaultMessage: 'Sample size', - })} - display="rowCompressed" - fullWidth - > - <EuiSelect - compressed - options={sampleSizeOptions} - value={fetchSize} - onChange={fetchSizeOnChange} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem style={{ width: '120px' }} grow={false}> - <EuiFormRow - label={i18n.translate('xpack.ml.splom.RandomScoringLabel', { - defaultMessage: 'Random scoring', - })} - display="rowCompressed" - fullWidth - > - <EuiSwitch - name="mlScatterplotMatrixRandomizeQuery" - label={randomizeQuery ? TOGGLE_ON : TOGGLE_OFF} - checked={randomizeQuery} - onChange={randomizeQueryOnChange} - disabled={isLoading} - /> - </EuiFormRow> - </EuiFlexItem> - {resultsField !== undefined && legendType === undefined && ( - <EuiFlexItem style={{ width: '120px' }} grow={false}> - <EuiFormRow - label={i18n.translate('xpack.ml.splom.dynamicSizeLabel', { - defaultMessage: 'Dynamic size', - })} - display="rowCompressed" - fullWidth - > - <EuiSwitch - name="mlScatterplotMatrixDynamicSize" - label={dynamicSize ? TOGGLE_ON : TOGGLE_OFF} - checked={dynamicSize} - onChange={dynamicSizeOnChange} - disabled={isLoading} - /> - </EuiFormRow> - </EuiFlexItem> - )} - </EuiFlexGroup> - - <div id={htmlId} className="mlScatterplotMatrix" /> - </> - )} - </> - ); -}; +export const ScatterplotMatrix: FC<ScatterplotMatrixViewProps> = (props) => ( + <Suspense fallback={<ScatterplotMatrixLoading />}> + <ScatterplotMatrixLazy {...props} /> + </Suspense> +); diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_loading.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_loading.tsx new file mode 100644 index 0000000000000..ccd4153769e9c --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_loading.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 React from 'react'; + +import { EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; + +export const ScatterplotMatrixLoading = () => { + return ( + <EuiText textAlign="center"> + <EuiSpacer size="l" /> + <EuiLoadingSpinner size="l" /> + <EuiSpacer size="l" /> + </EuiText> + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts index dd467161ff489..eada64b7a03ca 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts @@ -163,6 +163,7 @@ describe('getScatterplotMatrixVegaLiteSpec()', () => { type: 'nominal', }); expect(vegaLiteSpec.spec.encoding.tooltip).toEqual([ + { field: 'the-color-field', type: 'nominal' }, { field: 'x', type: 'quantitative' }, { field: 'y', type: 'quantitative' }, ]); diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts index 9e0834dd8b922..c943e5d1b06e3 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts @@ -35,6 +35,8 @@ export const getColorSpec = ( color?: string, legendType?: LegendType ) => { + // For outlier detection result pages coloring is done based on a threshold. + // This returns a Vega spec using a conditional to return the color. if (outliers) { return { condition: { @@ -45,6 +47,8 @@ export const getColorSpec = ( }; } + // Based on the type of the color field, + // this returns either a continuous or categorical color spec. if (color !== undefined && legendType !== undefined) { return { field: color, @@ -80,6 +84,8 @@ export const getScatterplotMatrixVegaLiteSpec = ( }); } + const colorSpec = getColorSpec(euiTheme, outliers, color, legendType); + return { $schema: 'https://vega.github.io/schema/vega-lite/v4.17.0.json', background: 'transparent', @@ -115,10 +121,10 @@ export const getScatterplotMatrixVegaLiteSpec = ( : { type: 'circle', opacity: 0.75, size: 8 }), }, encoding: { - color: getColorSpec(euiTheme, outliers, color, legendType), + color: colorSpec, ...(dynamicSize ? { - stroke: getColorSpec(euiTheme, outliers, color, legendType), + stroke: colorSpec, opacity: { condition: { value: 1, @@ -163,6 +169,7 @@ export const getScatterplotMatrixVegaLiteSpec = ( scale: { zero: false }, }, tooltip: [ + ...(color !== undefined ? [{ type: colorSpec.type, field: color }] : []), ...columns.map((d) => ({ type: LEGEND_TYPES.QUANTITATIVE, field: d })), ...(outliers ? [{ type: LEGEND_TYPES.QUANTITATIVE, field: OUTLIER_SCORE_FIELD, format: '.3f' }] diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_view.scss similarity index 100% rename from x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss rename to x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_view.scss diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_view.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_view.tsx new file mode 100644 index 0000000000000..0c065c1154a98 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_view.tsx @@ -0,0 +1,323 @@ +/* + * 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, { useMemo, useEffect, useState, FC } from 'react'; + +// There is still an issue with Vega Lite's typings with the strict mode Kibana is using. +// @ts-ignore +import { compile } from 'vega-lite/build-es5/vega-lite'; +import { parse, View, Warn } from 'vega'; +import { Handler } from 'vega-tooltip'; + +import { + htmlIdGenerator, + EuiComboBox, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiSwitch, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import type { SearchResponse7 } from '../../../../common/types/es_client'; +import type { ResultsSearchQuery } from '../../data_frame_analytics/common/analytics'; + +import { useMlApiContext } from '../../contexts/kibana'; + +import { getProcessedFields } from '../data_grid'; +import { useCurrentEuiTheme } from '../color_range_legend'; + +import { ScatterplotMatrixLoading } from './scatterplot_matrix_loading'; + +import { + getScatterplotMatrixVegaLiteSpec, + LegendType, + OUTLIER_SCORE_FIELD, +} from './scatterplot_matrix_vega_lite_spec'; + +import './scatterplot_matrix_view.scss'; + +const SCATTERPLOT_MATRIX_DEFAULT_FIELDS = 4; +const SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE = 1000; +const SCATTERPLOT_MATRIX_DEFAULT_FETCH_MIN_SIZE = 1; +const SCATTERPLOT_MATRIX_DEFAULT_FETCH_MAX_SIZE = 10000; + +const TOGGLE_ON = i18n.translate('xpack.ml.splom.toggleOn', { + defaultMessage: 'On', +}); +const TOGGLE_OFF = i18n.translate('xpack.ml.splom.toggleOff', { + defaultMessage: 'Off', +}); + +const sampleSizeOptions = [100, 1000, 10000].map((d) => ({ value: d, text: '' + d })); + +export interface ScatterplotMatrixViewProps { + fields: string[]; + index: string; + resultsField?: string; + color?: string; + legendType?: LegendType; + searchQuery?: ResultsSearchQuery; +} + +export const ScatterplotMatrixView: FC<ScatterplotMatrixViewProps> = ({ + fields: allFields, + index, + resultsField, + color, + legendType, + searchQuery, +}) => { + const { esSearch } = useMlApiContext(); + + // dynamicSize is optionally used for outlier charts where the scatterplot marks + // are sized according to outlier_score + const [dynamicSize, setDynamicSize] = useState<boolean>(false); + + // used to give the use the option to customize the fields used for the matrix axes + const [fields, setFields] = useState<string[]>([]); + + useEffect(() => { + const defaultFields = + allFields.length > SCATTERPLOT_MATRIX_DEFAULT_FIELDS + ? allFields.slice(0, SCATTERPLOT_MATRIX_DEFAULT_FIELDS) + : allFields; + setFields(defaultFields); + }, [allFields]); + + // the amount of documents to be fetched + const [fetchSize, setFetchSize] = useState<number>(SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE); + // flag to add a random score to the ES query to fetch documents + const [randomizeQuery, setRandomizeQuery] = useState<boolean>(false); + + const [isLoading, setIsLoading] = useState<boolean>(false); + + // contains the fetched documents and columns to be passed on to the Vega spec. + const [splom, setSplom] = useState<{ items: any[]; columns: string[] } | undefined>(); + + // formats the array of field names for EuiComboBox + const fieldOptions = useMemo( + () => + allFields.map((d) => ({ + label: d, + })), + [allFields] + ); + + const fieldsOnChange = (newFields: EuiComboBoxOptionOption[]) => { + setFields(newFields.map((d) => d.label)); + }; + + const fetchSizeOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => { + setFetchSize( + Math.min( + Math.max(parseInt(e.target.value, 10), SCATTERPLOT_MATRIX_DEFAULT_FETCH_MIN_SIZE), + SCATTERPLOT_MATRIX_DEFAULT_FETCH_MAX_SIZE + ) + ); + }; + + const randomizeQueryOnChange = () => { + setRandomizeQuery(!randomizeQuery); + }; + + const dynamicSizeOnChange = () => { + setDynamicSize(!dynamicSize); + }; + + const { euiTheme } = useCurrentEuiTheme(); + + useEffect(() => { + async function fetchSplom(options: { didCancel: boolean }) { + setIsLoading(true); + try { + const queryFields = [ + ...fields, + ...(color !== undefined ? [color] : []), + ...(legendType !== undefined ? [] : [`${resultsField}.${OUTLIER_SCORE_FIELD}`]), + ]; + + const queryFallback = searchQuery !== undefined ? searchQuery : { match_all: {} }; + const query = randomizeQuery + ? { + function_score: { + query: queryFallback, + random_score: { seed: 10, field: '_seq_no' }, + }, + } + : queryFallback; + + const resp: SearchResponse7 = await esSearch({ + index, + body: { + fields: queryFields, + _source: false, + query, + from: 0, + size: fetchSize, + }, + }); + + if (!options.didCancel) { + const items = resp.hits.hits.map((d) => + getProcessedFields(d.fields, (key: string) => + key.startsWith(`${resultsField}.feature_importance`) + ) + ); + + setSplom({ columns: fields, items }); + setIsLoading(false); + } + } catch (e) { + // TODO error handling + setIsLoading(false); + } + } + + const options = { didCancel: false }; + fetchSplom(options); + return () => { + options.didCancel = true; + }; + // stringify the fields array and search, otherwise the comparator will trigger on new but identical instances. + }, [fetchSize, JSON.stringify({ fields, searchQuery }), index, randomizeQuery, resultsField]); + + const htmlId = useMemo(() => htmlIdGenerator()(), []); + + useEffect(() => { + if (splom === undefined) { + return; + } + + const { items, columns } = splom; + + const values = + resultsField !== undefined + ? items + : items.map((d) => { + d[`${resultsField}.${OUTLIER_SCORE_FIELD}`] = 0; + return d; + }); + + const vegaSpec = getScatterplotMatrixVegaLiteSpec( + values, + columns, + euiTheme, + resultsField, + color, + legendType, + dynamicSize + ); + + const vgSpec = compile(vegaSpec).spec; + + const view = new View(parse(vgSpec)) + .logLevel(Warn) + .renderer('canvas') + .tooltip(new Handler().call) + .initialize(`#${htmlId}`); + + view.runAsync(); // evaluate and render the view + }, [resultsField, splom, color, legendType, dynamicSize]); + + return ( + <> + {splom === undefined ? ( + <ScatterplotMatrixLoading /> + ) : ( + <> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label={i18n.translate('xpack.ml.splom.fieldSelectionLabel', { + defaultMessage: 'Fields', + })} + display="rowCompressed" + fullWidth + > + <EuiComboBox + compressed + fullWidth + placeholder={i18n.translate('xpack.ml.splom.fieldSelectionPlaceholder', { + defaultMessage: 'Select fields', + })} + options={fieldOptions} + selectedOptions={fields.map((d) => ({ + label: d, + }))} + onChange={fieldsOnChange} + isClearable={true} + data-test-subj="mlScatterplotMatrixFieldsComboBox" + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem style={{ width: '200px' }} grow={false}> + <EuiFormRow + label={i18n.translate('xpack.ml.splom.sampleSizeLabel', { + defaultMessage: 'Sample size', + })} + display="rowCompressed" + fullWidth + > + <EuiSelect + compressed + options={sampleSizeOptions} + value={fetchSize} + onChange={fetchSizeOnChange} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem style={{ width: '120px' }} grow={false}> + <EuiFormRow + label={i18n.translate('xpack.ml.splom.randomScoringLabel', { + defaultMessage: 'Random scoring', + })} + display="rowCompressed" + fullWidth + > + <EuiSwitch + name="mlScatterplotMatrixRandomizeQuery" + label={randomizeQuery ? TOGGLE_ON : TOGGLE_OFF} + checked={randomizeQuery} + onChange={randomizeQueryOnChange} + disabled={isLoading} + /> + </EuiFormRow> + </EuiFlexItem> + {resultsField !== undefined && legendType === undefined && ( + <EuiFlexItem style={{ width: '120px' }} grow={false}> + <EuiFormRow + label={i18n.translate('xpack.ml.splom.dynamicSizeLabel', { + defaultMessage: 'Dynamic size', + })} + display="rowCompressed" + fullWidth + > + <EuiSwitch + name="mlScatterplotMatrixDynamicSize" + label={dynamicSize ? TOGGLE_ON : TOGGLE_OFF} + checked={dynamicSize} + onChange={dynamicSizeOnChange} + disabled={isLoading} + /> + </EuiFormRow> + </EuiFlexItem> + )} + </EuiFlexGroup> + + <div id={htmlId} className="mlScatterplotMatrix" data-test-subj="mlScatterplotMatrix" /> + </> + )} + </> + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default ScatterplotMatrixView; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/use_scatterplot_field_options.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/use_scatterplot_field_options.ts new file mode 100644 index 0000000000000..f5eedbc03951f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/use_scatterplot_field_options.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; + +import type { IndexPattern } from '../../../../../../../src/plugins/data/public'; + +import { ML__INCREMENTAL_ID } from '../../data_frame_analytics/common/fields'; + +export const useScatterplotFieldOptions = ( + indexPattern?: IndexPattern, + includes?: string[], + excludes?: string[], + resultsField = '' +): string[] => { + return useMemo(() => { + const fields: string[] = []; + + if (indexPattern === undefined || includes === undefined) { + return fields; + } + + if (includes.length > 1) { + fields.push( + ...includes.filter((d) => + indexPattern.fields.some((f) => f.name === d && f.type === 'number') + ) + ); + } else { + fields.push( + ...indexPattern.fields + .filter( + (f) => + f.type === 'number' && + !indexPattern.metaFields.includes(f.name) && + !f.name.startsWith(`${resultsField}.`) && + f.name !== ML__INCREMENTAL_ID + ) + .map((f) => f.name) + ); + } + + return Array.isArray(excludes) && excludes.length > 0 + ? fields.filter((f) => !excludes.includes(f)) + : fields; + }, [indexPattern, includes, excludes]); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_scatterplot_matrix_legend_type.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_scatterplot_matrix_legend_type.ts new file mode 100644 index 0000000000000..8850d42577bd9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_scatterplot_matrix_legend_type.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 { ANALYSIS_CONFIG_TYPE } from './analytics'; + +import { AnalyticsJobType } from '../pages/analytics_management/hooks/use_create_analytics_form/state'; + +import { LEGEND_TYPES } from '../../components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec'; + +export const getScatterplotMatrixLegendType = (jobType: AnalyticsJobType | 'unknown') => { + switch (jobType) { + case ANALYSIS_CONFIG_TYPE.CLASSIFICATION: + return LEGEND_TYPES.NOMINAL; + case ANALYSIS_CONFIG_TYPE.REGRESSION: + return LEGEND_TYPES.QUANTITATIVE; + default: + return undefined; + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 7ba3e910ddd32..d03f73ad13575 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -41,6 +41,7 @@ export { export { getIndexData } from './get_index_data'; export { getIndexFields } from './get_index_fields'; +export { getScatterplotMatrixLegendType } from './get_scatterplot_matrix_legend_type'; export { useResultsViewConfig } from './use_results_view_config'; export { DataFrameAnalyticsConfig } from '../../../../common/types/data_frame_analytics'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts index 185513f75a12c..361a1262a59f8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -102,6 +102,12 @@ export const useResultsViewConfig = (jobId: string) => { try { indexP = await mlContext.indexPatterns.get(destIndexPatternId); + + // Force refreshing the fields list here because a user directly coming + // from the job creation wizard might land on the page without the + // index pattern being fully initialized because it was created + // before the analytics job populated the destination index. + await mlContext.indexPatterns.refreshFields(indexP); } catch (e) { indexP = undefined; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index a5991f77e88e2..4b86f5ca12896 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -27,10 +27,10 @@ import { TRAINING_PERCENT_MAX, FieldSelectionItem, } from '../../../../common/analytics'; +import { getScatterplotMatrixLegendType } from '../../../../common/get_scatterplot_matrix_legend_type'; import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { Messages } from '../shared'; import { - AnalyticsJobType, DEFAULT_MODEL_MEMORY_LIMIT, State, } from '../../../analytics_management/hooks/use_create_analytics_form/state'; @@ -51,18 +51,7 @@ import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/sea import { ExplorationQueryBarProps } from '../../../analytics_exploration/components/exploration_query_bar/exploration_query_bar'; import { Query } from '../../../../../../../../../../src/plugins/data/common/query'; -import { LEGEND_TYPES, ScatterplotMatrix } from '../../../../../components/scatterplot_matrix'; - -const getScatterplotMatrixLegendType = (jobType: AnalyticsJobType) => { - switch (jobType) { - case ANALYSIS_CONFIG_TYPE.CLASSIFICATION: - return LEGEND_TYPES.NOMINAL; - case ANALYSIS_CONFIG_TYPE.REGRESSION: - return LEGEND_TYPES.QUANTITATIVE; - default: - return undefined; - } -}; +import { ScatterplotMatrix } from '../../../../../components/scatterplot_matrix'; const requiredFieldsErrorText = i18n.translate( 'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage', @@ -498,6 +487,7 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({ : undefined } legendType={getScatterplotMatrixLegendType(jobType)} + searchQuery={jobConfigQuery} /> </EuiPanel> <EuiSpacer /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_splom.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_splom.tsx index 5ec8963e0fc25..8c51c95d7fd63 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_splom.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_splom.tsx @@ -12,17 +12,14 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { ScatterplotMatrix } from '../../../../../components/scatterplot_matrix'; +import { + ScatterplotMatrix, + ScatterplotMatrixProps, +} from '../../../../../components/scatterplot_matrix'; import { ExpandableSection } from './expandable_section'; -interface ExpandableSectionSplomProps { - fields: string[]; - index: string; - resultsField?: string; -} - -export const ExpandableSectionSplom: FC<ExpandableSectionSplomProps> = (props) => { +export const ExpandableSectionSplom: FC<ScatterplotMatrixProps> = (props) => { const splomSectionHeaderItems = undefined; const splomSectionContent = ( <> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index 1329644322f33..46715af0ef0cb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -9,16 +9,21 @@ import React, { FC, useCallback, useState } from 'react'; import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { getAnalysisType, getDependentVar } from '../../../../../../../common/util/analytics_utils'; + +import { useScatterplotFieldOptions } from '../../../../../components/scatterplot_matrix'; + import { defaultSearchQuery, + getScatterplotMatrixLegendType, useResultsViewConfig, DataFrameAnalyticsConfig, } from '../../../../common'; -import { ResultsSearchQuery } from '../../../../common/analytics'; +import { ResultsSearchQuery, ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { DataFrameTaskStateType } from '../../../analytics_management/components/analytics_list/common'; -import { ExpandableSectionAnalytics } from '../expandable_section'; +import { ExpandableSectionAnalytics, ExpandableSectionSplom } from '../expandable_section'; import { ExplorationResultsTable } from '../exploration_results_table'; import { ExplorationQueryBar } from '../exploration_query_bar'; import { JobConfigErrorCallout } from '../job_config_error_callout'; @@ -99,6 +104,14 @@ export const ExplorationPageWrapper: FC<Props> = ({ language: pageUrlState.queryLanguage, }; + const resultsField = jobConfig?.dest.results_field ?? ''; + const scatterplotFieldOptions = useScatterplotFieldOptions( + indexPattern, + jobConfig?.analyzed_fields.includes, + jobConfig?.analyzed_fields.excludes, + resultsField + ); + if (indexPatternErrorMessage !== undefined) { return ( <EuiPanel grow={false}> @@ -125,6 +138,9 @@ export const ExplorationPageWrapper: FC<Props> = ({ ); } + const jobType = + jobConfig && jobConfig.analysis ? getAnalysisType(jobConfig?.analysis) : undefined; + return ( <> {typeof jobConfig?.description !== 'undefined' && ( @@ -179,6 +195,27 @@ export const ExplorationPageWrapper: FC<Props> = ({ <EvaluatePanel jobConfig={jobConfig} jobStatus={jobStatus} searchQuery={searchQuery} /> )} + {isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />} + {isLoadingJobConfig === false && + jobConfig !== undefined && + isInitialized === true && + typeof jobConfig?.id === 'string' && + scatterplotFieldOptions.length > 1 && + typeof jobConfig?.analysis !== 'undefined' && ( + <ExpandableSectionSplom + fields={scatterplotFieldOptions} + index={jobConfig?.dest.index} + color={ + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || + jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION + ? getDependentVar(jobConfig.analysis) + : undefined + } + legendType={getScatterplotMatrixLegendType(jobType)} + searchQuery={searchQuery} + /> + )} + {isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />} {isLoadingJobConfig === false && jobConfig !== undefined && diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 26eee9bc95d73..7e11e0bd97015 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, FC, useCallback } from 'react'; +import React, { useCallback, useState, FC } from 'react'; import { EuiCallOut, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; @@ -15,6 +15,7 @@ import { COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; +import { useScatterplotFieldOptions } from '../../../../../components/scatterplot_matrix'; import { SavedSearchQuery } from '../../../../../contexts/ml'; import { defaultSearchQuery, isOutlierAnalysis, useResultsViewConfig } from '../../../../common'; @@ -90,6 +91,13 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) = (d) => d.id === `${resultsField}.${FEATURE_INFLUENCE}.feature_name` ) === -1; + const scatterplotFieldOptions = useScatterplotFieldOptions( + indexPattern, + jobConfig?.analyzed_fields.includes, + jobConfig?.analyzed_fields.excludes, + resultsField + ); + if (indexPatternErrorMessage !== undefined) { return ( <EuiPanel grow={false}> @@ -126,11 +134,12 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) = </> )} {typeof jobConfig?.id === 'string' && <ExpandableSectionAnalytics jobId={jobConfig?.id} />} - {typeof jobConfig?.id === 'string' && jobConfig?.analyzed_fields.includes.length > 1 && ( + {typeof jobConfig?.id === 'string' && scatterplotFieldOptions.length > 1 && ( <ExpandableSectionSplom - fields={jobConfig?.analyzed_fields.includes} + fields={scatterplotFieldOptions} index={jobConfig?.dest.index} resultsField={jobConfig?.dest.results_field} + searchQuery={searchQuery} /> )} {showLegacyFeatureInfluenceFormatCallout && ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b50a7092e108d..4369cbf35594d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14209,8 +14209,8 @@ "xpack.ml.splom.dynamicSizeLabel": "動的サイズ", "xpack.ml.splom.fieldSelectionLabel": "フィールド", "xpack.ml.splom.fieldSelectionPlaceholder": "フィールドを選択", - "xpack.ml.splom.RandomScoringLabel": "ランダムスコアリング", - "xpack.ml.splom.SampleSizeLabel": "サンプルサイズ", + "xpack.ml.splom.randomScoringLabel": "ランダムスコアリング", + "xpack.ml.splom.sampleSizeLabel": "サンプルサイズ", "xpack.ml.splom.toggleOff": "オフ", "xpack.ml.splom.toggleOn": "オン", "xpack.ml.splomSpec.outlierScoreThresholdName": "異常スコアしきい値:", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a93b2c78690c1..d2504c8752c05 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14248,8 +14248,8 @@ "xpack.ml.splom.dynamicSizeLabel": "动态大小", "xpack.ml.splom.fieldSelectionLabel": "字段", "xpack.ml.splom.fieldSelectionPlaceholder": "选择字段", - "xpack.ml.splom.RandomScoringLabel": "随机评分", - "xpack.ml.splom.SampleSizeLabel": "样例大小", + "xpack.ml.splom.randomScoringLabel": "随机评分", + "xpack.ml.splom.sampleSizeLabel": "样例大小", "xpack.ml.splom.toggleOff": "关闭", "xpack.ml.splom.toggleOn": "开启", "xpack.ml.splomSpec.outlierScoreThresholdName": "离群值分数阈值:", diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 6b42306c08c92..b0f1e316e626a 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -40,6 +40,17 @@ export default function ({ getService }: FtrProviderContext) { modelMemory: '60mb', createIndexPattern: true, expected: { + scatterplotMatrixColorStats: [ + // background + { key: '#000000', value: 94 }, + // tick/grid/axis + { key: '#DDDDDD', value: 1 }, + { key: '#D3DAE6', value: 1 }, + { key: '#F5F7FA', value: 1 }, + // scatterplot circles + { key: '#6A717D', value: 1 }, + { key: '#54B39A', value: 1 }, + ], row: { type: 'classification', status: 'stopped', @@ -89,6 +100,12 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the include fields selection'); await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); + await ml.testExecution.logTestStep('displays the scatterplot matrix'); + await ml.dataFrameAnalyticsScatterplot.assertScatterplotMatrix( + 'mlAnalyticsCreateJobWizardScatterplotMatrixFormRow', + testData.expected.scatterplotMatrixColorStats + ); + await ml.testExecution.logTestStep('continues to the additional options step'); await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); @@ -207,6 +224,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsResults.assertResultsTableExists(); await ml.dataFrameAnalyticsResults.assertResultsTableTrainingFiltersExist(); await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + await ml.dataFrameAnalyticsScatterplot.assertScatterplotMatrix( + 'mlDFExpandableSection-splom', + testData.expected.scatterplotMatrixColorStats + ); }); it('displays the analytics job in the map view', async () => { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 53daa0cae2522..419239d1d15ca 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -49,6 +49,27 @@ export default function ({ getService }: FtrProviderContext) { { chartAvailable: true, id: 'Exterior2nd', legend: '3 categories' }, { chartAvailable: true, id: 'Fireplaces', legend: '0 - 3' }, ], + scatterplotMatrixColorStatsWizard: [ + // background + { key: '#000000', value: 91 }, + // tick/grid/axis + { key: '#6A717D', value: 2 }, + { key: '#F5F7FA', value: 2 }, + { key: '#D3DAE6', value: 1 }, + // scatterplot circles + { key: '#54B399', value: 1 }, + { key: '#54B39A', value: 1 }, + ], + scatterplotMatrixColorStatsResults: [ + // background + { key: '#000000', value: 91 }, + // tick/grid/axis, grey markers + // the red outlier color is not above the 1% threshold. + { key: '#6A717D', value: 2 }, + { key: '#98A2B3', value: 1 }, + { key: '#F5F7FA', value: 2 }, + { key: '#D3DAE6', value: 1 }, + ], row: { type: 'outlier_detection', status: 'stopped', @@ -105,6 +126,12 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the include fields selection'); await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); + await ml.testExecution.logTestStep('displays the scatterplot matrix'); + await ml.dataFrameAnalyticsScatterplot.assertScatterplotMatrix( + 'mlAnalyticsCreateJobWizardScatterplotMatrixFormRow', + testData.expected.scatterplotMatrixColorStatsWizard + ); + await ml.testExecution.logTestStep('continues to the additional options step'); await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); @@ -221,6 +248,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsResults.assertOutlierTablePanelExists(); await ml.dataFrameAnalyticsResults.assertResultsTableExists(); await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + await ml.dataFrameAnalyticsScatterplot.assertScatterplotMatrix( + 'mlDFExpandableSection-splom', + testData.expected.scatterplotMatrixColorStatsResults + ); }); it('displays the analytics job in the map view', async () => { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index fef22fcebc3ed..f1d19a82caa9b 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -39,6 +39,16 @@ export default function ({ getService }: FtrProviderContext) { modelMemory: '20mb', createIndexPattern: true, expected: { + scatterplotMatrixColorStats: [ + // background + { key: '#000000', value: 80 }, + // tick/grid/axis + { key: '#6A717D', value: 1 }, + { key: '#F5F7FA', value: 2 }, + { key: '#D3DAE6', value: 1 }, + // because a continuous color scale is used for the scatterplot circles, + // none of the generated colors is above the 1% threshold. + ], row: { type: 'regression', status: 'stopped', @@ -89,6 +99,12 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the include fields selection'); await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); + await ml.testExecution.logTestStep('displays the scatterplot matrix'); + await ml.dataFrameAnalyticsScatterplot.assertScatterplotMatrix( + 'mlAnalyticsCreateJobWizardScatterplotMatrixFormRow', + testData.expected.scatterplotMatrixColorStats + ); + await ml.testExecution.logTestStep('continues to the additional options step'); await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); @@ -207,6 +223,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsResults.assertResultsTableExists(); await ml.dataFrameAnalyticsResults.assertResultsTableTrainingFiltersExist(); await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + await ml.dataFrameAnalyticsScatterplot.assertScatterplotMatrix( + 'mlDFExpandableSection-splom', + testData.expected.scatterplotMatrixColorStats + ); }); it('displays the analytics job in the map view', async () => { diff --git a/x-pack/test/functional/services/canvas_element.ts b/x-pack/test/functional/services/canvas_element.ts index e2a42c5dc43c3..08ac38d970225 100644 --- a/x-pack/test/functional/services/canvas_element.ts +++ b/x-pack/test/functional/services/canvas_element.ts @@ -43,9 +43,13 @@ export async function CanvasElementProvider({ getService }: FtrProviderContext) public async getImageData(selector: string): Promise<number[]> { return await driver.executeScript( ` - const el = document.querySelector('${selector}'); - const ctx = el.getContext('2d'); - return ctx.getImageData(0, 0, el.width, el.height).data; + try { + const el = document.querySelector('${selector}'); + const ctx = el.getContext('2d'); + return ctx.getImageData(0, 0, el.width, el.height).data; + } catch(e) { + return []; + } ` ); } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_scatterplot.ts b/x-pack/test/functional/services/ml/data_frame_analytics_scatterplot.ts new file mode 100644 index 0000000000000..3472e5079c79a --- /dev/null +++ b/x-pack/test/functional/services/ml/data_frame_analytics_scatterplot.ts @@ -0,0 +1,40 @@ +/* + * 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'; + +export function MachineLearningDataFrameAnalyticsScatterplotProvider({ + getService, +}: FtrProviderContext) { + const canvasElement = getService('canvasElement'); + const testSubjects = getService('testSubjects'); + + return new (class AnalyticsScatterplot { + public async assertScatterplotMatrix( + dataTestSubj: string, + expectedColorStats: Array<{ + key: string; + value: number; + }> + ) { + await testSubjects.existOrFail(dataTestSubj); + await testSubjects.existOrFail('mlScatterplotMatrix'); + + const actualColorStats = await canvasElement.getColorStats( + `[data-test-subj="mlScatterplotMatrix"] canvas`, + expectedColorStats, + 1 + ); + expect(actualColorStats.every((d) => d.withinTolerance)).to.eql( + true, + `Color stats for scatterplot matrix should be within tolerance. Expected: '${JSON.stringify( + expectedColorStats + )}' (got '${JSON.stringify(actualColorStats)}')` + ); + } + })(); +} diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index c1a9ac304dd69..aa87bc5dc4772 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -17,6 +17,7 @@ import { MachineLearningDataFrameAnalyticsProvider } from './data_frame_analytic import { MachineLearningDataFrameAnalyticsCreationProvider } from './data_frame_analytics_creation'; import { MachineLearningDataFrameAnalyticsEditProvider } from './data_frame_analytics_edit'; import { MachineLearningDataFrameAnalyticsResultsProvider } from './data_frame_analytics_results'; +import { MachineLearningDataFrameAnalyticsScatterplotProvider } from './data_frame_analytics_scatterplot'; import { MachineLearningDataFrameAnalyticsMapProvider } from './data_frame_analytics_map'; import { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_analytics_table'; import { MachineLearningDataVisualizerProvider } from './data_visualizer'; @@ -63,6 +64,9 @@ export function MachineLearningProvider(context: FtrProviderContext) { const dataFrameAnalyticsResults = MachineLearningDataFrameAnalyticsResultsProvider(context); const dataFrameAnalyticsMap = MachineLearningDataFrameAnalyticsMapProvider(context); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); + const dataFrameAnalyticsScatterplot = MachineLearningDataFrameAnalyticsScatterplotProvider( + context + ); const dataVisualizer = MachineLearningDataVisualizerProvider(context); const dataVisualizerTable = MachineLearningDataVisualizerTableProvider(context, commonUI); @@ -105,6 +109,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { dataFrameAnalyticsResults, dataFrameAnalyticsMap, dataFrameAnalyticsTable, + dataFrameAnalyticsScatterplot, dataVisualizer, dataVisualizerFileBased, dataVisualizerIndexBased, From 53637d01580b4016b110e290f80283eae35b1408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= <alejandro.fernandez@elastic.co> Date: Mon, 1 Feb 2021 14:48:30 +0100 Subject: [PATCH 157/163] [Logs UI] <LogStream /> as a kibana embeddable (#88618) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-optimizer/limits.yml | 2 +- x-pack/plugins/infra/kibana.json | 2 +- .../public/components/log_stream/index.ts | 7 ++ .../log_stream/lazy_log_stream_wrapper.tsx | 4 +- .../log_stream/{index.tsx => log_stream.tsx} | 3 +- .../log_stream/log_stream_embeddable.tsx | 91 +++++++++++++++++++ .../log_stream_embeddable_factory.ts | 37 ++++++++ .../containers/logs/log_stream/index.ts | 22 ++++- x-pack/plugins/infra/public/plugin.ts | 9 ++ x-pack/plugins/infra/public/types.ts | 2 + 10 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/log_stream/index.ts rename x-pack/plugins/infra/public/components/log_stream/{index.tsx => log_stream.tsx} (98%) create mode 100644 x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx create mode 100644 x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index a13976d148738..794503656ba04 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexLifecycleManagement: 107090 indexManagement: 140608 indexPatternManagement: 154222 - infra: 197873 + infra: 204800 fleet: 415829 ingestPipelines: 58003 inputControlVis: 172675 diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index e84767f4931ca..d1fa83793d1dd 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -6,7 +6,7 @@ "features", "usageCollection", "spaces", - + "embeddable", "data", "dataEnhanced", "visTypeTimeseries", diff --git a/x-pack/plugins/infra/public/components/log_stream/index.ts b/x-pack/plugins/infra/public/components/log_stream/index.ts new file mode 100644 index 0000000000000..6abb292f919d9 --- /dev/null +++ b/x-pack/plugins/infra/public/components/log_stream/index.ts @@ -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 * from './log_stream'; diff --git a/x-pack/plugins/infra/public/components/log_stream/lazy_log_stream_wrapper.tsx b/x-pack/plugins/infra/public/components/log_stream/lazy_log_stream_wrapper.tsx index 65433aab15716..13eb6431f97a3 100644 --- a/x-pack/plugins/infra/public/components/log_stream/lazy_log_stream_wrapper.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/lazy_log_stream_wrapper.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; -import type { LogStreamProps } from './'; +import type { LogStreamProps } from './log_stream'; -const LazyLogStream = React.lazy(() => import('./')); +const LazyLogStream = React.lazy(() => import('./log_stream')); export const LazyLogStreamWrapper: React.FC<LogStreamProps> = (props) => ( <React.Suspense fallback={<div />}> diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx similarity index 98% rename from x-pack/plugins/infra/public/components/log_stream/index.tsx rename to x-pack/plugins/infra/public/components/log_stream/log_stream.tsx index b485a21221af2..b7410fda6f6fd 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx @@ -17,6 +17,7 @@ import { useLogStream } from '../../containers/logs/log_stream'; import { ScrollableLogTextStreamView } from '../logging/log_text_stream'; import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration'; import { JsonValue } from '../../../../../../src/plugins/kibana_utils/common'; +import { Query } from '../../../../../../src/plugins/data/common'; const PAGE_THRESHOLD = 2; @@ -55,7 +56,7 @@ export interface LogStreamProps { sourceId?: string; startTimestamp: number; endTimestamp: number; - query?: string; + query?: string | Query; center?: LogEntryCursor; highlight?: string; height?: string | number; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx new file mode 100644 index 0000000000000..0d6dfc50960f9 --- /dev/null +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { CoreStart } from 'kibana/public'; + +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; +import { Query, TimeRange } from '../../../../../../src/plugins/data/public'; +import { + Embeddable, + EmbeddableInput, + IContainer, +} from '../../../../../../src/plugins/embeddable/public'; +import { datemathToEpochMillis } from '../../utils/datemath'; +import { LazyLogStreamWrapper } from './lazy_log_stream_wrapper'; + +export const LOG_STREAM_EMBEDDABLE = 'LOG_STREAM_EMBEDDABLE'; + +export interface LogStreamEmbeddableInput extends EmbeddableInput { + timeRange: TimeRange; + query: Query; +} + +export class LogStreamEmbeddable extends Embeddable<LogStreamEmbeddableInput> { + public readonly type = LOG_STREAM_EMBEDDABLE; + private node?: HTMLElement; + + constructor( + private services: CoreStart, + initialInput: LogStreamEmbeddableInput, + parent?: IContainer + ) { + super(initialInput, {}, parent); + } + + public render(node: HTMLElement) { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + + this.renderComponent(); + } + + public reload() { + this.renderComponent(); + } + + public destroy() { + super.destroy(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } + + private renderComponent() { + if (!this.node) { + return; + } + + const startTimestamp = datemathToEpochMillis(this.input.timeRange.from); + const endTimestamp = datemathToEpochMillis(this.input.timeRange.to); + + if (!startTimestamp || !endTimestamp) { + return; + } + + ReactDOM.render( + <I18nProvider> + <EuiThemeProvider> + <KibanaContextProvider services={this.services}> + <div style={{ width: '100%' }}> + <LazyLogStreamWrapper + startTimestamp={startTimestamp} + endTimestamp={endTimestamp} + height="100%" + query={this.input.query} + /> + </div> + </KibanaContextProvider> + </EuiThemeProvider> + </I18nProvider>, + this.node + ); + } +} diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts new file mode 100644 index 0000000000000..f4d1b83a07593 --- /dev/null +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts @@ -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 { CoreStart } from 'kibana/public'; +import { + EmbeddableFactoryDefinition, + IContainer, +} from '../../../../../../src/plugins/embeddable/public'; +import { + LogStreamEmbeddable, + LOG_STREAM_EMBEDDABLE, + LogStreamEmbeddableInput, +} from './log_stream_embeddable'; + +export class LogStreamEmbeddableFactoryDefinition + implements EmbeddableFactoryDefinition<LogStreamEmbeddableInput> { + public readonly type = LOG_STREAM_EMBEDDABLE; + + constructor(private getCoreServices: () => Promise<CoreStart>) {} + + public async isEditable() { + const { application } = await this.getCoreServices(); + return application.capabilities.logs.save as boolean; + } + + public async create(initialInput: LogStreamEmbeddableInput, parent?: IContainer) { + const services = await this.getCoreServices(); + return new LogStreamEmbeddable(services, initialInput, parent); + } + + public getDisplayName() { + return 'Log stream'; + } +} diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index da7176125dae4..1d9a7a1b1d777 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -7,7 +7,7 @@ import { useMemo, useEffect } from 'react'; import useSetState from 'react-use/lib/useSetState'; import usePrevious from 'react-use/lib/usePrevious'; -import { esKuery } from '../../../../../../../src/plugins/data/public'; +import { esKuery, esQuery, Query } from '../../../../../../../src/plugins/data/public'; import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { LogEntryCursor, LogEntry } from '../../../../common/log_entry'; @@ -18,7 +18,7 @@ interface LogStreamProps { sourceId: string; startTimestamp: number; endTimestamp: number; - query?: string; + query?: string | Query; center?: LogEntryCursor; columns?: LogSourceConfigurationProperties['logColumns']; } @@ -84,9 +84,21 @@ export function useLogStream({ }, [prevEndTimestamp, endTimestamp, setState]); const parsedQuery = useMemo(() => { - return query - ? JSON.stringify(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query))) - : null; + if (!query) { + return null; + } + + let q; + + if (typeof query === 'string') { + q = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query)); + } else if (query.language === 'kuery') { + q = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query.query as string)); + } else if (query.language === 'lucene') { + q = esQuery.luceneStringToDsl(query.query as string); + } + + return JSON.stringify(q); }, [query]); // Callbacks diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 2bbd0067642c0..809046ee1e17b 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -19,6 +19,8 @@ import { } from './types'; import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers'; import { createMetricsHasData, createMetricsFetchData } from './metrics_overview_fetchers'; +import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/log_stream_embeddable'; +import { LogStreamEmbeddableFactoryDefinition } from './components/log_stream/log_stream_embeddable_factory'; export class Plugin implements InfraClientPluginClass { constructor(_context: PluginInitializerContext) {} @@ -46,6 +48,13 @@ export class Plugin implements InfraClientPluginClass { }); } + const getCoreServices = async () => (await core.getStartServices())[0]; + + pluginsSetup.embeddable.registerEmbeddableFactory( + LOG_STREAM_EMBEDDABLE, + new LogStreamEmbeddableFactoryDefinition(getCoreServices) + ); + core.application.register({ id: 'logs', title: i18n.translate('xpack.infra.logs.pluginTitle', { diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index f1052672978d5..037cfa4b7eb2d 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -7,6 +7,7 @@ import type { CoreSetup, CoreStart, Plugin as PluginClass } from 'kibana/public'; import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import type { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; import type { UsageCollectionSetup, UsageCollectionStart, @@ -33,6 +34,7 @@ export interface InfraClientSetupDeps { observability: ObservabilityPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionSetup; + embeddable: EmbeddableSetup; } export interface InfraClientStartDeps { From 3255f905c0ac8e8f7a639ea95858fec6bac07cf9 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar <dario.gieselaar@elastic.co> Date: Mon, 1 Feb 2021 15:14:17 +0100 Subject: [PATCH 158/163] [APM] Remove value_count aggregations (#89408) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../get_transaction_error_rate.ts | 4 +- .../index.ts | 31 +++++--------- .../lib/helpers/transaction_error_rate.ts | 41 +++++-------------- .../get_transaction_coordinates.ts | 16 +------- .../get_service_map_service_node_info.test.ts | 4 +- .../get_service_map_service_node_info.ts | 9 +--- .../__snapshots__/queries.test.ts.snap | 27 ------------ .../get_service_instance_transaction_stats.ts | 29 +++++-------- ..._timeseries_data_for_transaction_groups.ts | 6 ++- .../get_transaction_groups_for_page.ts | 9 ++-- .../merge_transaction_group_data.ts | 8 +--- .../get_service_transaction_stats.ts | 14 ++----- .../__snapshots__/queries.test.ts.snap | 10 ----- .../lib/transaction_groups/get_error_rate.ts | 10 ++++- .../get_transaction_group_stats.ts | 9 +--- .../get_throughput_charts/index.ts | 6 --- .../get_throughput_charts/transform.ts | 4 +- 17 files changed, 63 insertions(+), 174 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts index fae43ef148cfa..f6ddb15cbffa9 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts @@ -44,9 +44,7 @@ export async function getTransactionErrorRateChartPreview({ }, }; - const outcomes = getOutcomeAggregation({ - searchAggregatedTransactions: false, - }); + const outcomes = getOutcomeAggregation(); const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts index 64d9ebb192eb3..9ecf201ede1b7 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, omit } from 'lodash'; +import { isEmpty, omit, merge } from 'lodash'; import { EventOutcome } from '../../../../common/event_outcome'; import { processSignificantTermAggs, @@ -134,8 +134,7 @@ export async function getErrorRateTimeSeries({ extended_bounds: { min: start, max: end }, }, aggs: { - // TODO: add support for metrics - outcomes: getOutcomeAggregation({ searchAggregatedTransactions: false }), + outcomes: getOutcomeAggregation(), }, }; @@ -147,13 +146,12 @@ export async function getErrorRateTimeSeries({ }; return acc; }, - {} as Record< - string, - { + {} as { + [key: string]: { filter: AggregationOptionsByType['filter']; aggs: { timeseries: typeof timeseriesAgg }; - } - > + }; + } ); const params = { @@ -162,32 +160,25 @@ export async function getErrorRateTimeSeries({ body: { size: 0, query: { bool: { filter: backgroundFilters } }, - aggs: { - // overall aggs - timeseries: timeseriesAgg, - - // per term aggs - ...perTermAggs, - }, + aggs: merge({ timeseries: timeseriesAgg }, perTermAggs), }, }; const response = await apmEventClient.search(params); - type Agg = NonNullable<typeof response.aggregations>; + const { aggregations } = response; - if (!response.aggregations) { + if (!aggregations) { return {}; } return { overall: { timeseries: getTransactionErrorRateTimeSeries( - response.aggregations.timeseries.buckets + aggregations.timeseries.buckets ), }, significantTerms: topSigTerms.map((topSig, index) => { - // @ts-expect-error - const agg = response.aggregations[`term_${index}`] as Agg; + const agg = aggregations[`term_${index}`]!; return { ...topSig, diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 876fc6b822213..2d041006e0e27 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -10,40 +10,21 @@ import { AggregationOptionsByType, AggregationResultOf, } from '../../../../../typings/elasticsearch/aggregations'; -import { getTransactionDurationFieldForAggregatedTransactions } from './aggregated_transactions'; -export function getOutcomeAggregation({ - searchAggregatedTransactions, -}: { - searchAggregatedTransactions: boolean; -}) { - return { - terms: { - field: EVENT_OUTCOME, - include: [EventOutcome.failure, EventOutcome.success], - }, - aggs: { - // simply using the doc count to get the number of requests is not possible for transaction metrics (histograms) - // to work around this we get the number of transactions by counting the number of latency values - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }; -} +export const getOutcomeAggregation = () => ({ + terms: { + field: EVENT_OUTCOME, + include: [EventOutcome.failure, EventOutcome.success], + }, +}); + +type OutcomeAggregation = ReturnType<typeof getOutcomeAggregation>; export function calculateTransactionErrorPercentage( - outcomeResponse: AggregationResultOf< - ReturnType<typeof getOutcomeAggregation>, - {} - > + outcomeResponse: AggregationResultOf<OutcomeAggregation, {}> ) { const outcomes = Object.fromEntries( - outcomeResponse.buckets.map(({ key, count }) => [key, count.value]) + outcomeResponse.buckets.map(({ key, doc_count: count }) => [key, count]) ); const failedTransactions = outcomes[EventOutcome.failure] ?? 0; @@ -56,7 +37,7 @@ export function getTransactionErrorRateTimeSeries( buckets: AggregationResultOf< { date_histogram: AggregationOptionsByType['date_histogram']; - aggs: { outcomes: ReturnType<typeof getOutcomeAggregation> }; + aggs: { outcomes: OutcomeAggregation }; }, {} >['buckets'] 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 5531944fc7180..fa4bf6144fb6f 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 @@ -11,10 +11,7 @@ import { rangeFilter } from '../../../common/utils/range_filter'; import { Coordinates } from '../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../helpers/aggregated_transactions'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; export async function getTransactionCoordinates({ setup, @@ -49,15 +46,6 @@ export async function getTransactionCoordinates({ fixed_interval: bucketSize, min_doc_count: 0, }, - aggs: { - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, }, }, }, @@ -68,7 +56,7 @@ export async function getTransactionCoordinates({ return ( aggregations?.distribution.buckets.map((bucket) => ({ x: bucket.key, - y: bucket.count.value / deltaAsMinutes, + y: bucket.doc_count / deltaAsMinutes, })) || [] ); } 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 f7ca40ef1052c..173de796d47e4 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 @@ -52,8 +52,10 @@ describe('getServiceMapServiceNodeInfo', () => { apmEventClient: { search: () => Promise.resolve({ + hits: { + total: { value: 1 }, + }, aggregations: { - count: { value: 1 }, duration: { value: null }, avgCpuUsage: { value: null }, avgMemoryUsage: { value: null }, 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 82d339686f7ec..4fe9a1a75d43f 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 @@ -162,19 +162,12 @@ async function getTransactionStats({ ), }, }, - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, }, }, }; const response = await apmEventClient.search(params); - const totalRequests = response.aggregations?.count.value ?? 0; + const totalRequests = response.hits.total.value; return { avgTransactionDuration: response.aggregations?.duration.value ?? null, 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 21402e4c8dac0..239b909e1572c 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 @@ -122,13 +122,6 @@ Array [ }, }, "outcomes": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, - }, "terms": Object { "field": "event.outcome", "include": Array [ @@ -137,11 +130,6 @@ Array [ ], }, }, - "real_document_count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, "timeseries": Object { "aggs": Object { "avg_duration": Object { @@ -150,13 +138,6 @@ Array [ }, }, "outcomes": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, - }, "terms": Object { "field": "event.outcome", "include": Array [ @@ -165,11 +146,6 @@ Array [ ], }, }, - "real_document_count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, }, "date_histogram": Object { "extended_bounds": Object { @@ -184,9 +160,6 @@ Array [ }, "terms": Object { "field": "transaction.type", - "order": Object { - "real_document_count": "desc", - }, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts index 5880b5cbc9546..c5e5269c4409e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts @@ -30,18 +30,17 @@ export async function getServiceInstanceTransactionStats({ }: ServiceInstanceParams) { const { apmEventClient, start, end, esFilter } = setup; - const { intervalString } = getBucketSize({ start, end, numBuckets }); + const { intervalString, bucketSize } = getBucketSize({ + start, + end, + numBuckets, + }); const field = getTransactionDurationFieldForAggregatedTransactions( searchAggregatedTransactions ); const subAggs = { - count: { - value_count: { - field, - }, - }, avg_transaction_duration: { avg: { field, @@ -53,13 +52,6 @@ export async function getServiceInstanceTransactionStats({ [EVENT_OUTCOME]: EventOutcome.failure, }, }, - aggs: { - count: { - value_count: { - field, - }, - }, - }, }, }; @@ -113,12 +105,13 @@ export async function getServiceInstanceTransactionStats({ }); const deltaAsMinutes = (end - start) / 60 / 1000; + const bucketSizeInMinutes = bucketSize / 60; return ( response.aggregations?.[SERVICE_NODE_NAME].buckets.map( (serviceNodeBucket) => { const { - count, + doc_count: count, avg_transaction_duration: avgTransactionDuration, key, failures, @@ -128,17 +121,17 @@ export async function getServiceInstanceTransactionStats({ return { serviceNodeName: String(key), errorRate: { - value: failures.count.value / count.value, + value: failures.doc_count / count, timeseries: timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, - y: dateBucket.failures.count.value / dateBucket.count.value, + y: dateBucket.failures.doc_count / dateBucket.doc_count, })), }, throughput: { - value: count.value / deltaAsMinutes, + value: count / deltaAsMinutes, timeseries: timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, - y: dateBucket.count.value / deltaAsMinutes, + y: dateBucket.doc_count / bucketSizeInMinutes, })), }, latency: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts index 937155bc31602..745535f261673 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts @@ -17,6 +17,7 @@ import { import { ESFilter } from '../../../../../../typings/elasticsearch'; import { + getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, } from '../../helpers/aggregated_transactions'; @@ -76,6 +77,9 @@ export async function getTimeseriesDataForTransactionGroups({ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { range: rangeFilter(start, end) }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), ...esFilter, ], }, @@ -99,10 +103,8 @@ export async function getTimeseriesDataForTransactionGroups({ }, aggs: { ...getLatencyAggregation(latencyAggregationType, field), - transaction_count: { value_count: { field } }, [EVENT_OUTCOME]: { filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, - aggs: { transaction_count: { value_count: { field } } }, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts index ccccf946512dd..400c896e380b4 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts @@ -99,10 +99,8 @@ export async function getTransactionGroupsForPage({ }, aggs: { ...getLatencyAggregation(latencyAggregationType, field), - transaction_count: { value_count: { field } }, [EVENT_OUTCOME]: { filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, - aggs: { transaction_count: { value_count: { field } } }, }, }, }, @@ -113,9 +111,8 @@ export async function getTransactionGroupsForPage({ const transactionGroups = response.aggregations?.transaction_groups.buckets.map((bucket) => { const errorRate = - bucket.transaction_count.value > 0 - ? (bucket[EVENT_OUTCOME].transaction_count.value ?? 0) / - bucket.transaction_count.value + bucket.doc_count > 0 + ? bucket[EVENT_OUTCOME].doc_count / bucket.doc_count : null; return { @@ -124,7 +121,7 @@ export async function getTransactionGroupsForPage({ latencyAggregationType, aggregation: bucket.latency, }), - throughput: bucket.transaction_count.value / deltaAsMinutes, + throughput: bucket.doc_count / deltaAsMinutes, errorRate, }; }) ?? []; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts index a8794e3c09a40..b0b1cb09dd784 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts @@ -52,18 +52,14 @@ export function mergeTransactionGroupData({ ...acc.throughput, timeseries: acc.throughput.timeseries.concat({ x: point.key, - y: point.transaction_count.value / deltaAsMinutes, + y: point.doc_count / deltaAsMinutes, }), }, errorRate: { ...acc.errorRate, timeseries: acc.errorRate.timeseries.concat({ x: point.key, - y: - point.transaction_count.value > 0 - ? (point[EVENT_OUTCOME].transaction_count.value ?? 0) / - point.transaction_count.value - : null, + y: point[EVENT_OUTCOME].doc_count / point.doc_count, }), }, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index 0ee7080dc0834..efbc30169d178 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -51,16 +51,9 @@ export async function getServiceTransactionStats({ }: AggregationParams) { const { apmEventClient, start, end, esFilter } = setup; - const outcomes = getOutcomeAggregation({ searchAggregatedTransactions }); + const outcomes = getOutcomeAggregation(); const metrics = { - real_document_count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, avg_duration: { avg: { field: getTransactionDurationFieldForAggregatedTransactions( @@ -102,7 +95,6 @@ export async function getServiceTransactionStats({ transactionType: { terms: { field: TRANSACTION_TYPE, - order: { real_document_count: 'desc' }, }, aggs: { ...metrics, @@ -180,14 +172,14 @@ export async function getServiceTransactionStats({ }, transactionsPerMinute: { value: calculateAvgDuration({ - value: topTransactionTypeBucket.real_document_count.value, + value: topTransactionTypeBucket.doc_count, deltaAsMinutes, }), timeseries: topTransactionTypeBucket.timeseries.buckets.map( (dateBucket) => ({ x: dateBucket.key, y: calculateAvgDuration({ - value: dateBucket.real_document_count.value, + value: dateBucket.doc_count, deltaAsMinutes, }), }) 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 c678e7db711b6..89069d74bacf8 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 @@ -12,11 +12,6 @@ Array [ "aggs": Object { "transaction_groups": Object { "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, "transaction_type": Object { "top_hits": Object { "_source": Array [ @@ -226,11 +221,6 @@ Array [ "aggs": Object { "transaction_groups": Object { "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, "transaction_type": Object { "top_hits": Object { "_source": Array [ 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 dfd11203b87f1..a2388dddc7fd4 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 @@ -14,7 +14,10 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../common/event_outcome'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; import { getBucketSize } from '../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { @@ -55,12 +58,15 @@ export async function getErrorRate({ { terms: { [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success] }, }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), ...transactionNamefilter, ...transactionTypefilter, ...esFilter, ]; - const outcomes = getOutcomeAggregation({ searchAggregatedTransactions }); + const outcomes = getOutcomeAggregation(); const params = { apm: { 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 cfd3540446172..dba58cecad79b 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 @@ -66,13 +66,6 @@ export async function getCounts({ searchAggregatedTransactions, }: MetricParams) { const params = mergeRequestWithAggs(request, { - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, transaction_type: { top_hits: { size: 1, @@ -92,7 +85,7 @@ export async function getCounts({ return { key: bucket.key as BucketKey, - count: bucket.count.value, + count: bucket.doc_count, transactionType: source.transaction.type, }; }); diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts index be374ccfe3400..8dfb0a9f65878 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts @@ -15,7 +15,6 @@ import { rangeFilter } from '../../../../common/utils/range_filter'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, } from '../../../lib/helpers/aggregated_transactions'; import { getBucketSize } from '../../../lib/helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request'; @@ -56,10 +55,6 @@ async function searchThroughput({ filter.push({ term: { [TRANSACTION_NAME]: transactionName } }); } - const field = getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ); - const params = { apm: { events: [ @@ -82,7 +77,6 @@ async function searchThroughput({ min_doc_count: 0, extended_bounds: { min: start, max: end }, }, - aggs: { count: { value_count: { field } } }, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/transform.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/transform.ts index a12e36c0e9de4..7e43a0d76f70a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/transform.ts @@ -25,7 +25,7 @@ export function getThroughputBuckets({ return { x: bucket.key, // divide by minutes - y: bucket.count.value / (bucketSize / 60), + y: bucket.doc_count / (bucketSize / 60), }; }); @@ -34,7 +34,7 @@ export function getThroughputBuckets({ resultKey === '' ? NOT_AVAILABLE_LABEL : (resultKey as string); const docCountTotal = timeseries.buckets - .map((bucket) => bucket.count.value) + .map((bucket) => bucket.doc_count) .reduce((a, b) => a + b, 0); // calculate average throughput From c66124e1703c817a728d6417e5a985be5c10d68e Mon Sep 17 00:00:00 2001 From: Marco Liberati <dej611@users.noreply.github.com> Date: Mon, 1 Feb 2021 15:25:16 +0100 Subject: [PATCH 159/163] [Lens] Make Lens intervals default value adapt to histogram:maxBars Advanced Setting changes (#89305) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../indexpattern_datasource/indexpattern.tsx | 2 +- .../definitions/date_histogram.test.tsx | 10 ++-- .../definitions/filters/filters.test.tsx | 7 ++- .../operations/definitions/index.ts | 6 ++- .../definitions/last_value.test.tsx | 7 ++- .../definitions/percentile.test.tsx | 7 ++- .../definitions/ranges/range_editor.tsx | 11 +---- .../definitions/ranges/ranges.test.tsx | 49 +++++++++---------- .../operations/definitions/ranges/ranges.tsx | 8 +-- .../definitions/terms/terms.test.tsx | 13 +++-- .../indexpattern_datasource/to_expression.ts | 15 ++++-- 11 files changed, 75 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index c309212eed164..7f77a7ce199b6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -167,7 +167,7 @@ export function getIndexPatternDatasource({ }); }, - toExpression, + toExpression: (state, layerId) => toExpression(state, layerId, uiSettings), renderDataPanel( domElement: Element, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index abd033c0db4cf..22275533b9554 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -83,9 +83,11 @@ const indexPattern2: IndexPattern = { ]), }; +const uiSettingsMock = {} as IUiSettingsClient; + const defaultOptions = { storage: {} as IStorageWrapper, - uiSettings: {} as IUiSettingsClient, + uiSettings: uiSettingsMock, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1y', @@ -200,7 +202,8 @@ describe('date_histogram', () => { layer.columns.col1 as DateHistogramIndexPatternColumn, 'col1', indexPattern1, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -252,7 +255,8 @@ describe('date_histogram', () => { }, ]), }, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index 86767fbc8b469..3657013fa0bfa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -16,9 +16,11 @@ import type { IndexPatternLayer } from '../../../types'; import { createMockedIndexPattern } from '../../../mocks'; import { FilterPopover } from './filter_popover'; +const uiSettingsMock = {} as IUiSettingsClient; + const defaultProps = { storage: {} as IStorageWrapper, - uiSettings: {} as IUiSettingsClient, + uiSettings: uiSettingsMock, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), @@ -84,7 +86,8 @@ describe('filters', () => { layer.columns.col1 as FiltersIndexPatternColumn, 'col1', createMockedIndexPattern(), - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 1cdaff53c5458..0c0aa34bb40b3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -239,7 +239,8 @@ interface FieldlessOperationDefinition<C extends BaseIndexPatternColumn> { column: C, columnId: string, indexPattern: IndexPattern, - layer: IndexPatternLayer + layer: IndexPatternLayer, + uiSettings: IUiSettingsClient ) => ExpressionAstFunction; } @@ -283,7 +284,8 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> { column: C, columnId: string, indexPattern: IndexPattern, - layer: IndexPatternLayer + layer: IndexPatternLayer, + uiSettings: IUiSettingsClient ) => ExpressionAstFunction; /** * Optional function to return the suffix used for ES bucket paths and esaggs column id. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index 96b12a714e613..8d5ab50770111 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -15,9 +15,11 @@ import { LastValueIndexPatternColumn } from './last_value'; import { lastValueOperation } from './index'; import type { IndexPattern, IndexPatternLayer } from '../../types'; +const uiSettingsMock = {} as IUiSettingsClient; + const defaultProps = { storage: {} as IStorageWrapper, - uiSettings: {} as IUiSettingsClient, + uiSettings: uiSettingsMock, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), @@ -70,7 +72,8 @@ describe('last_value', () => { { ...lastValueColumn, params: { ...lastValueColumn.params } }, 'col1', {} as IndexPattern, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index c22eec62ea1ab..a340e17121e90 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -17,9 +17,11 @@ import { EuiFieldNumber } from '@elastic/eui'; import { act } from 'react-dom/test-utils'; import { EuiFormRow } from '@elastic/eui'; +const uiSettingsMock = {} as IUiSettingsClient; + const defaultProps = { storage: {} as IStorageWrapper, - uiSettings: {} as IUiSettingsClient, + uiSettings: uiSettingsMock, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), @@ -72,7 +74,8 @@ describe('percentile', () => { percentileColumn, 'col1', {} as IndexPattern, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx index ad5c146ff6624..d9698252177b0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -190,15 +190,6 @@ export const RangeEditor = ({ }) => { const [isAdvancedEditor, toggleAdvancedEditor] = useState(params.type === MODES.Range); - // if the maxBars in the params is set to auto refresh it with the default value only on bootstrap - useEffect(() => { - if (!isAdvancedEditor) { - if (params.maxBars !== maxBars) { - setParam('maxBars', maxBars); - } - } - }, [maxBars, params.maxBars, setParam, isAdvancedEditor]); - if (isAdvancedEditor) { return ( <AdvancedRangeEditor diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index 987c8971aa310..c55dd90c6be7b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -51,13 +51,15 @@ dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ p type ReactMouseEvent = React.MouseEvent<HTMLAnchorElement, MouseEvent> & React.MouseEvent<HTMLButtonElement, MouseEvent>; +// need this for MAX_HISTOGRAM value +const uiSettingsMock = ({ + get: jest.fn().mockReturnValue(100), +} as unknown) as IUiSettingsClient; + const sourceField = 'MyField'; const defaultOptions = { storage: {} as IStorageWrapper, - // need this for MAX_HISTOGRAM value - uiSettings: ({ - get: () => 100, - } as unknown) as IUiSettingsClient, + uiSettings: uiSettingsMock, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1y', @@ -143,7 +145,8 @@ describe('ranges', () => { layer.columns.col1 as RangeIndexPatternColumn, 'col1', {} as IndexPattern, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toMatchInlineSnapshot(` Object { @@ -166,6 +169,9 @@ describe('ranges', () => { "interval": Array [ "auto", ], + "maxBars": Array [ + 49.5, + ], "min_doc_count": Array [ false, ], @@ -186,7 +192,8 @@ describe('ranges', () => { layer.columns.col1 as RangeIndexPatternColumn, 'col1', {} as IndexPattern, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( @@ -206,7 +213,8 @@ describe('ranges', () => { layer.columns.col1 as RangeIndexPatternColumn, 'col1', {} as IndexPattern, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( @@ -226,7 +234,8 @@ describe('ranges', () => { layer.columns.col1 as RangeIndexPatternColumn, 'col1', {} as IndexPattern, - layer + layer, + uiSettingsMock ); expect((esAggsFn as { arguments: unknown }).arguments).toEqual( @@ -275,7 +284,7 @@ describe('ranges', () => { it('should start update the state with the default maxBars value', () => { const updateLayerSpy = jest.fn(); - mount( + const instance = mount( <InlineOptions {...defaultOptions} layer={layer} @@ -285,19 +294,7 @@ describe('ranges', () => { /> ); - expect(updateLayerSpy).toHaveBeenCalledWith({ - ...layer, - columns: { - ...layer.columns, - col1: { - ...layer.columns.col1, - params: { - ...layer.columns.col1.params, - maxBars: GRANULARITY_DEFAULT_VALUE, - }, - }, - }, - }); + expect(instance.find(EuiRange).prop('value')).toEqual(String(GRANULARITY_DEFAULT_VALUE)); }); it('should update state when changing Max bars number', () => { @@ -313,8 +310,6 @@ describe('ranges', () => { /> ); - // There's a useEffect in the component that updates the value on bootstrap - // because there's a debouncer, wait a bit before calling onChange act(() => { jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); @@ -358,8 +353,6 @@ describe('ranges', () => { /> ); - // There's a useEffect in the component that updates the value on bootstrap - // because there's a debouncer, wait a bit before calling onChange act(() => { jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); // minus button @@ -368,6 +361,7 @@ describe('ranges', () => { .find('button') .prop('onClick')!({} as ReactMouseEvent); jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + instance.update(); }); expect(updateLayerSpy).toHaveBeenCalledWith({ @@ -391,6 +385,7 @@ describe('ranges', () => { .find('button') .prop('onClick')!({} as ReactMouseEvent); jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + instance.update(); }); expect(updateLayerSpy).toHaveBeenCalledWith({ @@ -788,7 +783,7 @@ describe('ranges', () => { instance.find(EuiLink).first().prop('onClick')!({} as ReactMouseEvent); }); - expect(updateLayerSpy.mock.calls[1][0].columns.col1.params.format).toEqual({ + expect(updateLayerSpy.mock.calls[0][0].columns.col1.params.format).toEqual({ id: 'custom', params: { decimals: 3 }, }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index aa5cc8255a584..d8622a5aedf7d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -132,7 +132,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field sourceField: field.name, }; }, - toEsAggsFn: (column, columnId) => { + toEsAggsFn: (column, columnId, indexPattern, layer, uiSettings) => { const { sourceField, params } = column; if (params.type === MODES.Range) { return buildExpressionFunction<AggFunctionsMapping['aggRange']>('aggRange', { @@ -158,13 +158,15 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field ), }).toAst(); } + const maxBarsDefaultValue = + (uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS) - MIN_HISTOGRAM_BARS) / 2; + return buildExpressionFunction<AggFunctionsMapping['aggHistogram']>('aggHistogram', { id: columnId, enabled: true, schema: 'segment', field: sourceField, - // fallback to 0 in case of empty string - maxBars: params.maxBars === AUTO_BARS ? undefined : params.maxBars, + maxBars: params.maxBars === AUTO_BARS ? maxBarsDefaultValue : params.maxBars, interval: 'auto', has_extended_bounds: false, min_doc_count: false, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index d60992bda2e2a..3e25e127b37f4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -17,9 +17,11 @@ import type { TermsIndexPatternColumn } from '.'; import { termsOperation } from '../index'; import { IndexPattern, IndexPatternLayer } from '../../../types'; +const uiSettingsMock = {} as IUiSettingsClient; + const defaultProps = { storage: {} as IStorageWrapper, - uiSettings: {} as IUiSettingsClient, + uiSettings: uiSettingsMock, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), @@ -66,7 +68,8 @@ describe('terms', () => { { ...termsColumn, params: { ...termsColumn.params, otherBucket: true } }, 'col1', {} as IndexPattern, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -89,7 +92,8 @@ describe('terms', () => { }, 'col1', {} as IndexPattern, - layer + layer, + uiSettingsMock ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -129,7 +133,8 @@ describe('terms', () => { }, }, }, - } + }, + uiSettingsMock ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 38f51f24aae7d..c9ee77a9f5e15 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import type { IUiSettingsClient } from 'kibana/public'; import { EsaggsExpressionFunctionDefinition, IndexPatternLoadExpressionFunctionDefinition, @@ -24,7 +25,8 @@ import { getEsAggsSuffix } from './operations/definitions/helpers'; function getExpressionForLayer( layer: IndexPatternLayer, - indexPattern: IndexPattern + indexPattern: IndexPattern, + uiSettings: IUiSettingsClient ): ExpressionAstExpression | null { const { columns, columnOrder } = layer; if (columnOrder.length === 0) { @@ -44,7 +46,7 @@ function getExpressionForLayer( aggs.push( buildExpression({ type: 'expression', - chain: [def.toEsAggsFn(col, colId, indexPattern, layer)], + chain: [def.toEsAggsFn(col, colId, indexPattern, layer, uiSettings)], }) ); } @@ -184,11 +186,16 @@ function getExpressionForLayer( return null; } -export function toExpression(state: IndexPatternPrivateState, layerId: string) { +export function toExpression( + state: IndexPatternPrivateState, + layerId: string, + uiSettings: IUiSettingsClient +) { if (state.layers[layerId]) { return getExpressionForLayer( state.layers[layerId], - state.indexPatterns[state.layers[layerId].indexPatternId] + state.indexPatterns[state.layers[layerId].indexPatternId], + uiSettings ); } From 03636a07fe23ef80b46d3f0a6958f7164abc4138 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering <skaapgif@gmail.com> Date: Mon, 1 Feb 2021 15:46:16 +0100 Subject: [PATCH 160/163] Migrations v2: don't auto-create indices + FTR/esArchiver support (#85778) * Migrations V2 on by default * esArchiver delete migrations v2 indices * Fix saved_objects_management api_integration tests * Try to fix v2 migrations for pre-release builds * esArchiver delete auto-created v2 migration indices like .kibana_8.0.0 * Try to fix v2 migrations for pre-release builds * Use require_alias to prevent auto-created saved objects index * Wrap SO routes until core logs all internal errors * Fix api_integration tests requiring an empty kibana index * Delete corrupt saved object from lens archives * Update docs * Fix ui_settings tests * Fix core jest tests * Fix type errors * Fix accessibility tests * Fix plugin functional tests * Fix api_integration tests after merging in master * Fix plugin functional tests #2 * EsArchiver: Don't reset ui settings after the .kibana index was deleted * Fix functional management/visualize tests * Fix oss security functional tests * EsArchiver clean task manager indices to fix alerting api integration tests * migrationsv2 correctly handle unknown saved object type mappings * Revert "Try to fix v2 migrations for pre-release builds" This reverts commit a1a1567501d528a087c4d8de2a10f90a9878f845. * Revert "Try to fix v2 migrations for pre-release builds" This reverts commit a9a935558c4e5a08f5e9c3d40c1acad3cb54eda7. * Re-enable v2 migrations in tests after merging in master * Try to fix async dashboard functional test * Restore UiSettings defaults after emptyKibanaIndex() * Review feedback: rename test to match behaviour --- ...orhelpers.createindexaliasnotfounderror.md | 22 ++ ...helpers.decorateindexaliasnotfounderror.md | 23 ++ ...savedobjectserrorhelpers.isgeneralerror.md | 22 ++ ...in-core-server.savedobjectserrorhelpers.md | 3 + .../src/actions/empty_kibana_index.ts | 3 +- packages/kbn-es-archiver/src/es_archiver.ts | 2 +- .../src/lib/indices/kibana_index.ts | 8 +- .../saved_objects/migrationsv2/model.test.ts | 202 +++++++++++++++--- .../saved_objects/migrationsv2/model.ts | 20 +- .../saved_objects/routes/bulk_create.ts | 3 +- .../server/saved_objects/routes/bulk_get.ts | 3 +- .../saved_objects/routes/bulk_update.ts | 3 +- .../server/saved_objects/routes/create.ts | 3 +- .../server/saved_objects/routes/delete.ts | 3 +- .../server/saved_objects/routes/export.ts | 4 +- src/core/server/saved_objects/routes/find.ts | 3 +- src/core/server/saved_objects/routes/get.ts | 3 +- .../server/saved_objects/routes/import.ts | 4 +- .../server/saved_objects/routes/migrate.ts | 3 +- .../routes/resolve_import_errors.ts | 5 +- .../server/saved_objects/routes/update.ts | 3 +- .../server/saved_objects/routes/utils.test.ts | 75 +++++++ src/core/server/saved_objects/routes/utils.ts | 34 ++- .../service/lib/decorate_es_error.test.ts | 21 ++ .../service/lib/decorate_es_error.ts | 6 + .../saved_objects/service/lib/errors.ts | 17 ++ .../service/lib/repository.test.js | 9 +- .../saved_objects/service/lib/repository.ts | 33 ++- src/core/server/server.api.md | 6 + .../integration_tests/doc_exists.ts | 6 +- .../integration_tests/doc_missing.ts | 6 +- .../doc_missing_and_index_read_only.ts | 12 +- .../integration_tests/index.test.ts | 13 +- .../integration_tests/lib/servers.ts | 3 - src/core/test_helpers/kbn_server.ts | 2 +- test/accessibility/apps/kibana_overview.ts | 3 +- test/api_integration/apis/home/sample_data.ts | 4 + .../apis/saved_objects/bulk_create.ts | 44 ++-- .../apis/saved_objects/bulk_get.ts | 2 +- .../apis/saved_objects/bulk_update.ts | 16 +- .../apis/saved_objects/create.ts | 48 +---- .../apis/saved_objects/delete.ts | 2 +- .../apis/saved_objects/export.ts | 2 +- .../apis/saved_objects/find.ts | 14 +- .../api_integration/apis/saved_objects/get.ts | 2 +- .../saved_objects/resolve_import_errors.ts | 54 ++++- .../apis/saved_objects/update.ts | 13 +- .../apis/saved_objects_management/find.ts | 4 +- .../apis/saved_objects_management/get.ts | 2 +- test/api_integration/apis/search/search.ts | 1 + test/api_integration/apis/telemetry/opt_in.ts | 3 + .../apis/telemetry/telemetry_local.ts | 1 + .../apis/ui_counters/ui_counters.ts | 5 + .../apis/ui_metric/ui_metric.ts | 5 + test/common/config.js | 2 - .../kibana_server/extend_es_archiver.js | 4 +- .../apps/management/_import_objects.ts | 5 +- .../apps/management/_index_pattern_filter.js | 3 +- .../apps/management/_index_patterns_empty.ts | 3 +- .../management/_mgmt_import_saved_objects.js | 3 +- .../apps/management/_test_huge_fields.js | 1 + test/functional/apps/management/index.ts | 2 - .../input_control_vis/input_control_range.ts | 2 - .../test_suites/core_plugins/applications.ts | 2 + .../test_suites/data_plugin/index_patterns.ts | 4 + .../import_warnings.ts | 7 +- .../insecure_cluster_warning.ts | 1 + .../tests/alerting/index.ts | 4 - .../apps/dashboard/_async_dashboard.ts | 2 + .../es_archives/visualize/default/data.json | 24 +-- .../reporting_without_security.config.ts | 1 - 71 files changed, 645 insertions(+), 238 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md new file mode 100644 index 0000000000000..2b897db7bba4c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md @@ -0,0 +1,22 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) + +## SavedObjectsErrorHelpers.createIndexAliasNotFoundError() method + +<b>Signature:</b> + +```typescript +static createIndexAliasNotFoundError(alias: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| alias | <code>string</code> | | + +<b>Returns:</b> + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md new file mode 100644 index 0000000000000..c7e10fc42ead1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md @@ -0,0 +1,23 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [decorateIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) + +## SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError() method + +<b>Signature:</b> + +```typescript +static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | <code>Error</code> | | +| alias | <code>string</code> | | + +<b>Returns:</b> + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md new file mode 100644 index 0000000000000..4b4ede2f77a7e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md @@ -0,0 +1,22 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isGeneralError](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) + +## SavedObjectsErrorHelpers.isGeneralError() method + +<b>Signature:</b> + +```typescript +static isGeneralError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | <code>Error | DecoratedError</code> | | + +<b>Returns:</b> + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 9b69012ed5f12..2dc78f2df3a83 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -18,6 +18,7 @@ export declare class SavedObjectsErrorHelpers | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | <code>static</code> | | | [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | <code>static</code> | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | <code>static</code> | | +| [createIndexAliasNotFoundError(alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) | <code>static</code> | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | <code>static</code> | | | [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | <code>static</code> | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | <code>static</code> | | @@ -27,6 +28,7 @@ export declare class SavedObjectsErrorHelpers | [decorateEsUnavailableError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | <code>static</code> | | | [decorateForbiddenError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | <code>static</code> | | | [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | <code>static</code> | | +| [decorateIndexAliasNotFoundError(error, alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) | <code>static</code> | | | [decorateNotAuthorizedError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md) | <code>static</code> | | | [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | <code>static</code> | | | [decorateTooManyRequestsError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md) | <code>static</code> | | @@ -35,6 +37,7 @@ export declare class SavedObjectsErrorHelpers | [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | <code>static</code> | | | [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | <code>static</code> | | | [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | <code>static</code> | | +| [isGeneralError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) | <code>static</code> | | | [isInvalidVersionError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md) | <code>static</code> | | | [isNotAuthorizedError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md) | <code>static</code> | | | [isNotFoundError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md) | <code>static</code> | | diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index 56c75c5aca419..6272d6ba00ee8 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -25,5 +25,6 @@ export async function emptyKibanaIndexAction({ await cleanKibanaIndices({ client, stats, log, kibanaPluginIds }); await migrateKibanaIndex({ client, kbnClient }); - return stats; + stats.createdIndex('.kibana'); + return stats.toJSON(); } diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index f101c5d6867f1..8601dedad0e27 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -155,7 +155,7 @@ export class EsArchiver { * @return Promise */ async emptyKibanaIndex() { - await emptyKibanaIndexAction({ + return await emptyKibanaIndexAction({ client: this.client, log: this.log, kbnClient: this.kbnClient, diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index 0459a4301cf6b..91c0bd8343a36 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -76,7 +76,9 @@ export async function migrateKibanaIndex({ */ async function fetchKibanaIndices(client: Client) { const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + const isKibanaIndex = (index: string) => + /^\.kibana(:?_\d*)?$/.test(index) || + /^\.kibana(_task_manager)?_(pre)?\d+\.\d+\.\d+/.test(index); return kibanaIndices.map((x: { index: string }) => x.index).filter(isKibanaIndex); } @@ -103,7 +105,7 @@ export async function cleanKibanaIndices({ while (true) { const resp = await client.deleteByQuery({ - index: `.kibana`, + index: `.kibana,.kibana_task_manager`, body: { query: { bool: { @@ -115,7 +117,7 @@ export async function cleanKibanaIndices({ }, }, }, - ignore: [409], + ignore: [404, 409], }); if (resp.total !== resp.deleted) { diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index d5ab85c54a728..a9aa69960b1c2 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -182,6 +182,21 @@ describe('migrations v2 model', () => { versionAlias: '.kibana_7.11.0', versionIndex: '.kibana_7.11.0_001', }; + const mappingsWithUnknownType = { + properties: { + disabled_saved_object_type: { + properties: { + value: { type: 'keyword' }, + }, + }, + }, + _meta: { + migrationMappingPropertyHashes: { + disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0', + }, + }, + }; + test('INIT -> OUTDATED_DOCUMENTS_SEARCH if .kibana is already pointing to the target index', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { @@ -189,38 +204,27 @@ describe('migrations v2 model', () => { '.kibana': {}, '.kibana_7.11.0': {}, }, - mappings: { - properties: { - disabled_saved_object_type: { - properties: { - value: { type: 'keyword' }, - }, - }, - }, - _meta: { - migrationMappingPropertyHashes: { - disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0', - }, - }, - }, + mappings: mappingsWithUnknownType, settings: {}, }, }); const newState = model(initState, res); expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH'); + // This snapshot asserts that we merge the + // migrationMappingPropertyHashes of the existing index, but we leave + // the mappings for the disabled_saved_object_type untouched. There + // might be another Kibana instance that knows about this type and + // needs these mappings in place. expect(newState.targetIndexMappings).toMatchInlineSnapshot(` Object { "_meta": Object { "migrationMappingPropertyHashes": Object { + "disabled_saved_object_type": "7997cf5a56cc02bdc9c93361bde732b0", "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", }, }, "properties": Object { - "disabled_saved_object_type": Object { - "dynamic": false, - "properties": Object {}, - }, "new_saved_object_type": Object { "properties": Object { "value": Object { @@ -271,7 +275,7 @@ describe('migrations v2 model', () => { '.kibana': {}, '.kibana_7.12.0': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, '.kibana_7.11.0_001': { @@ -288,12 +292,37 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_7.invalid.0_001'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); }); test('INIT -> SET_SOURCE_WRITE_BLOCK when migrating from a v2 migrations index (>= 7.11.0)', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { aliases: { '.kibana': {}, '.kibana_7.11.0': {} }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, '.kibana_3': { @@ -319,6 +348,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_7.11.0_001'), targetIndex: '.kibana_7.12.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -328,7 +382,7 @@ describe('migrations v2 model', () => { aliases: { '.kibana': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -339,6 +393,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_3'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -346,7 +425,7 @@ describe('migrations v2 model', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana': { aliases: {}, - mappings: { properties: {}, _meta: {} }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -357,6 +436,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_pre6.5.0_001'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -366,7 +470,7 @@ describe('migrations v2 model', () => { aliases: { 'my-saved-objects': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -386,6 +490,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('my-saved-objects_3'), targetIndex: 'my-saved-objects_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -395,7 +524,7 @@ describe('migrations v2 model', () => { aliases: { 'my-saved-objects': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -416,6 +545,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('my-saved-objects_7.11.0'), targetIndex: 'my-saved-objects_7.12.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 1119edde8e268..3556bb611bb67 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -60,13 +60,13 @@ function throwBadResponse(state: State, res: any): never { * Merge the _meta.migrationMappingPropertyHashes mappings of an index with * the given target mappings. * - * @remarks Mapping updates are commutative (deeply merged) by Elasticsearch, - * except for the _meta key. The source index we're migrating from might - * contain documents created by a plugin that is disabled in the Kibana - * instance performing this migration. We merge the - * _meta.migrationMappingPropertyHashes mappings from the source index into - * the targetMappings to ensure that any `migrationPropertyHashes` for - * disabled plugins aren't lost. + * @remarks When another instance already completed a migration, the existing + * target index might contain documents and mappings created by a plugin that + * is disabled in the current Kibana instance performing this migration. + * Mapping updates are commutative (deeply merged) by Elasticsearch, except + * for the `_meta` key. By merging the `_meta.migrationMappingPropertyHashes` + * mappings from the existing target index index into the targetMappings we + * ensure that any `migrationPropertyHashes` for disabled plugins aren't lost. * * Right now we don't use these `migrationPropertyHashes` but it could be used * in the future to detect if mappings were changed. If mappings weren't @@ -209,7 +209,7 @@ export const model = (currentState: State, resW: ResponseType<AllActionStates>): // index sourceIndex: Option.none, targetIndex: `${stateP.indexPrefix}_${stateP.kibanaVersion}_001`, - targetIndexMappings: disableUnknownTypeMappingFields( + targetIndexMappings: mergeMigrationMappingPropertyHashes( stateP.targetIndexMappings, indices[aliases[stateP.currentAlias]].mappings ), @@ -242,7 +242,7 @@ export const model = (currentState: State, resW: ResponseType<AllActionStates>): controlState: 'SET_SOURCE_WRITE_BLOCK', sourceIndex: Option.some(source) as Option.Some<string>, targetIndex: target, - targetIndexMappings: mergeMigrationMappingPropertyHashes( + targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, indices[source].mappings ), @@ -279,7 +279,7 @@ export const model = (currentState: State, resW: ResponseType<AllActionStates>): controlState: 'LEGACY_SET_WRITE_BLOCK', sourceIndex: Option.some(legacyReindexTarget) as Option.Some<string>, targetIndex: target, - targetIndexMappings: mergeMigrationMappingPropertyHashes( + targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, indices[stateP.legacyIndex].mappings ), diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 6d57eaa3777e6..b85747985e523 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -44,7 +45,7 @@ export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: Rout ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/bulk_get.ts b/src/core/server/saved_objects/routes/bulk_get.ts index a260301633668..580bf26a4e529 100644 --- a/src/core/server/saved_objects/routes/bulk_get.ts +++ b/src/core/server/saved_objects/routes/bulk_get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -28,7 +29,7 @@ export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDe ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {}); diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index f9b8d4a2f567f..e592adc72a244 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -39,7 +40,7 @@ export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: Rout ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {}); diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index fd256abac3526..f6043ca96398d 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -43,7 +44,7 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { overwrite } = req.query; const { diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts index a7846c3dc845b..b127f64b74a0c 100644 --- a/src/core/server/saved_objects/routes/delete.ts +++ b/src/core/server/saved_objects/routes/delete.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -28,7 +29,7 @@ export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { force } = req.query; diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 9b40855afec2e..f064cf1ca0ec1 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -18,7 +18,7 @@ import { SavedObjectsExportByObjectOptions, SavedObjectsExportError, } from '../export'; -import { validateTypes, validateObjects } from './utils'; +import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { config: SavedObjectConfig; @@ -163,7 +163,7 @@ export const registerExportRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const cleaned = cleanOptions(req.body); const supportedTypes = context.core.savedObjects.typeRegistry .getImportableAndExportableTypes() diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index 747070e54e5ad..c814fd310dc52 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -49,7 +50,7 @@ export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDepen }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const query = req.query; const namespaces = diff --git a/src/core/server/saved_objects/routes/get.ts b/src/core/server/saved_objects/routes/get.ts index c66a11dcf0cdd..2dd812f35cefd 100644 --- a/src/core/server/saved_objects/routes/get.ts +++ b/src/core/server/saved_objects/routes/get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -25,7 +26,7 @@ export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDepend }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 6c4c759460ce3..5fd132acafbed 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -13,7 +13,7 @@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; -import { createSavedObjectsStreamFromNdJson } from './utils'; +import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; @@ -61,7 +61,7 @@ export const registerImportRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite, createNewCopies } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/migrate.ts b/src/core/server/saved_objects/routes/migrate.ts index 8b347d4725b08..7c2f4bfb06710 100644 --- a/src/core/server/saved_objects/routes/migrate.ts +++ b/src/core/server/saved_objects/routes/migrate.ts @@ -8,6 +8,7 @@ import { IRouter } from '../../http'; import { IKibanaMigrator } from '../migrations'; +import { catchAndReturnBoomErrors } from './utils'; export const registerMigrateRoute = ( router: IRouter, @@ -21,7 +22,7 @@ export const registerMigrateRoute = ( tags: ['access:migrateSavedObjects'], }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const migrator = await migratorPromise; await migrator.runMigrations({ rerun: true }); return res.ok({ diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 0cf976c30b311..6f0a3d028baf9 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -13,8 +13,7 @@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; -import { createSavedObjectsStreamFromNdJson } from './utils'; - +import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; coreUsageData: CoreUsageDataSetup; @@ -69,7 +68,7 @@ export const registerResolveImportErrorsRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { createNewCopies } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts index 17cfd438d47bf..dbc69f743df76 100644 --- a/src/core/server/saved_objects/routes/update.ts +++ b/src/core/server/saved_objects/routes/update.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -38,7 +39,7 @@ export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { attributes, version, references } = req.body; const options = { version, references }; diff --git a/src/core/server/saved_objects/routes/utils.test.ts b/src/core/server/saved_objects/routes/utils.test.ts index ade7b03f6a8c2..1d7e86e288b18 100644 --- a/src/core/server/saved_objects/routes/utils.test.ts +++ b/src/core/server/saved_objects/routes/utils.test.ts @@ -9,6 +9,15 @@ import { createSavedObjectsStreamFromNdJson, validateTypes, validateObjects } from './utils'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; +import { catchAndReturnBoomErrors } from './utils'; +import Boom from '@hapi/boom'; +import { + KibanaRequest, + RequestHandler, + RequestHandlerContext, + KibanaResponseFactory, + kibanaResponseFactory, +} from '../../'; async function readStreamToCompletion(stream: Readable) { return createPromiseFromStreams([stream, createConcatStream([])]); @@ -143,3 +152,69 @@ describe('validateObjects', () => { ).toBeUndefined(); }); }); + +describe('catchAndReturnBoomErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest<any, any, any>; + let response: KibanaResponseFactory; + + const createHandler = (handler: () => any): RequestHandler<any, any, any> => () => { + return handler(); + }; + + beforeEach(() => { + context = {} as any; + request = {} as any; + response = kibanaResponseFactory; + }); + + it('should pass-though call parameters to the handler', async () => { + const handler = jest.fn(); + const wrapped = catchAndReturnBoomErrors(handler); + await wrapped(context, request, response); + expect(handler).toHaveBeenCalledWith(context, request, response); + }); + + it('should pass-though result from the handler', async () => { + const handler = createHandler(() => { + return 'handler-response'; + }); + const wrapped = catchAndReturnBoomErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBe('handler-response'); + }); + + it('should intercept and convert thrown Boom errors', async () => { + const handler = createHandler(() => { + throw Boom.notFound('not there'); + }); + const wrapped = catchAndReturnBoomErrors(handler); + const result = await wrapped(context, request, response); + expect(result.status).toBe(404); + expect(result.payload).toEqual({ + error: 'Not Found', + message: 'not there', + statusCode: 404, + }); + }); + + it('should re-throw non-Boom errors', async () => { + const handler = createHandler(() => { + throw new Error('something went bad'); + }); + const wrapped = catchAndReturnBoomErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); + + it('should re-throw Boom internal/500 errors', async () => { + const handler = createHandler(() => { + throw Boom.internal(); + }); + const wrapped = catchAndReturnBoomErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: Internal Server Error]` + ); + }); +}); diff --git a/src/core/server/saved_objects/routes/utils.ts b/src/core/server/saved_objects/routes/utils.ts index b9e7df48a4b4c..269f3f0698561 100644 --- a/src/core/server/saved_objects/routes/utils.ts +++ b/src/core/server/saved_objects/routes/utils.ts @@ -7,7 +7,11 @@ */ import { Readable } from 'stream'; -import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server'; +import { + RequestHandlerWrapper, + SavedObject, + SavedObjectsExportResultDetails, +} from 'src/core/server'; import { createSplitStream, createMapStream, @@ -16,6 +20,7 @@ import { createListStream, createConcatStream, } from '@kbn/utils'; +import Boom from '@hapi/boom'; export async function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) { const savedObjects = await createPromiseFromStreams([ @@ -52,3 +57,30 @@ export function validateObjects( .join(', ')}`; } } + +/** + * Catches errors thrown by saved object route handlers and returns an error + * with the payload and statusCode of the boom error. + * + * This is very close to the core `router.handleLegacyErrors` except that it + * throws internal errors (statusCode: 500) so that the internal error's + * message get logged by Core. + * + * TODO: Remove once https://github.com/elastic/kibana/issues/65291 is fixed. + */ +export const catchAndReturnBoomErrors: RequestHandlerWrapper = (handler) => { + return async (context, request, response) => { + try { + return await handler(context, request, response); + } catch (e) { + if (Boom.isBoom(e) && e.output.statusCode !== 500) { + return response.customError({ + body: e.output.payload, + statusCode: e.output.statusCode, + headers: e.output.headers as { [key: string]: string }, + }); + } + throw e; + } + }; +}; diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index cc497ca6348b8..da1ebec2c0f7d 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -109,6 +109,27 @@ describe('savedObjectsClient/decorateEsError', () => { expect(SavedObjectsErrorHelpers.isNotFoundError(genericError)).toBe(true); }); + it('if saved objects index does not exist makes NotFound a SavedObjectsClient/generalError', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { + error: { + reason: + 'no such index [.kibana_8.0.0] and [require_alias] request flag is [true] and [.kibana_8.0.0] is not an alias', + }, + }, + }) + ); + expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(false); + const genericError = decorateEsError(error); + expect(genericError.message).toEqual( + `Saved object index alias [.kibana_8.0.0] not found: Response Error` + ); + expect(genericError.output.statusCode).toBe(500); + expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(true); + }); + it('makes BadRequest a SavedObjectsClient/BadRequest error', () => { const error = new esErrors.ResponseError( elasticsearchClientMock.createApiResponse({ statusCode: 400 }) diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index 40f18c9c94c25..aabca2d602cb3 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -63,6 +63,12 @@ export function decorateEsError(error: EsErrors) { } if (responseErrors.isNotFound(error.statusCode)) { + const match = error?.meta?.body?.error?.reason?.match( + /no such index \[(.+)\] and \[require_alias\] request flag is \[true\] and \[.+\] is not an alias/ + ); + if (match?.length > 0) { + return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(error, match[1]); + } return SavedObjectsErrorHelpers.createGenericNotFoundError(); } diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index f216e72efbcf8..c348196aaba21 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -135,6 +135,19 @@ export class SavedObjectsErrorHelpers { return decorate(Boom.notFound(), CODE_NOT_FOUND, 404); } + public static createIndexAliasNotFoundError(alias: string) { + return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(Boom.internal(), alias); + } + + public static decorateIndexAliasNotFoundError(error: Error, alias: string) { + return decorate( + error, + CODE_GENERAL_ERROR, + 500, + `Saved object index alias [${alias}] not found` + ); + } + public static isNotFoundError(error: Error | DecoratedError) { return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND; } @@ -185,4 +198,8 @@ export class SavedObjectsErrorHelpers { public static decorateGeneralError(error: Error, reason?: string) { return decorate(error, CODE_GENERAL_ERROR, 500, reason); } + + public static isGeneralError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_GENERAL_ERROR; + } } diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 216e1c4bd2d3c..68fdea0f9eb25 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -18,6 +18,7 @@ import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { esKuery } from '../../es_query'; +import { errors as EsErrors } from '@elastic/elasticsearch'; const { nodeTypes } = esKuery; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -4341,8 +4342,14 @@ describe('SavedObjectsRepository', () => { }); it(`throws when ES is unable to find the document during update`, async () => { + const notFoundError = new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { error: { type: 'es_type', reason: 'es_reason' } }, + }) + ); client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createErrorTransportRequestPromise(notFoundError) ); await expectNotFoundError(type, id); expect(client.update).toHaveBeenCalledTimes(1); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 2993d4234bd2e..da80971635a93 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -299,6 +299,7 @@ export class SavedObjectsRepository { refresh, body: raw._source, ...(overwrite && version ? decodeRequestVersion(version) : {}), + require_alias: true, }; const { body } = @@ -469,6 +470,7 @@ export class SavedObjectsRepository { const bulkResponse = bulkCreateParams.length ? await this.client.bulk({ refresh, + require_alias: true, body: bulkCreateParams, }) : undefined; @@ -1117,8 +1119,8 @@ export class SavedObjectsRepository { ...(Array.isArray(references) && { references }), }; - const { body, statusCode } = await this.client.update( - { + const { body } = await this.client + .update({ id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), ...getExpectedVersionProperties(version, preflightResult), @@ -1128,14 +1130,15 @@ export class SavedObjectsRepository { doc, }, _source_includes: ['namespace', 'namespaces', 'originId'], - }, - { ignore: [404] } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } + require_alias: true, + }) + .catch((err) => { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + throw err; + }); const { originId } = body.get._source; let namespaces = []; @@ -1496,6 +1499,7 @@ export class SavedObjectsRepository { refresh, body: bulkUpdateParams, _source_includes: ['originId'], + require_alias: true, }) : undefined; @@ -1712,6 +1716,7 @@ export class SavedObjectsRepository { id: raw._id, index: this.getIndexForType(type), refresh, + require_alias: true, _source: 'true', body: { script: { @@ -1933,12 +1938,18 @@ export class SavedObjectsRepository { } } -function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) { +function getBulkOperationError( + error: { type: string; reason?: string; index?: string }, + type: string, + id: string +) { switch (error.type) { case 'version_conflict_engine_exception': return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)); case 'document_missing_exception': return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + case 'index_not_found_exception': + return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!)); default: return { message: error.reason || JSON.stringify(error), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index aadd16bde0ee6..9d5114e645f6e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2336,6 +2336,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) + static createIndexAliasNotFoundError(alias: string): DecoratedError; + // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; // (undocumented) static createTooManyRequestsError(type: string, id: string): DecoratedError; @@ -2354,6 +2356,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static decorateGeneralError(error: Error, reason?: string): DecoratedError; // (undocumented) + static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError; + // (undocumented) static decorateNotAuthorizedError(error: Error, reason?: string): DecoratedError; // (undocumented) static decorateRequestEntityTooLargeError(error: Error, reason?: string): DecoratedError; @@ -2370,6 +2374,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static isForbiddenError(error: Error | DecoratedError): boolean; // (undocumented) + static isGeneralError(error: Error | DecoratedError): boolean; + // (undocumented) static isInvalidVersionError(error: Error | DecoratedError): boolean; // (undocumented) static isNotAuthorizedError(error: Error | DecoratedError): boolean; diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts index aa6f98ddf2d03..d100b89af9609 100644 --- a/src/core/server/ui_settings/integration_tests/doc_exists.ts +++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docExistsSuite() { +export const docExistsSuite = (savedObjectsIndex: string) => () => { async function setup(options: any = {}) { const { initialSettings } = options; @@ -16,7 +16,7 @@ export function docExistsSuite() { // delete the kibana index to ensure we start fresh await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { conflicts: 'proceed', query: { match_all: {} }, @@ -212,4 +212,4 @@ export function docExistsSuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts index 501976e3823f1..822ffe398b87d 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docMissingSuite() { +export const docMissingSuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { const { kbnServer, callCluster } = getServices(); @@ -22,7 +22,7 @@ export function docMissingSuite() { // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { query: { match_all: {} }, }, @@ -136,4 +136,4 @@ export function docMissingSuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts index b2ff1b2f1d4ab..997d51e36abdc 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docMissingAndIndexReadOnlySuite() { +export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { const { kbnServer, callCluster } = getServices(); @@ -22,7 +22,7 @@ export function docMissingAndIndexReadOnlySuite() { // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { query: { match_all: {} }, }, @@ -30,7 +30,7 @@ export function docMissingAndIndexReadOnlySuite() { // set the index to read only await callCluster('indices.putSettings', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { index: { blocks: { @@ -42,11 +42,11 @@ export function docMissingAndIndexReadOnlySuite() { }); afterEach(async () => { - const { kbnServer, callCluster } = getServices(); + const { callCluster } = getServices(); // disable the read only block await callCluster('indices.putSettings', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { index: { blocks: { @@ -142,4 +142,4 @@ export function docMissingAndIndexReadOnlySuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index f415f1d73de7d..e27e6c4e46874 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -6,20 +6,25 @@ * Public License, v 1. */ +import { Env } from '@kbn/config'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import { getEnvOptions } from '@kbn/config/target/mocks'; import { startServers, stopServers } from './lib'; - import { docExistsSuite } from './doc_exists'; import { docMissingSuite } from './doc_missing'; import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; +const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; +const savedObjectIndex = `.kibana_${kibanaVersion}_001`; + describe('uiSettings/routes', function () { jest.setTimeout(10000); beforeAll(startServers); /* eslint-disable jest/valid-describe */ - describe('doc missing', docMissingSuite); - describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite); - describe('doc exists', docExistsSuite); + describe('doc missing', docMissingSuite(savedObjectIndex)); + describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex)); + describe('doc exists', docExistsSuite(savedObjectIndex)); /* eslint-enable jest/valid-describe */ afterAll(stopServers); }); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index b5198b19007d0..f181272030ae1 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -37,9 +37,6 @@ export async function startServers() { adjustTimeout: (t) => jest.setTimeout(t), settings: { kbn: { - migrations: { - enableV2: false, - }, uiSettings: { overrides: { foo: 'bar', diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 0007e1fcca0a5..6fe6819a0981a 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -40,7 +40,7 @@ const DEFAULTS_SETTINGS = { }, logging: { silent: true }, plugins: {}, - migrations: { skip: true }, + migrations: { skip: false }, }; const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { diff --git a/test/accessibility/apps/kibana_overview.ts b/test/accessibility/apps/kibana_overview.ts index c26a042b10e72..eb0b54ad07aa7 100644 --- a/test/accessibility/apps/kibana_overview.ts +++ b/test/accessibility/apps/kibana_overview.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); before(async () => { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('kibanaOverview'); }); @@ -25,7 +25,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { useActualUrl: true, }); await PageObjects.home.removeSampleDataSet('flights'); - await esArchiver.unload('empty_kibana'); }); it('Getting started view', async () => { diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index 042aff1375267..ebda93b12dc20 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -11,11 +11,15 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const MILLISECOND_IN_WEEK = 1000 * 60 * 60 * 24 * 7; describe('sample data apis', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); describe('list', () => { it('should return list of sample data sets with installed status', async () => { const resp = await supertest.get(`/api/sample_data`).set('kbn-xsrf', 'kibana').expect(200); diff --git a/test/api_integration/apis/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index a548172365b07..d7cdee16214a8 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.ts +++ b/test/api_integration/apis/saved_objects/bulk_create.ts @@ -97,10 +97,11 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); - it('should return 200 with individual responses', async () => + it('should return 200 with errors', async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)); await supertest .post('/api/saved_objects/_bulk_create') .send(BULK_REQUESTS) @@ -109,38 +110,27 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ saved_objects: [ { - type: 'visualization', - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - updated_at: resp.body.saved_objects[0].updated_at, - version: resp.body.saved_objects[0].version, - attributes: { - title: 'An existing visualization', - }, - references: [], - namespaces: ['default'], - migrationVersion: { - visualization: resp.body.saved_objects[0].migrationVersion.visualization, + id: BULK_REQUESTS[0].id, + type: BULK_REQUESTS[0].type, + error: { + error: 'Internal Server Error', + message: 'An internal server error occurred', + statusCode: 500, }, - coreMigrationVersion: KIBANA_VERSION, // updated from 1.2.3 to the latest kibana version }, { - type: 'dashboard', - id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', - updated_at: resp.body.saved_objects[1].updated_at, - version: resp.body.saved_objects[1].version, - attributes: { - title: 'A great new dashboard', - }, - references: [], - namespaces: ['default'], - migrationVersion: { - dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, + id: BULK_REQUESTS[1].id, + type: BULK_REQUESTS[1].type, + error: { + error: 'Internal Server Error', + message: 'An internal server error occurred', + statusCode: 500, }, - coreMigrationVersion: KIBANA_VERSION, }, ], }); - })); + }); + }); }); }); } diff --git a/test/api_integration/apis/saved_objects/bulk_get.ts b/test/api_integration/apis/saved_objects/bulk_get.ts index 46631225f8e8a..b9536843d30c9 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.ts +++ b/test/api_integration/apis/saved_objects/bulk_get.ts @@ -108,7 +108,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); it('should return 200 with individual responses', async () => diff --git a/test/api_integration/apis/saved_objects/bulk_update.ts b/test/api_integration/apis/saved_objects/bulk_update.ts index 5a2496b6dde81..2cf3ade406a93 100644 --- a/test/api_integration/apis/saved_objects/bulk_update.ts +++ b/test/api_integration/apis/saved_objects/bulk_update.ts @@ -235,10 +235,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); - it('should return generic 404', async () => { + it('should return 200 with errors', async () => { const response = await supertest .put(`/api/saved_objects/_bulk_update`) .send([ @@ -267,9 +267,9 @@ export default function ({ getService }: FtrProviderContext) { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', type: 'visualization', error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', }, }); @@ -277,9 +277,9 @@ export default function ({ getService }: FtrProviderContext) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', type: 'dashboard', error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', }, }); }); diff --git a/test/api_integration/apis/saved_objects/create.ts b/test/api_integration/apis/saved_objects/create.ts index 551e082630e8f..833cb127d0023 100644 --- a/test/api_integration/apis/saved_objects/create.ts +++ b/test/api_integration/apis/saved_objects/create.ts @@ -82,10 +82,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); - it('should return 200 and create kibana index', async () => { + it('should return 500 and not auto-create saved objects index', async () => { await supertest .post(`/api/saved_objects/visualization`) .send({ @@ -93,50 +93,16 @@ export default function ({ getService }: FtrProviderContext) { title: 'My favorite vis', }, }) - .expect(200) + .expect(500) .then((resp) => { - // loose uuid validation - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - migrationVersion: resp.body.migrationVersion, - coreMigrationVersion: KIBANA_VERSION, - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - title: 'My favorite vis', - }, - references: [], - namespaces: ['default'], + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, }); - expect(resp.body.migrationVersion).to.be.ok(); }); - expect((await es.indices.exists({ index: '.kibana' })).body).to.be(true); - }); - - it('result should have the latest coreMigrationVersion', async () => { - await supertest - .post(`/api/saved_objects/visualization`) - .send({ - attributes: { - title: 'My favorite vis', - }, - coreMigrationVersion: '1.2.3', - }) - .expect(200) - .then((resp) => { - expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION); - }); + expect((await es.indices.exists({ index: '.kibana' })).body).to.be(false); }); }); }); diff --git a/test/api_integration/apis/saved_objects/delete.ts b/test/api_integration/apis/saved_objects/delete.ts index 9ba51b4b91468..d2dd4454bdf1e 100644 --- a/test/api_integration/apis/saved_objects/delete.ts +++ b/test/api_integration/apis/saved_objects/delete.ts @@ -44,7 +44,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); it('returns generic 404 when kibana index is missing', async () => diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index a45191f24d872..c0d5430070951 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -534,7 +534,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); it('should return empty response', async () => { diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts index 7aa4de86baa69..5f549dc6c5780 100644 --- a/test/api_integration/apis/saved_objects/find.ts +++ b/test/api_integration/apis/saved_objects/find.ts @@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -174,7 +174,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -209,7 +209,7 @@ export default function ({ getService }: FtrProviderContext) { score: 0, type: 'visualization', updated_at: '2017-09-21T18:51:23.794Z', - version: 'WzYsMV0=', + version: 'WzIyLDJd', }, ], }); @@ -256,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.saved_objects[0].migrationVersion, coreMigrationVersion: KIBANA_VERSION, updated_at: '2017-09-21T18:51:23.794Z', - version: 'WzIsMV0=', + version: 'WzE4LDJd', }, ], }); @@ -426,11 +426,11 @@ export default function ({ getService }: FtrProviderContext) { })); }); - describe.skip('without kibana index', () => { + describe('without kibana index', () => { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); it('should return 200 with empty response', async () => diff --git a/test/api_integration/apis/saved_objects/get.ts b/test/api_integration/apis/saved_objects/get.ts index ff47b9df218dc..9bb6e32004c81 100644 --- a/test/api_integration/apis/saved_objects/get.ts +++ b/test/api_integration/apis/saved_objects/get.ts @@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); it('should return basic 404 without mentioning index', async () => diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.ts b/test/api_integration/apis/saved_objects/resolve_import_errors.ts index 3686c46b229b1..042741476bb8e 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.ts +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const es = getService('legacyEs'); describe('resolve_import_errors', () => { // mock success results including metadata @@ -34,7 +35,14 @@ export default function ({ getService }: FtrProviderContext) { describe('without kibana index', () => { // Cleanup data that got created in import - after(() => esArchiver.unload('saved_objects/basic')); + before( + async () => + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana*', + ignore: [404], + }) + ); it('should return 200 and import nothing when empty parameters are passed in', async () => { await supertest @@ -51,7 +59,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should return 200 and import everything when overwrite parameters contains all objects', async () => { + it('should return 200 with internal server errors', async () => { await supertest .post('/api/saved_objects/_resolve_import_errors') .field( @@ -78,12 +86,42 @@ export default function ({ getService }: FtrProviderContext) { .expect(200) .then((resp) => { expect(resp.body).to.eql({ - success: true, - successCount: 3, - successResults: [ - { ...indexPattern, overwrite: true }, - { ...visualization, overwrite: true }, - { ...dashboard, overwrite: true }, + successCount: 0, + success: false, + errors: [ + { + ...indexPattern, + ...{ title: indexPattern.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, + { + ...visualization, + ...{ title: visualization.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, + { + ...dashboard, + ...{ title: dashboard.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, ], warnings: [], }); diff --git a/test/api_integration/apis/saved_objects/update.ts b/test/api_integration/apis/saved_objects/update.ts index d5346e82ce98c..da7285a430fdd 100644 --- a/test/api_integration/apis/saved_objects/update.ts +++ b/test/api_integration/apis/saved_objects/update.ts @@ -121,10 +121,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); - it('should return generic 404', async () => + it('should return 500', async () => await supertest .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) .send({ @@ -132,13 +132,12 @@ export default function ({ getService }: FtrProviderContext) { title: 'My second favorite vis', }, }) - .expect(404) + .expect(500) .then((resp) => { expect(resp.body).eql({ - statusCode: 404, - error: 'Not Found', - message: - 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred.', }); })); }); diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index acc01c73de674..8453b542903a4 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -42,7 +42,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); it('should return 200 with empty response', async () => diff --git a/test/api_integration/apis/saved_objects_management/get.ts b/test/api_integration/apis/saved_objects_management/get.ts index bc05d7e392bb9..70e1faa9fd22b 100644 --- a/test/api_integration/apis/saved_objects_management/get.ts +++ b/test/api_integration/apis/saved_objects_management/get.ts @@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await es.indices.delete({ index: '.kibana' }, { ignore: [404] }) + await es.indices.delete({ index: '.kibana*' }, { ignore: [404] }) ); it('should return 404 for object that no longer exists', async () => diff --git a/test/api_integration/apis/search/search.ts b/test/api_integration/apis/search/search.ts index 155705f81fa8a..e43c449309306 100644 --- a/test/api_integration/apis/search/search.ts +++ b/test/api_integration/apis/search/search.ts @@ -17,6 +17,7 @@ export default function ({ getService }: FtrProviderContext) { describe('search', () => { before(async () => { + await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('../../../functional/fixtures/es_archiver/logstash_functional'); }); diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts index f03b33e61965e..ba5f46c38211f 100644 --- a/test/api_integration/apis/telemetry/opt_in.ts +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -14,10 +14,13 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function optInTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + describe('/api/telemetry/v2/optIn API', () => { let defaultAttributes: TelemetrySavedObjectAttributes; let kibanaVersion: any; before(async () => { + await esArchiver.emptyKibanaIndex(); const kibanaVersionAccessor = kibanaServer.version; kibanaVersion = await kibanaVersionAccessor.get(); defaultAttributes = diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts index 25d29a807bdad..650846015a4a2 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/test/api_integration/apis/telemetry/telemetry_local.ts @@ -177,6 +177,7 @@ export default function ({ getService }: FtrProviderContext) { describe('basic behaviour', () => { let savedObjectIds: string[] = []; before('create application usage entries', async () => { + await esArchiver.emptyKibanaIndex(); savedObjectIds = await Promise.all([ createSavedObject(), createSavedObject('appView1'), diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index 1cf16fe433bf9..8d60c79c9698d 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const createUiCounterEvent = (eventName: string, type: UiCounterMetricType, count = 1) => ({ @@ -23,6 +24,10 @@ export default function ({ getService }: FtrProviderContext) { }); describe('UI Counters API', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + const dayDate = moment().format('DDMMYYYY'); it('stores ui counter events in savedObjects', async () => { diff --git a/test/api_integration/apis/ui_metric/ui_metric.ts b/test/api_integration/apis/ui_metric/ui_metric.ts index d330cb037d1a1..e3b3b2ec4c542 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.ts +++ b/test/api_integration/apis/ui_metric/ui_metric.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const createStatsMetric = ( @@ -34,6 +35,10 @@ export default function ({ getService }: FtrProviderContext) { }); describe('ui_metric savedObject data', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + it('increments the count field in the document defined by the {app}/{action_type} path', async () => { const reportManager = new ReportManager(); const uiStatsMetric = createStatsMetric('myEvent'); diff --git a/test/common/config.js b/test/common/config.js index b6d12444b7017..8a42e6c87b214 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -61,8 +61,6 @@ export default function () { ...(!!process.env.CODE_COVERAGE ? [`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'coverage')}`] : []), - // Disable v2 migrations in tests for now - '--migrations.enableV2=false', ], }, services, diff --git a/test/common/services/kibana_server/extend_es_archiver.js b/test/common/services/kibana_server/extend_es_archiver.js index 5390b43a87187..1d76bc4473767 100644 --- a/test/common/services/kibana_server/extend_es_archiver.js +++ b/test/common/services/kibana_server/extend_es_archiver.js @@ -6,7 +6,7 @@ * Public License, v 1. */ -const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload']; +const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload', 'emptyKibanaIndex']; const KIBANA_INDEX = '.kibana'; export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) { @@ -25,7 +25,7 @@ export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) const statsKeys = Object.keys(stats); const kibanaKeys = statsKeys.filter( // this also matches stats keys like '.kibana_1' and '.kibana_2,.kibana_1' - (key) => key.includes(KIBANA_INDEX) && (stats[key].created || stats[key].deleted) + (key) => key.includes(KIBANA_INDEX) && stats[key].created ); // if the kibana index was created by the esArchiver then update the uiSettings diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 754406938e47b..e18f2a7485444 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -27,9 +27,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe.skip('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { + await esArchiver.load('management'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('management'); await PageObjects.settings.clickKibanaSavedObjects(); }); @@ -213,10 +213,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('.json file', () => { beforeEach(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await esArchiver.load('saved_objects_imports'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('saved_objects_imports'); await PageObjects.settings.clickKibanaSavedObjects(); }); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index eae53682b6ccf..91ea13348d611 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -12,10 +12,11 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings']); + const esArchiver = getService('esArchiver'); describe('index pattern filter', function describeIndexTests() { before(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await esArchiver.emptyKibanaIndex(); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts index 3b89e05d4b582..4e86de6d70653 100644 --- a/test/functional/apps/management/_index_patterns_empty.ts +++ b/test/functional/apps/management/_index_patterns_empty.ts @@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('index pattern empty view', () => { before(async () => { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await esArchiver.unload('logstash_functional'); await esArchiver.unload('makelogs'); await kibanaServer.uiSettings.replace({}); @@ -27,7 +27,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); // @ts-expect-error await es.transport.request({ diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 45a18d5932764..87eca2c7a5a65 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -18,14 +18,13 @@ export default function ({ getService, getPageObjects }) { describe('mgmt saved objects', function describeIndexTests() { beforeEach(async function () { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await esArchiver.load('discover'); await PageObjects.settings.navigateTo(); }); afterEach(async function () { await esArchiver.unload('discover'); - await esArchiver.load('empty_kibana'); }); it('should import saved objects mgmt', async function () { diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 2ab619276d2b9..c1fca31e695cb 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); + await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('large_fields'); await PageObjects.settings.createIndexPattern('testhuge', 'date'); }); diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index ca89853875027..06e652f9f3e59 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -14,13 +14,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('management', function () { before(async () => { await esArchiver.unload('logstash_functional'); - await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); }); after(async () => { await esArchiver.unload('makelogs'); - await esArchiver.unload('empty_kibana'); }); describe('', function () { diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index 9b48e78246b37..613b1a162eb63 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); const find = getService('find'); const security = getService('security'); const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); @@ -53,7 +52,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); await esArchiver.load('visualize'); - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await security.testUser.restoreDefaults(); }); }); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index e72d032f63469..52924d8c93280 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const find = getService('find'); const retry = getService('retry'); const deployment = getService('deployment'); + const esArchiver = getService('esArchiver'); const loadingScreenNotShown = async () => expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false); @@ -50,6 +51,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide describe('ui applications', function describeIndexTests() { before(async () => { + await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('foo'); }); diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index ba12e2df16d41..0cd53a5e1b764 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -12,8 +12,12 @@ import '../../plugins/core_provider_plugin/types'; export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('index patterns', function () { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); let indexPatternId = ''; it('can create an index pattern', async () => { diff --git a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts index 71663b19b35cb..b60e4b4a1d8b7 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts @@ -10,10 +10,15 @@ import path from 'path'; import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getPageObjects }: PluginFunctionalProviderContext) { +export default function ({ getPageObjects, getService }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); + const esArchiver = getService('esArchiver'); describe('import warnings', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + beforeEach(async () => { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); diff --git a/test/security_functional/insecure_cluster_warning.ts b/test/security_functional/insecure_cluster_warning.ts index 181c4cf2b46b7..2f7656b743a51 100644 --- a/test/security_functional/insecure_cluster_warning.ts +++ b/test/security_functional/insecure_cluster_warning.ts @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await browser.setLocalStorageItem('insecureClusterWarningVisibility', ''); await esArchiver.unload('hamlet'); + await esArchiver.emptyKibanaIndex(); }); it('should not warn when the cluster contains no user data', async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 8ed979a171169..2a256266697e6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -11,10 +11,6 @@ import { setupSpacesAndUsers, tearDown } from '..'; export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { describe('Alerts', () => { describe('legacy alerts', () => { - before(async () => { - await setupSpacesAndUsers(getService); - }); - after(async () => { await tearDown(getService); }); diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index 8851c83dea1ff..fceccb4609bd7 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -12,6 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); const log = getService('log'); const pieChart = getService('pieChart'); const find = getService('find'); @@ -29,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('sample data dashboard', function describeIndexTests() { before(async () => { + await esArchiver.emptyKibanaIndex(); await PageObjects.common.sleep(5000); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json index fe29bad0fa381..26b033e28b4da 100644 --- a/x-pack/test/functional/es_archives/visualize/default/data.json +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -125,26 +125,8 @@ { "type": "doc", "value": { - "id": "custom-space:index-pattern:metricbeat-*", - "index": ".kibana_1", - "source": { - "index-pattern": { - "fieldFormatMap": "{\"aerospike.namespace.device.available.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.device.free.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.device.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.device.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.free.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.memory.used.data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.index.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.sindex.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"aws.rds.disk_usage.bin_log.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.free_local_storage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.free_storage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.freeable_memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.latency.commit\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.ddl\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.dml\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.insert\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.read\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.select\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.update\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.write\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.replica_lag.sec\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.swap_usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.volume_used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_daily_storage.bucket.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.downloaded.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.latency.first_byte.ms\":{\"id\":\"duration\",\"params\":{}},\"aws.s3_request.latency.total_request.ms\":{\"id\":\"duration\",\"params\":{}},\"aws.s3_request.requests.select_returned.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.requests.select_scanned.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.uploaded.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.sqs.oldest_message_age.sec\":{\"id\":\"duration\",\"params\":{}},\"aws.sqs.sent_message_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.degraded.ratio\":{\"id\":\"percent\",\"params\":{}},\"ceph.cluster_status.misplace.ratio\":{\"id\":\"percent\",\"params\":{}},\"ceph.cluster_status.pg.avail_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.data_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.total_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.used_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.traffic.read_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.traffic.write_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.log.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.misc.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.sst.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.total.byte\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.used.byte\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.used.pct\":{\"id\":\"percent\",\"params\":{}},\"ceph.pool_disk.stats.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.pool_disk.stats.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"client.bytes\":{\"id\":\"bytes\",\"params\":{}},\"client.nat.port\":{\"id\":\"string\",\"params\":{}},\"client.port\":{\"id\":\"string\",\"params\":{}},\"coredns.stats.dns.request.duration.ns.sum\":{\"id\":\"duration\",\"params\":{}},\"couchbase.bucket.data.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.disk.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.quota.ram.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.quota.use.pct\":{\"id\":\"percent\",\"params\":{}},\"couchbase.cluster.hdd.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.quota.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.used.by_data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.total.per_node.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.total.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.used.per_node.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.used.by_data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.couch.docs.data_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.couch.docs.disk_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.mcd_memory.allocated.bytes\":{\"id\":\"bytes\",\"params\":{}},\"destination.bytes\":{\"id\":\"bytes\",\"params\":{}},\"destination.nat.port\":{\"id\":\"string\",\"params\":{}},\"destination.port\":{\"id\":\"string\",\"params\":{}},\"docker.cpu.core.*.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.kernel.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.system.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.user.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.diskio.summary.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.commit.peak\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.commit.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.limit\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.private_working_set.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.rss.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.memory.rss.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.usage.max\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.usage.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.memory.usage.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.inbound.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.outbound.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.primaries.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.primaries.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.total.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.total.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.total.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.total.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.heap.init.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.heap.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.nonheap.init.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.nonheap.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.indices.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.disk.mvcc_db_total_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.memory.go_memstats_alloc.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.network.client_grpc_received.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.network.client_grpc_sent.bytes\":{\"id\":\"bytes\",\"params\":{}},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\",\"params\":{}},\"event.severity\":{\"id\":\"string\",\"params\":{}},\"golang.heap.allocations.active\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.allocated\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.idle\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.total\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.gc.next_gc_limit\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.obtained\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.released\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.stack\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.total\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.info.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"haproxy.info.memory.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.info.ssl.frontend.session_reuse.pct\":{\"id\":\"percent\",\"params\":{}},\"haproxy.stat.compressor.bypassed.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.response.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.throttle.pct\":{\"id\":\"percent\",\"params\":{}},\"http.request.body.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.request.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.body.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.status_code\":{\"id\":\"string\",\"params\":{}},\"kibana.stats.process.memory.heap.size_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kibana.stats.process.memory.heap.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kibana.stats.process.memory.heap.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.cpu.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.cpu.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.logs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.logs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.logs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.request.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.memory.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.allocatable.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.network.rx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.network.tx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.cpu.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.cpu.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.working_set.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.network.rx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.network.tx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.avg_obj_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.data_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.extent_free_list.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.file_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.index_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.storage_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.replstatus.headroom.max\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.headroom.min\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.lag.max\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.lag.min\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.oplog.size.allocated\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.replstatus.oplog.size.used\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.extra_info.heap_usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.dirty.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.maximum.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.max_file_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mysql.status.bytes.received\":{\"id\":\"bytes\",\"params\":{}},\"mysql.status.bytes.sent\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.cpu\":{\"id\":\"percent\",\"params\":{}},\"nats.stats.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.mem.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.uptime\":{\"id\":\"duration\",\"params\":{}},\"nats.subscriptions.cache.hit_rate\":{\"id\":\"percent\",\"params\":{}},\"network.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"process.pgid\":{\"id\":\"string\",\"params\":{}},\"process.pid\":{\"id\":\"string\",\"params\":{}},\"process.ppid\":{\"id\":\"string\",\"params\":{}},\"process.thread.id\":{\"id\":\"string\",\"params\":{}},\"rabbitmq.connection.frame_max\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.disk.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.disk.free.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.gc.reclaimed.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.io.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.io.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.mem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.queue.consumers.utilisation.pct\":{\"id\":\"percent\",\"params\":{}},\"rabbitmq.queue.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.active\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.allocated\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.fragmentation.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.resident\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.fragmentation.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.max.value\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.dataset\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.lua\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.peak\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.rss\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.value\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.buffer.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.copy_on_write.last_size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.rewrite.buffer.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.rewrite.current_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.aof.rewrite.last_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.aof.size.base\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.size.current\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.rdb.bgsave.current_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.rdb.bgsave.last_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.rdb.copy_on_write.last_size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.replication.backlog.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.replication.master.last_io_seconds_ago\":{\"id\":\"duration\",\"params\":{}},\"redis.info.replication.master.sync.last_io_seconds_ago\":{\"id\":\"duration\",\"params\":{}},\"redis.info.replication.master.sync.left_bytes\":{\"id\":\"bytes\",\"params\":{}},\"server.bytes\":{\"id\":\"bytes\",\"params\":{}},\"server.nat.port\":{\"id\":\"string\",\"params\":{}},\"server.port\":{\"id\":\"string\",\"params\":{}},\"source.bytes\":{\"id\":\"bytes\",\"params\":{}},\"source.nat.port\":{\"id\":\"string\",\"params\":{}},\"source.port\":{\"id\":\"string\",\"params\":{}},\"system.core.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.iowait.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.irq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.nice.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.softirq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.steal.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.system.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.user.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.idle.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.iowait.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.iowait.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.irq.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.irq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.nice.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.nice.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.softirq.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.softirq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.steal.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.steal.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.system.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.system.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.total.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.user.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.user.pct\":{\"id\":\"percent\",\"params\":{}},\"system.diskio.iostat.read.per_sec.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.iostat.write.per_sec.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.entropy.pct\":{\"id\":\"percent\",\"params\":{}},\"system.filesystem.available\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.free\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.total\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.fsstat.total_size.free\":{\"id\":\"bytes\",\"params\":{}},\"system.fsstat.total_size.total\":{\"id\":\"bytes\",\"params\":{}},\"system.fsstat.total_size.used\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.default_size\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.free\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.reserved\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.surplus\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.total\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.swap.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.total\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.total\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.blkio.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.active_anon.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.active_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.cache.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.hierarchical_memory_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.hierarchical_memsw_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.inactive_anon.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.inactive_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.mapped_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.rss_huge.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.swap.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.unevictable.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.memory.rss.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.memory.share\":{\"id\":\"bytes\",\"params\":{}},\"system.process.memory.size\":{\"id\":\"bytes\",\"params\":{}},\"system.socket.summary.tcp.memory\":{\"id\":\"bytes\",\"params\":{}},\"system.socket.summary.udp.memory\":{\"id\":\"bytes\",\"params\":{}},\"system.uptime.duration.ms\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\"}},\"url.port\":{\"id\":\"string\",\"params\":{}},\"vsphere.datastore.capacity.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.used.pct\":{\"id\":\"percent\",\"params\":{}},\"vsphere.host.memory.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.host.memory.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.host.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.free.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.total.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.used.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.used.host.bytes\":{\"id\":\"bytes\",\"params\":{}},\"windows.service.uptime.ms\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\"}}}", - "timeFieldName": "@timestamp", - "title": "metricbeat-*" - }, - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "type": "index-pattern", - "updated_at": "2020-01-22T15:34:59.061Z" - } - } -} - -{ - "type": "doc", - "value": { + "index": ".kibana", + "type": "doc", "id": "index-pattern:logstash-*", "index": ".kibana_1", "source": { @@ -297,4 +279,4 @@ "updated_at": "2019-07-17T17:54:26.378Z" } } -} \ No newline at end of file +} diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index 11182bbcdb3b0..4a95a15169b59 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -32,7 +32,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { kbnTestServer: { ...apiConfig.get('kbnTestServer'), serverArgs: [ - `--migrations.enableV2=false`, `--elasticsearch.hosts=${formatUrl(esTestConfig.getUrlParts())}`, `--logging.json=false`, `--server.maxPayloadBytes=1679958`, From 688b918888a4d9956652394ad09b65b05a63a27e Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet <pierre.gayvallet@elastic.co> Date: Mon, 1 Feb 2021 16:00:48 +0100 Subject: [PATCH 161/163] migrate legacy_export plugin to tsproject ref (#89858) --- src/plugins/legacy_export/tsconfig.json | 16 ++++++++++++++++ test/tsconfig.json | 8 +++++++- tsconfig.json | 2 ++ tsconfig.refs.json | 3 ++- x-pack/test/tsconfig.json | 5 +++-- x-pack/tsconfig.json | 11 ++++++----- 6 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 src/plugins/legacy_export/tsconfig.json diff --git a/src/plugins/legacy_export/tsconfig.json b/src/plugins/legacy_export/tsconfig.json new file mode 100644 index 0000000000000..ec006d492499e --- /dev/null +++ b/src/plugins/legacy_export/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" } + ] +} diff --git a/test/tsconfig.json b/test/tsconfig.json index c8e6e69586ca0..4df74f526077e 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -4,7 +4,12 @@ "incremental": false, "types": ["node", "flot"] }, - "include": ["**/*", "../typings/elastic__node_crypto.d.ts", "typings/**/*", "../packages/kbn-test/types/ftr_globals/**/*"], + "include": [ + "**/*", + "../typings/elastic__node_crypto.d.ts", + "typings/**/*", + "../packages/kbn-test/types/ftr_globals/**/*" + ], "exclude": ["plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"], "references": [ { "path": "../src/core/tsconfig.json" }, @@ -34,5 +39,6 @@ { "path": "../src/plugins/ui_actions/tsconfig.json" }, { "path": "../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../src/plugins/legacy_export/tsconfig.json" } ] } diff --git a/tsconfig.json b/tsconfig.json index d8fb2804242bc..ee46e075f2df1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,6 +27,7 @@ "src/plugins/kibana_react/**/*", "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", + "src/plugins/legacy_export/**/*", "src/plugins/management/**/*", "src/plugins/maps_legacy/**/*", "src/plugins/navigation/**/*", @@ -85,6 +86,7 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/legacy_export/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/maps_legacy/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 9a65b385b7820..16c5b6c116998 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -20,6 +20,7 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/legacy_export/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/maps_legacy/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, @@ -51,6 +52,6 @@ { "path": "./src/plugins/vis_type_vega/tsconfig.json" }, { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, { "path": "./src/plugins/visualizations/tsconfig.json" }, - { "path": "./src/plugins/visualize/tsconfig.json" }, + { "path": "./src/plugins/visualize/tsconfig.json" } ] } diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 5232af0dd304b..12cd2896faaa8 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../src/plugins/legacy_export/tsconfig.json" }, { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" }, { "path": "../../src/plugins/saved_objects/tsconfig.json" }, @@ -36,8 +37,8 @@ { "path": "../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../plugins/actions/tsconfig.json"}, - { "path": "../plugins/alerts/tsconfig.json"}, + { "path": "../plugins/actions/tsconfig.json" }, + { "path": "../plugins/alerts/tsconfig.json" }, { "path": "../plugins/console_extensions/tsconfig.json" }, { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/enterprise_search/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 1be6b5cf84cda..85e285f3c83ac 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -67,6 +67,7 @@ { "path": "../src/plugins/kibana_react/tsconfig.json" }, { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../src/plugins/legacy_export/tsconfig.json" }, { "path": "../src/plugins/management/tsconfig.json" }, { "path": "../src/plugins/navigation/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, @@ -82,8 +83,8 @@ { "path": "../src/plugins/ui_actions/tsconfig.json" }, { "path": "../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, - { "path": "./plugins/actions/tsconfig.json"}, - { "path": "./plugins/alerts/tsconfig.json"}, + { "path": "./plugins/actions/tsconfig.json" }, + { "path": "./plugins/alerts/tsconfig.json" }, { "path": "./plugins/beats_management/tsconfig.json" }, { "path": "./plugins/canvas/tsconfig.json" }, { "path": "./plugins/cloud/tsconfig.json" }, @@ -110,12 +111,12 @@ { "path": "./plugins/searchprofiler/tsconfig.json" }, { "path": "./plugins/security/tsconfig.json" }, { "path": "./plugins/spaces/tsconfig.json" }, - { "path": "./plugins/stack_alerts/tsconfig.json"}, + { "path": "./plugins/stack_alerts/tsconfig.json" }, { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "./plugins/translations/tsconfig.json" }, - { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, + { "path": "./plugins/triggers_actions_ui/tsconfig.json" }, { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/watcher/tsconfig.json" } ] } From 57453f1709970b2bdf219b5bef8f408fb0a9ec41 Mon Sep 17 00:00:00 2001 From: ymao1 <ying.mao@elastic.co> Date: Mon, 1 Feb 2021 10:17:59 -0500 Subject: [PATCH 162/163] Some fixes from backport (#89746) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../stack_alerts/common/build_sorted_events_query.ts | 4 ++-- .../server/alert_types/es_query/alert_type.ts | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts index 92425433bf814..b9a65cf1a7489 100644 --- a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts @@ -87,7 +87,7 @@ export const buildSortedEventsQuery = ({ ...searchQuery.body, search_after: [searchAfterSortId], }, - }; + } as ESSearchRequest; } - return searchQuery; + return searchQuery as ESSearchRequest; }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index b8190340c4d68..a0da622a73cee 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -175,7 +175,17 @@ export function getAlertType( { bool: { must_not: [ - { bool: { filter: [{ range: { [params.timeField]: { lte: timestamp } } }] } }, + { + bool: { + filter: [ + { + range: { + [params.timeField]: { lte: new Date(timestamp).toISOString() }, + }, + }, + ], + }, + }, ], }, }, From 19332c097a3849b52aafd17970566d4808f0f1e9 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Mon, 1 Feb 2021 16:40:25 +0100 Subject: [PATCH 163/163] Deprecate and remove usages of elasticsearch.logQueries (#89296) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...n-core-server.elasticsearchclientconfig.md | 2 +- ...e-server.elasticsearchconfig.logqueries.md | 13 -- ...-plugin-core-server.elasticsearchconfig.md | 1 - ...erver.legacyclusterclient._constructor_.md | 3 +- ...-plugin-core-server.legacyclusterclient.md | 2 +- ...-server.legacyelasticsearchclientconfig.md | 2 +- docs/setup/settings.asciidoc | 2 +- .../client/client_config.test.ts | 1 - .../elasticsearch/client/client_config.ts | 1 - .../client/cluster_client.test.ts | 84 ++++++--- .../elasticsearch/client/cluster_client.ts | 5 +- .../client/configure_client.test.ts | 174 +++--------------- .../elasticsearch/client/configure_client.ts | 14 +- .../elasticsearch_config.test.ts | 1 - .../elasticsearch/elasticsearch_config.ts | 11 +- .../elasticsearch_service.test.ts | 12 +- .../elasticsearch/elasticsearch_service.ts | 6 +- .../legacy/cluster_client.test.ts | 91 ++++++--- .../elasticsearch/legacy/cluster_client.ts | 5 +- .../elasticsearch_client_config.test.ts | 99 +++------- .../legacy/elasticsearch_client_config.ts | 14 +- src/core/server/server.api.md | 7 +- .../server/es_client/instantiate_client.ts | 1 - 23 files changed, 216 insertions(+), 335 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.logqueries.md diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md index 1ba359e81b9c6..a854e5ddad19a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md @@ -9,7 +9,7 @@ Configuration options to be used to create a [cluster client](./kibana-plugin-co <b>Signature:</b> ```typescript -export declare type ElasticsearchClientConfig = Pick<ElasticsearchConfig, 'customHeaders' | 'logQueries' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' | 'sniffInterval' | 'hosts' | 'username' | 'password'> & { +export declare type ElasticsearchClientConfig = Pick<ElasticsearchConfig, 'customHeaders' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' | 'sniffInterval' | 'hosts' | 'username' | 'password'> & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; ssl?: Partial<ElasticsearchConfig['ssl']>; diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.logqueries.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.logqueries.md deleted file mode 100644 index 001fb7bfeeb97..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.logqueries.md +++ /dev/null @@ -1,13 +0,0 @@ -<!-- Do not edit this file. It is automatically generated by API Documenter. --> - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) > [logQueries](./kibana-plugin-core-server.elasticsearchconfig.logqueries.md) - -## ElasticsearchConfig.logQueries property - -Specifies whether all queries to the client should be logged (status code, method, query etc.). - -<b>Signature:</b> - -```typescript -readonly logQueries: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md index 5ec3ce7f41859..d87ea63d59b8d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md @@ -27,7 +27,6 @@ export declare class ElasticsearchConfig | [healthCheckDelay](./kibana-plugin-core-server.elasticsearchconfig.healthcheckdelay.md) | | <code>Duration</code> | The interval between health check requests Kibana sends to the Elasticsearch. | | [hosts](./kibana-plugin-core-server.elasticsearchconfig.hosts.md) | | <code>string[]</code> | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | | [ignoreVersionMismatch](./kibana-plugin-core-server.elasticsearchconfig.ignoreversionmismatch.md) | | <code>boolean</code> | Whether to allow kibana to connect to a non-compatible elasticsearch node. | -| [logQueries](./kibana-plugin-core-server.elasticsearchconfig.logqueries.md) | | <code>boolean</code> | Specifies whether all queries to the client should be logged (status code, method, query etc.). | | [password](./kibana-plugin-core-server.elasticsearchconfig.password.md) | | <code>string</code> | If Elasticsearch is protected with basic authentication, this setting provides the password that the Kibana server uses to perform its administrative functions. | | [pingTimeout](./kibana-plugin-core-server.elasticsearchconfig.pingtimeout.md) | | <code>Duration</code> | Timeout after which PING HTTP request will be aborted and retried. | | [requestHeadersWhitelist](./kibana-plugin-core-server.elasticsearchconfig.requestheaderswhitelist.md) | | <code>string[]</code> | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md index 823f34bd7dd23..ed2763d980279 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyClusterClient` class <b>Signature:</b> ```typescript -constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); +constructor(config: LegacyElasticsearchClientConfig, log: Logger, type: string, getAuthHeaders?: GetAuthHeaders); ``` ## Parameters @@ -18,5 +18,6 @@ constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders | --- | --- | --- | | config | <code>LegacyElasticsearchClientConfig</code> | | | log | <code>Logger</code> | | +| type | <code>string</code> | | | getAuthHeaders | <code>GetAuthHeaders</code> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md index d24aeb44ca86a..0872e5ba7c219 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md @@ -21,7 +21,7 @@ export declare class LegacyClusterClient implements ILegacyClusterClient | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(config, log, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the <code>LegacyClusterClient</code> class | +| [(constructor)(config, log, type, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the <code>LegacyClusterClient</code> class | ## Properties diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md index 78f7bf582d355..b028a09bee453 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md @@ -11,7 +11,7 @@ <b>Signature:</b> ```typescript -export declare type LegacyElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<ElasticsearchConfig, 'apiVersion' | 'customHeaders' | 'logQueries' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password'> & { +export declare type LegacyElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<ElasticsearchConfig, 'apiVersion' | 'customHeaders' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password'> & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 26f095c59c644..ecdb41c897b12 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -59,7 +59,7 @@ To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. | `elasticsearch.logQueries:` - | Log queries sent to {es}. Requires <<logging-verbose, `logging.verbose`>> set to `true`. + | *deprecated* This setting is no longer used and will get removed in Kibana 8.0. Instead, set <<logging-verbose, `logging.verbose`>> to `true` This is useful for seeing the query DSL generated by applications that currently do not have an inspector, for example Timelion and Monitoring. *Default: `false`* diff --git a/src/core/server/elasticsearch/client/client_config.test.ts b/src/core/server/elasticsearch/client/client_config.test.ts index 57bc7407a9a0f..768d165d5f8be 100644 --- a/src/core/server/elasticsearch/client/client_config.test.ts +++ b/src/core/server/elasticsearch/client/client_config.test.ts @@ -15,7 +15,6 @@ const createConfig = ( ): ElasticsearchClientConfig => { return { customHeaders: {}, - logQueries: false, sniffOnStart: false, sniffOnConnectionFault: false, sniffInterval: false, diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts index 5762ef16704a5..01d2222a45e3a 100644 --- a/src/core/server/elasticsearch/client/client_config.ts +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -22,7 +22,6 @@ import { DEFAULT_HEADERS } from '../default_headers'; export type ElasticsearchClientConfig = Pick< ElasticsearchConfig, | 'customHeaders' - | 'logQueries' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts index b94bf08f1185b..1d6d373ec185c 100644 --- a/src/core/server/elasticsearch/client/cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/cluster_client.test.ts @@ -19,7 +19,6 @@ const createConfig = ( parts: Partial<ElasticsearchClientConfig> = {} ): ElasticsearchClientConfig => { return { - logQueries: false, sniffOnStart: false, sniffOnConnectionFault: false, sniffInterval: false, @@ -57,16 +56,25 @@ describe('ClusterClient', () => { it('creates a single internal and scoped client during initialization', () => { const config = createConfig(); - new ClusterClient(config, logger, getAuthHeaders); + new ClusterClient(config, logger, 'custom-type', getAuthHeaders); expect(configureClientMock).toHaveBeenCalledTimes(2); - expect(configureClientMock).toHaveBeenCalledWith(config, { logger }); - expect(configureClientMock).toHaveBeenCalledWith(config, { logger, scoped: true }); + expect(configureClientMock).toHaveBeenCalledWith(config, { logger, type: 'custom-type' }); + expect(configureClientMock).toHaveBeenCalledWith(config, { + logger, + type: 'custom-type', + scoped: true, + }); }); describe('#asInternalUser', () => { it('returns the internal client', () => { - const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const clusterClient = new ClusterClient( + createConfig(), + logger, + 'custom-type', + getAuthHeaders + ); expect(clusterClient.asInternalUser).toBe(internalClient); }); @@ -74,7 +82,12 @@ describe('ClusterClient', () => { describe('#asScoped', () => { it('returns a scoped cluster client bound to the request', () => { - const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const clusterClient = new ClusterClient( + createConfig(), + logger, + 'custom-type', + getAuthHeaders + ); const request = httpServerMock.createKibanaRequest(); const scopedClusterClient = clusterClient.asScoped(request); @@ -87,7 +100,12 @@ describe('ClusterClient', () => { }); it('returns a distinct scoped cluster client on each call', () => { - const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const clusterClient = new ClusterClient( + createConfig(), + logger, + 'custom-type', + getAuthHeaders + ); const request = httpServerMock.createKibanaRequest(); const scopedClusterClient1 = clusterClient.asScoped(request); @@ -105,7 +123,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({}); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({ headers: { foo: 'bar', @@ -130,7 +148,7 @@ describe('ClusterClient', () => { other: 'nope', }); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({}); clusterClient.asScoped(request); @@ -150,7 +168,7 @@ describe('ClusterClient', () => { other: 'nope', }); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({ headers: { authorization: 'override', @@ -175,7 +193,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({}); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({}); clusterClient.asScoped(request); @@ -195,7 +213,7 @@ describe('ClusterClient', () => { const config = createConfig(); getAuthHeaders.mockReturnValue({}); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'my-fake-id', requestUuid: 'ignore-this-id' }, }); @@ -223,7 +241,7 @@ describe('ClusterClient', () => { foo: 'auth', }); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({}); clusterClient.asScoped(request); @@ -249,7 +267,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({}); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({ headers: { foo: 'request' }, }); @@ -276,7 +294,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({}); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest(); clusterClient.asScoped(request); @@ -297,7 +315,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({}); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({ headers: { [headerKey]: 'foo' }, }); @@ -321,7 +339,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({}); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = httpServerMock.createKibanaRequest({ headers: { foo: 'request' }, kibanaRequestState: { requestId: 'from request', requestUuid: 'ignore-this-id' }, @@ -344,7 +362,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({}); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = { headers: { authorization: 'auth', @@ -368,7 +386,7 @@ describe('ClusterClient', () => { authorization: 'auth', }); - const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); const request = { headers: { foo: 'bar', @@ -387,7 +405,12 @@ describe('ClusterClient', () => { describe('#close', () => { it('closes both underlying clients', async () => { - const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const clusterClient = new ClusterClient( + createConfig(), + logger, + 'custom-type', + getAuthHeaders + ); await clusterClient.close(); @@ -398,7 +421,12 @@ describe('ClusterClient', () => { it('waits for both clients to close', async (done) => { expect.assertions(4); - const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const clusterClient = new ClusterClient( + createConfig(), + logger, + 'custom-type', + getAuthHeaders + ); let internalClientClosed = false; let scopedClientClosed = false; @@ -436,7 +464,12 @@ describe('ClusterClient', () => { }); it('return a rejected promise is any client rejects', async () => { - const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const clusterClient = new ClusterClient( + createConfig(), + logger, + 'custom-type', + getAuthHeaders + ); internalClient.close.mockRejectedValue(new Error('error closing client')); @@ -446,7 +479,12 @@ describe('ClusterClient', () => { }); it('does nothing after the first call', async () => { - const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const clusterClient = new ClusterClient( + createConfig(), + logger, + 'custom-type', + getAuthHeaders + ); await clusterClient.close(); diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index 87d59e7417aa9..7e6a7f8ae53e6 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -60,10 +60,11 @@ export class ClusterClient implements ICustomClusterClient { constructor( private readonly config: ElasticsearchClientConfig, logger: Logger, + type: string, private readonly getAuthHeaders: GetAuthHeaders = noop ) { - this.asInternalUser = configureClient(config, { logger }); - this.rootScopedClient = configureClient(config, { logger, scoped: true }); + this.asInternalUser = configureClient(config, { logger, type }); + this.rootScopedClient = configureClient(config, { logger, type, scoped: true }); } asScoped(request: ScopeableRequest) { diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index 3486c210de1f9..548dc44aa4965 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -76,14 +76,14 @@ describe('configureClient', () => { }); it('calls `parseClientOptions` with the correct parameters', () => { - configureClient(config, { logger, scoped: false }); + configureClient(config, { logger, type: 'test', scoped: false }); expect(parseClientOptionsMock).toHaveBeenCalledTimes(1); expect(parseClientOptionsMock).toHaveBeenCalledWith(config, false); parseClientOptionsMock.mockClear(); - configureClient(config, { logger, scoped: true }); + configureClient(config, { logger, type: 'test', scoped: true }); expect(parseClientOptionsMock).toHaveBeenCalledTimes(1); expect(parseClientOptionsMock).toHaveBeenCalledWith(config, true); @@ -95,7 +95,7 @@ describe('configureClient', () => { }; parseClientOptionsMock.mockReturnValue(parsedOptions); - const client = configureClient(config, { logger, scoped: false }); + const client = configureClient(config, { logger, type: 'test', scoped: false }); expect(ClientMock).toHaveBeenCalledTimes(1); expect(ClientMock).toHaveBeenCalledWith(parsedOptions); @@ -103,7 +103,7 @@ describe('configureClient', () => { }); it('listens to client on `response` events', () => { - const client = configureClient(config, { logger, scoped: false }); + const client = configureClient(config, { logger, type: 'test', scoped: false }); expect(client.on).toHaveBeenCalledTimes(1); expect(client.on).toHaveBeenCalledWith('response', expect.any(Function)); @@ -122,38 +122,15 @@ describe('configureClient', () => { }, }); } - describe('does not log whrn "logQueries: false"', () => { - it('response', () => { - const client = configureClient(config, { logger, scoped: false }); - const response = createResponseWithBody({ - seq_no_primary_term: true, - query: { - term: { user: 'kimchy' }, - }, - }); - - client.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toHaveLength(0); - }); - - it('error', () => { - const client = configureClient(config, { logger, scoped: false }); - - const response = createApiResponse({ body: {} }); - client.emit('response', new errors.TimeoutError('message', response), response); - expect(loggingSystemMock.collect(logger).error).toHaveLength(0); + describe('logs each query', () => { + it('creates a query logger context based on the `type` parameter', () => { + configureClient(createFakeConfig(), { logger, type: 'test123' }); + expect(logger.get).toHaveBeenCalledWith('query', 'test123'); }); - }); - describe('logs each queries if `logQueries` is true', () => { it('when request body is an object', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createResponseWithBody({ seq_no_primary_term: true, @@ -169,23 +146,13 @@ describe('configureClient', () => { "200 GET /foo?hello=dolly {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", - Object { - "tags": Array [ - "query", - ], - }, ], ] `); }); it('when request body is a string', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createResponseWithBody( JSON.stringify({ @@ -203,23 +170,13 @@ describe('configureClient', () => { "200 GET /foo?hello=dolly {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", - Object { - "tags": Array [ - "query", - ], - }, ], ] `); }); it('when request body is a buffer', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createResponseWithBody( Buffer.from( @@ -239,23 +196,13 @@ describe('configureClient', () => { "200 GET /foo?hello=dolly [buffer]", - Object { - "tags": Array [ - "query", - ], - }, ], ] `); }); it('when request body is a readable stream', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createResponseWithBody( Readable.from( @@ -275,23 +222,13 @@ describe('configureClient', () => { "200 GET /foo?hello=dolly [stream]", - Object { - "tags": Array [ - "query", - ], - }, ], ] `); }); it('when request body is not defined', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createResponseWithBody(); @@ -301,23 +238,13 @@ describe('configureClient', () => { Array [ "200 GET /foo?hello=dolly", - Object { - "tags": Array [ - "query", - ], - }, ], ] `); }); it('properly encode queries', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createApiResponse({ body: {}, @@ -336,23 +263,13 @@ describe('configureClient', () => { Array [ "200 GET /foo?city=M%C3%BCnich", - Object { - "tags": Array [ - "query", - ], - }, ], ] `); }); - it('logs queries even in case of errors if `logQueries` is true', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + it('logs queries even in case of errors', () => { + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createApiResponse({ statusCode: 500, @@ -375,7 +292,7 @@ describe('configureClient', () => { }); client.emit('response', new errors.ResponseError(response), response); - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` Array [ Array [ "500 @@ -386,40 +303,13 @@ describe('configureClient', () => { `); }); - it('does not log queries if `logQueries` is false', () => { - const client = configureClient( - createFakeConfig({ - logQueries: false, - }), - { logger, scoped: false } - ); - - const response = createApiResponse({ - body: {}, - statusCode: 200, - params: { - method: 'GET', - path: '/foo', - }, - }); - - client.emit('response', null, response); - - expect(logger.debug).not.toHaveBeenCalled(); - }); - - it('logs error when the client emits an @elastic/elasticsearch error', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + it('logs debug when the client emits an @elastic/elasticsearch error', () => { + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createApiResponse({ body: {} }); client.emit('response', new errors.TimeoutError('message', response), response); - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` Array [ Array [ "[TimeoutError]: message", @@ -428,13 +318,8 @@ describe('configureClient', () => { `); }); - it('logs error when the client emits an ResponseError returned by elasticsearch', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + it('logs debug when the client emits an ResponseError returned by elasticsearch', () => { + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); const response = createApiResponse({ statusCode: 400, @@ -453,7 +338,7 @@ describe('configureClient', () => { }); client.emit('response', new errors.ResponseError(response), response); - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` Array [ Array [ "400 @@ -464,12 +349,7 @@ describe('configureClient', () => { }); it('logs default error info when the error response body is empty', () => { - const client = configureClient( - createFakeConfig({ - logQueries: true, - }), - { logger, scoped: false } - ); + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); let response = createApiResponse({ statusCode: 400, @@ -484,7 +364,7 @@ describe('configureClient', () => { }); client.emit('response', new errors.ResponseError(response), response); - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` Array [ Array [ "400 @@ -493,7 +373,7 @@ describe('configureClient', () => { ] `); - logger.error.mockClear(); + logger.debug.mockClear(); response = createApiResponse({ statusCode: 400, @@ -506,7 +386,7 @@ describe('configureClient', () => { }); client.emit('response', new errors.ResponseError(response), response); - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` Array [ Array [ "400 diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index 00cbd1958d817..bac792d1293a6 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -15,12 +15,12 @@ import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; export const configureClient = ( config: ElasticsearchClientConfig, - { logger, scoped = false }: { logger: Logger; scoped?: boolean } + { logger, type, scoped = false }: { logger: Logger; type: string; scoped?: boolean } ): Client => { const clientOptions = parseClientOptions(config, scoped); const client = new Client(clientOptions); - addLogging(client, logger, config.logQueries); + addLogging(client, logger.get('query', type)); return client; }; @@ -67,15 +67,13 @@ function getResponseMessage(event: RequestEvent): string { return `${event.statusCode}\n${params.method} ${url}${body}`; } -const addLogging = (client: Client, logger: Logger, logQueries: boolean) => { +const addLogging = (client: Client, logger: Logger) => { client.on('response', (error, event) => { - if (event && logQueries) { + if (event) { if (error) { - logger.error(getErrorMessage(error, event)); + logger.debug(getErrorMessage(error, event)); } else { - logger.debug(getResponseMessage(event), { - tags: ['query'], - }); + logger.debug(getResponseMessage(event)); } } }); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 803733fddb71c..e76de913a9d91 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -47,7 +47,6 @@ test('set correct defaults', () => { "http://localhost:9200", ], "ignoreVersionMismatch": false, - "logQueries": false, "password": undefined, "pingTimeout": "PT30S", "requestHeadersWhitelist": Array [ diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index b90ae2609f1e3..afda47ca8851b 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -133,6 +133,10 @@ const deprecations: ConfigDeprecationProvider = () => [ log( `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` ); + } else if (es.logQueries === true) { + log( + `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".` + ); } return settings; }, @@ -164,12 +168,6 @@ export class ElasticsearchConfig { */ public readonly apiVersion: string; - /** - * Specifies whether all queries to the client should be logged (status code, - * method, query etc.). - */ - public readonly logQueries: boolean; - /** * Hosts that the client will connect to. If sniffing is enabled, this list will * be used as seeds to discover the rest of your cluster. @@ -248,7 +246,6 @@ export class ElasticsearchConfig { constructor(rawConfig: ElasticsearchConfigType) { this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch; this.apiVersion = rawConfig.apiVersion; - this.logQueries = rawConfig.logQueries; this.hosts = Array.isArray(rawConfig.hosts) ? rawConfig.hosts : [rawConfig.hosts]; this.requestHeadersWhitelist = Array.isArray(rawConfig.requestHeadersWhitelist) ? rawConfig.requestHeadersWhitelist diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index a6d966b346072..3129ccfb5a67e 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -92,14 +92,15 @@ describe('#setup', () => { // reset all mocks called during setup phase MockLegacyClusterClient.mockClear(); - const customConfig = { logQueries: true }; + const customConfig = { keepAlive: true }; const clusterClient = setupContract.legacy.createClient('some-custom-type', customConfig); expect(clusterClient).toBe(mockLegacyClusterClientInstance); expect(MockLegacyClusterClient).toHaveBeenCalledWith( expect.objectContaining(customConfig), - expect.objectContaining({ context: ['elasticsearch', 'some-custom-type'] }), + expect.objectContaining({ context: ['elasticsearch'] }), + 'some-custom-type', expect.any(Function) ); }); @@ -267,7 +268,7 @@ describe('#start', () => { // reset all mocks called during setup phase MockClusterClient.mockClear(); - const customConfig = { logQueries: true }; + const customConfig = { keepAlive: true }; const clusterClient = startContract.createClient('custom-type', customConfig); expect(clusterClient).toBe(mockClusterClientInstance); @@ -275,7 +276,8 @@ describe('#start', () => { expect(MockClusterClient).toHaveBeenCalledTimes(1); expect(MockClusterClient).toHaveBeenCalledWith( expect.objectContaining(customConfig), - expect.objectContaining({ context: ['elasticsearch', 'custom-type'] }), + expect.objectContaining({ context: ['elasticsearch'] }), + 'custom-type', expect.any(Function) ); }); @@ -286,7 +288,7 @@ describe('#start', () => { // reset all mocks called during setup phase MockClusterClient.mockClear(); - const customConfig = { logQueries: true }; + const customConfig = { keepAlive: true }; startContract.createClient('custom-type', customConfig); startContract.createClient('another-type', customConfig); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 2d97f6e5c3121..fd3d546bb77b9 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -126,7 +126,8 @@ export class ElasticsearchService private createClusterClient(type: string, config: ElasticsearchClientConfig) { return new ClusterClient( config, - this.coreContext.logger.get('elasticsearch', type), + this.coreContext.logger.get('elasticsearch'), + type, this.getAuthHeaders ); } @@ -134,7 +135,8 @@ export class ElasticsearchService private createLegacyClusterClient(type: string, config: LegacyElasticsearchClientConfig) { return new LegacyClusterClient( config, - this.coreContext.logger.get('elasticsearch', type), + this.coreContext.logger.get('elasticsearch'), + type, this.getAuthHeaders ); } diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts index 97a49cd9eb9f4..177181608bee9 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts @@ -31,11 +31,15 @@ test('#constructor creates client with parsed config', () => { const mockEsConfig = { apiVersion: 'es-version' } as any; const mockLogger = logger.get(); - const clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + const clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); expect(clusterClient).toBeDefined(); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith(mockEsConfig, mockLogger); + expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( + mockEsConfig, + mockLogger, + 'custom-type' + ); expect(MockClient).toHaveBeenCalledTimes(1); expect(MockClient).toHaveBeenCalledWith(mockEsClientConfig); @@ -57,7 +61,11 @@ describe('#callAsInternalUser', () => { }; MockClient.mockImplementation(() => mockEsClientInstance); - clusterClient = new LegacyClusterClient({ apiVersion: 'es-version' } as any, logger.get()); + clusterClient = new LegacyClusterClient( + { apiVersion: 'es-version' } as any, + logger.get(), + 'custom-type' + ); }); test('fails if cluster client is closed', async () => { @@ -226,7 +234,7 @@ describe('#asScoped', () => { requestHeadersWhitelist: ['one', 'two'], } as any; - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); jest.clearAllMocks(); }); @@ -237,10 +245,15 @@ describe('#asScoped', () => { expect(firstScopedClusterClient).toBeDefined(); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith(mockEsConfig, mockLogger, { - auth: false, - ignoreCertAndKey: true, - }); + expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( + mockEsConfig, + mockLogger, + 'custom-type', + { + auth: false, + ignoreCertAndKey: true, + } + ); expect(MockClient).toHaveBeenCalledTimes(1); expect(MockClient).toHaveBeenCalledWith( @@ -261,42 +274,57 @@ describe('#asScoped', () => { test('properly configures `ignoreCertAndKey` for various configurations', () => { // Config without SSL. - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith(mockEsConfig, mockLogger, { - auth: false, - ignoreCertAndKey: true, - }); + expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( + mockEsConfig, + mockLogger, + 'custom-type', + { + auth: false, + ignoreCertAndKey: true, + } + ); // Config ssl.alwaysPresentCertificate === false mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: false } } as any; - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith(mockEsConfig, mockLogger, { - auth: false, - ignoreCertAndKey: true, - }); + expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( + mockEsConfig, + mockLogger, + 'custom-type', + { + auth: false, + ignoreCertAndKey: true, + } + ); // Config ssl.alwaysPresentCertificate === true mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: true } } as any; - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith(mockEsConfig, mockLogger, { - auth: false, - ignoreCertAndKey: false, - }); + expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( + mockEsConfig, + mockLogger, + 'custom-type', + { + auth: false, + ignoreCertAndKey: false, + } + ); }); test('passes only filtered headers to the scoped cluster client', () => { @@ -345,7 +373,7 @@ describe('#asScoped', () => { }); test('does not fail when scope to not defined request', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); clusterClient.asScoped(); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( @@ -356,7 +384,7 @@ describe('#asScoped', () => { }); test('does not fail when scope to a request without headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); clusterClient.asScoped({} as any); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( @@ -367,7 +395,7 @@ describe('#asScoped', () => { }); test('calls getAuthHeaders and filters results for a real request', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type', () => ({ one: '1', three: '3', })); @@ -381,7 +409,9 @@ describe('#asScoped', () => { }); test('getAuthHeaders results rewrite extends a request headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ one: 'foo' })); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type', () => ({ + one: 'foo', + })); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1', two: '2' } })); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( @@ -392,7 +422,7 @@ describe('#asScoped', () => { }); test("doesn't call getAuthHeaders for a fake request", async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({})); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type', () => ({})); clusterClient.asScoped({ headers: { one: 'foo' } }); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); @@ -404,7 +434,7 @@ describe('#asScoped', () => { }); test('filters a fake request headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); @@ -431,7 +461,8 @@ describe('#close', () => { clusterClient = new LegacyClusterClient( { apiVersion: 'es-version', requestHeadersWhitelist: [] } as any, - logger.get() + logger.get(), + 'custom-type' ); }); diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 9cac713920331..64e1382fee201 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -121,9 +121,10 @@ export class LegacyClusterClient implements ILegacyClusterClient { constructor( private readonly config: LegacyElasticsearchClientConfig, private readonly log: Logger, + private readonly type: string, private readonly getAuthHeaders: GetAuthHeaders = noop ) { - this.client = new Client(parseElasticsearchClientConfig(config, log)); + this.client = new Client(parseElasticsearchClientConfig(config, log, type)); } /** @@ -186,7 +187,7 @@ export class LegacyClusterClient implements ILegacyClusterClient { // between all scoped client instances. if (this.scopedClient === undefined) { this.scopedClient = new Client( - parseElasticsearchClientConfig(this.config, this.log, { + parseElasticsearchClientConfig(this.config, this.log, this.type, { auth: false, ignoreCertAndKey: !this.config.ssl || !this.config.ssl.alwaysPresentCertificate, }) diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts index 5dac353c1094c..6c79f2c568caa 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts @@ -22,13 +22,13 @@ test('parses minimally specified config', () => { { apiVersion: 'master', customHeaders: { xsrf: 'something' }, - logQueries: false, sniffOnStart: false, sniffOnConnectionFault: false, hosts: ['http://localhost/elasticsearch'], requestHeadersWhitelist: [], }, - logger.get() + logger.get(), + 'custom-type' ) ).toMatchInlineSnapshot(` Object { @@ -58,7 +58,6 @@ test('parses fully specified config', () => { const elasticsearchConfig: LegacyElasticsearchClientConfig = { apiVersion: 'v7.0.0', customHeaders: { xsrf: 'something' }, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: [ @@ -84,7 +83,8 @@ test('parses fully specified config', () => { const elasticsearchClientConfig = parseElasticsearchClientConfig( elasticsearchConfig, - logger.get() + logger.get(), + 'custom-type' ); // Check that original references aren't used. @@ -163,7 +163,6 @@ test('parses config timeouts of moment.Duration type', () => { { apiVersion: 'master', customHeaders: { xsrf: 'something' }, - logQueries: false, sniffOnStart: false, sniffOnConnectionFault: false, pingTimeout: duration(100, 'ms'), @@ -172,7 +171,8 @@ test('parses config timeouts of moment.Duration type', () => { hosts: ['http://localhost:9200/elasticsearch'], requestHeadersWhitelist: [], }, - logger.get() + logger.get(), + 'custom-type' ) ).toMatchInlineSnapshot(` Object { @@ -208,7 +208,6 @@ describe('#auth', () => { { apiVersion: 'v7.0.0', customHeaders: { xsrf: 'something' }, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: ['http://user:password@localhost/elasticsearch', 'https://es.local'], @@ -217,6 +216,7 @@ describe('#auth', () => { requestHeadersWhitelist: [], }, logger.get(), + 'custom-type', { auth: false } ) ).toMatchInlineSnapshot(` @@ -260,7 +260,6 @@ describe('#auth', () => { { apiVersion: 'v7.0.0', customHeaders: { xsrf: 'something' }, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: ['https://es.local'], @@ -268,6 +267,7 @@ describe('#auth', () => { password: 'changeme', }, logger.get(), + 'custom-type', { auth: true } ) ).toMatchInlineSnapshot(` @@ -300,7 +300,6 @@ describe('#auth', () => { { apiVersion: 'v7.0.0', customHeaders: { xsrf: 'something' }, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: ['https://es.local'], @@ -308,6 +307,7 @@ describe('#auth', () => { username: 'elastic', }, logger.get(), + 'custom-type', { auth: true } ) ).toMatchInlineSnapshot(` @@ -342,13 +342,13 @@ describe('#customHeaders', () => { { apiVersion: 'master', customHeaders: { [headerKey]: 'foo' }, - logQueries: false, sniffOnStart: false, sniffOnConnectionFault: false, hosts: ['http://localhost/elasticsearch'], requestHeadersWhitelist: [], }, - logger.get() + logger.get(), + 'custom-type' ); expect(parsedConfig.hosts[0].headers).toEqual({ [headerKey]: 'foo', @@ -357,62 +357,18 @@ describe('#customHeaders', () => { }); describe('#log', () => { - test('default logger with #logQueries = false', () => { + test('default logger', () => { const parsedConfig = parseElasticsearchClientConfig( { apiVersion: 'master', customHeaders: { xsrf: 'something' }, - logQueries: false, sniffOnStart: false, sniffOnConnectionFault: false, hosts: ['http://localhost/elasticsearch'], requestHeadersWhitelist: [], }, - logger.get() - ); - - const esLogger = new parsedConfig.log(); - esLogger.error('some-error'); - esLogger.warning('some-warning'); - esLogger.trace('some-trace'); - esLogger.info('some-info'); - esLogger.debug('some-debug'); - - expect(typeof esLogger.close).toBe('function'); - - expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(` - Object { - "debug": Array [], - "error": Array [ - Array [ - "some-error", - ], - ], - "fatal": Array [], - "info": Array [], - "log": Array [], - "trace": Array [], - "warn": Array [ - Array [ - "some-warning", - ], - ], - } - `); - }); - - test('default logger with #logQueries = true', () => { - const parsedConfig = parseElasticsearchClientConfig( - { - apiVersion: 'master', - customHeaders: { xsrf: 'something' }, - logQueries: true, - sniffOnStart: false, - sniffOnConnectionFault: false, - hosts: ['http://localhost/elasticsearch'], - requestHeadersWhitelist: [], - }, - logger.get() + logger.get(), + 'custom-type' ); const esLogger = new parsedConfig.log(); @@ -433,11 +389,6 @@ describe('#log', () => { "304 METHOD /some-path ?query=2", - Object { - "tags": Array [ - "query", - ], - }, ], ], "error": Array [ @@ -465,14 +416,14 @@ describe('#log', () => { { apiVersion: 'master', customHeaders: { xsrf: 'something' }, - logQueries: true, sniffOnStart: false, sniffOnConnectionFault: false, hosts: ['http://localhost/elasticsearch'], requestHeadersWhitelist: [], log: customLogger, }, - logger.get() + logger.get(), + 'custom-type' ); expect(parsedConfig.log).toBe(customLogger); @@ -486,14 +437,14 @@ describe('#ssl', () => { { apiVersion: 'v7.0.0', customHeaders: {}, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: ['https://es.local'], requestHeadersWhitelist: [], ssl: { verificationMode: 'none' }, }, - logger.get() + logger.get(), + 'custom-type' ) ).toMatchInlineSnapshot(` Object { @@ -527,14 +478,14 @@ describe('#ssl', () => { { apiVersion: 'v7.0.0', customHeaders: {}, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: ['https://es.local'], requestHeadersWhitelist: [], ssl: { verificationMode: 'certificate' }, }, - logger.get() + logger.get(), + 'custom-type' ); // `checkServerIdentity` shouldn't check hostname when verificationMode is certificate. @@ -576,14 +527,14 @@ describe('#ssl', () => { { apiVersion: 'v7.0.0', customHeaders: {}, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: ['https://es.local'], requestHeadersWhitelist: [], ssl: { verificationMode: 'full' }, }, - logger.get() + logger.get(), + 'custom-type' ) ).toMatchInlineSnapshot(` Object { @@ -618,14 +569,14 @@ describe('#ssl', () => { { apiVersion: 'v7.0.0', customHeaders: {}, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: ['https://es.local'], requestHeadersWhitelist: [], ssl: { verificationMode: 'misspelled' as any }, }, - logger.get() + logger.get(), + 'custom-type' ) ).toThrowErrorMatchingInlineSnapshot(`"Unknown ssl verificationMode: misspelled"`); }); @@ -636,7 +587,6 @@ describe('#ssl', () => { { apiVersion: 'v7.0.0', customHeaders: {}, - logQueries: true, sniffOnStart: true, sniffOnConnectionFault: true, hosts: ['https://es.local'], @@ -651,6 +601,7 @@ describe('#ssl', () => { }, }, logger.get(), + 'custom-type', { ignoreCertAndKey: true } ) ).toMatchInlineSnapshot(` diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts index ecd2e30097060..66b6046e4516d 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts @@ -29,7 +29,6 @@ export type LegacyElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | ElasticsearchConfig, | 'apiVersion' | 'customHeaders' - | 'logQueries' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' @@ -76,6 +75,7 @@ type ExtendedConfigOptions = ConfigOptions & export function parseElasticsearchClientConfig( config: LegacyElasticsearchClientConfig, log: Logger, + type: string, { ignoreCertAndKey = false, auth = true }: LegacyElasticsearchClientConfigOverrides = {} ) { const esClientConfig: ExtendedConfigOptions = { @@ -91,7 +91,7 @@ export function parseElasticsearchClientConfig( }; if (esClientConfig.log == null) { - esClientConfig.log = getLoggerClass(log, config.logQueries); + esClientConfig.log = getLoggerClass(log, type); } if (config.pingTimeout != null) { @@ -180,7 +180,9 @@ function getDurationAsMs(duration: number | Duration) { return duration.asMilliseconds(); } -function getLoggerClass(log: Logger, logQueries = false) { +function getLoggerClass(log: Logger, type: string) { + const queryLogger = log.get('query', type); + return class ElasticsearchClientLogging { public error(err: string | Error) { log.error(err); @@ -197,11 +199,7 @@ function getLoggerClass(log: Logger, logQueries = false) { _: unknown, statusCode: string ) { - if (logQueries) { - log.debug(`${statusCode}\n${method} ${options.path}\n${query ? query.trim() : ''}`, { - tags: ['query'], - }); - } + queryLogger.debug(`${statusCode}\n${method} ${options.path}\n${query ? query.trim() : ''}`); } // elasticsearch-js expects the following functions to exist diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9d5114e645f6e..f3191c5625f8d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -843,7 +843,7 @@ export type ElasticsearchClient = Omit<KibanaClient, 'connectionPool' | 'transpo }; // @public -export type ElasticsearchClientConfig = Pick<ElasticsearchConfig, 'customHeaders' | 'logQueries' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' | 'sniffInterval' | 'hosts' | 'username' | 'password'> & { +export type ElasticsearchClientConfig = Pick<ElasticsearchConfig, 'customHeaders' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' | 'sniffInterval' | 'hosts' | 'username' | 'password'> & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; ssl?: Partial<ElasticsearchConfig['ssl']>; @@ -859,7 +859,6 @@ export class ElasticsearchConfig { readonly healthCheckDelay: Duration; readonly hosts: string[]; readonly ignoreVersionMismatch: boolean; - readonly logQueries: boolean; readonly password?: string; readonly pingTimeout: Duration; readonly requestHeadersWhitelist: string[]; @@ -1531,7 +1530,7 @@ export interface LegacyCallAPIOptions { // @public @deprecated export class LegacyClusterClient implements ILegacyClusterClient { - constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); + constructor(config: LegacyElasticsearchClientConfig, log: Logger, type: string, getAuthHeaders?: GetAuthHeaders); asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; // @deprecated callAsInternalUser: LegacyAPICaller; @@ -1553,7 +1552,7 @@ export interface LegacyConfig { } // @public @deprecated (undocumented) -export type LegacyElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<ElasticsearchConfig, 'apiVersion' | 'customHeaders' | 'logQueries' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password'> & { +export type LegacyElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<ElasticsearchConfig, 'apiVersion' | 'customHeaders' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password'> & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; diff --git a/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts b/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts index 734caa7374686..3336e65da2b11 100644 --- a/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts +++ b/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts @@ -30,7 +30,6 @@ export function instantiateClient( const cluster = createClient('monitoring', { ...(isMonitoringCluster ? elasticsearchConfig : {}), plugins: [monitoringBulk, monitoringEndpointDisableWatches], - logQueries: Boolean(elasticsearchConfig.logQueries), } as ESClusterConfig); const configSource = isMonitoringCluster ? 'monitoring' : 'production';

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