From f53b1470974257a985d28fb14bfc4901c271f411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Thu, 7 May 2020 11:15:47 +0200 Subject: [PATCH 01/16] [Logs UI] Disable search bar when live stream is on. (#65491) --- .../components/autocomplete_field/autocomplete_field.tsx | 3 +++ x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx | 1 + 2 files changed, 4 insertions(+) diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 2abef7d71e65aa..6bbd67ce932c69 100644 --- a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -26,6 +26,7 @@ interface AutocompleteFieldProps { placeholder?: string; suggestions: QuerySuggestion[]; value: string; + disabled?: boolean; autoFocus?: boolean; 'aria-label'?: string; } @@ -55,6 +56,7 @@ export class AutocompleteField extends React.Component< isValid, placeholder, value, + disabled, 'aria-label': ariaLabel, } = this.props; const { areSuggestionsVisible, selectedIndex } = this.state; @@ -64,6 +66,7 @@ export class AutocompleteField extends React.Component< { isLoadingSuggestions={isLoadingSuggestions} isValid={isFilterQueryDraftValid} loadSuggestions={loadSuggestions} + disabled={isStreaming} onChange={(expression: string) => { setSurroundingLogsId(null); setLogFilterQueryDraft(expression); From 8a8647ab951282bf61d490702b22b60a55af2309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 7 May 2020 11:33:50 +0200 Subject: [PATCH 02/16] [Logs + Metrics UI] Prevent component errors from breaking the whole UI (#65456) --- .../pages/logs/log_entry_categories/page.tsx | 14 +- .../public/pages/logs/log_entry_rate/page.tsx | 14 +- .../plugins/infra/public/pages/logs/page.tsx | 12 +- .../source_configuration_settings.tsx | 5 +- .../infra/public/pages/logs/stream/page.tsx | 15 +- .../infra/public/pages/metrics/index.tsx | 194 +++++++++--------- .../pages/metrics/inventory_view/index.tsx | 121 +++++------ .../metrics/metric_detail/page_providers.tsx | 14 +- .../pages/metrics/metrics_explorer/index.tsx | 12 +- .../infra/public/pages/metrics/settings.tsx | 13 +- 10 files changed, 218 insertions(+), 196 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page.tsx index 64e83a6eaa4976..ad7893183c4df7 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page.tsx @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; - import { ColumnarPage } from '../../../components/page'; import { LogEntryCategoriesPageContent } from './page_content'; import { LogEntryCategoriesPageProviders } from './page_providers'; export const LogEntryCategoriesPage = () => { return ( - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx index 5ff5cd4db7168f..16751fabd6e964 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page.tsx @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; - import { ColumnarPage } from '../../../components/page'; import { LogEntryRatePageContent } from './page_content'; import { LogEntryRatePageProviders } from './page_providers'; export const LogEntryRatePage = () => { return ( - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/page.tsx b/x-pack/plugins/infra/public/pages/logs/page.tsx index 08049183d0a186..018f89fbb23c43 100644 --- a/x-pack/plugins/infra/public/pages/logs/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page.tsx @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; - import { LogsPageContent } from './page_content'; import { LogsPageProviders } from './page_providers'; -export const LogsPage: React.FunctionComponent = ({ match }) => { +export const LogsPage: React.FunctionComponent = () => { return ( - - - + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx index 88b1441f0ba7c1..363b1b76271041 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -7,6 +7,7 @@ import { EuiButton, EuiCallOut, + EuiErrorBoundary, EuiFlexGroup, EuiFlexItem, EuiPanel, @@ -74,7 +75,7 @@ export const LogsSettingsPage = () => { } return ( - <> + { - + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx index 712d625052140a..bc25d7c49b1297 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { ColumnarPage } from '../../../components/page'; @@ -15,11 +16,13 @@ export const StreamPage = () => { useTrackPageview({ app: 'infra_logs', path: 'stream' }); useTrackPageview({ app: 'infra_logs', path: 'stream', delay: 15000 }); return ( - - - - - - + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index dbf71665ea869a..91362d9098e344 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiErrorBoundary, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; @@ -36,103 +36,105 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; return ( - - - - - - - - + + + + + + + -
- - - - - - - - - - - + - - - ( - - {({ configuration, createDerivedIndexPattern }) => ( - - - {configuration ? ( - - ) : ( - - )} - - )} - - )} +
- - - - - - - + + + + + + + + + + + + + + + ( + + {({ configuration, createDerivedIndexPattern }) => ( + + + {configuration ? ( + + ) : ( + + )} + + )} + + )} + /> + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 3a2c33d1c824c5..ebb8243369b3c4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiErrorBoundary, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; @@ -41,65 +41,70 @@ export const SnapshotPage = () => { }); return ( - - - i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { - defaultMessage: '{previousTitle} | Inventory', - values: { - previousTitle, - }, - }) - } - /> - {isLoading ? ( - - ) : metricIndicesExist ? ( - <> - - - - ) : hasFailedLoadingSource ? ( - - ) : ( - - - - {i18n.translate('xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', { - defaultMessage: 'View setup instructions', - })} - - - {uiCapabilities?.infrastructure?.configureSource ? ( + + + + i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { + defaultMessage: '{previousTitle} | Inventory', + values: { + previousTitle, + }, + }) + } + /> + {isLoading ? ( + + ) : metricIndicesExist ? ( + <> + + + + ) : hasFailedLoadingSource ? ( + + ) : ( + - - {i18n.translate('xpack.infra.configureSourceActionLabel', { - defaultMessage: 'Change source configuration', - })} - + {i18n.translate( + 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', + { + defaultMessage: 'View setup instructions', + } + )} + - ) : null} - - } - data-test-subj="noMetricsIndicesPrompt" - /> - )} - + {uiCapabilities?.infrastructure?.configureSource ? ( + + + {i18n.translate('xpack.infra.configureSourceActionLabel', { + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + data-test-subj="noMetricsIndicesPrompt" + /> + )} + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx index 597977d9d2735d..dcd1c1d949971d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; - import { Source } from '../../../containers/source'; import { MetricsTimeProvider } from './hooks/use_metrics_time'; export const withMetricPageProviders = (Component: React.ComponentType) => ( props: T ) => ( - - - - - + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index a213671e9436eb..8b703b1177c8c8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import React from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; +import { useTrackPageview } from '../../../../../observability/public'; +import { SourceQuery } from '../../../../common/graphql/types'; import { DocumentTitle } from '../../../components/document_title'; +import { NoData } from '../../../components/empty_states'; import { MetricsExplorerCharts } from './components/charts'; import { MetricsExplorerToolbar } from './components/toolbar'; -import { SourceQuery } from '../../../../common/graphql/types'; -import { NoData } from '../../../components/empty_states'; import { useMetricsExplorerState } from './hooks/use_metric_explorer_state'; -import { useTrackPageview } from '../../../../../observability/public'; interface MetricsExplorerPageProps { source: SourceQuery.Query['source']['configuration']; @@ -45,7 +45,7 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); return ( - + i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', { @@ -95,6 +95,6 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl onTimeChange={handleTimeChange} /> )} - + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings.tsx index 9414eb7d3e5640..7d4f35b19da7de 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings.tsx @@ -4,16 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; -import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings'; export const MetricsSettingsPage = () => { const uiCapabilities = useKibana().services.application?.capabilities; return ( - + + + ); }; From 83a088cb499a90daa76162237b459c80f4fedfe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 7 May 2020 11:47:50 +0200 Subject: [PATCH 03/16] =?UTF-8?q?[Mappings=20editor]=C2=A0Add=20component?= =?UTF-8?q?=20integration=20tests=20(#63853)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__jest__/components/index_table.test.js | 9 + .../client_integration/datatypes/index.ts | 8 + .../datatypes/shape_datatype.test.tsx | 81 ++++ .../datatypes/text_datatype.test.tsx | 459 ++++++++++++++++++ .../client_integration/edit_field.test.tsx | 127 +++++ .../client_integration/helpers/index.ts | 11 +- .../helpers/mappings_editor.helpers.tsx | 228 ++++++++- .../client_integration/mapped_fields.test.tsx | 104 ++++ .../mappings_editor.test.tsx | 252 +++++++++- .../configuration_form/configuration_form.tsx | 15 +- .../dynamic_mapping_section.tsx | 2 + .../meta_field_section/meta_field_section.tsx | 1 + .../configuration_form/routing_section.tsx | 6 +- .../source_field_section.tsx | 6 +- .../field_parameters/analyzer_parameter.tsx | 16 +- .../analyzer_parameter_selects.tsx | 11 +- .../field_parameters/analyzers_parameter.tsx | 7 + .../field_parameters/index_parameter.tsx | 1 + .../advanced_parameters_section.tsx | 4 +- .../fields/edit_field/edit_field.tsx | 5 +- .../fields/edit_field/edit_field_form_row.tsx | 18 +- .../document_fields/fields/fields_list.tsx | 2 +- .../fields/fields_list_item.tsx | 22 +- .../templates_form/templates_form.tsx | 10 +- .../mappings_editor/lib/utils.test.ts | 47 +- .../components/mappings_editor/lib/utils.ts | 43 +- .../mappings_editor/mappings_editor.tsx | 22 +- .../mappings_editor/mappings_state.tsx | 42 +- .../template_form/steps/step_mappings.tsx | 4 +- x-pack/test_utils/testbed/testbed.ts | 42 +- x-pack/test_utils/testbed/types.ts | 25 +- 31 files changed, 1513 insertions(+), 117 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 15c3ef0b845624..84fbc04aa5a313 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -8,6 +8,15 @@ import React from 'react'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { MemoryRouter } from 'react-router-dom'; + +/** + * The below import is required to avoid a console error warn from brace package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import * as stubWebWorker from '../../../../test_utils/stub_web_worker'; // eslint-disable-line no-unused-vars + import { AppWithoutRouter } from '../../public/application/app'; import { AppContextProvider } from '../../public/application/app_context'; import { Provider } from 'react-redux'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/index.ts new file mode 100644 index 00000000000000..eac68770d3de23 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/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 { defaultShapeParameters } from './shape_datatype.test'; +export { defaultTextParameters } from './text_datatype.test'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx new file mode 100644 index 00000000000000..19bf6973472ff3 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); +const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + +// Parameters automatically added to the shape datatype when saved (with the default values) +export const defaultShapeParameters = { + type: 'shape', + coerce: false, + ignore_malformed: false, + ignore_z_value: true, +}; + +describe('Mappings editor: shape datatype', () => { + let testBed: MappingsEditorTestBed; + + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + + test('initial view and default parameters values', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'shape', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { + exists, + waitFor, + waitForFn, + actions: { startEditField, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout to edit the field + await act(async () => { + startEditField('myField'); + }); + + await waitFor('mappingsEditorFieldEdit'); + + // Save the field and close the flyout + await act(async () => { + await updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + // It should have the default parameters values added + updatedMappings.properties.myField = { + type: 'shape', + ...defaultShapeParameters, + }; + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx new file mode 100644 index 00000000000000..2bfaa884a01329 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx @@ -0,0 +1,459 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; +import { getFieldConfig } from '../../../lib'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); +const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + +// Parameters automatically added to the text datatype when saved (with the default values) +export const defaultTextParameters = { + type: 'text', + eager_global_ordinals: false, + fielddata: false, + index: true, + index_options: 'positions', + index_phrases: false, + norms: true, + store: false, +}; + +describe('Mappings editor: text datatype', () => { + let testBed: MappingsEditorTestBed; + + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + + afterEach(() => { + onChangeHandler.mockReset(); + }); + + test('initial view and default parameters values', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'text', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { + exists, + waitFor, + waitForFn, + actions: { startEditField, getToggleValue, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout to edit the field + await act(async () => { + startEditField('myField'); + }); + + await waitFor('mappingsEditorFieldEdit'); + + // It should have searchable ("index" param) active by default + const indexFieldConfig = getFieldConfig('index'); + expect(getToggleValue('indexParameter.formRowToggle')).toBe(indexFieldConfig.defaultValue); + + // Save the field and close the flyout + await act(async () => { + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + // It should have the default parameters values added + updatedMappings.properties.myField = { + type: 'text', + ...defaultTextParameters, + }; + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + }, 30000); + + test('analyzer parameter: default values', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'text', + // Should have 2 dropdown selects: + // The first one set to 'language' and the second one set to 'french + search_quote_analyzer: 'french', + }, + }, + }; + + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + + const { + find, + exists, + waitFor, + waitForFn, + form: { selectCheckBox, setSelectValue }, + actions: { + startEditField, + getCheckboxValue, + showAdvancedSettings, + updateFieldAndCloseFlyout, + }, + } = testBed; + const fieldToEdit = 'myField'; + + // Start edit and immediately save to have all the default values + await act(async () => { + startEditField(fieldToEdit); + }); + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + await act(async () => { + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + ({ data } = await getMappingsEditorData()); + + let updatedMappings: any = { + ...defaultMappings, + properties: { + myField: { + ...defaultMappings.properties.myField, + ...defaultTextParameters, + }, + }, + }; + expect(data).toEqual(updatedMappings); + + // Re-open the edit panel + await act(async () => { + startEditField('myField'); + }); + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + // When no analyzer is defined, defaults to "Index default" + let indexAnalyzerValue = find('indexAnalyzer.select').props().value; + expect(indexAnalyzerValue).toEqual('index_default'); + + const searchQuoteAnalyzerSelects = find('searchQuoteAnalyzer.select'); + + expect(searchQuoteAnalyzerSelects.length).toBe(2); + expect(searchQuoteAnalyzerSelects.at(0).props().value).toBe('language'); + expect(searchQuoteAnalyzerSelects.at(1).props().value).toBe( + defaultMappings.properties.myField.search_quote_analyzer + ); + + // When no "search_analyzer" is defined, the checkBox should be checked + let isUseSameAnalyzerForSearchChecked = getCheckboxValue( + 'useSameAnalyzerForSearchCheckBox.input' + ); + expect(isUseSameAnalyzerForSearchChecked).toBe(true); + + // And the search analyzer select should not exist + expect(exists('searchAnalyzer')).toBe(false); + + // Uncheck the "Use same analyzer for search" checkbox and wait for the search analyzer select + await act(async () => { + selectCheckBox('useSameAnalyzerForSearchCheckBox.input', false); + }); + + await waitFor('searchAnalyzer'); + + let searchAnalyzerValue = find('searchAnalyzer.select').props().value; + expect(searchAnalyzerValue).toEqual('index_default'); + + await act(async () => { + // Change the value of the 3 analyzers + setSelectValue('indexAnalyzer.select', 'standard'); + setSelectValue('searchAnalyzer.select', 'simple'); + setSelectValue(find('searchQuoteAnalyzer.select').at(0), 'whitespace'); + }); + + // Make sure the second dropdown select has been removed + await waitForFn( + async () => find('searchQuoteAnalyzer.select').length === 1, + 'Error waiting for the second dropdown select of search quote analyzer to be removed' + ); + + await act(async () => { + // Save & close + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + updatedMappings = { + ...updatedMappings, + properties: { + myField: { + ...updatedMappings.properties.myField, + analyzer: 'standard', + search_analyzer: 'simple', + search_quote_analyzer: 'whitespace', + }, + }, + }; + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + + // Re-open the flyout and make sure the select have the correct updated value + await act(async () => { + startEditField('myField'); + }); + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + isUseSameAnalyzerForSearchChecked = getCheckboxValue('useSameAnalyzerForSearchCheckBox.input'); + expect(isUseSameAnalyzerForSearchChecked).toBe(false); + + indexAnalyzerValue = find('indexAnalyzer.select').props().value; + searchAnalyzerValue = find('searchAnalyzer.select').props().value; + const searchQuoteAnalyzerValue = find('searchQuoteAnalyzer.select').props().value; + + expect(indexAnalyzerValue).toBe('standard'); + expect(searchAnalyzerValue).toBe('simple'); + expect(searchQuoteAnalyzerValue).toBe('whitespace'); + }, 30000); + + test('analyzer parameter: custom analyzer (external plugin)', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'text', + analyzer: 'myCustomIndexAnalyzer', + search_analyzer: 'myCustomSearchAnalyzer', + search_quote_analyzer: 'myCustomSearchQuoteAnalyzer', + }, + }, + }; + + let updatedMappings: any = { + ...defaultMappings, + properties: { + myField: { + ...defaultMappings.properties.myField, + ...defaultTextParameters, + }, + }, + }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { + find, + exists, + waitFor, + waitForFn, + component, + form: { setInputValue, setSelectValue }, + actions: { startEditField, showAdvancedSettings, updateFieldAndCloseFlyout }, + } = testBed; + const fieldToEdit = 'myField'; + + await act(async () => { + startEditField(fieldToEdit); + }); + + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + expect(exists('indexAnalyzer-custom')).toBe(true); + expect(exists('searchAnalyzer-custom')).toBe(true); + expect(exists('searchQuoteAnalyzer-custom')).toBe(true); + + const indexAnalyzerValue = find('indexAnalyzer-custom.input').props().value; + const searchAnalyzerValue = find('searchAnalyzer-custom.input').props().value; + const searchQuoteAnalyzerValue = find('searchQuoteAnalyzer-custom.input').props().value; + + expect(indexAnalyzerValue).toBe(defaultMappings.properties.myField.analyzer); + expect(searchAnalyzerValue).toBe(defaultMappings.properties.myField.search_analyzer); + expect(searchQuoteAnalyzerValue).toBe(defaultMappings.properties.myField.search_quote_analyzer); + + const updatedIndexAnalyzer = 'newCustomIndexAnalyzer'; + const updatedSearchAnalyzer = 'whitespace'; + + await act(async () => { + // Change the index analyzer to another custom one + setInputValue('indexAnalyzer-custom.input', updatedIndexAnalyzer); + + // Change the search analyzer to a built-in analyzer + find('searchAnalyzer-toggleCustomButton').simulate('click'); + component.update(); + }); + + await waitFor('searchAnalyzer'); + + await act(async () => { + setSelectValue('searchAnalyzer.select', updatedSearchAnalyzer); + + // Change the searchQuote to use built-in analyzer + // By default it means using the "index default" + find('searchQuoteAnalyzer-toggleCustomButton').simulate('click'); + component.update(); + }); + + await waitFor('searchQuoteAnalyzer'); + + await act(async () => { + // Save & close + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + ({ data } = await getMappingsEditorData()); + + updatedMappings = { + ...updatedMappings, + properties: { + myField: { + ...updatedMappings.properties.myField, + analyzer: updatedIndexAnalyzer, + search_analyzer: updatedSearchAnalyzer, + search_quote_analyzer: undefined, // Index default means not declaring the analyzer + }, + }, + }; + + expect(data).toEqual(updatedMappings); + }, 30000); + + test('analyzer parameter: custom analyzer (from index settings)', async () => { + const indexSettings = { + analysis: { + analyzer: { + customAnalyzer_1: {}, + customAnalyzer_2: {}, + customAnalyzer_3: {}, + }, + }, + }; + + const customAnalyzers = Object.keys(indexSettings.analysis.analyzer); + + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + type: 'text', + analyzer: customAnalyzers[0], + }, + }, + }; + + let updatedMappings: any = { + ...defaultMappings, + properties: { + myField: { + ...defaultMappings.properties.myField, + ...defaultTextParameters, + }, + }, + }; + + testBed = await setup({ + value: defaultMappings, + onChange: onChangeHandler, + indexSettings, + }); + + const { + find, + exists, + waitFor, + waitForFn, + form: { setSelectValue }, + actions: { startEditField, showAdvancedSettings, updateFieldAndCloseFlyout }, + } = testBed; + const fieldToEdit = 'myField'; + + await act(async () => { + startEditField(fieldToEdit); + }); + await waitFor('mappingsEditorFieldEdit'); + await showAdvancedSettings(); + + // It should have 2 selects + const indexAnalyzerSelects = find('indexAnalyzer.select'); + + expect(indexAnalyzerSelects.length).toBe(2); + expect(indexAnalyzerSelects.at(0).props().value).toBe('custom'); + expect(indexAnalyzerSelects.at(1).props().value).toBe( + defaultMappings.properties.myField.analyzer + ); + + // Access the list of option of the second dropdown select + const subSelectOptions = indexAnalyzerSelects + .at(1) + .find('option') + .map(wrapper => wrapper.text()); + + expect(subSelectOptions).toEqual(customAnalyzers); + + await act(async () => { + // Change the custom analyzer dropdown to another one from the index settings + setSelectValue(find('indexAnalyzer.select').at(1), customAnalyzers[2]); + + // Save & close + updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + ({ data } = await getMappingsEditorData()); + + updatedMappings = { + ...updatedMappings, + properties: { + myField: { + ...updatedMappings.properties.myField, + analyzer: customAnalyzers[2], + }, + }, + }; + + expect(data).toEqual(updatedMappings); + }, 30000); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx new file mode 100644 index 00000000000000..4af5f82d851e38 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from './helpers'; +import { defaultTextParameters, defaultShapeParameters } from './datatypes'; +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); +const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + +describe('Mappings editor: edit field', () => { + let testBed: MappingsEditorTestBed; + + afterEach(() => { + onChangeHandler.mockReset(); + }); + + test('should open a flyout with the correct field to edit', async () => { + const defaultMappings = { + properties: { + user: { + type: 'object', + properties: { + address: { + type: 'object', + properties: { + street: { type: 'text' }, + }, + }, + }, + }, + }, + }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + // Make sure all the fields are expanded and present in the DOM + await testBed.actions.expandAllFieldsAndReturnMetadata(); + }); + + const { + find, + waitFor, + actions: { startEditField }, + } = testBed; + // Open the flyout to edit the field + await act(async () => { + startEditField('user.address.street'); + }); + + await waitFor('mappingsEditorFieldEdit'); + + // It should have the correct title + expect(find('mappingsEditorFieldEdit.flyoutTitle').text()).toEqual(`Edit field 'street'`); + + // It should have the correct field path + expect(find('mappingsEditorFieldEdit.fieldPath').text()).toEqual('user > address > street'); + + // The advanced settings should be hidden initially + expect(find('mappingsEditorFieldEdit.advancedSettings').props().style.display).toEqual('none'); + }); + + test('should update form parameters when changing the field datatype', async () => { + const defaultMappings = { + _meta: {}, + _source: {}, + properties: { + myField: { + ...defaultTextParameters, + }, + }, + }; + + await act(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { + find, + exists, + waitFor, + waitForFn, + component, + actions: { startEditField, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout, change the field type and save it + await act(async () => { + startEditField('myField'); + }); + + await waitFor('mappingsEditorFieldEdit'); + + await act(async () => { + // Change the field type + find('mappingsEditorFieldEdit.fieldType').simulate('change', [ + { label: 'Shape', value: defaultShapeParameters.type }, + ]); + component.update(); + }); + + await act(async () => { + await updateFieldAndCloseFlyout(); + }); + + await waitForFn( + async () => exists('mappingsEditorFieldEdit') === false, + 'Error waiting for the details flyout to close' + ); + + const { data } = await getMappingsEditorData(); + + const updatedMappings = { + ...defaultMappings, + properties: { + myField: { + ...defaultShapeParameters, + }, + }, + }; + + expect(data).toEqual(updatedMappings); + }, 15000); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts index fa6bee56349e95..afdc039ae77d2a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts @@ -3,7 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { setup as mappingsEditorSetup, MappingsEditorTestBed } from './mappings_editor.helpers'; +import { + setup as mappingsEditorSetup, + MappingsEditorTestBed, + DomFields, + getMappingsEditorDataFactory, +} from './mappings_editor.helpers'; export { nextTick, @@ -13,7 +18,7 @@ export { } from '../../../../../../../../../test_utils'; export const componentHelpers = { - mappingsEditor: { setup: mappingsEditorSetup }, + mappingsEditor: { setup: mappingsEditorSetup, getMappingsEditorDataFactory }, }; -export { MappingsEditorTestBed }; +export { MappingsEditorTestBed, DomFields }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index c8c8ef8bfe9b3d..58242ec35018c8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -4,7 +4,11 @@ * 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 { ReactWrapper } from 'enzyme'; + import { registerTestBed, TestBed, nextTick } from '../../../../../../../../../test_utils'; +import { getChildFieldsName } from '../../../lib'; import { MappingsEditor } from '../../../mappings_editor'; jest.mock('@elastic/eui', () => ({ @@ -14,6 +18,7 @@ jest.mock('@elastic/eui', () => ({ EuiComboBox: (props: any) => ( { props.onChange([syntheticEvent['0']]); }} @@ -29,14 +34,121 @@ jest.mock('@elastic/eui', () => ({ }} /> ), + // Mocking EuiSuperSelect to be able to easily change its value + // with a `myWrapper.simulate('change', { target: { value: 'someValue' } })` + EuiSuperSelect: (props: any) => ( + { + props.onChange(e.target.value); + }} + /> + ), })); +export interface DomFields { + [key: string]: { + type: string; + properties?: DomFields; + fields?: DomFields; + }; +} + const createActions = (testBed: TestBed) => { - const { find, waitFor, form, component } = testBed; + const { find, exists, waitFor, waitForFn, form, component } = testBed; + + const getFieldInfo = (testSubjectField: string): { name: string; type: string } => { + const name = find(`${testSubjectField}-fieldName` as TestSubjects).text(); + const type = find(`${testSubjectField}-datatype` as TestSubjects).props()['data-type-value']; + return { name, type }; + }; + + const expandField = async ( + field: ReactWrapper + ): Promise<{ hasChildren: boolean; testSubjectField: string }> => { + /** + * Field list item have 2 test subject assigned to them: + * data-test-subj="fieldsListItem " + * + * We read the second one as it is unique. + */ + const testSubjectField = (field.props() as any)['data-test-subj'] + .split(' ') + .filter((subj: string) => subj !== 'fieldsListItem')[0] as string; + + const expandButton = find(`${testSubjectField}.toggleExpandButton` as TestSubjects); + + // No expand button, so this field is not expanded + if (expandButton.length === 0) { + return { hasChildren: false, testSubjectField }; + } + + const isExpanded = (expandButton.props()['aria-label'] as string).includes('Collapse'); + + if (!isExpanded) { + expandButton.simulate('click'); + } + + // Wait for the children FieldList to be in the DOM + await waitFor(`${testSubjectField}.fieldsList` as TestSubjects); + + return { hasChildren: true, testSubjectField }; + }; + + /** + * Expand all the children of a field and return a metadata object of the fields found in the DOM. + * + * @param fieldName The field under wich we want to expand all the children. + * If no fieldName is provided, we expand all the **root** level fields. + */ + const expandAllFieldsAndReturnMetadata = async ( + fieldName?: string, + domTreeMetadata: DomFields = {} + ): Promise => { + const fields = find( + fieldName ? (`${fieldName}.fieldsList.fieldsListItem` as TestSubjects) : 'fieldsListItem' + ).map(wrapper => wrapper); // convert to Array for our for of loop below + + for (const field of fields) { + const { hasChildren, testSubjectField } = await expandField(field); + + // Read the info from the DOM about that field and add it to our domFieldMeta + const { name, type } = getFieldInfo(testSubjectField); + domTreeMetadata[name] = { + type, + }; + + if (hasChildren) { + // Update our metadata object + const childFieldName = getChildFieldsName(type as any)!; + domTreeMetadata[name][childFieldName] = {}; + + // Expand its children + await expandAllFieldsAndReturnMetadata( + testSubjectField, + domTreeMetadata[name][childFieldName] + ); + } + } + + return domTreeMetadata; + }; + + // Get a nested field in the rendered DOM tree + const getFieldAt = (path: string) => { + const testSubjectField = `${path.split('.').join('')}Field`; + return find(testSubjectField as TestSubjects); + }; const addField = async (name: string, type: string) => { const currentCount = find('fieldsListItem').length; + if (!exists('createFieldForm')) { + find('addFieldButton').simulate('click'); + await waitFor('createFieldForm'); + } + form.setInputValue('nameParameterInput', name); find('createFieldForm.fieldType').simulate('change', [ { @@ -54,6 +166,36 @@ const createActions = (testBed: TestBed) => { await waitFor('fieldsListItem', currentCount + 1); }; + const startEditField = (path: string) => { + const field = getFieldAt(path); + find('editFieldButton', field).simulate('click'); + component.update(); + }; + + const updateFieldAndCloseFlyout = () => { + find('mappingsEditorFieldEdit.editFieldUpdateButton').simulate('click'); + component.update(); + }; + + const showAdvancedSettings = async () => { + const checkIsVisible = async () => + find('mappingsEditorFieldEdit.advancedSettings').props().style.display === 'block'; + + if (await checkIsVisible()) { + // Already opened, nothing else to do + return; + } + + await act(async () => { + find('mappingsEditorFieldEdit.toggleAdvancedSetting').simulate('click'); + }); + + await waitForFn( + checkIsVisible, + 'Error waiting for the advanced settings CSS style.display to be "block"' + ); + }; + const selectTab = async (tab: 'fields' | 'templates' | 'advanced') => { const index = ['fields', 'templates', 'advanced'].indexOf(tab); const tabIdToContentMap: { [key: string]: TestSubjects } = { @@ -87,11 +229,33 @@ const createActions = (testBed: TestBed) => { return value; }; + const getComboBoxValue = (testSubject: TestSubjects) => { + const value = find(testSubject).props()['data-currentvalue']; + if (value === undefined) { + return []; + } + return value.map(({ label }: any) => label); + }; + + const getToggleValue = (testSubject: TestSubjects): boolean => + find(testSubject).props()['aria-checked']; + + const getCheckboxValue = (testSubject: TestSubjects): boolean => + find(testSubject).props().checked; + return { selectTab, + getFieldAt, addField, + expandAllFieldsAndReturnMetadata, + startEditField, + updateFieldAndCloseFlyout, + showAdvancedSettings, updateJsonEditor, getJsonEditorValue, + getComboBoxValue, + getToggleValue, + getCheckboxValue, }; }; @@ -109,6 +273,33 @@ export const setup = async (props: any = { onUpdate() {} }): Promise) => { + /** + * Helper to access the latest data sent to the onChange handler back to the consumer of the . + * Read the latest call with its argument passed and build the mappings object from it. + */ + return async () => { + const mockCalls = onChangeHandler.mock.calls; + + if (mockCalls.length === 0) { + throw new Error( + `Can't access data forwarded as the onChange() prop handler hasn't been called.` + ); + } + + const [arg] = mockCalls[mockCalls.length - 1]; + const { isValid, validate, getData } = arg; + + const isMappingsValid = isValid === undefined ? await act(validate) : isValid; + const data = getData(isMappingsValid); + + return { + isValid: isMappingsValid, + data, + }; + }; +}; + export type MappingsEditorTestBed = TestBed & { actions: ReturnType; }; @@ -116,7 +307,9 @@ export type MappingsEditorTestBed = TestBed & { export type TestSubjects = | 'formTab' | 'mappingsEditor' + | 'fieldsList' | 'fieldsListItem' + | 'fieldsListItem.fieldName' | 'fieldName' | 'mappingTypesDetectedCallout' | 'documentFields' @@ -126,7 +319,38 @@ export type TestSubjects = | 'advancedConfiguration.numericDetection.input' | 'advancedConfiguration.dynamicMappingsToggle' | 'advancedConfiguration.dynamicMappingsToggle.input' + | 'advancedConfiguration.metaField' + | 'advancedConfiguration.routingRequiredToggle.input' + | 'sourceField.includesField' + | 'sourceField.excludesField' | 'dynamicTemplatesEditor' | 'nameParameterInput' + | 'addFieldButton' + | 'editFieldButton' + | 'toggleExpandButton' + | 'createFieldForm' | 'createFieldForm.fieldType' - | 'createFieldForm.addButton'; + | 'createFieldForm.addButton' + | 'mappingsEditorFieldEdit' + | 'mappingsEditorFieldEdit.fieldType' + | 'mappingsEditorFieldEdit.editFieldUpdateButton' + | 'mappingsEditorFieldEdit.flyoutTitle' + | 'mappingsEditorFieldEdit.documentationLink' + | 'mappingsEditorFieldEdit.fieldPath' + | 'mappingsEditorFieldEdit.advancedSettings' + | 'mappingsEditorFieldEdit.toggleAdvancedSetting' + | 'indexParameter.formRowToggle' + | 'indexAnalyzer.select' + | 'searchAnalyzer' + | 'searchAnalyzer.select' + | 'searchQuoteAnalyzer' + | 'searchQuoteAnalyzer.select' + | 'indexAnalyzer-custom' + | 'indexAnalyzer-custom.input' + | 'searchAnalyzer-toggleCustomButton' + | 'searchAnalyzer-custom' + | 'searchAnalyzer-custom.input' + | 'searchQuoteAnalyzer-custom' + | 'searchQuoteAnalyzer-toggleCustomButton' + | 'searchQuoteAnalyzer-custom.input' + | 'useSameAnalyzerForSearchCheckBox.input'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx new file mode 100644 index 00000000000000..8989e85d9f188d --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed, DomFields, nextTick } from './helpers'; + +const { setup } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); + +describe('Mappings editor: mapped fields', () => { + afterEach(() => { + onChangeHandler.mockReset(); + }); + + describe('', () => { + let testBed: MappingsEditorTestBed; + const defaultMappings = { + properties: { + myField: { + type: 'text', + fields: { + raw: { + type: 'keyword', + }, + simpleAnalyzer: { + type: 'text', + }, + }, + }, + myObject: { + type: 'object', + properties: { + deeplyNested: { + type: 'object', + properties: { + title: { + type: 'text', + fields: { + raw: { type: 'keyword' }, + }, + }, + }, + }, + }, + }, + }, + }; + + test('should correctly represent the fields in the DOM tree', async () => { + await act(async () => { + testBed = await setup({ + value: defaultMappings, + onChange: onChangeHandler, + }); + }); + + const { + actions: { expandAllFieldsAndReturnMetadata }, + } = testBed; + + let domTreeMetadata: DomFields = {}; + await act(async () => { + domTreeMetadata = await expandAllFieldsAndReturnMetadata(); + }); + + expect(domTreeMetadata).toEqual(defaultMappings.properties); + }); + + test('should allow to be controlled by parent component and update on prop change', async () => { + await act(async () => { + testBed = await setup({ + value: defaultMappings, + onChange: onChangeHandler, + }); + }); + + const { + component, + setProps, + actions: { expandAllFieldsAndReturnMetadata }, + } = testBed; + + const newMappings = { properties: { hello: { type: 'text' } } }; + let domTreeMetadata: DomFields = {}; + + await act(async () => { + // Change the `value` prop of our + setProps({ value: newMappings }); + + // Don't ask me why but the 3 following lines are all required + component.update(); + await nextTick(); + component.update(); + + domTreeMetadata = await expandAllFieldsAndReturnMetadata(); + }); + + expect(domTreeMetadata).toEqual(newMappings.properties); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index 0cf5bf3f4453ff..f516dfdb372ce7 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -5,15 +5,55 @@ */ import { act } from 'react-dom/test-utils'; -import { componentHelpers, MappingsEditorTestBed, nextTick, getRandomString } from './helpers'; +import { componentHelpers, MappingsEditorTestBed, nextTick } from './helpers'; -const { setup } = componentHelpers.mappingsEditor; -const mockOnUpdate = () => undefined; +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; +const onChangeHandler = jest.fn(); +const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + +describe('Mappings editor: core', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + + afterEach(() => { + onChangeHandler.mockReset(); + }); + + test('default behaviour', async () => { + const defaultMappings = { + properties: { + user: { + // No type defined for user + properties: { + name: { type: 'text' }, + }, + }, + }, + }; + + await setup({ value: defaultMappings, onChange: onChangeHandler }); + + const expectedMappings = { + _meta: {}, // Was not defined so an empty object is returned + _source: {}, // Was not defined so an empty object is returned + ...defaultMappings, + properties: { + user: { + type: 'object', // Was not defined so it defaults to "object" type + ...defaultMappings.properties.user, + }, + }, + }; + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(expectedMappings); + }); -describe('', () => { describe('multiple mappings detection', () => { test('should show a warning when multiple mappings are detected', async () => { - const defaultValue = { + const value = { type1: { properties: { name1: { @@ -29,7 +69,7 @@ describe('', () => { }, }, }; - const testBed = await setup({ onUpdate: mockOnUpdate, defaultValue }); + const testBed = await setup({ onChange: onChangeHandler, value }); const { exists } = testBed; expect(exists('mappingsEditor')).toBe(true); @@ -38,14 +78,14 @@ describe('', () => { }); test('should not show a warning when mappings a single-type', async () => { - const defaultValue = { + const value = { properties: { name1: { type: 'keyword', }, }, }; - const testBed = await setup({ onUpdate: mockOnUpdate, defaultValue }); + const testBed = await setup({ onChange: onChangeHandler, value }); const { exists } = testBed; expect(exists('mappingsEditor')).toBe(true); @@ -62,12 +102,12 @@ describe('', () => { let testBed: MappingsEditorTestBed; beforeEach(async () => { - testBed = await setup({ defaultValue: defaultMappings, onUpdate() {} }); + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); }); test('should keep the changes when switching tabs', async () => { const { - actions: { addField, selectTab, updateJsonEditor, getJsonEditorValue }, + actions: { addField, selectTab, updateJsonEditor, getJsonEditorValue, getToggleValue }, component, find, exists, @@ -79,7 +119,7 @@ describe('', () => { // ------------------------------------- expect(find('fieldsListItem').length).toEqual(0); // Check that we start with an empty list - const newField = { name: getRandomString(), type: 'text' }; + const newField = { name: 'John', type: 'text' }; await act(async () => { await addField(newField.name, newField.type); }); @@ -101,7 +141,6 @@ describe('', () => { // Update the dynamic templates editor value const updatedValueTemplates = [{ after: 'bar' }]; - await act(async () => { await updateJsonEditor('dynamicTemplatesEditor', updatedValueTemplates); await nextTick(); @@ -118,9 +157,9 @@ describe('', () => { await selectTab('advanced'); }); - let isDynamicMappingsEnabled = find( + let isDynamicMappingsEnabled = getToggleValue( 'advancedConfiguration.dynamicMappingsToggle.input' - ).props()['aria-checked']; + ); expect(isDynamicMappingsEnabled).toBe(true); let isNumericDetectionVisible = exists('advancedConfiguration.numericDetection'); @@ -134,9 +173,9 @@ describe('', () => { await nextTick(); }); - isDynamicMappingsEnabled = find('advancedConfiguration.dynamicMappingsToggle.input').props()[ - 'aria-checked' - ]; + isDynamicMappingsEnabled = getToggleValue( + 'advancedConfiguration.dynamicMappingsToggle.input' + ); expect(isDynamicMappingsEnabled).toBe(false); isNumericDetectionVisible = exists('advancedConfiguration.numericDetection'); @@ -166,12 +205,185 @@ describe('', () => { await selectTab('advanced'); }); - isDynamicMappingsEnabled = find('advancedConfiguration.dynamicMappingsToggle.input').props()[ - 'aria-checked' - ]; + isDynamicMappingsEnabled = getToggleValue( + 'advancedConfiguration.dynamicMappingsToggle.input' + ); expect(isDynamicMappingsEnabled).toBe(false); isNumericDetectionVisible = exists('advancedConfiguration.numericDetection'); expect(isNumericDetectionVisible).toBe(false); }); }); + + describe('component props', () => { + /** + * Note: the "indexSettings" prop will be tested along with the "analyzer" parameter on a text datatype field, + * as it is the only place where it is consumed by the mappings editor. + * + * The test that covers it is text_datatype.test.tsx: "analyzer parameter: custom analyzer (from index settings)" + */ + const defaultMappings: any = { + dynamic: true, + numeric_detection: false, + date_detection: true, + properties: { + title: { type: 'text' }, + address: { + type: 'object', + properties: { + street: { type: 'text' }, + city: { type: 'text' }, + }, + }, + }, + dynamic_templates: [{ initial: 'value' }], + _source: { + enabled: true, + includes: ['field1', 'field2'], + excludes: ['field3'], + }, + _meta: { + some: 'metaData', + }, + _routing: { + required: false, + }, + }; + + let testBed: MappingsEditorTestBed; + + beforeEach(async () => { + testBed = await setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + test('props.value => should prepopulate the editor data', async () => { + const { + actions: { selectTab, getJsonEditorValue, getComboBoxValue, getToggleValue }, + find, + } = testBed; + + /** + * Mapped fields + */ + // Test that root-level mappings "properties" are rendered as root-level "DOM tree items" + const fields = find('fieldsListItem.fieldName').map(item => item.text()); + expect(fields).toEqual(Object.keys(defaultMappings.properties).sort()); + + /** + * Dynamic templates + */ + await act(async () => { + await selectTab('templates'); + }); + + // Test that dynamic templates JSON is rendered in the templates editor + const templatesValue = getJsonEditorValue('dynamicTemplatesEditor'); + expect(templatesValue).toEqual(defaultMappings.dynamic_templates); + + /** + * Advanced settings + */ + await act(async () => { + await selectTab('advanced'); + }); + + const isDynamicMappingsEnabled = getToggleValue( + 'advancedConfiguration.dynamicMappingsToggle.input' + ); + expect(isDynamicMappingsEnabled).toBe(defaultMappings.dynamic); + + const isNumericDetectionEnabled = getToggleValue( + 'advancedConfiguration.numericDetection.input' + ); + expect(isNumericDetectionEnabled).toBe(defaultMappings.numeric_detection); + + expect(getComboBoxValue('sourceField.includesField')).toEqual( + defaultMappings._source.includes + ); + expect(getComboBoxValue('sourceField.excludesField')).toEqual( + defaultMappings._source.excludes + ); + + const metaFieldValue = getJsonEditorValue('advancedConfiguration.metaField'); + expect(metaFieldValue).toEqual(defaultMappings._meta); + + const isRoutingRequired = getToggleValue('advancedConfiguration.routingRequiredToggle.input'); + expect(isRoutingRequired).toBe(defaultMappings._routing.required); + }); + + test('props.onChange() => should forward the changes to the consumer component', async () => { + let updatedMappings = { ...defaultMappings }; + + const { + actions: { addField, selectTab, updateJsonEditor }, + component, + form, + } = testBed; + + /** + * Mapped fields + */ + const newField = { name: 'someNewField', type: 'text' }; + updatedMappings = { + ...updatedMappings, + properties: { + ...updatedMappings.properties, + [newField.name]: { type: 'text' }, + }, + }; + + await act(async () => { + await addField(newField.name, newField.type); + }); + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + + /** + * Dynamic templates + */ + await act(async () => { + await selectTab('templates'); + }); + + const updatedTemplatesValue = [{ someTemplateProp: 'updated' }]; + updatedMappings = { + ...updatedMappings, + dynamic_templates: updatedTemplatesValue, + }; + + await act(async () => { + await updateJsonEditor('dynamicTemplatesEditor', updatedTemplatesValue); + await nextTick(); + component.update(); + }); + + ({ data } = await getMappingsEditorData()); + expect(data).toEqual(updatedMappings); + + /** + * Advanced settings + */ + await act(async () => { + await selectTab('advanced'); + }); + + // Disbable dynamic mappings + await act(async () => { + form.toggleEuiSwitch('advancedConfiguration.dynamicMappingsToggle.input'); + }); + + ({ data } = await getMappingsEditorData()); + + // When we disable dynamic mappings, we set it to "false" and remove date and numeric detections + updatedMappings = { + ...updatedMappings, + dynamic: false, + date_detection: undefined, + dynamic_date_formats: undefined, + numeric_detection: undefined, + }; + + expect(data).toEqual(updatedMappings); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index 6b33d4450c3ae2..c84756cab8e886 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useRef } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { useForm, Form, SerializerFunc } from '../../shared_imports'; +import { GenericObject } from '../../types'; import { Types, useDispatch } from '../../mappings_state'; import { DynamicMappingSection } from './dynamic_mapping_section'; import { SourceFieldSection } from './source_field_section'; @@ -17,10 +18,10 @@ import { configurationFormSchema } from './configuration_form_schema'; type MappingsConfiguration = Types['MappingsConfiguration']; interface Props { - defaultValue?: MappingsConfiguration; + value?: MappingsConfiguration; } -const stringifyJson = (json: { [key: string]: any }) => +const stringifyJson = (json: GenericObject) => Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}'; const formSerializer: SerializerFunc = formData => { @@ -57,7 +58,7 @@ const formSerializer: SerializerFunc = formData => { }; }; -const formDeserializer = (formData: { [key: string]: any }) => { +const formDeserializer = (formData: GenericObject) => { const { dynamic, numeric_detection, @@ -86,14 +87,14 @@ const formDeserializer = (formData: { [key: string]: any }) => { }; }; -export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { +export const ConfigurationForm = React.memo(({ value }: Props) => { const didMountRef = useRef(false); const { form } = useForm({ schema: configurationFormSchema, serializer: formSerializer, deserializer: formDeserializer, - defaultValue, + defaultValue: value, }); const dispatch = useDispatch(); @@ -114,14 +115,14 @@ export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { useEffect(() => { if (didMountRef.current) { - // If the defaultValue has changed (it probably means that we have loaded a new JSON) + // If the value has changed (it probably means that we have loaded a new JSON) // we need to reset the form to update the fields values. form.reset({ resetValues: true }); } else { // Avoid reseting the form on component mount. didMountRef.current = true; } - }, [defaultValue]); // eslint-disable-line react-hooks/exhaustive-deps + }, [value]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { return () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx index cb9b464d270ce3..c1a2b195a3f576 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx @@ -67,6 +67,7 @@ export const DynamicMappingSection = () => ( return ( <> @@ -87,6 +88,7 @@ export const DynamicMappingSection = () => ( } else { return ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx index 68b76a1203ad52..7185016029e00e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx @@ -46,6 +46,7 @@ export const MetaFieldSection = () => ( 'aria-label': i18n.translate('xpack.idxMgmt.mappingsEditor.metaFieldEditorAriaLabel', { defaultMessage: '_meta field data editor', }), + 'data-test-subj': 'metaField', }, }} /> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx index 7f434d6f834b2b..f06b292bc33c8e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx @@ -35,7 +35,11 @@ export const RoutingSection = () => { /> } > - + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx index f79741d9a1a9f1..4278598dfc7c16 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx @@ -65,7 +65,7 @@ export const SourceFieldSection = () => { ); const renderFormFields = () => ( - <> +
{({ label, helpText, value, setValue }) => ( @@ -89,6 +89,7 @@ export const SourceFieldSection = () => { setValue([...(value as ComboBoxOption[]), newOption]); }} fullWidth + data-test-subj="includesField" /> )} @@ -119,11 +120,12 @@ export const SourceFieldSection = () => { setValue([...(value as ComboBoxOption[]), newOption]); }} fullWidth + data-test-subj="excludesField" /> )} - +
); return ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx index a97e3b227311c7..569af5d21cdb09 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx @@ -25,6 +25,7 @@ interface Props { label?: string; config?: FieldConfig; allowsIndexDefaultOption?: boolean; + 'data-test-subj'?: string; } const ANALYZER_OPTIONS = PARAMETERS_OPTIONS.analyzer!; @@ -68,6 +69,7 @@ export const AnalyzerParameter = ({ label, config, allowsIndexDefaultOption = true, + 'data-test-subj': dataTestSubj, }: Props) => { const indexSettings = useIndexSettings(); const customAnalyzers = getCustomAnalyzers(indexSettings); @@ -131,6 +133,11 @@ export const AnalyzerParameter = ({ !isDefaultValueInOptions && !isDefaultValueInSubOptions ); + const [selectsDefaultValue, setSelectsDefaultValue] = useState({ + main: mainValue, + sub: subValue, + }); + const fieldConfig = config ? config : getFieldConfig('analyzer'); const fieldConfigWithLabel = label !== undefined ? { ...fieldConfig, label } : fieldConfig; @@ -142,6 +149,7 @@ export const AnalyzerParameter = ({ } field.reset({ resetValue: false }); + setSelectsDefaultValue({ main: undefined, sub: undefined }); setIsCustom(!isCustom); }; @@ -154,6 +162,7 @@ export const AnalyzerParameter = ({ size="xs" onClick={toggleCustom(field)} className="mappingsEditor__selectWithCustom__button" + data-test-subj={`${dataTestSubj}-toggleCustomButton`} > {isCustom ? i18n.translate('xpack.idxMgmt.mappingsEditor.predefinedButtonLabel', { @@ -169,17 +178,18 @@ export const AnalyzerParameter = ({ // around the field. - + ) : ( )} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx index a91231352c1684..a44fd2257f52b2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx @@ -36,6 +36,7 @@ interface Props { config: FieldConfig; options: Options; mapOptionsToSubOptions: MapOptionsToSubOptions; + 'data-test-subj'?: string; } export const AnalyzerParameterSelects = ({ @@ -45,6 +46,7 @@ export const AnalyzerParameterSelects = ({ config, options, mapOptionsToSubOptions, + 'data-test-subj': dataTestSubj, }: Props) => { const { form } = useForm({ defaultValue: { main: mainDefaultValue, sub: subDefaultValue } }); @@ -76,11 +78,16 @@ export const AnalyzerParameterSelects = ({ const isSuperSelect = areOptionsSuperSelect(opts); return isSuperSelect ? ( - + ) : ( ); }; @@ -102,9 +109,9 @@ export const AnalyzerParameterSelects = ({ diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx index 0cf22946bf60a6..f99aa4d1eca9a5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx @@ -34,6 +34,7 @@ export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: P href: documentationService.getAnalyzerLink(), }} withToggle={false} + data-test-subj="analyzerParameters" > {({ useSameAnalyzerForSearch }) => { @@ -50,6 +51,7 @@ export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: P path="analyzer" label={label} defaultValue={field.source.analyzer as string} + data-test-subj="indexAnalyzer" /> ); }} @@ -60,6 +62,9 @@ export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: P @@ -94,6 +100,7 @@ export const AnalyzersParameter = ({ field, withSearchQuoteAnalyzer = false }: P path="search_quote_analyzer" defaultValue={field.source.search_quote_analyzer as string} config={getFieldConfig('search_quote_analyzer')} + data-test-subj="searchQuoteAnalyzer" /> )} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx index fec8e49a1991ca..3e91e97eef618a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx @@ -39,6 +39,7 @@ export const IndexParameter = ({ href: documentationService.getIndexLink(), }} formFieldPath="index" + data-test-subj="indexParameter" > {/* index_options */} {hasIndexOptions ? ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx index 03c774227924ea..2046675881c29f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx @@ -23,7 +23,7 @@ export const AdvancedParametersSection = ({ children }: Props) => {
- + {isVisible ? i18n.translate('xpack.idxMgmt.mappingsEditor.advancedSettings.hideButtonLabel', { defaultMessage: 'Hide advanced settings', @@ -33,7 +33,7 @@ export const AdvancedParametersSection = ({ children }: Props) => { })} -
+
{/* We ned to wrap the children inside a "div" to have our css :first-child rule */}
{children}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 489424a07e04d8..854270f313e59c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -96,7 +96,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props
{/* Title */} -

+

{isMultiField ? i18n.translate( 'xpack.idxMgmt.mappingsEditor.editMultiFieldTitle', @@ -127,6 +127,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props href={linkDocumentation} target="_blank" iconType="help" + data-test-subj="documentationLink" > {i18n.translate( 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', @@ -146,7 +147,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props {/* Field path */} - + {field.path.join(' > ')} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx index 97a7d205c13553..1c079c8d5cf879 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx @@ -42,6 +42,7 @@ interface Props { children?: React.ReactNode | ChildrenFunc; withToggle?: boolean; configPath?: ParameterName; + 'data-test-subj'?: string; } export const EditFieldFormRow = React.memo( @@ -54,6 +55,7 @@ export const EditFieldFormRow = React.memo( children, withToggle = true, configPath, + 'data-test-subj': dataTestSubj, }: Props) => { const form = useFormContext(); @@ -87,7 +89,7 @@ export const EditFieldFormRow = React.memo( label={title} checked={isContentVisible} onChange={onToggle} - data-test-subj="input" + data-test-subj="formRowToggle" showLabel={false} /> ) : ( @@ -99,7 +101,17 @@ export const EditFieldFormRow = React.memo( }} > {field => { - return ; + return ( + + ); }} ); @@ -165,7 +177,7 @@ export const EditFieldFormRow = React.memo( ); return ( - + {toggle} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx index 6df86d561a532d..c0d922e0d1d373 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -18,7 +18,7 @@ export const FieldsList = React.memo(function FieldsListComponent({ fields, tree return null; } return ( -
    +
      {fields.map((field, index) => (
      {source.name} - + {isMultiField ? i18n.translate('xpack.idxMgmt.mappingsEditor.multiFieldBadgeLabel', { defaultMessage: '{dataType} multi-field', values: { - dataType: TYPE_DEFINITION[source.type].label, + dataType: getTypeLabelFromType(source.type), }, }) : getTypeLabelFromType(source.type)} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx index 3c4d6b08ebe449..f4aa17bf6fed92 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx @@ -16,7 +16,7 @@ import { documentationService } from '../../../../services/documentation'; type MappingsTemplates = Types['MappingsTemplates']; interface Props { - defaultValue?: MappingsTemplates; + value?: MappingsTemplates; } const stringifyJson = (json: { [key: string]: any }) => @@ -50,14 +50,14 @@ const formDeserializer = (formData: { [key: string]: any }) => { }; }; -export const TemplatesForm = React.memo(({ defaultValue }: Props) => { +export const TemplatesForm = React.memo(({ value }: Props) => { const didMountRef = useRef(false); const { form } = useForm({ schema: templatesFormSchema, serializer: formSerializer, deserializer: formDeserializer, - defaultValue, + defaultValue: value, }); const dispatch = useDispatch(); @@ -73,14 +73,14 @@ export const TemplatesForm = React.memo(({ defaultValue }: Props) => { useEffect(() => { if (didMountRef.current) { - // If the defaultValue has changed (it probably means that we have loaded a new JSON) + // If the value has changed (it probably means that we have loaded a new JSON) // we need to reset the form to update the fields values. form.reset({ resetValues: true }); } else { // Avoid reseting the form on component mount. didMountRef.current = true; } - }, [defaultValue]); // eslint-disable-line react-hooks/exhaustive-deps + }, [value]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { return () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index 0431ea472643b7..4b610ff0b401df 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -6,7 +6,7 @@ jest.mock('../constants', () => ({ MAIN_DATA_TYPE_DEFINITION: {} })); -import { isStateValid } from './utils'; +import { isStateValid, stripUndefinedValues } from './utils'; describe('utils', () => { describe('isStateValid()', () => { @@ -62,4 +62,49 @@ describe('utils', () => { expect(isStateValid(components)).toBe(false); }); }); + + describe('stripUndefinedValues()', () => { + test('should remove all undefined value recursively', () => { + const myDate = new Date(); + + const dataIN = { + someString: 'world', + someNumber: 123, + someBoolean: true, + someArray: [1, 2, 3], + someEmptyObject: {}, + someDate: myDate, + falsey1: 0, + falsey2: '', + stripThis: undefined, + nested: { + value: 'bar', + stripThis: undefined, + deepNested: { + value: 'baz', + stripThis: undefined, + }, + }, + }; + + const dataOUT = { + someString: 'world', + someNumber: 123, + someBoolean: true, + someArray: [1, 2, 3], + someEmptyObject: {}, + someDate: myDate, + falsey1: 0, + falsey2: '', + nested: { + value: 'bar', + deepNested: { + value: 'baz', + }, + }, + }; + + expect(stripUndefinedValues(dataIN)).toEqual(dataOUT); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index cece26618ced87..306e0448df379b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -17,6 +17,7 @@ import { ChildFieldName, ParameterName, ComboBoxOption, + GenericObject, } from '../types'; import { @@ -32,11 +33,9 @@ import { State } from '../reducer'; import { FieldConfig } from '../shared_imports'; import { TreeItem } from '../components/tree'; -export const getUniqueId = () => { - return uuid.v4(); -}; +export const getUniqueId = () => uuid.v4(); -const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { +export const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { if (dataType === 'text' || dataType === 'keyword') { return 'fields'; } else if (dataType === 'object' || dataType === 'nested') { @@ -508,3 +507,39 @@ export const isStateValid = (state: State): boolean | undefined => return isValid && value.isValid; }, true as undefined | boolean); + +/** + * This helper removes all the keys on an object with an "undefined" value. + * To avoid sending updates from the mappings editor with this type of object: + * + *``` + * { + * "dyamic": undefined, + * "date_detection": undefined, + * "dynamic": undefined, + * "dynamic_date_formats": undefined, + * "dynamic_templates": undefined, + * "numeric_detection": undefined, + * "properties": { + * "title": { "type": "text" } + * } + * } + *``` + * + * @param obj The object to retrieve the undefined values from + * @param recursive A flag to strip recursively into children objects + */ +export const stripUndefinedValues = (obj: GenericObject, recursive = true): T => + Object.entries(obj).reduce((acc, [key, value]) => { + if (value === undefined) { + return acc; + } + + if (Array.isArray(value) || value instanceof Date || value === null) { + return { ...acc, [key]: value }; + } + + return recursive && typeof value === 'object' + ? { ...acc, [key]: stripUndefinedValues(value, recursive) } + : { ...acc, [key]: value }; + }, {} as T); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index 316fee55526a3b..46dc1176f62b4b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -21,18 +21,18 @@ import { MappingsState, Props as MappingsStateProps, Types } from './mappings_st import { IndexSettingsProvider } from './index_settings_context'; interface Props { - onUpdate: MappingsStateProps['onUpdate']; - defaultValue?: { [key: string]: any }; + onChange: MappingsStateProps['onChange']; + value?: { [key: string]: any }; indexSettings?: IndexSettings; } type TabName = 'fields' | 'advanced' | 'templates'; -export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSettings }: Props) => { +export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Props) => { const [selectedTab, selectTab] = useState('fields'); const { parsedDefaultValue, multipleMappingsDeclared } = useMemo(() => { - const mappingsDefinition = extractMappingsDefinition(defaultValue); + const mappingsDefinition = extractMappingsDefinition(value); if (mappingsDefinition === null) { return { multipleMappingsDeclared: true }; @@ -67,18 +67,18 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting }; return { parsedDefaultValue: parsed, multipleMappingsDeclared: false }; - }, [defaultValue]); + }, [value]); useEffect(() => { if (multipleMappingsDeclared) { // We set the data getter here as the user won't be able to make any changes - onUpdate({ - getData: () => defaultValue! as Types['Mappings'], + onChange({ + getData: () => value! as Types['Mappings'], validate: () => Promise.resolve(true), isValid: true, }); } - }, [multipleMappingsDeclared, onUpdate, defaultValue]); + }, [multipleMappingsDeclared, onChange, value]); const changeTab = async (tab: TabName, state: State) => { if (selectedTab === 'advanced') { @@ -108,12 +108,12 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting ) : ( - + {({ state }) => { const tabToContentMap = { fields: , - templates: , - advanced: , + templates: , + advanced: , }; return ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx index a9d26b953b96e0..280ea5c3dd28ce 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx @@ -16,7 +16,7 @@ import { Dispatch, } from './reducer'; import { Field } from './types'; -import { normalize, deNormalize } from './lib'; +import { normalize, deNormalize, stripUndefinedValues } from './lib'; type Mappings = MappingsTemplates & MappingsConfiguration & { @@ -43,36 +43,34 @@ const DispatchContext = createContext(undefined); export interface Props { children: (params: { state: State }) => React.ReactNode; - defaultValue: { + value: { templates: MappingsTemplates; configuration: MappingsConfiguration; fields: { [key: string]: Field }; }; - onUpdate: OnUpdateHandler; + onChange: OnUpdateHandler; } -export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => { +export const MappingsState = React.memo(({ children, onChange, value }: Props) => { const didMountRef = useRef(false); - const parsedFieldsDefaultValue = useMemo(() => normalize(defaultValue.fields), [ - defaultValue.fields, - ]); + const parsedFieldsDefaultValue = useMemo(() => normalize(value.fields), [value.fields]); const initialState: State = { isValid: undefined, configuration: { - defaultValue: defaultValue.configuration, + defaultValue: value.configuration, data: { - raw: defaultValue.configuration, - format: () => defaultValue.configuration, + raw: value.configuration, + format: () => value.configuration, }, validate: () => Promise.resolve(true), }, templates: { - defaultValue: defaultValue.templates, + defaultValue: value.templates, data: { - raw: defaultValue.templates, - format: () => defaultValue.templates, + raw: value.templates, + format: () => value.templates, }, validate: () => Promise.resolve(true), }, @@ -105,7 +103,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P const bypassFieldFormValidation = state.documentFields.status === 'creatingField' && emptyNameValue; - onUpdate({ + onChange({ // Output a mappings object from the user's input. getData: (isValid: boolean) => { let nextState = state; @@ -135,8 +133,10 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P const templatesData = nextState.templates.data.format(); return { - ...configurationData, - ...templatesData, + ...stripUndefinedValues({ + ...configurationData, + ...templatesData, + }), properties: fields, }; }, @@ -169,26 +169,26 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P }, isValid: state.isValid, }); - }, [state, onUpdate]); + }, [state, onChange]); useEffect(() => { /** - * If the defaultValue has changed that probably means that we have loaded + * If the value has changed that probably means that we have loaded * new data from JSON. We need to update our state with the new mappings. */ if (didMountRef.current) { dispatch({ type: 'editor.replaceMappings', value: { - configuration: defaultValue.configuration, - templates: defaultValue.templates, + configuration: value.configuration, + templates: value.templates, fields: parsedFieldsDefaultValue, }, }); } else { didMountRef.current = true; } - }, [defaultValue, parsedFieldsDefaultValue]); + }, [value, parsedFieldsDefaultValue]); return ( diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx index cf9b57dcbcb14d..d74dd435ecdae0 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx @@ -101,8 +101,8 @@ export const StepMappings: React.FunctionComponent = ({ {/* Mappings code editor */} diff --git a/x-pack/test_utils/testbed/testbed.ts b/x-pack/test_utils/testbed/testbed.ts index 9bf07f953595c7..b6ec0f8997e1cf 100644 --- a/x-pack/test_utils/testbed/testbed.ts +++ b/x-pack/test_utils/testbed/testbed.ts @@ -5,6 +5,7 @@ */ import { ComponentType, ReactWrapper } from 'enzyme'; + import { findTestSubject } from '../find_test_subject'; import { reactRouterMock } from '../router_helpers'; import { @@ -138,33 +139,23 @@ export const registerTestBed = ( }); }; - const waitFor: TestBed['waitFor'] = async (testSubject: T, count = 1) => { + const waitForFn: TestBed['waitForFn'] = async (predicate, errMessage) => { const triggeredAt = Date.now(); - /** - * The way jest run tests in parallel + the not deterministic DOM update from React "hooks" - * add flakiness to the tests. This is especially true for component integration tests that - * make many update to the DOM. - * - * For this reason, when we _know_ that an element should be there after we updated some state, - * we will give it 30 seconds to appear in the DOM, checking every 100 ms for its presence. - */ const MAX_WAIT_TIME = 30000; - const WAIT_INTERVAL = 100; + const WAIT_INTERVAL = 50; const process = async (): Promise => { - const elemFound = exists(testSubject, count); + const isOK = await predicate(); - if (elemFound) { + if (isOK) { // Great! nothing else to do here. return; } const timeElapsed = Date.now() - triggeredAt; if (timeElapsed > MAX_WAIT_TIME) { - throw new Error( - `I waited patiently for the "${testSubject}" test subject to appear with no luck. It is nowhere to be found!` - ); + throw new Error(errMessage); } return new Promise(resolve => setTimeout(resolve, WAIT_INTERVAL)).then(() => { @@ -176,6 +167,13 @@ export const registerTestBed = ( return process(); }; + const waitFor: TestBed['waitFor'] = (testSubject: T, count = 1) => { + return waitForFn( + () => Promise.resolve(exists(testSubject, count)), + `I waited patiently for the "${testSubject}" test subject to appear with no luck. It is nowhere to be found!` + ); + }; + /** * ---------------------------------------------------------------- * Forms @@ -201,6 +199,18 @@ export const registerTestBed = ( return new Promise(resolve => setTimeout(resolve)); }; + const setSelectValue: TestBed['form']['setSelectValue'] = (select, value) => { + const formSelect = typeof select === 'string' ? find(select) : (select as ReactWrapper); + + if (!formSelect.length) { + throw new Error(`Select "${select}" was not found.`); + } + + formSelect.simulate('change', { target: { value } }); + + component.update(); + }; + const selectCheckBox: TestBed['form']['selectCheckBox'] = ( testSubject, isChecked = true @@ -293,11 +303,13 @@ export const registerTestBed = ( find, setProps, waitFor, + waitForFn, table: { getMetaData, }, form: { setInputValue, + setSelectValue, selectCheckBox, toggleEuiSwitch, setComboBoxValue, diff --git a/x-pack/test_utils/testbed/types.ts b/x-pack/test_utils/testbed/types.ts index f3704bb463ecf9..4cc7deac601563 100644 --- a/x-pack/test_utils/testbed/types.ts +++ b/x-pack/test_utils/testbed/types.ts @@ -41,7 +41,7 @@ export interface TestBed { * * @example * - ```ts + ```typescript find('nameInput'); // or more specific, // "nameInput" is a child of "myForm" @@ -61,6 +61,7 @@ export interface TestBed { * and we need to wait for the data to be fetched (and bypass any "loading" state). */ waitFor: (testSubject: T, count?: number) => Promise; + waitForFn: (predicate: () => Promise, errMessage: string) => Promise; form: { /** * Set the value of a form text input. @@ -79,6 +80,28 @@ export interface TestBed { value: string, isAsync?: boolean ) => Promise | void; + /** + * Set the value of a or a mocked + * For the you need to mock it like this + * + ```typescript + jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + EuiSuperSelect: (props: any) => ( + { + props.onChange(e.target.value); + }} + /> + ), + })); + ``` + * @param select The form select. Can either be a data-test-subj or a reactWrapper (can be a nested path. e.g. "myForm.myInput"). + * @param value The value to set + */ + setSelectValue: (select: T | ReactWrapper, value: string) => void; /** * Select or unselect a form checkbox. * From dddeec51b72bb9cd49e3b8eccfc7004ba983da0d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 7 May 2020 11:52:28 +0200 Subject: [PATCH 04/16] [ML] Fix the limit control on the Anomaly explorer page (#65459) * [ML] persist limit control value * [ML] remove console statement * [ML] fix default value --- .../plugins/ml/public/application/explorer/explorer.js | 9 ++------- .../explorer/select_limit/select_limit.test.tsx | 2 -- .../application/explorer/select_limit/select_limit.tsx | 6 +++--- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 86d16776b68e2a..36dac05add5570 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import DragSelect from 'dragselect/dist/ds.min.js'; import { Subject } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { takeUntil } from 'rxjs/operators'; import { EuiFlexGroup, @@ -169,12 +169,7 @@ export class Explorer extends React.Component { }; componentDidMount() { - limit$ - .pipe( - takeUntil(this._unsubscribeAll), - map(d => d.val) - ) - .subscribe(explorerService.setSwimlaneLimit); + limit$.pipe(takeUntil(this._unsubscribeAll)).subscribe(explorerService.setSwimlaneLimit); // Required to redraw the time series chart when the container is resized. this.resizeChecker = new ResizeChecker(this.resizeRef.current); diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx index 657f1c6c7af2ed..cf65419e4bd801 100644 --- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx +++ b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx @@ -9,8 +9,6 @@ import { act } from 'react-dom/test-utils'; import { shallow } from 'enzyme'; import { SelectLimit } from './select_limit'; -jest.useFakeTimers(); - describe('SelectLimit', () => { test('creates correct initial selected value', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx index 383d07eb7a9f60..03e3273b808327 100644 --- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx +++ b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx @@ -9,7 +9,7 @@ */ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { Subject } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { EuiSelect } from '@elastic/eui'; @@ -20,13 +20,13 @@ const euiOptions = limitOptions.map(limit => ({ text: `${limit}`, })); -export const limit$ = new Subject(); export const defaultLimit = limitOptions[1]; +export const limit$ = new BehaviorSubject(defaultLimit); export const useSwimlaneLimit = (): [number, (newLimit: number) => void] => { const limit = useObservable(limit$, defaultLimit); - return [limit, (newLimit: number) => limit$.next(newLimit)]; + return [limit!, (newLimit: number) => limit$.next(newLimit)]; }; export const SelectLimit = () => { From ab5943c71d2f52d73f15562251389a4597cefdb1 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 7 May 2020 12:10:15 +0200 Subject: [PATCH 05/16] [ML] Hide selector helper in Anomaly Explorer swimlane (#65522) --- x-pack/plugins/ml/public/application/explorer/_explorer.scss | 4 ++++ x-pack/plugins/ml/public/application/explorer/explorer.js | 1 + 2 files changed, 5 insertions(+) diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index cfcba081983c2c..a46f35cbd4d205 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -1,3 +1,7 @@ +.ml-swimlane-selector { + visibility: hidden; +} + .ml-explorer { width: 100%; display: inline-block; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 36dac05add5570..8fd24798178073 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -120,6 +120,7 @@ export class Explorer extends React.Component { disableDragSelectOnMouseLeave = true; dragSelect = new DragSelect({ + selectorClass: 'ml-swimlane-selector', selectables: document.getElementsByClassName('sl-cell'), callback(elements) { if (elements.length > 1 && !ALLOW_CELL_RANGE_SELECTION) { From 8b862fea069566a03b4bc23902cf5c6db6741aa3 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 7 May 2020 13:21:47 +0300 Subject: [PATCH 06/16] Change the copy and the id from blacklist to block list for consistency (#65419) Co-authored-by: Elastic Machine --- x-pack/plugins/graph/public/angular/templates/index.html | 4 ++-- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/graph/public/angular/templates/index.html b/x-pack/plugins/graph/public/angular/templates/index.html index 8555658596179a..939d92518e271c 100644 --- a/x-pack/plugins/graph/public/angular/templates/index.html +++ b/x-pack/plugins/graph/public/angular/templates/index.html @@ -122,8 +122,8 @@ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1adc77267c44f0..0d050f7bf98427 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6589,7 +6589,6 @@ "xpack.graph.sidebar.selectionsTitle": "選択項目", "xpack.graph.sidebar.styleVerticesTitle": "スタイルが選択された頂点", "xpack.graph.sidebar.topMenu.addLinksButtonTooltip": "既存の用語の間にリンクを追加します", - "xpack.graph.sidebar.topMenu.blacklistButtonTooltip": "選択項目がワークスペースに戻らないようブラックリストに追加します", "xpack.graph.sidebar.topMenu.customStyleButtonTooltip": "選択された頂点のカスタムスタイル", "xpack.graph.sidebar.topMenu.drillDownButtonTooltip": "ドリルダウン", "xpack.graph.sidebar.topMenu.expandSelectionButtonTooltip": "選択項目を拡張", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a57b517123e77a..21113d55b46415 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6594,7 +6594,6 @@ "xpack.graph.sidebar.selectionsTitle": "选择的内容", "xpack.graph.sidebar.styleVerticesTitle": "样式选择的顶点", "xpack.graph.sidebar.topMenu.addLinksButtonTooltip": "在现有字词之间添加链接", - "xpack.graph.sidebar.topMenu.blacklistButtonTooltip": "返回工作空间时选择的黑名单", "xpack.graph.sidebar.topMenu.customStyleButtonTooltip": "定制样式选择的顶点", "xpack.graph.sidebar.topMenu.drillDownButtonTooltip": "向下钻取", "xpack.graph.sidebar.topMenu.expandSelectionButtonTooltip": "展开选择内容", From e723a8f9169b34109913728d7157efeedeba9b8b Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Thu, 7 May 2020 13:34:07 +0300 Subject: [PATCH 07/16] Move remaining home assets to the new platform (#65053) * Migrate tutorial resources Closes #55710 * Added type to context in apm plugin index * Removed context parameter from ApmPlugin * Generated apm plugin by generate_plugin script * Removed getGreeting declaration * Remove unused assets and comment previewImagePaths * Removed unnecessary types file * Move assets and sample_data_resources, update snapshot Co-authored-by: Elastic Machine --- .../dashboard_empty_screen.test.tsx.snap | 8 ++++---- .../public/application/dashboard_empty_screen.tsx | 4 ++-- .../application/components/sample_data/index.tsx | 2 +- .../public}/assets/illustration_elastic_heart.png | Bin .../sample_data_resources/ecommerce/dashboard.png | Bin .../ecommerce/dashboard_dark.png | Bin .../sample_data_resources/flights/dashboard.png | Bin .../flights/dashboard_dark.png | Bin .../sample_data_resources/logs/dashboard.png | Bin .../sample_data_resources/logs/dashboard_dark.png | Bin .../home/public}/assets/welcome_graphic_dark_2x.png | Bin .../public}/assets/welcome_graphic_light_2x.png | Bin .../sample_data/data_sets/ecommerce/index.ts | 4 ++-- .../services/sample_data/data_sets/flights/index.ts | 4 ++-- .../services/sample_data/data_sets/logs/index.ts | 4 ++-- 15 files changed, 13 insertions(+), 13 deletions(-) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public}/assets/illustration_elastic_heart.png (100%) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public/assets}/sample_data_resources/ecommerce/dashboard.png (100%) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public/assets}/sample_data_resources/ecommerce/dashboard_dark.png (100%) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public/assets}/sample_data_resources/flights/dashboard.png (100%) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public/assets}/sample_data_resources/flights/dashboard_dark.png (100%) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public/assets}/sample_data_resources/logs/dashboard.png (100%) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public/assets}/sample_data_resources/logs/dashboard_dark.png (100%) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public}/assets/welcome_graphic_dark_2x.png (100%) rename src/{legacy/core_plugins/kibana/public/home => plugins/home/public}/assets/welcome_graphic_light_2x.png (100%) diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 1bc85fa110ca0b..698c124d2d8057 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -301,7 +301,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` >
      @@ -995,7 +995,7 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] >
      diff --git a/src/plugins/dashboard/public/application/dashboard_empty_screen.tsx b/src/plugins/dashboard/public/application/dashboard_empty_screen.tsx index 8bf205b8cb5070..955d5244ce1904 100644 --- a/src/plugins/dashboard/public/application/dashboard_empty_screen.tsx +++ b/src/plugins/dashboard/public/application/dashboard_empty_screen.tsx @@ -50,8 +50,8 @@ export function DashboardEmptyScreen({ }: DashboardEmptyScreenProps) { const IS_DARK_THEME = uiSettings.get('theme:darkMode'); const emptyStateGraphicURL = IS_DARK_THEME - ? '/plugins/kibana/home/assets/welcome_graphic_dark_2x.png' - : '/plugins/kibana/home/assets/welcome_graphic_light_2x.png'; + ? '/plugins/home/assets/welcome_graphic_dark_2x.png' + : '/plugins/home/assets/welcome_graphic_light_2x.png'; const linkToVisualizeParagraph = (

      } description={ diff --git a/src/legacy/core_plugins/kibana/public/home/assets/illustration_elastic_heart.png b/src/plugins/home/public/assets/illustration_elastic_heart.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/assets/illustration_elastic_heart.png rename to src/plugins/home/public/assets/illustration_elastic_heart.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/ecommerce/dashboard.png b/src/plugins/home/public/assets/sample_data_resources/ecommerce/dashboard.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/ecommerce/dashboard.png rename to src/plugins/home/public/assets/sample_data_resources/ecommerce/dashboard.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/ecommerce/dashboard_dark.png b/src/plugins/home/public/assets/sample_data_resources/ecommerce/dashboard_dark.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/ecommerce/dashboard_dark.png rename to src/plugins/home/public/assets/sample_data_resources/ecommerce/dashboard_dark.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/flights/dashboard.png b/src/plugins/home/public/assets/sample_data_resources/flights/dashboard.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/flights/dashboard.png rename to src/plugins/home/public/assets/sample_data_resources/flights/dashboard.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/flights/dashboard_dark.png b/src/plugins/home/public/assets/sample_data_resources/flights/dashboard_dark.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/flights/dashboard_dark.png rename to src/plugins/home/public/assets/sample_data_resources/flights/dashboard_dark.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/logs/dashboard.png b/src/plugins/home/public/assets/sample_data_resources/logs/dashboard.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/logs/dashboard.png rename to src/plugins/home/public/assets/sample_data_resources/logs/dashboard.png diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_resources/logs/dashboard_dark.png b/src/plugins/home/public/assets/sample_data_resources/logs/dashboard_dark.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/sample_data_resources/logs/dashboard_dark.png rename to src/plugins/home/public/assets/sample_data_resources/logs/dashboard_dark.png diff --git a/src/legacy/core_plugins/kibana/public/home/assets/welcome_graphic_dark_2x.png b/src/plugins/home/public/assets/welcome_graphic_dark_2x.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/assets/welcome_graphic_dark_2x.png rename to src/plugins/home/public/assets/welcome_graphic_dark_2x.png diff --git a/src/legacy/core_plugins/kibana/public/home/assets/welcome_graphic_light_2x.png b/src/plugins/home/public/assets/welcome_graphic_light_2x.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/assets/welcome_graphic_light_2x.png rename to src/plugins/home/public/assets/welcome_graphic_light_2x.png diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts index 3e16187c443432..b0cc2e2db3cc92 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts @@ -36,8 +36,8 @@ export const ecommerceSpecProvider = function(): SampleDatasetSchema { id: 'ecommerce', name: ecommerceName, description: ecommerceDescription, - previewImagePath: '/plugins/kibana/home/sample_data_resources/ecommerce/dashboard.png', - darkPreviewImagePath: '/plugins/kibana/home/sample_data_resources/ecommerce/dashboard_dark.png', + previewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard.png', + darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard_dark.png', overviewDashboard: '722b74f0-b882-11e8-a6d9-e546fe2bba5f', appLinks: initialAppLinks, defaultIndex: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts index d63ea8f7fb4930..fc3cb6094b5eaa 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts @@ -36,8 +36,8 @@ export const flightsSpecProvider = function(): SampleDatasetSchema { id: 'flights', name: flightsName, description: flightsDescription, - previewImagePath: '/plugins/kibana/home/sample_data_resources/flights/dashboard.png', - darkPreviewImagePath: '/plugins/kibana/home/sample_data_resources/flights/dashboard_dark.png', + previewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard.png', + darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard_dark.png', overviewDashboard: '7adfa750-4c81-11e8-b3d7-01146121b73d', appLinks: initialAppLinks, defaultIndex: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts index bb6e2982f59a08..d8f205dff24e8b 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts @@ -36,8 +36,8 @@ export const logsSpecProvider = function(): SampleDatasetSchema { id: 'logs', name: logsName, description: logsDescription, - previewImagePath: '/plugins/kibana/home/sample_data_resources/logs/dashboard.png', - darkPreviewImagePath: '/plugins/kibana/home/sample_data_resources/logs/dashboard_dark.png', + previewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard.png', + darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard_dark.png', overviewDashboard: 'edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b', appLinks: initialAppLinks, defaultIndex: '90943e30-9a47-11e8-b64d-95841ca0b247', From 6ef45e17d4908f8129f0ce9f1aee9a818938a281 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Thu, 7 May 2020 14:15:37 +0300 Subject: [PATCH 08/16] =?UTF-8?q?Migrate=20test=20plugins=20=E2=87=92=20NP?= =?UTF-8?q?=20(kbn=5Ftp=5Fembeddable=5Fexplorer)=20(#64756)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrated kbn_tp_embeddable_explorer to the new platform. * Added discover as a dependency * fixed types * Updated typescript task * revert previous commit Co-authored-by: Elastic Machine --- .../kbn_tp_embeddable_explorer/index.ts | 39 -------- .../kbn_tp_embeddable_explorer/kibana.json | 16 +++ .../public/{np_ready/public => }/app/app.tsx | 0 .../app/dashboard_container_example.tsx | 2 +- .../public => }/app/dashboard_input.ts | 2 +- .../public/{np_ready/public => }/app/index.ts | 0 .../{np_ready/public => }/embeddable_api.ts | 9 +- .../public/{np_ready/public => }/index.ts | 0 .../public/initialize.ts | 20 ---- .../public/np_ready/kibana.json | 10 -- .../public/np_ready/public/index.html | 3 - .../public/np_ready/public/legacy.ts | 90 ----------------- .../public/np_ready/public/plugin.tsx | 84 ---------------- .../public/plugin.tsx | 98 +++++++++++++++++++ 14 files changed, 122 insertions(+), 251 deletions(-) delete mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts create mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/kibana.json rename test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/{np_ready/public => }/app/app.tsx (100%) rename test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/{np_ready/public => }/app/dashboard_container_example.tsx (98%) rename test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/{np_ready/public => }/app/dashboard_input.ts (96%) rename test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/{np_ready/public => }/app/index.ts (100%) rename test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/{np_ready/public => }/embeddable_api.ts (74%) rename test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/{np_ready/public => }/index.ts (100%) delete mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/initialize.ts delete mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/kibana.json delete mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.html delete mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts delete mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx create mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts deleted file mode 100644 index 99f54277be5d2e..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Legacy } from 'kibana'; - -// eslint-disable-next-line import/no-default-export -export default function(kibana: any) { - return new kibana.Plugin({ - require: ['kibana'], - uiExports: { - app: { - title: 'Embeddable Explorer', - order: 1, - main: 'plugins/kbn_tp_embeddable_explorer/np_ready/public/legacy', - }, - }, - init(server: Legacy.Server) { - server.injectUiAppVars('kbn_tp_embeddable_explorer', async () => - server.getInjectedUiAppVars('kibana') - ); - }, - }); -} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/kibana.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/kibana.json new file mode 100644 index 00000000000000..6c8d51ccb8651f --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "kbn_tp_embeddable_explorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "requiredPlugins": [ + "visTypeMarkdown", + "visTypeVislib", + "data", + "embeddable", + "uiActions", + "inspector", + "discover" + ], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx similarity index 100% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx similarity index 98% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx index 16c2840d6a32e8..e56b82378ddf7c 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx @@ -24,7 +24,7 @@ import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, DashboardContainerInput, -} from '../../../../../../../../src/plugins/dashboard/public'; +} from '../../../../../../src/plugins/dashboard/public'; import { dashboardInput } from './dashboard_input'; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts similarity index 96% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts index 37ef8cad948cb7..6f4e1f052f5e09 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts @@ -18,7 +18,7 @@ */ import { ViewMode, CONTACT_CARD_EMBEDDABLE, HELLO_WORLD_EMBEDDABLE } from '../embeddable_api'; -import { DashboardContainerInput } from '../../../../../../../../src/plugins/dashboard/public'; +import { DashboardContainerInput } from '../../../../../../src/plugins/dashboard/public'; export const dashboardInput: DashboardContainerInput = { panels: { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/index.ts similarity index 100% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/index.ts rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/index.ts diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddable_api.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddable_api.ts similarity index 74% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddable_api.ts rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddable_api.ts index dd25bebf899200..9f6597fefa1e40 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddable_api.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddable_api.ts @@ -17,6 +17,9 @@ * under the License. */ -export * from '../../../../../../../src/plugins/embeddable/public'; -export * from '../../../../../../../src/plugins/embeddable/public/lib/test_samples'; -export { HELLO_WORLD_EMBEDDABLE } from '../../../../../../../examples/embeddable_examples/public'; +export * from '../../../../../src/plugins/embeddable/public'; +export * from '../../../../../src/plugins/embeddable/public/lib/test_samples'; +export { + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, +} from '../../../../../examples/embeddable_examples/public'; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.ts similarity index 100% rename from test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.ts rename to test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.ts diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/initialize.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/initialize.ts deleted file mode 100644 index a4bc3cf17026c3..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/initialize.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './np_ready/public/legacy'; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/kibana.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/kibana.json deleted file mode 100644 index d0d0784eae8d33..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "kbn_tp_embeddable_explorer", - "version": "kibana", - "requiredPlugins": [ - "embeddable", - "inspector" - ], - "server": false, - "ui": true -} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.html b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.html deleted file mode 100644 index a242631e1638f3..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -

      ANGULAR STUFF!
      - diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts deleted file mode 100644 index 6d125bc3002e00..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import 'ui/autoload/all'; - -import 'uiExports/interpreter'; -import 'uiExports/embeddableFactories'; -import 'uiExports/embeddableActions'; -import 'uiExports/contextMenuActions'; -import 'uiExports/devTools'; -import 'uiExports/docViews'; -import 'uiExports/embeddableActions'; -import 'uiExports/fieldFormatEditors'; -import 'uiExports/fieldFormats'; -import 'uiExports/home'; -import 'uiExports/indexManagement'; -import 'uiExports/inspectorViews'; -import 'uiExports/savedObjectTypes'; -import 'uiExports/search'; -import 'uiExports/shareContextMenuExtensions'; -import 'uiExports/visTypes'; -import 'uiExports/visualize'; - -import { npSetup, npStart } from 'ui/new_platform'; -import { ExitFullScreenButton } from 'ui/exit_full_screen'; -import uiRoutes from 'ui/routes'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -/* eslint-enable @kbn/eslint/no-restricted-paths */ - -import template from './index.html'; - -import { plugin } from '.'; - -const pluginInstance = plugin({} as any); - -export const setup = pluginInstance.setup(npSetup.core, { - embeddable: npSetup.plugins.embeddable, - inspector: npSetup.plugins.inspector, - __LEGACY: { - ExitFullScreenButton, - }, -}); - -let rendered = false; -const onRenderCompleteListeners: Array<() => void> = []; - -uiRoutes.enable(); -uiRoutes.defaults(/\embeddable_explorer/, {}); -uiRoutes.when('/', { - template, - controller($scope) { - $scope.$$postDigest(() => { - rendered = true; - onRenderCompleteListeners.forEach(listener => listener()); - }); - }, -}); - -export const start = pluginInstance.start(npStart.core, { - embeddable: npStart.plugins.embeddable, - inspector: npStart.plugins.inspector, - uiActions: npStart.plugins.uiActions, - __LEGACY: { - ExitFullScreenButton, - onRenderComplete: (renderCompleteListener: () => void) => { - if (rendered) { - renderCompleteListener(); - } else { - onRenderCompleteListeners.push(renderCompleteListener); - } - }, - }, -}); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx deleted file mode 100644 index b47e84216dd16e..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; -import { createHelloWorldAction } from '../../../../../../../src/plugins/ui_actions/public/tests/test_samples'; - -import { - Start as InspectorStartContract, - Setup as InspectorSetupContract, -} from '../../../../../../../src/plugins/inspector/public'; - -import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; - -const REACT_ROOT_ID = 'embeddableExplorerRoot'; - -import { SayHelloAction, createSendMessageAction } from './embeddable_api'; -import { App } from './app'; -import { - EmbeddableStart, - EmbeddableSetup, -} from '.../../../../../../../src/plugins/embeddable/public'; - -export interface SetupDependencies { - embeddable: EmbeddableSetup; - inspector: InspectorSetupContract; - __LEGACY: { - ExitFullScreenButton: React.ComponentType; - }; -} - -interface StartDependencies { - embeddable: EmbeddableStart; - uiActions: UiActionsStart; - inspector: InspectorStartContract; - __LEGACY: { - ExitFullScreenButton: React.ComponentType; - onRenderComplete: (onRenderComplete: () => void) => void; - }; -} - -export type EmbeddableExplorerSetup = void; -export type EmbeddableExplorerStart = void; - -export class EmbeddableExplorerPublicPlugin - implements - Plugin { - public setup(core: CoreSetup, setupDeps: SetupDependencies): EmbeddableExplorerSetup {} - - public start(core: CoreStart, plugins: StartDependencies): EmbeddableExplorerStart { - const helloWorldAction = createHelloWorldAction(core.overlays); - const sayHelloAction = new SayHelloAction(alert); - const sendMessageAction = createSendMessageAction(core.overlays); - - plugins.uiActions.registerAction(sayHelloAction); - plugins.uiActions.registerAction(sendMessageAction); - - plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, helloWorldAction); - - plugins.__LEGACY.onRenderComplete(() => { - const root = document.getElementById(REACT_ROOT_ID); - ReactDOM.render(, root); - }); - } - - public stop() {} -} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx new file mode 100644 index 00000000000000..f99d89ca630bbc --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { CoreSetup, Plugin, AppMountParameters } from 'kibana/public'; +import { UiActionsStart, UiActionsSetup } from '../../../../../src/plugins/ui_actions/public'; +import { createHelloWorldAction } from '../../../../../src/plugins/ui_actions/public/tests/test_samples'; + +import { + Start as InspectorStartContract, + Setup as InspectorSetupContract, +} from '../../../../../src/plugins/inspector/public'; + +import { App } from './app'; +import { + CONTEXT_MENU_TRIGGER, + CONTACT_CARD_EMBEDDABLE, + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, + ContactCardEmbeddableFactory, + SayHelloAction, + createSendMessageAction, +} from './embeddable_api'; +import { + EmbeddableStart, + EmbeddableSetup, +} from '.../../../../../../../src/plugins/embeddable/public'; + +export interface SetupDependencies { + embeddable: EmbeddableSetup; + inspector: InspectorSetupContract; + uiActions: UiActionsSetup; +} + +interface StartDependencies { + embeddable: EmbeddableStart; + uiActions: UiActionsStart; + inspector: InspectorStartContract; +} + +export type EmbeddableExplorerSetup = void; +export type EmbeddableExplorerStart = void; + +export class EmbeddableExplorerPublicPlugin + implements + Plugin { + public setup(core: CoreSetup, setupDeps: SetupDependencies): EmbeddableExplorerSetup { + const helloWorldAction = createHelloWorldAction({} as any); + const sayHelloAction = new SayHelloAction(alert); + const sendMessageAction = createSendMessageAction({} as any); + + setupDeps.uiActions.registerAction(helloWorldAction); + setupDeps.uiActions.registerAction(sayHelloAction); + setupDeps.uiActions.registerAction(sendMessageAction); + + setupDeps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction.id); + + setupDeps.embeddable.registerEmbeddableFactory( + HELLO_WORLD_EMBEDDABLE, + new HelloWorldEmbeddableFactory() + ); + + setupDeps.embeddable.registerEmbeddableFactory( + CONTACT_CARD_EMBEDDABLE, + new ContactCardEmbeddableFactory((() => null) as any, {} as any) + ); + + core.application.register({ + id: 'EmbeddableExplorer', + title: 'Embeddable Explorer', + async mount(params: AppMountParameters) { + const startPlugins = (await core.getStartServices())[1] as StartDependencies; + render(, params.element); + + return () => unmountComponentAtNode(params.element); + }, + }); + } + + public start() {} + public stop() {} +} From 3604f5d21ae4ea5d72873196afe62056a2f0496c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 7 May 2020 13:18:56 +0200 Subject: [PATCH 09/16] [Drilldowns] Preserve state when selecting different action factory (#65074) Co-authored-by: Elastic Machine --- .../action_wizard/action_wizard.tsx | 6 +- .../components/action_wizard/test_data.tsx | 2 +- ...onnected_flyout_manage_drilldowns.test.tsx | 36 ++++++ .../flyout_drilldown_wizard.tsx | 103 ++++++++++++------ .../form_drilldown_wizard.tsx | 2 +- 5 files changed, 112 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx index 867ead688d23d0..4d14226777a0b8 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx @@ -32,9 +32,9 @@ export interface ActionWizardProps { /** * Action factory selected changed - * null - means user click "change" and removed action factory selection + * empty - means user click "change" and removed action factory selection */ - onActionFactoryChange: (actionFactory: ActionFactory | null) => void; + onActionFactoryChange: (actionFactory?: ActionFactory) => void; /** * current config for currently selected action factory @@ -71,7 +71,7 @@ export const ActionWizard: React.FC = ({ actionFactory={currentActionFactory} showDeselect={actionFactories.length > 1} onDeselect={() => { - onActionFactoryChange(null); + onActionFactoryChange(undefined); }} context={context} config={config} diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx index c3e749f163c949..692e86b53f09d7 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx @@ -167,7 +167,7 @@ export function Demo({ actionFactories }: { actionFactories: Array({}); - function changeActionFactory(newActionFactory: ActionFactory | null) { + function changeActionFactory(newActionFactory?: ActionFactory) { if (!newActionFactory) { // removing action factory return setState({}); diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index 6749b41e81fc70..52c53f32ff09b4 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -173,6 +173,42 @@ test('Create only mode', async () => { expect(await mockDynamicActionManager.state.get().events.length).toBe(1); }); +test('After switching between action factories state is restored', async () => { + const screen = render( + + ); + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + fireEvent.change(screen.getByLabelText(/name/i), { + target: { value: 'test' }, + }); + fireEvent.click(screen.getByText(/Go to URL/i)); + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: 'https://elastic.co' }, + }); + + // change to dashboard + fireEvent.click(screen.getByText(/change/i)); + fireEvent.click(screen.getByText(/Go to Dashboard/i)); + + // change back to url + fireEvent.click(screen.getByText(/change/i)); + fireEvent.click(screen.getByText(/Go to URL/i)); + + expect(screen.getByLabelText(/url/i)).toHaveValue('https://elastic.co'); + expect(screen.getByLabelText(/name/i)).toHaveValue('test'); + + fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); + await wait(() => expect(notifications.toasts.addSuccess).toBeCalled()); + expect(await (mockDynamicActionManager.state.get().events[0].action.config as any).url).toBe( + 'https://elastic.co' + ); +}); + test.todo("Error when can't fetch drilldown list"); test("Error when can't save drilldown changes", async () => { diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index 8541aae06ff0c7..1f775a5ff103f1 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -41,6 +41,72 @@ export interface FlyoutDrilldownWizardProps void; + setActionConfig: (actionConfig: object) => void; + setActionFactory: (actionFactory?: ActionFactory) => void; + } +] { + const [wizardConfig, setWizardConfig] = useState( + () => + initialDrilldownWizardConfig ?? { + name: '', + } + ); + const [actionConfigCache, setActionConfigCache] = useState>( + initialDrilldownWizardConfig?.actionFactory + ? { + [initialDrilldownWizardConfig.actionFactory + .id]: initialDrilldownWizardConfig.actionConfig!, + } + : {} + ); + + return [ + wizardConfig, + { + setName: (name: string) => { + setWizardConfig({ + ...wizardConfig, + name, + }); + }, + setActionConfig: (actionConfig: object) => { + setWizardConfig({ + ...wizardConfig, + actionConfig, + }); + }, + setActionFactory: (actionFactory?: ActionFactory) => { + if (actionFactory) { + setWizardConfig({ + ...wizardConfig, + actionFactory, + actionConfig: actionConfigCache[actionFactory.id] ?? actionFactory.createConfig(), + }); + } else { + if (wizardConfig.actionFactory?.id) { + setActionConfigCache({ + ...actionConfigCache, + [wizardConfig.actionFactory.id]: wizardConfig.actionConfig!, + }); + } + + setWizardConfig({ + ...wizardConfig, + actionFactory: undefined, + actionConfig: undefined, + }); + } + }, + }, + ]; +} + export function FlyoutDrilldownWizard({ onClose, onBack, @@ -53,11 +119,8 @@ export function FlyoutDrilldownWizard) { - const [wizardConfig, setWizardConfig] = useState( - () => - initialDrilldownWizardConfig ?? { - name: '', - } + const [wizardConfig, { setActionFactory, setActionConfig, setName }] = useWizardConfigState( + initialDrilldownWizardConfig ); const isActionValid = ( @@ -95,35 +158,11 @@ export function FlyoutDrilldownWizard { - setWizardConfig({ - ...wizardConfig, - name: newName, - }); - }} + onNameChange={setName} actionConfig={wizardConfig.actionConfig} - onActionConfigChange={newActionConfig => { - setWizardConfig({ - ...wizardConfig, - actionConfig: newActionConfig, - }); - }} + onActionConfigChange={setActionConfig} currentActionFactory={wizardConfig.actionFactory} - onActionFactoryChange={actionFactory => { - if (!actionFactory) { - setWizardConfig({ - ...wizardConfig, - actionFactory: undefined, - actionConfig: undefined, - }); - } else { - setWizardConfig({ - ...wizardConfig, - actionFactory, - actionConfig: actionFactory.createConfig(), - }); - } - }} + onActionFactoryChange={setActionFactory} actionFactories={drilldownActionFactories} actionFactoryContext={actionFactoryContext!} /> diff --git a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx index 93b3710bf6cc66..3bed81a9719216 100644 --- a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -19,7 +19,7 @@ export interface FormDrilldownWizardProps { onNameChange?: (name: string) => void; currentActionFactory?: ActionFactory; - onActionFactoryChange?: (actionFactory: ActionFactory | null) => void; + onActionFactoryChange?: (actionFactory?: ActionFactory) => void; actionFactoryContext: object; actionConfig?: object; From 55e4c7f9a78d81ae213471b68c8b4c2075e1616d Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 7 May 2020 12:23:42 +0100 Subject: [PATCH 10/16] [ML] Consolidating shared types and util functions (#65247) * [ML] Consolidating shared types and util functions * including formatter * adding missing includes * removing unused export * ignoring numeral type error Co-authored-by: Elastic Machine --- .../field_type_icon/field_type_icon.js | 2 -- .../message_call_out/message_call_out.js | 2 -- .../validate_job/validate_job_view.js | 2 -- .../file_based/components/utils/utils.ts | 1 + .../explorer_chart_distribution.js | 2 -- .../explorer_chart_single_metric.js | 2 -- .../explorer/explorer_swimlane.tsx | 2 -- .../forecasting_modal/forecasting_modal.js | 2 -- .../forecasting_modal/run_controls.js | 2 -- x-pack/plugins/ml/public/index.ts | 4 ++-- x-pack/plugins/ml/public/shared.ts | 22 +++++++++++++++++++ x-pack/plugins/ml/server/index.ts | 1 + x-pack/plugins/ml/server/shared.ts | 7 ++++++ .../public/components/ml_popover/types.ts | 2 +- .../siem/server/lib/machine_learning/index.ts | 2 +- .../public/__mocks__/shared_imports.ts | 6 ++--- .../public/app/common/aggregations.ts | 2 +- .../transform/public/shared_imports.ts | 7 +++--- .../common/charts/duration_line_bar_list.tsx | 4 ++-- .../components/monitor/ml/ml_integeration.tsx | 2 +- .../monitor_duration_container.tsx | 2 +- .../uptime/public/state/actions/ml_anomaly.ts | 8 ++++--- .../uptime/public/state/api/ml_anomaly.ts | 8 ++++--- .../public/state/reducers/ml_anomaly.ts | 3 +-- 24 files changed, 57 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/ml/public/shared.ts create mode 100644 x-pack/plugins/ml/server/shared.ts diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js index a3c60a87636f96..1853c3d629c3e3 100644 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js +++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js @@ -9,8 +9,6 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { getMLJobTypeAriaLabel } from '../../util/field_types_utils'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/components/message_call_out/message_call_out.js b/x-pack/plugins/ml/public/application/components/message_call_out/message_call_out.js index 9a122a0eea7005..9a1260ecfdd45d 100644 --- a/x-pack/plugins/ml/public/application/components/message_call_out/message_call_out.js +++ b/x-pack/plugins/ml/public/application/components/message_call_out/message_call_out.js @@ -14,8 +14,6 @@ import PropTypes from 'prop-types'; import { EuiCallOut } from '@elastic/eui'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { MESSAGE_LEVEL } from '../../../../common/constants/message_levels'; function getCallOutAttributes(message, status) { diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index 98e027ec4f3656..6001d7cbf6f617 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -30,8 +30,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { getDocLinks } from '../../util/dependency_cache'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { VALIDATION_STATUS } from '../../../../common/constants/validation'; import { getMostSevereMessageStatus } from '../../../../common/util/validation_utils'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 7d966949624c1f..3b82a34b889b71 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -5,6 +5,7 @@ */ import { isEqual } from 'lodash'; +// @ts-ignore import numeral from '@elastic/numeral'; import { ml } from '../../../../services/ml_api_service'; import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 03426869b0ccfc..2b577c978eb139 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -17,8 +17,6 @@ import d3 from 'd3'; import $ from 'jquery'; import moment from 'moment'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { formatHumanReadableDateTime } from '../../util/date_utils'; import { formatValue } from '../../formatters/format_value'; import { getSeverityColor, getSeverityWithLow } from '../../../../common/util/anomaly_utils'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 82041af39ca15e..531a24493c9610 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -17,8 +17,6 @@ import d3 from 'd3'; import $ from 'jquery'; import moment from 'moment'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { formatHumanReadableDateTime } from '../../util/date_utils'; import { formatValue } from '../../formatters/format_value'; import { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx index bf1a3b424edb91..8a8a826e1831f3 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx @@ -14,8 +14,6 @@ import _ from 'lodash'; import d3 from 'd3'; import moment from 'moment'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { i18n } from '@kbn/i18n'; import { Subscription } from 'rxjs'; import { TooltipValue } from '@elastic/charts'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 64f20667931184..eded8460d2205f 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -15,8 +15,6 @@ import React, { Component } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../../common/constants/states'; import { MESSAGE_LEVEL } from '../../../../../common/constants/message_levels'; import { isJobVersionGte } from '../../../../../common/util/job_utils'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index 7dd06268f7f8dd..3208697073b8e4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -23,8 +23,6 @@ import { EuiToolTip, } from '@elastic/eui'; -// don't use something like plugins/ml/../common -// because it won't work with the jest tests import { JOB_STATE } from '../../../../../common/constants/states'; import { FORECAST_DURATION_MAX_DAYS } from './forecasting_modal'; import { ForecastProgress } from './forecast_progress'; diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index c23d042822816d..a9ffb1a5bf5792 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -13,7 +13,6 @@ import { MlSetupDependencies, MlStartDependencies, } from './plugin'; -import { getMetricChangeDescription } from './application/formatters/metric_change_description'; export const plugin: PluginInitializer< MlPluginSetup, @@ -22,4 +21,5 @@ export const plugin: PluginInitializer< MlStartDependencies > = () => new MlPlugin(); -export { MlPluginSetup, MlPluginStart, getMetricChangeDescription }; +export { MlPluginSetup, MlPluginStart }; +export * from './shared'; diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts new file mode 100644 index 00000000000000..6821cb7ef0f945 --- /dev/null +++ b/x-pack/plugins/ml/public/shared.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. + */ + +export * from '../common/constants/anomalies'; + +export * from '../common/types/data_recognizer'; +export * from '../common/types/capabilities'; +export * from '../common/types/anomalies'; +export * from '../common/types/modules'; +export * from '../common/types/audit_message'; + +export * from '../common/util/anomaly_utils'; +export * from '../common/util/errors'; +export * from '../common/util/validators'; + +export * from './application/formatters/metric_change_description'; + +export * from './application/components/data_grid'; +export * from './application/data_frame_analytics/common'; diff --git a/x-pack/plugins/ml/server/index.ts b/x-pack/plugins/ml/server/index.ts index 175c20bf49c947..4c27854ec719bd 100644 --- a/x-pack/plugins/ml/server/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -7,5 +7,6 @@ import { PluginInitializerContext } from 'kibana/server'; import { MlServerPlugin } from './plugin'; export { MlPluginSetup, MlPluginStart } from './plugin'; +export * from './shared'; export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); diff --git a/x-pack/plugins/ml/server/shared.ts b/x-pack/plugins/ml/server/shared.ts new file mode 100644 index 00000000000000..1e50950bc3bced --- /dev/null +++ b/x-pack/plugins/ml/server/shared.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 '../common/types/anomalies'; diff --git a/x-pack/plugins/siem/public/components/ml_popover/types.ts b/x-pack/plugins/siem/public/components/ml_popover/types.ts index 58d40c298b3294..005f93650a8eb0 100644 --- a/x-pack/plugins/siem/public/components/ml_popover/types.ts +++ b/x-pack/plugins/siem/public/components/ml_popover/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditMessageBase } from '../../../../ml/common/types/audit_message'; +import { AuditMessageBase } from '../../../../ml/public'; import { MlError } from '../ml/types'; export interface Group { diff --git a/x-pack/plugins/siem/server/lib/machine_learning/index.ts b/x-pack/plugins/siem/server/lib/machine_learning/index.ts index eb09fdde3cce31..865a3cf51604d4 100644 --- a/x-pack/plugins/siem/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/siem/server/lib/machine_learning/index.ts @@ -6,7 +6,7 @@ import { SearchResponse, SearchParams } from 'elasticsearch'; -import { AnomalyRecordDoc as Anomaly } from '../../../../ml/common/types/anomalies'; +import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; export { Anomaly }; export type AnomalyResults = SearchResponse; diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 9d8106a1366d6d..e115e086f45b56 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -14,8 +14,8 @@ export const useRequest = jest.fn(() => ({ })); // just passing through the reimports -export { getErrorMessage } from '../../../ml/common/util/errors'; export { + getErrorMessage, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, @@ -27,5 +27,5 @@ export { SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, -} from '../../../ml/public/application/components/data_grid'; -export { INDEX_STATUS } from '../../../ml/public/application/data_frame_analytics/common'; + INDEX_STATUS, +} from '../../../ml/public'; diff --git a/x-pack/plugins/transform/public/app/common/aggregations.ts b/x-pack/plugins/transform/public/app/common/aggregations.ts index 038d68ff37d876..397a58006f1d1a 100644 --- a/x-pack/plugins/transform/public/app/common/aggregations.ts +++ b/x-pack/plugins/transform/public/app/common/aggregations.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { composeValidators, patternValidator } from '../../../../ml/common/util/validators'; +import { composeValidators, patternValidator } from '../../../../ml/public'; export type AggName = string; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index bcd8e53e3d1919..3737377de2d5ee 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -16,9 +16,8 @@ export { useRequest, } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; -export { getErrorMessage } from '../../ml/common/util/errors'; - export { + getErrorMessage, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, @@ -30,5 +29,5 @@ export { SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, -} from '../../ml/public/application/components/data_grid'; -export { INDEX_STATUS } from '../../ml/public/application/data_frame_analytics/common'; + INDEX_STATUS, +} from '../../ml/public'; diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx index ceb1e700f293e2..5e41c4b74fd5d3 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx @@ -9,11 +9,11 @@ import moment from 'moment'; import { AnnotationTooltipFormatter, RectAnnotation } from '@elastic/charts'; import { RectAnnotationDatum } from '@elastic/charts/dist/chart_types/xy_chart/utils/specs'; import { AnnotationTooltip } from './annotation_tooltip'; -import { ANOMALY_SEVERITY } from '../../../../../../plugins/ml/common/constants/anomalies'; import { + ANOMALY_SEVERITY, getSeverityColor, getSeverityType, -} from '../../../../../../plugins/ml/common/util/anomaly_utils'; +} from '../../../../../../plugins/ml/public'; interface Props { anomalies: any; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx index 5330ac6e12e987..e66808f76d24a2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx @@ -20,7 +20,7 @@ import { getMLJobId } from '../../../state/api/ml_anomaly'; import * as labels from './translations'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ManageMLJobComponent } from './manage_ml_job'; -import { JobStat } from '../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobStat } from '../../../../../../plugins/ml/public'; import { useMonitorId } from '../../../hooks'; export const MLIntegrationComponent = () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 52d4f620f84b3f..b586c1241290bc 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -20,7 +20,7 @@ import { } from '../../../state/selectors'; import { UptimeRefreshContext } from '../../../contexts'; import { getMLJobId } from '../../../state/api/ml_anomaly'; -import { JobStat } from '../../../../../ml/common/types/data_recognizer'; +import { JobStat } from '../../../../../ml/public'; import { MonitorDurationComponent } from './monitor_duration'; import { MonitorIdParam } from '../../../../common/types'; diff --git a/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts index 441a3cefdf204d..6b564ba0e83e44 100644 --- a/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts @@ -6,15 +6,17 @@ import { createAction } from 'redux-actions'; import { createAsyncAction } from './utils'; -import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; -import { AnomaliesTableRecord } from '../../../../../plugins/ml/common/types/anomalies'; +import { + MlCapabilitiesResponse, + AnomaliesTableRecord, + JobExistResult, +} from '../../../../../plugins/ml/public'; import { CreateMLJobSuccess, DeleteJobResults, MonitorIdParam, HeartbeatIndicesParam, } from './types'; -import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; export const resetMLState = createAction('RESET_ML_STATE'); diff --git a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts index ff2ad8ba0745ff..158d7b631a8b88 100644 --- a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts @@ -8,15 +8,17 @@ import moment from 'moment'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; -import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; +import { + MlCapabilitiesResponse, + DataRecognizerConfigResponse, + JobExistResult, +} from '../../../../../plugins/ml/public'; import { CreateMLJobSuccess, DeleteJobResults, MonitorIdParam, HeartbeatIndicesParam, } from '../actions/types'; -import { DataRecognizerConfigResponse } from '../../../../../plugins/ml/common/types/modules'; -import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; const getJobPrefix = (monitorId: string) => { // ML App doesn't support upper case characters in job name diff --git a/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts index 9a4a949ac4ede4..9f2da19d24208b 100644 --- a/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts @@ -16,9 +16,8 @@ import { } from '../actions'; import { getAsyncInitialState, handleAsyncAction } from './utils'; import { AsyncInitialState } from './types'; -import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; +import { MlCapabilitiesResponse, JobExistResult } from '../../../../../plugins/ml/public'; import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types'; -import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; export interface MLJobState { mlJob: AsyncInitialState; From 8373247da026c9c14f9ca0d864c9e0f8a21d0ae3 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 7 May 2020 14:36:35 +0200 Subject: [PATCH 11/16] reduce uptime plugin initial bundle size (#65257) --- x-pack/plugins/uptime/public/apps/plugin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index c64ca7c3d48432..c6a7eb261d8fd2 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -12,7 +12,6 @@ import { import { UMFrontendLibs } from '../lib/lib'; import { PLUGIN } from '../../common/constants'; import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; -import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; @@ -61,6 +60,10 @@ export class UptimePlugin implements Plugin Date: Thu, 7 May 2020 07:57:34 -0600 Subject: [PATCH 12/16] Fixes the client to setup SSL with the CA certificates for testing (#65598) ## Summary Fixes the non-legacy ES test client to work with SSL. Without this if you try to migrate `siem rules` or `alerting` or `CASE` or anything else that is using SSL based tests you get this error when trying to use the non-legacy: ```ts // pull in non-legacy service for functional tests const es = getService('es'); ``` ```ts // use it somewhere where your config.ts is utilizing SSL in a functional test // ... ``` In your console you get this error: ```ts ConnectionError: self signed certificate in certificate chain at onResponse (node_modules/@elastic/elasticsearch/lib/Transport.js:205:13) at ClientRequest.request.on.err (node_modules/@elastic/elasticsearch/lib/Connection.js:98:9) at TLSSocket.socketErrorListener (_http_client.js:401:9) at emitErrorNT (internal/streams/destroy.js:91:8) at emitErrorAndCloseNT (internal/streams/destroy.js:59:3) at process._tickCallback (internal/process/next_tick.js:63:19) ``` This fixes that by adding the CA certs from test to the ES test client. --- test/common/services/elasticsearch.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/common/services/elasticsearch.ts b/test/common/services/elasticsearch.ts index 63c4bfeeb4ce77..0436dc901292d5 100644 --- a/test/common/services/elasticsearch.ts +++ b/test/common/services/elasticsearch.ts @@ -18,8 +18,9 @@ */ import { format as formatUrl } from 'url'; - +import fs from 'fs'; import { Client } from '@elastic/elasticsearch'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -27,6 +28,9 @@ export function ElasticsearchProvider({ getService }: FtrProviderContext) { const config = getService('config'); return new Client({ + ssl: { + ca: fs.readFileSync(CA_CERT_PATH, 'utf-8'), + }, nodes: [formatUrl(config.get('servers.elasticsearch'))], requestTimeout: config.get('timeouts.esRequestTimeout'), }); From 6a6b3edd7f2409cc7991c88c0ec7e42a0ecd81b2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 7 May 2020 15:58:16 +0200 Subject: [PATCH 13/16] [ML] Migrate server side Mocha tests to Jest. (#65651) Migrates job validation related server side tests from Mocha to Jest. --- ...mator.js => bucket_span_estimator.test.ts} | 71 ++++------ .../mock_farequote_cardinality.json | 0 .../mock_farequote_search_response.json | 0 .../mock_field_caps.json | 0 .../mock_it_search_response.json | 0 .../mock_time_field.json | 0 .../mock_time_field_nested.json | 0 .../mock_time_range.json | 0 .../models/job_validation/job_validation.d.ts | 17 ++- ...b_validation.js => job_validation.test.ts} | 100 ++++++++------ .../models/job_validation/messages.d.ts | 10 ++ ...t_span.js => validate_bucket_span.test.ts} | 44 +++--- .../job_validation/validate_cardinality.d.ts | 5 +- ...nality.js => validate_cardinality.test.ts} | 125 +++++++++++------- ...encers.js => validate_influencers.test.ts} | 65 ++++----- ...influencers.js => validate_influencers.ts} | 8 +- ...e_range.js => validate_time_range.test.ts} | 69 ++++++---- .../job_validation/validate_time_range.ts | 6 +- 18 files changed, 298 insertions(+), 222 deletions(-) rename x-pack/plugins/ml/server/models/bucket_span_estimator/{__tests__/bucket_span_estimator.js => bucket_span_estimator.test.ts} (56%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__ => __mocks__}/mock_farequote_cardinality.json (100%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__ => __mocks__}/mock_farequote_search_response.json (100%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__ => __mocks__}/mock_field_caps.json (100%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__ => __mocks__}/mock_it_search_response.json (100%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__ => __mocks__}/mock_time_field.json (100%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__ => __mocks__}/mock_time_field_nested.json (100%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__ => __mocks__}/mock_time_range.json (100%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__/job_validation.js => job_validation.test.ts} (80%) create mode 100644 x-pack/plugins/ml/server/models/job_validation/messages.d.ts rename x-pack/plugins/ml/server/models/job_validation/{__tests__/validate_bucket_span.js => validate_bucket_span.test.ts} (81%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__/validate_cardinality.js => validate_cardinality.test.ts} (69%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__/validate_influencers.js => validate_influencers.test.ts} (63%) rename x-pack/plugins/ml/server/models/job_validation/{validate_influencers.js => validate_influencers.ts} (89%) rename x-pack/plugins/ml/server/models/job_validation/{__tests__/validate_time_range.js => validate_time_range.test.ts} (76%) diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts similarity index 56% rename from x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts index a0dacc38e58352..f5daadfe86be0c 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { estimateBucketSpanFactory } from '../bucket_span_estimator'; +import { APICaller } from 'kibana/server'; + +import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; + +import { estimateBucketSpanFactory, BucketSpanEstimatorData } from './bucket_span_estimator'; // Mock callWithRequest with the ability to simulate returning different // permission settings. On each call using `ml.privilegeCheck` we retrieve @@ -14,7 +17,7 @@ import { estimateBucketSpanFactory } from '../bucket_span_estimator'; // sufficient permissions should be returned, the second time insufficient // permissions. const permissions = [false, true]; -const callWithRequest = method => { +const callWithRequest: APICaller = (method: string) => { return new Promise(resolve => { if (method === 'ml.privilegeCheck') { resolve({ @@ -28,34 +31,19 @@ const callWithRequest = method => { return; } resolve({}); - }); + }) as Promise; }; -const callWithInternalUser = () => { +const callWithInternalUser: APICaller = () => { return new Promise(resolve => { resolve({}); - }); + }) as Promise; }; -// mock xpack_main plugin -function mockXpackMainPluginFactory(isEnabled = false, licenseType = 'platinum') { - return { - info: { - isAvailable: () => true, - feature: () => ({ - isEnabled: () => isEnabled, - }), - license: { - getType: () => licenseType, - }, - }, - }; -} - // mock configuration to be passed to the estimator -const formConfig = { - aggTypes: ['count'], - duration: {}, +const formConfig: BucketSpanEstimatorData = { + aggTypes: [ES_AGGREGATION.COUNT], + duration: { start: 0, end: 1 }, fields: [null], index: '', query: { @@ -64,13 +52,15 @@ const formConfig = { must_not: [], }, }, + splitField: undefined, + timeField: undefined, }; describe('ML - BucketSpanEstimator', () => { it('call factory', () => { expect(function() { - estimateBucketSpanFactory(callWithRequest, callWithInternalUser); - }).to.not.throwError('Not initialized.'); + estimateBucketSpanFactory(callWithRequest, callWithInternalUser, false); + }).not.toThrow('Not initialized.'); }); it('call factory and estimator with security disabled', done => { @@ -78,44 +68,29 @@ describe('ML - BucketSpanEstimator', () => { const estimateBucketSpan = estimateBucketSpanFactory( callWithRequest, callWithInternalUser, - mockXpackMainPluginFactory() + true ); estimateBucketSpan(formConfig).catch(catchData => { - expect(catchData).to.be('Unable to retrieve cluster setting search.max_buckets'); + expect(catchData).toBe('Unable to retrieve cluster setting search.max_buckets'); done(); }); - }).to.not.throwError('Not initialized.'); + }).not.toThrow('Not initialized.'); }); - it('call factory and estimator with security enabled and sufficient permissions.', done => { + it('call factory and estimator with security enabled.', done => { expect(function() { const estimateBucketSpan = estimateBucketSpanFactory( callWithRequest, callWithInternalUser, - mockXpackMainPluginFactory(true) + false ); estimateBucketSpan(formConfig).catch(catchData => { - expect(catchData).to.be('Unable to retrieve cluster setting search.max_buckets'); + expect(catchData).toBe('Unable to retrieve cluster setting search.max_buckets'); done(); }); - }).to.not.throwError('Not initialized.'); - }); - - it('call factory and estimator with security enabled and insufficient permissions.', done => { - expect(function() { - const estimateBucketSpan = estimateBucketSpanFactory( - callWithRequest, - callWithInternalUser, - mockXpackMainPluginFactory(true) - ); - - estimateBucketSpan(formConfig).catch(catchData => { - expect(catchData).to.be('Insufficient permissions to call bucket span estimation.'); - done(); - }); - }).to.not.throwError('Not initialized.'); + }).not.toThrow('Not initialized.'); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_cardinality.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_cardinality.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_search_response.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_search_response.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_field_caps.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_field_caps.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_it_search_response.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_it_search_response.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field_nested.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field_nested.json diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_range.json similarity index 100% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_range.json diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts index 33f5d5ec95fad5..6a9a7a0c13395c 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts @@ -6,14 +6,19 @@ import { APICaller } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; + +import { DeepPartial } from '../../../common/types/common'; + import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; -type ValidateJobPayload = TypeOf; +import { ValidationMessage } from './messages'; + +export type ValidateJobPayload = TypeOf; export function validateJob( callAsCurrentUser: APICaller, - payload: ValidateJobPayload, - kbnVersion: string, - callAsInternalUser: APICaller, - isSecurityDisabled: boolean -): string[]; + payload?: DeepPartial, + kbnVersion?: string, + callAsInternalUser?: APICaller, + isSecurityDisabled?: boolean +): Promise; diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts similarity index 80% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js rename to x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index 726a8e8d8db853..9851f80a42d5bb 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -4,16 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { validateJob } from '../job_validation'; +import { APICaller } from 'kibana/server'; + +import { validateJob } from './job_validation'; // mock callWithRequest -const callWithRequest = () => { +const callWithRequest: APICaller = (method: string) => { return new Promise(resolve => { + if (method === 'fieldCaps') { + resolve({ fields: [] }); + return; + } resolve({}); - }); + }) as Promise; }; +// Note: The tests cast `payload` as any +// so we can simulate possible runtime payloads +// that don't satisfy the TypeScript specs. describe('ML - validateJob', () => { it('calling factory without payload throws an error', done => { validateJob(callWithRequest).then( @@ -61,7 +69,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_empty', 'detectors_empty', 'bucket_span_empty', @@ -70,10 +78,14 @@ describe('ML - validateJob', () => { }); }); - const jobIdTests = (testIds, messageId) => { + const jobIdTests = (testIds: string[], messageId: string) => { const promises = testIds.map(id => { - const payload = { job: { analysis_config: { detectors: [] } } }; - payload.job.job_id = id; + const payload = { + job: { + analysis_config: { detectors: [] }, + job_id: id, + }, + }; return validateJob(callWithRequest, payload).catch(() => { new Error('Promise should not fail for jobIdTests.'); }); @@ -81,19 +93,21 @@ describe('ML - validateJob', () => { return Promise.all(promises).then(testResults => { testResults.forEach(messages => { - const ids = messages.map(m => m.id); - expect(ids.includes(messageId)).to.equal(true); + expect(Array.isArray(messages)).toBe(true); + if (Array.isArray(messages)) { + const ids = messages.map(m => m.id); + expect(ids.includes(messageId)).toBe(true); + } }); }); }; - const jobGroupIdTest = (testIds, messageId) => { - const payload = { job: { analysis_config: { detectors: [] } } }; - payload.job.groups = testIds; + const jobGroupIdTest = (testIds: string[], messageId: string) => { + const payload = { job: { analysis_config: { detectors: [] }, groups: testIds } }; return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes(messageId)).to.equal(true); + expect(ids.includes(messageId)).toBe(true); }); }; @@ -126,10 +140,9 @@ describe('ML - validateJob', () => { return jobGroupIdTest(validTestIds, 'job_group_id_valid'); }); - const bucketSpanFormatTests = (testFormats, messageId) => { + const bucketSpanFormatTests = (testFormats: string[], messageId: string) => { const promises = testFormats.map(format => { - const payload = { job: { analysis_config: { detectors: [] } } }; - payload.job.analysis_config.bucket_span = format; + const payload = { job: { analysis_config: { bucket_span: format, detectors: [] } } }; return validateJob(callWithRequest, payload).catch(() => { new Error('Promise should not fail for bucketSpanFormatTests.'); }); @@ -137,8 +150,11 @@ describe('ML - validateJob', () => { return Promise.all(promises).then(testResults => { testResults.forEach(messages => { - const ids = messages.map(m => m.id); - expect(ids.includes(messageId)).to.equal(true); + expect(Array.isArray(messages)).toBe(true); + if (Array.isArray(messages)) { + const ids = messages.map(m => m.id); + expect(ids.includes(messageId)).toBe(true); + } }); }); }; @@ -152,7 +168,7 @@ describe('ML - validateJob', () => { }); it('at least one detector function is empty', () => { - const payload = { job: { analysis_config: { detectors: [] } } }; + const payload = { job: { analysis_config: { detectors: [] as Array<{ function?: string }> } } }; payload.job.analysis_config.detectors.push({ function: 'count', }); @@ -165,19 +181,19 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes('detectors_function_empty')).to.equal(true); + expect(ids.includes('detectors_function_empty')).toBe(true); }); }); it('detector function is not empty', () => { - const payload = { job: { analysis_config: { detectors: [] } } }; + const payload = { job: { analysis_config: { detectors: [] as Array<{ function?: string }> } } }; payload.job.analysis_config.detectors.push({ function: 'count', }); return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes('detectors_function_not_empty')).to.equal(true); + expect(ids.includes('detectors_function_not_empty')).toBe(true); }); }); @@ -189,7 +205,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes('index_fields_invalid')).to.equal(true); + expect(ids.includes('index_fields_invalid')).toBe(true); }); }); @@ -201,11 +217,11 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids.includes('index_fields_valid')).to.equal(true); + expect(ids.includes('index_fields_valid')).toBe(true); }); }); - const getBasicPayload = () => ({ + const getBasicPayload = (): any => ({ job: { job_id: 'test', analysis_config: { @@ -214,7 +230,7 @@ describe('ML - validateJob', () => { { function: 'count', }, - ], + ] as Array<{ function: string; by_field_name?: string; partition_field_name?: string }>, influencers: [], }, data_description: { time_field: '@timestamp' }, @@ -224,7 +240,7 @@ describe('ML - validateJob', () => { }); it('throws an error because job.analysis_config.influencers is not an Array', done => { - const payload = getBasicPayload(); + const payload = getBasicPayload() as any; delete payload.job.analysis_config.influencers; validateJob(callWithRequest, payload).then( @@ -237,11 +253,11 @@ describe('ML - validateJob', () => { }); it('detect duplicate detectors', () => { - const payload = getBasicPayload(); + const payload = getBasicPayload() as any; payload.job.analysis_config.detectors.push({ function: 'count' }); return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'detectors_duplicates', @@ -253,7 +269,7 @@ describe('ML - validateJob', () => { }); it('dedupe duplicate messages', () => { - const payload = getBasicPayload(); + const payload = getBasicPayload() as any; // in this test setup, the following configuration passes // the duplicate detectors check, but would return the same // 'field_not_aggregatable' message for both detectors. @@ -264,7 +280,7 @@ describe('ML - validateJob', () => { ]; return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -278,7 +294,7 @@ describe('ML - validateJob', () => { const payload = getBasicPayload(); return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -288,7 +304,7 @@ describe('ML - validateJob', () => { }); it('categorization job using mlcategory passes aggregatable field check', () => { - const payload = { + const payload: any = { job: { job_id: 'categorization_test', analysis_config: { @@ -310,7 +326,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -322,7 +338,7 @@ describe('ML - validateJob', () => { }); it('non-existent field reported as non aggregatable', () => { - const payload = { + const payload: any = { job: { job_id: 'categorization_test', analysis_config: { @@ -343,7 +359,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -354,7 +370,7 @@ describe('ML - validateJob', () => { }); it('script field not reported as non aggregatable', () => { - const payload = { + const payload: any = { job: { job_id: 'categorization_test', analysis_config: { @@ -385,7 +401,7 @@ describe('ML - validateJob', () => { return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([ + expect(ids).toStrictEqual([ 'job_id_valid', 'detectors_function_not_empty', 'index_fields_valid', @@ -399,19 +415,19 @@ describe('ML - validateJob', () => { // the following two tests validate the correct template rendering of // urls in messages with {{version}} in them to be replaced with the // specified version. (defaulting to 'current') - const docsTestPayload = getBasicPayload(); + const docsTestPayload = getBasicPayload() as any; docsTestPayload.job.analysis_config.detectors = [{ function: 'count', by_field_name: 'airline' }]; it('creates a docs url pointing to the current docs version', () => { return validateJob(callWithRequest, docsTestPayload).then(messages => { const message = messages[messages.findIndex(m => m.id === 'field_not_aggregatable')]; - expect(message.url.search('/current/')).not.to.be(-1); + expect(message.url.search('/current/')).not.toBe(-1); }); }); it('creates a docs url pointing to the master docs version', () => { return validateJob(callWithRequest, docsTestPayload, 'master').then(messages => { const message = messages[messages.findIndex(m => m.id === 'field_not_aggregatable')]; - expect(message.url.search('/master/')).not.to.be(-1); + expect(message.url.search('/master/')).not.toBe(-1); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/messages.d.ts b/x-pack/plugins/ml/server/models/job_validation/messages.d.ts new file mode 100644 index 00000000000000..772d78b4187dd0 --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_validation/messages.d.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 interface ValidationMessage { + id: string; + url: string; +} diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts similarity index 81% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js rename to x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts index 3dc2bee1e8705f..4001697d743200 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts @@ -4,22 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { validateBucketSpan } from '../validate_bucket_span'; -import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../common/constants/validation'; +import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../common/constants/validation'; + +import { ValidationMessage } from './messages'; +// @ts-ignore +import { validateBucketSpan } from './validate_bucket_span'; // farequote2017 snapshot snapshot mock search response // it returns a mock for the response of PolledDataChecker's search request // to get an aggregation of non_empty_buckets with an interval of 1m. // this allows us to test bucket span estimation. -import mockFareQuoteSearchResponse from './mock_farequote_search_response'; +import mockFareQuoteSearchResponse from './__mocks__/mock_farequote_search_response.json'; // it_ops_app_logs 2017 snapshot mock search response // sparse data with a low number of buckets -import mockItSearchResponse from './mock_it_search_response'; +import mockItSearchResponse from './__mocks__/mock_it_search_response.json'; // mock callWithRequestFactory -const callWithRequestFactory = mockSearchResponse => { +const callWithRequestFactory = (mockSearchResponse: any) => { return () => { return new Promise(resolve => { resolve(mockSearchResponse); @@ -86,17 +88,17 @@ describe('ML - validateBucketSpan', () => { }; return validateBucketSpan(callWithRequestFactory(mockFareQuoteSearchResponse), job).then( - messages => { + (messages: ValidationMessage[]) => { const ids = messages.map(m => m.id); - expect(ids).to.eql([]); + expect(ids).toStrictEqual([]); } ); }); - const getJobConfig = bucketSpan => ({ + const getJobConfig = (bucketSpan: string) => ({ analysis_config: { bucket_span: bucketSpan, - detectors: [], + detectors: [] as Array<{ function?: string }>, influencers: [], }, data_description: { time_field: '@timestamp' }, @@ -111,9 +113,9 @@ describe('ML - validateBucketSpan', () => { callWithRequestFactory(mockFareQuoteSearchResponse), job, duration - ).then(messages => { + ).then((messages: ValidationMessage[]) => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_bucket_span']); + expect(ids).toStrictEqual(['success_bucket_span']); }); }); @@ -125,9 +127,9 @@ describe('ML - validateBucketSpan', () => { callWithRequestFactory(mockFareQuoteSearchResponse), job, duration - ).then(messages => { + ).then((messages: ValidationMessage[]) => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['bucket_span_high']); + expect(ids).toStrictEqual(['bucket_span_high']); }); }); @@ -135,14 +137,18 @@ describe('ML - validateBucketSpan', () => { return; } - const testBucketSpan = (bucketSpan, mockSearchResponse, test) => { + const testBucketSpan = ( + bucketSpan: string, + mockSearchResponse: any, + test: (ids: string[]) => void + ) => { const job = getJobConfig(bucketSpan); job.analysis_config.detectors.push({ function: 'count', }); return validateBucketSpan(callWithRequestFactory(mockSearchResponse), job, {}).then( - messages => { + (messages: ValidationMessage[]) => { const ids = messages.map(m => m.id); test(ids); } @@ -151,13 +157,13 @@ describe('ML - validateBucketSpan', () => { it('farequote count detector, bucket span estimation matches 15m', () => { return testBucketSpan('15m', mockFareQuoteSearchResponse, ids => { - expect(ids).to.eql(['success_bucket_span']); + expect(ids).toStrictEqual(['success_bucket_span']); }); }); it('farequote count detector, bucket span estimation does not match 1m', () => { return testBucketSpan('1m', mockFareQuoteSearchResponse, ids => { - expect(ids).to.eql(['bucket_span_estimation_mismatch']); + expect(ids).toStrictEqual(['bucket_span_estimation_mismatch']); }); }); @@ -167,7 +173,7 @@ describe('ML - validateBucketSpan', () => { // should result in a lower bucket span estimation. it('it_ops_app_logs count detector, bucket span estimation matches 6h', () => { return testBucketSpan('6h', mockItSearchResponse, ids => { - expect(ids).to.eql(['success_bucket_span']); + expect(ids).toStrictEqual(['success_bucket_span']); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts index 22d2fec0beddc4..2fad1252e64465 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts @@ -7,4 +7,7 @@ import { APICaller } from 'kibana/server'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; -export function validateCardinality(callAsCurrentUser: APICaller, job: CombinedJob): any[]; +export function validateCardinality( + callAsCurrentUser: APICaller, + job?: CombinedJob +): Promise; diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts similarity index 69% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js rename to x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts index 9617982a66b0e5..e5111629f1182a 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts @@ -5,11 +5,15 @@ */ import _ from 'lodash'; -import expect from '@kbn/expect'; -import { validateCardinality } from '../validate_cardinality'; -import mockFareQuoteCardinality from './mock_farequote_cardinality'; -import mockFieldCaps from './mock_field_caps'; +import { APICaller } from 'kibana/server'; + +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; + +import mockFareQuoteCardinality from './__mocks__/mock_farequote_cardinality.json'; +import mockFieldCaps from './__mocks__/mock_field_caps.json'; + +import { validateCardinality } from './validate_cardinality'; const mockResponses = { search: mockFareQuoteCardinality, @@ -17,8 +21,8 @@ const mockResponses = { }; // mock callWithRequestFactory -const callWithRequestFactory = (responses, fail = false) => { - return requestName => { +const callWithRequestFactory = (responses: Record, fail = false): APICaller => { + return (requestName: string) => { return new Promise((resolve, reject) => { const response = responses[requestName]; if (fail) { @@ -26,7 +30,7 @@ const callWithRequestFactory = (responses, fail = false) => { } else { resolve(response); } - }); + }) as Promise; }; }; @@ -39,21 +43,23 @@ describe('ML - validateCardinality', () => { }); it('called with non-valid job argument #1, missing analysis_config', done => { - validateCardinality(callWithRequestFactory(mockResponses), {}).then( + validateCardinality(callWithRequestFactory(mockResponses), {} as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); }); it('called with non-valid job argument #2, missing datafeed_config', done => { - validateCardinality(callWithRequestFactory(mockResponses), { analysis_config: {} }).then( + validateCardinality(callWithRequestFactory(mockResponses), { + analysis_config: {}, + } as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); }); it('called with non-valid job argument #3, missing datafeed_config.indices', done => { - const job = { analysis_config: {}, datafeed_config: {} }; + const job = { analysis_config: {}, datafeed_config: {} } as CombinedJob; validateCardinality(callWithRequestFactory(mockResponses), job).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() @@ -61,7 +67,10 @@ describe('ML - validateCardinality', () => { }); it('called with non-valid job argument #4, missing data_description', done => { - const job = { analysis_config: {}, datafeed_config: { indices: [] } }; + const job = ({ + analysis_config: {}, + datafeed_config: { indices: [] }, + } as unknown) as CombinedJob; validateCardinality(callWithRequestFactory(mockResponses), job).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() @@ -69,7 +78,11 @@ describe('ML - validateCardinality', () => { }); it('called with non-valid job argument #5, missing data_description.time_field', done => { - const job = { analysis_config: {}, data_description: {}, datafeed_config: { indices: [] } }; + const job = ({ + analysis_config: {}, + data_description: {}, + datafeed_config: { indices: [] }, + } as unknown) as CombinedJob; validateCardinality(callWithRequestFactory(mockResponses), job).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() @@ -77,11 +90,11 @@ describe('ML - validateCardinality', () => { }); it('called with non-valid job argument #6, missing analysis_config.influencers', done => { - const job = { + const job = ({ analysis_config: {}, datafeed_config: { indices: [] }, data_description: { time_field: '@timestamp' }, - }; + } as unknown) as CombinedJob; validateCardinality(callWithRequestFactory(mockResponses), job).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() @@ -89,21 +102,21 @@ describe('ML - validateCardinality', () => { }); it('minimum job configuration to pass cardinality check code', () => { - const job = { + const job = ({ analysis_config: { detectors: [], influencers: [] }, data_description: { time_field: '@timestamp' }, datafeed_config: { indices: [], }, - }; + } as unknown) as CombinedJob; return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([]); + expect(ids).toStrictEqual([]); }); }); - const getJobConfig = fieldName => ({ + const getJobConfig = (fieldName: string) => ({ analysis_config: { detectors: [ { @@ -119,11 +132,18 @@ describe('ML - validateCardinality', () => { }, }); - const testCardinality = (fieldName, cardinality, test) => { + const testCardinality = ( + fieldName: string, + cardinality: number, + test: (ids: string[]) => void + ) => { const job = getJobConfig(fieldName); const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; - return validateCardinality(callWithRequestFactory(mockCardinality), job, {}).then(messages => { + return validateCardinality( + callWithRequestFactory(mockCardinality), + (job as unknown) as CombinedJob + ).then(messages => { const ids = messages.map(m => m.id); test(ids); }); @@ -132,26 +152,34 @@ describe('ML - validateCardinality', () => { it(`field '_source' not aggregatable`, () => { const job = getJobConfig('partition_field_name'); job.analysis_config.detectors[0].partition_field_name = '_source'; - return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => { + return validateCardinality( + callWithRequestFactory(mockResponses), + (job as unknown) as CombinedJob + ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['field_not_aggregatable']); + expect(ids).toStrictEqual(['field_not_aggregatable']); }); }); it(`field 'airline' aggregatable`, () => { const job = getJobConfig('partition_field_name'); - return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => { + return validateCardinality( + callWithRequestFactory(mockResponses), + (job as unknown) as CombinedJob + ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it('field not aggregatable', () => { const job = getJobConfig('partition_field_name'); - return validateCardinality(callWithRequestFactory({}), job).then(messages => { - const ids = messages.map(m => m.id); - expect(ids).to.eql(['field_not_aggregatable']); - }); + return validateCardinality(callWithRequestFactory({}), (job as unknown) as CombinedJob).then( + messages => { + const ids = messages.map(m => m.id); + expect(ids).toStrictEqual(['field_not_aggregatable']); + } + ); }); it('fields not aggregatable', () => { @@ -160,107 +188,110 @@ describe('ML - validateCardinality', () => { function: 'count', partition_field_name: 'airline', }); - return validateCardinality(callWithRequestFactory({}, true), job).then(messages => { + return validateCardinality( + callWithRequestFactory({}, true), + (job as unknown) as CombinedJob + ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['fields_not_aggregatable']); + expect(ids).toStrictEqual(['fields_not_aggregatable']); }); }); it('valid partition field cardinality', () => { return testCardinality('partition_field_name', 50, ids => { - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it('too high partition field cardinality', () => { return testCardinality('partition_field_name', 1001, ids => { - expect(ids).to.eql(['cardinality_partition_field']); + expect(ids).toStrictEqual(['cardinality_partition_field']); }); }); it('valid by field cardinality', () => { return testCardinality('by_field_name', 50, ids => { - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it('too high by field cardinality', () => { return testCardinality('by_field_name', 1001, ids => { - expect(ids).to.eql(['cardinality_by_field']); + expect(ids).toStrictEqual(['cardinality_by_field']); }); }); it('valid over field cardinality', () => { return testCardinality('over_field_name', 50, ids => { - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it('too low over field cardinality', () => { return testCardinality('over_field_name', 9, ids => { - expect(ids).to.eql(['cardinality_over_field_low']); + expect(ids).toStrictEqual(['cardinality_over_field_low']); }); }); it('too high over field cardinality', () => { return testCardinality('over_field_name', 1000001, ids => { - expect(ids).to.eql(['cardinality_over_field_high']); + expect(ids).toStrictEqual(['cardinality_over_field_high']); }); }); const cardinality = 10000; it(`disabled model_plot, over field cardinality of ${cardinality} doesn't trigger a warning`, () => { - const job = getJobConfig('over_field_name'); + const job = (getJobConfig('over_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: false }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_cardinality']); + expect(ids).toStrictEqual(['success_cardinality']); }); }); it(`enabled model_plot, over field cardinality of ${cardinality} triggers a model plot warning`, () => { - const job = getJobConfig('over_field_name'); + const job = (getJobConfig('over_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: true }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['cardinality_model_plot_high']); + expect(ids).toStrictEqual(['cardinality_model_plot_high']); }); }); it(`disabled model_plot, by field cardinality of ${cardinality} triggers a field cardinality warning`, () => { - const job = getJobConfig('by_field_name'); + const job = (getJobConfig('by_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: false }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['cardinality_by_field']); + expect(ids).toStrictEqual(['cardinality_by_field']); }); }); it(`enabled model_plot, by field cardinality of ${cardinality} triggers a model plot warning and field cardinality warning`, () => { - const job = getJobConfig('by_field_name'); + const job = (getJobConfig('by_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: true }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['cardinality_model_plot_high', 'cardinality_by_field']); + expect(ids).toStrictEqual(['cardinality_model_plot_high', 'cardinality_by_field']); }); }); it(`enabled model_plot with terms, by field cardinality of ${cardinality} triggers just field cardinality warning`, () => { - const job = getJobConfig('by_field_name'); + const job = (getJobConfig('by_field_name') as unknown) as CombinedJob; job.model_plot_config = { enabled: true, terms: 'AAL,AAB' }; const mockCardinality = _.cloneDeep(mockResponses); mockCardinality.search.aggregations.airline_cardinality.value = cardinality; return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['cardinality_by_field']); + expect(ids).toStrictEqual(['cardinality_by_field']); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts similarity index 63% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js rename to x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts index 06b2e5205fdbde..df3310ad9f5e82 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts @@ -4,19 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { validateInfluencers } from '../validate_influencers'; +import { APICaller } from 'kibana/server'; + +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; + +import { validateInfluencers } from './validate_influencers'; describe('ML - validateInfluencers', () => { it('called without arguments throws an error', done => { - validateInfluencers().then( + validateInfluencers( + (undefined as unknown) as APICaller, + (undefined as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without job argument.')), () => done() ); }); it('called with non-valid job argument #1, missing analysis_config', done => { - validateInfluencers(undefined, {}).then( + validateInfluencers((undefined as unknown) as APICaller, ({} as unknown) as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -28,7 +34,7 @@ describe('ML - validateInfluencers', () => { datafeed_config: { indices: [] }, data_description: { time_field: '@timestamp' }, }; - validateInfluencers(undefined, job).then( + validateInfluencers((undefined as unknown) as APICaller, (job as unknown) as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -40,25 +46,29 @@ describe('ML - validateInfluencers', () => { datafeed_config: { indices: [] }, data_description: { time_field: '@timestamp' }, }; - validateInfluencers(undefined, job).then( + validateInfluencers((undefined as unknown) as APICaller, (job as unknown) as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); }); - const getJobConfig = (influencers = [], detectors = []) => ({ - analysis_config: { detectors, influencers }, - data_description: { time_field: '@timestamp' }, - datafeed_config: { - indices: [], - }, - }); + const getJobConfig: ( + influencers?: string[], + detectors?: CombinedJob['analysis_config']['detectors'] + ) => CombinedJob = (influencers = [], detectors = []) => + (({ + analysis_config: { detectors, influencers }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [], + }, + } as unknown) as CombinedJob); it('success_influencer', () => { const job = getJobConfig(['airline']); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_influencers']); + expect(ids).toStrictEqual(['success_influencers']); }); }); @@ -69,31 +79,30 @@ describe('ML - validateInfluencers', () => { { detector_description: 'count', function: 'count', - rules: [], detector_index: 0, }, ] ); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql([]); + expect(ids).toStrictEqual([]); }); }); it('influencer_low', () => { const job = getJobConfig(); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['influencer_low']); + expect(ids).toStrictEqual(['influencer_low']); }); }); it('influencer_high', () => { const job = getJobConfig(['i1', 'i2', 'i3', 'i4']); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['influencer_high']); + expect(ids).toStrictEqual(['influencer_high']); }); }); @@ -105,14 +114,13 @@ describe('ML - validateInfluencers', () => { detector_description: 'count', function: 'count', partition_field_name: 'airline', - rules: [], detector_index: 0, }, ] ); - return validateInfluencers(undefined, job).then(messages => { + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['influencer_low_suggestion']); + expect(ids).toStrictEqual(['influencer_low_suggestion']); }); }); @@ -124,27 +132,24 @@ describe('ML - validateInfluencers', () => { detector_description: 'count', function: 'count', partition_field_name: 'partition_field', - rules: [], detector_index: 0, }, { detector_description: 'count', function: 'count', by_field_name: 'by_field', - rules: [], detector_index: 0, }, { detector_description: 'count', function: 'count', over_field_name: 'over_field', - rules: [], detector_index: 0, }, ] ); - return validateInfluencers(undefined, job).then(messages => { - expect(messages).to.eql([ + return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => { + expect(messages).toStrictEqual([ { id: 'influencer_low_suggestions', influencerSuggestion: '["partition_field","by_field","over_field"]', diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts similarity index 89% rename from x-pack/plugins/ml/server/models/job_validation/validate_influencers.js rename to x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts index 60fd5c37b99586..e54ffc4586a8e3 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_influencers.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts @@ -4,19 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; + import { validateJobObject } from './validate_job_object'; const INFLUENCER_LOW_THRESHOLD = 0; const INFLUENCER_HIGH_THRESHOLD = 4; const DETECTOR_FIELD_NAMES_THRESHOLD = 1; -export async function validateInfluencers(callWithRequest, job) { +export async function validateInfluencers(callWithRequest: APICaller, job: CombinedJob) { validateJobObject(job); const messages = []; const influencers = job.analysis_config.influencers; - const detectorFieldNames = []; + const detectorFieldNames: string[] = []; job.analysis_config.detectors.forEach(d => { if (d.by_field_name) { detectorFieldNames.push(d.by_field_name); diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts similarity index 76% rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js rename to x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts index e3ef62e5074850..2c3b2dd4dc6ae1 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts @@ -5,28 +5,32 @@ */ import _ from 'lodash'; -import expect from '@kbn/expect'; -import { isValidTimeField, validateTimeRange } from '../validate_time_range'; -import mockTimeField from './mock_time_field'; -import mockTimeFieldNested from './mock_time_field_nested'; -import mockTimeRange from './mock_time_range'; +import { APICaller } from 'kibana/server'; + +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; + +import { isValidTimeField, validateTimeRange } from './validate_time_range'; + +import mockTimeField from './__mocks__/mock_time_field.json'; +import mockTimeFieldNested from './__mocks__/mock_time_field_nested.json'; +import mockTimeRange from './__mocks__/mock_time_range.json'; const mockSearchResponse = { fieldCaps: mockTimeField, search: mockTimeRange, }; -const callWithRequestFactory = resp => { - return path => { +const callWithRequestFactory = (resp: any): APICaller => { + return (path: string) => { return new Promise(resolve => { resolve(resp[path]); - }); + }) as Promise; }; }; function getMinimalValidJob() { - return { + return ({ analysis_config: { bucket_span: '15m', detectors: [], @@ -36,12 +40,15 @@ function getMinimalValidJob() { datafeed_config: { indices: [], }, - }; + } as unknown) as CombinedJob; } describe('ML - isValidTimeField', () => { it('called without job config argument triggers Promise rejection', done => { - isValidTimeField(callWithRequestFactory(mockSearchResponse)).then( + isValidTimeField( + callWithRequestFactory(mockSearchResponse), + (undefined as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without job argument.')), () => done() ); @@ -50,7 +57,7 @@ describe('ML - isValidTimeField', () => { it('time_field `@timestamp`', done => { isValidTimeField(callWithRequestFactory(mockSearchResponse), getMinimalValidJob()).then( valid => { - expect(valid).to.be(true); + expect(valid).toBe(true); done(); }, () => done(new Error('isValidTimeField Promise failed for time_field `@timestamp`.')) @@ -71,7 +78,7 @@ describe('ML - isValidTimeField', () => { mockJobConfigNestedDate ).then( valid => { - expect(valid).to.be(true); + expect(valid).toBe(true); done(); }, () => done(new Error('isValidTimeField Promise failed for time_field `metadata.timestamp`.')) @@ -81,14 +88,19 @@ describe('ML - isValidTimeField', () => { describe('ML - validateTimeRange', () => { it('called without arguments', done => { - validateTimeRange(callWithRequestFactory(mockSearchResponse)).then( + validateTimeRange( + callWithRequestFactory(mockSearchResponse), + (undefined as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without job argument.')), () => done() ); }); it('called with non-valid job argument #2, missing datafeed_config', done => { - validateTimeRange(callWithRequestFactory(mockSearchResponse), { analysis_config: {} }).then( + validateTimeRange(callWithRequestFactory(mockSearchResponse), ({ + analysis_config: {}, + } as unknown) as CombinedJob).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -96,7 +108,10 @@ describe('ML - validateTimeRange', () => { it('called with non-valid job argument #3, missing datafeed_config.indices', done => { const job = { analysis_config: {}, datafeed_config: {} }; - validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then( + validateTimeRange( + callWithRequestFactory(mockSearchResponse), + (job as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -104,7 +119,10 @@ describe('ML - validateTimeRange', () => { it('called with non-valid job argument #4, missing data_description', done => { const job = { analysis_config: {}, datafeed_config: { indices: [] } }; - validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then( + validateTimeRange( + callWithRequestFactory(mockSearchResponse), + (job as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -112,7 +130,10 @@ describe('ML - validateTimeRange', () => { it('called with non-valid job argument #5, missing data_description.time_field', done => { const job = { analysis_config: {}, data_description: {}, datafeed_config: { indices: [] } }; - validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then( + validateTimeRange( + callWithRequestFactory(mockSearchResponse), + (job as unknown) as CombinedJob + ).then( () => done(new Error('Promise should not resolve for this test without valid job argument.')), () => done() ); @@ -128,7 +149,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_field_invalid']); + expect(ids).toStrictEqual(['time_field_invalid']); }); }); @@ -142,7 +163,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_range_short']); + expect(ids).toStrictEqual(['time_range_short']); }); }); @@ -154,7 +175,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_range_short']); + expect(ids).toStrictEqual(['time_range_short']); }); }); @@ -166,7 +187,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_range_short']); + expect(ids).toStrictEqual(['time_range_short']); }); }); @@ -178,7 +199,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['success_time_range']); + expect(ids).toStrictEqual(['success_time_range']); }); }); @@ -190,7 +211,7 @@ describe('ML - validateTimeRange', () => { duration ).then(messages => { const ids = messages.map(m => m.id); - expect(ids).to.eql(['time_range_before_epoch']); + expect(ids).toStrictEqual(['time_range_before_epoch']); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts index 5f734387698512..4fb09af94dcc68 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts @@ -37,9 +37,9 @@ export async function isValidTimeField(callAsCurrentUser: APICaller, job: Combin fields: [timeField], }); - let fieldType = fieldCaps.fields[timeField]?.date?.type; + let fieldType = fieldCaps?.fields[timeField]?.date?.type; if (fieldType === undefined) { - fieldType = fieldCaps.fields[timeField]?.date_nanos?.type; + fieldType = fieldCaps?.fields[timeField]?.date_nanos?.type; } return fieldType === ES_FIELD_TYPES.DATE || fieldType === ES_FIELD_TYPES.DATE_NANOS; } @@ -47,7 +47,7 @@ export async function isValidTimeField(callAsCurrentUser: APICaller, job: Combin export async function validateTimeRange( callAsCurrentUser: APICaller, job: CombinedJob, - timeRange: TimeRange | undefined + timeRange?: TimeRange ) { const messages: ValidateTimeRangeMessage[] = []; From 459daa7f230961bead48fe3f334bef65148d90b5 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 7 May 2020 10:01:51 -0400 Subject: [PATCH 14/16] [Lens] Use rules of hooks with linting (#65593) --- .eslintrc.js | 1 - .../editor_frame/config_panel/layer_panel.tsx | 12 +-- .../editor_frame/workspace_panel.tsx | 20 ++--- .../datapanel.test.tsx | 12 ++- .../indexpattern_datasource/datapanel.tsx | 87 +++++++++---------- .../dimension_panel/bucket_nesting_editor.tsx | 2 +- .../indexpattern_datasource/field_item.tsx | 31 ++++--- 7 files changed, 86 insertions(+), 79 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 56c06902e062b5..f1e0b7d9353e8b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -112,7 +112,6 @@ module.exports = { files: ['x-pack/plugins/lens/**/*.{js,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', - 'react-hooks/rules-of-hooks': 'off', }, }, { 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 f7be82dd34ba39..81476e8fa37089 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 @@ -43,6 +43,12 @@ export function LayerPanel( } ) { const dragDropContext = useContext(DragContext); + const [popoverState, setPopoverState] = useState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; if (!datasourcePublicAPI) { @@ -74,12 +80,6 @@ export function LayerPanel( dateRange: props.framePublicAPI.dateRange, }; - const [popoverState, setPopoverState] = useState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); const isEmptyLayer = !groups.some(d => d.accessors.length > 0); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index 1f741ca37934fc..e246d8e27a7089 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx @@ -122,6 +122,16 @@ export function InnerWorkspacePanel({ framePublicAPI.filters, ]); + useEffect(() => { + // reset expression error if component attempts to run it again + if (expression && localState.expressionBuildError) { + setLocalState(s => ({ + ...s, + expressionBuildError: undefined, + })); + } + }, [expression]); + function onDrop() { if (suggestionForDraggedField) { trackUiEvent('drop_onto_workspace'); @@ -174,16 +184,6 @@ export function InnerWorkspacePanel({ } function renderVisualization() { - useEffect(() => { - // reset expression error if component attempts to run it again - if (expression && localState.expressionBuildError) { - setLocalState(s => ({ - ...s, - expressionBuildError: undefined, - })); - } - }, [expression]); - if (expression === null) { return renderEmptyWorkspace(); } 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 c396f0efee42ed..5e3b32f6961e68 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -258,7 +258,17 @@ describe('IndexPattern Data Panel', () => { it('should render a warning if there are no index patterns', () => { const wrapper = shallowWithIntl( - + {} }} + changeIndexPattern={jest.fn()} + /> ); expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 79dcdafd916b4c..b013f2b9d22a67 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -144,21 +144,49 @@ export function IndexPatternDataPanel({ indexPatternList.map(x => `${x.title}:${x.timeFieldName}`).join(','), ]} /> - + + {Object.keys(indexPatterns).length === 0 ? ( + + + +

      + +

      +
      +
      +
      + ) : ( + + )} ); } @@ -194,35 +222,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ onChangeIndexPattern: (newId: string) => void; existingFields: IndexPatternPrivateState['existingFields']; }) { - if (Object.keys(indexPatterns).length === 0) { - return ( - - - -

      - -

      -
      -
      -
      - ); - } - const [localState, setLocalState] = useState({ nameFilter: '', typeFilter: [], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx index 04e13fead6fca0..7e2af6a19b0413 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx @@ -127,7 +127,7 @@ export function BucketNestingEditor({ defaultMessage: 'Entire data set', }), }, - ...aggColumns, + ...aggColumns.map(({ value, text }) => ({ value, text })), ]} value={prevColumn} onChange={e => setColumns(nestColumn(layer.columnOrder, e.target.value, columnId))} 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 c4d2a6f8780c6a..5f0fa95ad0022a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -251,22 +251,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - - if (props.isLoading) { - return ; - } else if ( - (!props.histogram || props.histogram.buckets.length === 0) && - (!props.topValues || props.topValues.buckets.length === 0) - ) { - return ( - - {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { - defaultMessage: 'No data to display.', - })} - - ); - } - let histogramDefault = !!props.histogram; const totalValuesCount = @@ -309,6 +293,21 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { let title = <>; + if (props.isLoading) { + return ; + } else if ( + (!props.histogram || props.histogram.buckets.length === 0) && + (!props.topValues || props.topValues.buckets.length === 0) + ) { + return ( + + {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { + defaultMessage: 'No data to display.', + })} + + ); + } + if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { title = ( Date: Thu, 7 May 2020 08:06:49 -0600 Subject: [PATCH 15/16] [SIEM][Detection Engine] Fixes import bug with non existent signals index (#65595) See: https://github.com/elastic/kibana/issues/65565 * Fixes it to where if there is an import without an index then the rule is not created ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../routes/rules/import_rules_route.test.ts | 32 +- .../routes/rules/import_rules_route.ts | 294 +++++++++--------- .../security_and_spaces/tests/import_rules.ts | 51 ++- 3 files changed, 200 insertions(+), 177 deletions(-) diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index e7db2282258804..91685a68a60ae9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -122,20 +122,11 @@ describe('import_rules_route', () => { clients.siemClient.getSignalsIndex.mockReturnValue('mockSignalsIndex'); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); const response = await server.inject(request, context); - expect(response.status).toEqual(200); + expect(response.status).toEqual(400); expect(response.body).toEqual({ - errors: [ - { - error: { - message: - 'To create a rule, the index must exist first. Index mockSignalsIndex does not exist', - status_code: 409, - }, - rule_id: 'rule-1', - }, - ], - success: false, - success_count: 0, + message: + 'To create a rule, the index must exist first. Index mockSignalsIndex does not exist', + status_code: 400, }); }); @@ -145,19 +136,10 @@ describe('import_rules_route', () => { }); const response = await server.inject(request, context); - expect(response.status).toEqual(200); + expect(response.status).toEqual(500); expect(response.body).toEqual({ - errors: [ - { - error: { - message: 'Test error', - status_code: 400, - }, - rule_id: 'rule-1', - }, - ], - success: false, - success_count: 0, + message: 'Test error', + status_code: 500, }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 4d86f0bec6502a..9ba083ae48086e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -75,6 +75,14 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { body: `Invalid file extension ${fileExtension}`, }); } + const signalsIndex = siemClient.getSignalsIndex(); + const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, signalsIndex); + if (!indexExists) { + return siemResponse.error({ + statusCode: 400, + body: `To create a rule, the index must exist first. Index ${signalsIndex} does not exist`, + }); + } const objectLimit = config.maxRuleImportExportSize; const readStream = createRulesStreamFromNdJson(objectLimit); @@ -94,166 +102,150 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { const batchParseObjects = chunkParseObjects.shift() ?? []; const newImportRuleResponse = await Promise.all( batchParseObjects.reduce>>((accum, parsedRule) => { - const importsWorkerPromise = new Promise( - async (resolve, reject) => { - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedRule.message, - }) - ); - return null; - } - const { - anomaly_threshold: anomalyThreshold, - description, - enabled, - false_positives: falsePositives, - from, - immutable, - query, - language, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - note, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - exceptions_list, - } = parsedRule; + const importsWorkerPromise = new Promise(async resolve => { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + statusCode: 400, + message: parsedRule.message, + }) + ); + return null; + } + const { + anomaly_threshold: anomalyThreshold, + description, + enabled, + false_positives: falsePositives, + from, + immutable, + query, + language, + machine_learning_job_id: machineLearningJobId, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + note, + timeline_id: timelineId, + timeline_title: timelineTitle, + version, + exceptions_list, + } = parsedRule; - try { - validateLicenseForRuleType({ - license: context.licensing.license, - ruleType: type, - }); + try { + validateLicenseForRuleType({ + license: context.licensing.license, + ruleType: type, + }); - const signalsIndex = siemClient.getSignalsIndex(); - const indexExists = await getIndexExists( - clusterClient.callAsCurrentUser, - signalsIndex - ); - if (!indexExists) { - resolve( - createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `To create a rule, the index must exist first. Index ${signalsIndex} does not exist`, - }) - ); - } - const rule = await readRules({ alertsClient, ruleId }); - if (rule == null) { - await createRules({ - alertsClient, - anomalyThreshold, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - machineLearningJobId, - outputIndex: signalsIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - note, - version, - exceptions_list, - actions: [], // Actions are not imported nor exported at this time - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null && request.query.overwrite) { - await patchRules({ - alertsClient, - savedObjectsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - id: undefined, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - note, - version, - exceptions_list, - anomalyThreshold, - machineLearningJobId, - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null) { - resolve( - createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, - }) - ); - } - } catch (err) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule == null) { + await createRules({ + alertsClient, + anomalyThreshold, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + machineLearningJobId, + outputIndex: signalsIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + note, + version, + exceptions_list, + actions: [], // Actions are not imported nor exported at this time + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null && request.query.overwrite) { + await patchRules({ + alertsClient, + savedObjectsClient, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + id: undefined, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + note, + version, + exceptions_list, + anomalyThreshold, + machineLearningJobId, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null) { resolve( createBulkErrorObject({ ruleId, - statusCode: 400, - message: err.message, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, }) ); } + } catch (err) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + }) + ); } - ); + }); return [...accum, importsWorkerPromise]; }, []) ); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index 4def508fabbc31..868dafedc6849f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -25,7 +25,56 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('legacyEs'); describe('import_rules', () => { - describe('importing rules', () => { + describe('importing rules without an index', () => { + it('should not create a rule if the index does not exist', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(400); + + // We have to wait up to 5 seconds for any unresolved promises to flush + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Try to fetch the rule which should still be a 404 (not found) + const { body } = await supertest.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`).send(); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "rule-1" not found', + }); + }); + + it('should return an error that the index needs to be created before you are able to import a single rule', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + + it('should return an error that the index needs to be created before you are able to import two rules', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + }); + + describe('importing rules with an index', () => { beforeEach(async () => { await createSignalsIndex(supertest); }); From 834e3bf8ef64825100ac088675b9da026c7fa769 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 7 May 2020 16:24:17 +0200 Subject: [PATCH 16/16] [ML] Transforms: Fix API error message display for edit flyout. (#65494) Fixes an issue where the transform edit flyout would be hidden if an error occurred and the user closed the error toast. This fixes it by showing the error message within an callout in the flyout itself. The bug is a side effect of the problem with the edit-button and it's corresponding React tree being within the transform list actions popover which will be solved in a follow up but possibly not for 7.8.0 which makes this workaround necessary. --- .../edit_transform_flyout.tsx | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index a794b7e7c21438..d3dae0a8c8b63c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiFlyout, @@ -18,11 +19,10 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiOverlayMask, + EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; - import { getErrorMessage } from '../../../../../shared_imports'; import { @@ -30,8 +30,7 @@ import { TransformPivotConfig, REFRESH_TRANSFORM_LIST_STATE, } from '../../../../common'; -import { ToastNotificationText } from '../../../../components'; -import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; +import { useToastNotifications } from '../../../../app_dependencies'; import { useApi } from '../../../../hooks/use_api'; @@ -48,13 +47,14 @@ interface EditTransformFlyoutProps { } export const EditTransformFlyout: FC = ({ closeFlyout, config }) => { - const { overlays } = useAppDependencies(); const api = useApi(); const toastNotifications = useToastNotifications(); const [state, dispatch] = useEditTransformFlyout(config); + const [errorMessage, setErrorMessage] = useState(undefined); async function submitFormHandler() { + setErrorMessage(undefined); const requestConfig = applyFormFieldsToTransformConfig(config, state.formFields); const transformId = config.id; @@ -69,12 +69,7 @@ export const EditTransformFlyout: FC = ({ closeFlyout, closeFlyout(); refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.transformList.editTransformGenericErrorMessage', { - defaultMessage: 'An error occurred calling the API endpoint to update transforms.', - }), - text: toMountPoint(), - }); + setErrorMessage(getErrorMessage(e)); } } @@ -97,6 +92,24 @@ export const EditTransformFlyout: FC = ({ closeFlyout, }> + {errorMessage !== undefined && ( + <> + + +

      {errorMessage}

      +
      + + )}