From 26f088970ed2ae66e06a99a77320601a5ed3a272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Wed, 17 Feb 2021 12:11:48 +0100 Subject: [PATCH 1/3] [DOCS] Adds machine learning to the security section of alerting (#91501) --- docs/user/alerting/alert-types.asciidoc | 2 +- docs/user/alerting/alerting-getting-started.asciidoc | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 016ecc3167298d..0877f067eee217 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -2,7 +2,7 @@ [[alert-types]] == Standard stack alert types -{kib} supplies alert types in two ways: some are built into {kib} (these are known as stack alerts), while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. +{kib} supplies alert types in two ways: some are built into {kib} (these are known as stack alerts), while domain-specific alert types are registered by {kib} apps such as <>, <>, <>, and <>. This section covers stack alerts. For domain-specific alert types, refer to the documentation for that app. Users will need `all` access to the *Stack Alerts* feature to be able to create and edit any of the alerts listed below. diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 06370c64aedf82..6186fce8a51c49 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -172,6 +172,7 @@ To access alerting in a space, a user must have access to one of the following f * Alerting * <> * <> +* <> * <> * <> * <> @@ -202,4 +203,4 @@ If an alert requires certain privileges to run such as index privileges, keep in For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. --- \ No newline at end of file +-- From edc11d96801731ac54ece7e4e7fd100c72c074dc Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 17 Feb 2021 12:57:23 +0100 Subject: [PATCH 2/3] [Ingest Pipelines] Preserve unknown fields in processors (#91146) * keep known and unknown options in processor config * added test for preserving unknown fields * refactor form for NOT stripping empty field values, also allow empty "value" for set and gsub * remove unused i18n * fix user agent form serialization, update field help text * remove out of date translation Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pipeline_processors_editor.helpers.tsx | 11 ++++ .../pipeline_processors_editor.test.tsx | 65 +++++++++++++++---- .../processor_form.container.tsx | 17 ++++- .../common_fields/common_processor_fields.tsx | 2 + .../processors/common_fields/target_field.tsx | 3 +- .../processor_form/processors/csv.tsx | 7 +- .../processor_form/processors/date.tsx | 6 +- .../processors/date_index_name.tsx | 10 +-- .../processor_form/processors/dissect.tsx | 3 +- .../processors/dot_expander.tsx | 3 + .../processor_form/processors/geoip.tsx | 2 +- .../processor_form/processors/gsub.tsx | 15 ++--- .../processor_form/processors/kv.tsx | 3 + .../processor_form/processors/script.tsx | 2 +- .../processor_form/processors/set.tsx | 43 ++++++------ .../processor_form/processors/shared.ts | 8 ++- .../processor_form/processors/sort.tsx | 4 +- .../processor_form/processors/user_agent.tsx | 3 +- .../context/processors_context.tsx | 13 +++- .../ingest_pipelines/public/shared_imports.ts | 1 + .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 22 files changed, 154 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx index 55188805d571cd..fabb6a46c49435 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -154,6 +154,17 @@ const createActions = (testBed: TestBed) => { find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click'); }); }, + openProcessorEditor: (processorSelector: string) => { + act(() => { + find(`${processorSelector}.manageItemButton`).simulate('click'); + }); + component.update(); + }, + submitProcessorForm: async () => { + await act(async () => { + find('editProcessorForm.submitButton').simulate('click'); + }); + }, }; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx index 1c698043a8bc78..e89e91c1cbaa91 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx @@ -22,6 +22,13 @@ const testProcessors: Pick = { replacement: '$17$2', }, }, + { + set: { + field: 'test', + value: 'test', + unknown_field_foo: 'unknown_value', + }, + }, ], }; @@ -79,11 +86,37 @@ describe('Pipeline Editor', () => { await actions.addProcessor('processors', 'test', { if: '1 == 1' }); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(3); - const [a, b, c] = processors; + expect(processors.length).toBe(4); + const [a, b, c, d] = processors; expect(a).toEqual(testProcessors.processors[0]); expect(b).toEqual(testProcessors.processors[1]); - expect(c).toEqual({ test: { if: '1 == 1' } }); + expect(c).toEqual(testProcessors.processors[2]); + expect(d).toEqual({ test: { if: '1 == 1' } }); + }); + + it('edits a processor without removing unknown processor.options', async () => { + const { actions, exists, form } = testBed; + // Open the edit processor form for the set processor + actions.openProcessorEditor('processors>2'); + expect(exists('editProcessorForm')).toBeTruthy(); + form.setInputValue('editProcessorForm.valueFieldInput', 'test44'); + await actions.submitProcessorForm(); + const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; + const { + processors: { 2: setProcessor }, + } = onUpdateResult.getData(); + // The original field should still be unchanged + expect(testProcessors.processors[2].set.value).toBe('test'); + expect(setProcessor.set).toEqual({ + description: undefined, + field: 'test', + ignore_empty_value: undefined, + ignore_failure: undefined, + override: undefined, + // This unknown_field is not supported in the form + unknown_field_foo: 'unknown_value', + value: 'test44', + }); }); it('removes a processor', () => { @@ -92,7 +125,7 @@ describe('Pipeline Editor', () => { actions.removeProcessor('processors>0'); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(1); + expect(processors.length).toBe(2); expect(processors[0]).toEqual({ gsub: { field: '_index', @@ -107,7 +140,11 @@ describe('Pipeline Editor', () => { actions.moveProcessor('processors>0', 'dropButtonBelow-processors>1'); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors).toEqual(testProcessors.processors.slice(0).reverse()); + expect(processors).toEqual([ + testProcessors.processors[1], + testProcessors.processors[0], + testProcessors.processors[2], + ]); }); it('adds an on-failure processor to a processor', async () => { @@ -121,7 +158,7 @@ describe('Pipeline Editor', () => { expect(exists(`${processorSelector}.addProcessor`)); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(2); + expect(processors.length).toBe(3); expect(processors[0]).toEqual(testProcessors.processors[0]); // should be unchanged expect(processors[1].gsub).toEqual({ ...testProcessors.processors[1].gsub, @@ -135,7 +172,7 @@ describe('Pipeline Editor', () => { actions.moveProcessor('processors>0', 'dropButtonBelow-processors>1>onFailure>0'); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(1); + expect(processors.length).toBe(2); expect(processors[0].gsub.on_failure).toEqual([ { test: { if: '1 == 3' }, @@ -150,7 +187,7 @@ describe('Pipeline Editor', () => { actions.duplicateProcessor('processors>1'); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(3); + expect(processors.length).toBe(4); const duplicatedProcessor = { gsub: { ...testProcessors.processors[1].gsub, @@ -161,6 +198,7 @@ describe('Pipeline Editor', () => { testProcessors.processors[0], duplicatedProcessor, duplicatedProcessor, + testProcessors.processors[2], ]); }); @@ -182,14 +220,17 @@ describe('Pipeline Editor', () => { actions.moveProcessor('processors>0', 'dropButtonBelow-onFailure>0'); const [onUpdateResult1] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const data1 = onUpdateResult1.getData(); - expect(data1.processors.length).toBe(1); + expect(data1.processors.length).toBe(2); expect(data1.on_failure.length).toBe(2); - expect(data1.processors).toEqual([testProcessors.processors[1]]); + expect(data1.processors).toEqual([ + testProcessors.processors[1], + testProcessors.processors[2], + ]); expect(data1.on_failure).toEqual([{ test: { if: '1 == 5' } }, testProcessors.processors[0]]); actions.moveProcessor('onFailure>1', 'dropButtonAbove-processors>0'); const [onUpdateResult2] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const data2 = onUpdateResult2.getData(); - expect(data2.processors.length).toBe(2); + expect(data2.processors.length).toBe(3); expect(data2.on_failure.length).toBe(1); expect(data2.processors).toEqual(testProcessors.processors); expect(data2.on_failure).toEqual([{ test: { if: '1 == 5' } }]); @@ -208,7 +249,7 @@ describe('Pipeline Editor', () => { actions.moveProcessor('processors>0', 'onFailure.dropButtonEmptyTree'); const [onUpdateResult2] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const data = onUpdateResult2.getData(); - expect(data.processors).toEqual([testProcessors.processors[1]]); + expect(data.processors).toEqual([testProcessors.processors[1], testProcessors.processors[2]]); expect(data.on_failure).toEqual([testProcessors.processors[0]]); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx index 5ce8cf09cc5942..cce473ec9a214f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx @@ -7,7 +7,13 @@ import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; -import { useForm, OnFormUpdateArg, FormData, useKibana } from '../../../../../shared_imports'; +import { + useForm, + OnFormUpdateArg, + FormData, + FormOptions, + useKibana, +} from '../../../../../shared_imports'; import { ProcessorInternal } from '../../types'; import { EditProcessorForm } from './edit_processor_form'; @@ -33,6 +39,14 @@ interface Props { processor?: ProcessorInternal; } +const formOptions: FormOptions = { + /** + * This is important for allowing configuration of empty text fields in certain processors that + * remove values from their inputs. + */ + stripEmptyFields: false, +}; + export const ProcessorFormContainer: FunctionComponent = ({ processor, onFormUpdate, @@ -81,6 +95,7 @@ export const ProcessorFormContainer: FunctionComponent = ({ const { form } = useForm({ defaultValue: { fields: getProcessor().options }, serializer: formSerializer, + options: formOptions, }); const { subscribe } = form; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx index 12226608a46821..f6449c3cc24d42 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx @@ -38,6 +38,7 @@ const ignoreFailureConfig: FieldConfig = { }; const ifConfig: FieldConfig = { + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel', { defaultMessage: 'Condition (optional)', }), @@ -48,6 +49,7 @@ const ifConfig: FieldConfig = { }; const tagConfig: FieldConfig = { + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel', { defaultMessage: 'Tag (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx index 1caf5ffd3fb1e5..b603a131e10b09 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx @@ -10,12 +10,13 @@ import { i18n } from '@kbn/i18n'; import { Field, FIELD_TYPES, UseField, FieldConfig } from '../../../../../../../shared_imports'; -import { FieldsConfig } from '../shared'; +import { FieldsConfig, from } from '../shared'; const fieldsConfig: FieldsConfig = { target_field: { type: FIELD_TYPES.TEXT, deserializer: String, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldLabel', { defaultMessage: 'Target field (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx index 19176a27a07781..b192ee0494bb3c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx @@ -24,7 +24,7 @@ import { FieldsConfig } from './shared'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldNameField } from './common_fields/field_name_field'; -import { to } from './shared'; +import { to, from } from './shared'; const { minLengthField } = fieldValidators; @@ -72,7 +72,7 @@ const fieldsConfig: FieldsConfig = { /* Optional fields config */ separator: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.separatorFieldLabel', { defaultMessage: 'Separator (optional)', }), @@ -91,7 +91,7 @@ const fieldsConfig: FieldsConfig = { }, quote: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.quoteFieldLabel', { defaultMessage: 'Quote (optional)', }), @@ -121,6 +121,7 @@ const fieldsConfig: FieldsConfig = { }, empty_value: { type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldLabel', { defaultMessage: 'Empty value (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx index e8e956daff2074..ca541a9e6d6195 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx @@ -18,7 +18,7 @@ import { ComboBoxField, } from '../../../../../../shared_imports'; -import { FieldsConfig, to } from './shared'; +import { FieldsConfig, to, from } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; @@ -53,7 +53,7 @@ const fieldsConfig: FieldsConfig = { /* Optional fields config */ timezone: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.timezoneFieldLabel', { defaultMessage: 'Timezone (optional)', }), @@ -67,7 +67,7 @@ const fieldsConfig: FieldsConfig = { }, locale: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.localeFieldLabel', { defaultMessage: 'Locale (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx index 182b9ecd845e92..5c5b5ff89fd20c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx @@ -19,7 +19,7 @@ import { SelectField, } from '../../../../../../shared_imports'; -import { FieldsConfig, to } from './shared'; +import { FieldsConfig, to, from } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; const { emptyField } = fieldValidators; @@ -57,7 +57,7 @@ const fieldsConfig: FieldsConfig = { /* Optional fields config */ index_name_prefix: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldLabel', { @@ -71,7 +71,7 @@ const fieldsConfig: FieldsConfig = { }, index_name_format: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldLabel', { @@ -108,7 +108,7 @@ const fieldsConfig: FieldsConfig = { }, timezone: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneFieldLabel', { @@ -125,7 +125,7 @@ const fieldsConfig: FieldsConfig = { }, locale: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.localeFieldLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx index 641a6e73d90251..6652ad277cc265 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx @@ -22,7 +22,7 @@ import { import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; -import { EDITOR_PX_HEIGHT } from './shared'; +import { EDITOR_PX_HEIGHT, from } from './shared'; const { emptyField } = fieldValidators; @@ -72,6 +72,7 @@ const getFieldsConfig = (esDocUrl: string): Record => { /* Optional field config */ append_separator: { type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx index 0bbcb7a2eefb34..4bbc242cf0ef88 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx @@ -12,9 +12,12 @@ import { FieldConfig, FIELD_TYPES, UseField, Field } from '../../../../../../sha import { FieldNameField } from './common_fields/field_name_field'; +import { from } from './shared'; + const fieldsConfig: Record = { path: { type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathFieldLabel', { defaultMessage: 'Path', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx index 7848872800df45..6a1f86977d8db5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx @@ -22,7 +22,7 @@ const fieldsConfig: FieldsConfig = { /* Optional field config */ database_file: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v === 'GeoLite2-City.mmdb' ? undefined : v), + serializer: (v) => (v === 'GeoLite2-City.mmdb' || v === '' ? undefined : v), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.geoIPForm.databaseFileLabel', { defaultMessage: 'Database file (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx index 8835f3775a90f0..edfa59ea80281c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx @@ -41,6 +41,7 @@ const fieldsConfig: FieldsConfig = { ], }, + // This is a required field, but we exclude validation because we accept empty values as '' replacement: { type: FIELD_TYPES.TEXT, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldLabel', { @@ -48,17 +49,11 @@ const fieldsConfig: FieldsConfig = { }), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldHelpText', - { defaultMessage: 'Replacement text for matches.' } - ), - validations: [ { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError', { - defaultMessage: 'A value is required.', - }) - ), - }, - ], + defaultMessage: + 'Replacement text for matches. A blank value will remove the matched text from the resulting text.', + } + ), }, }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx index 2d17e3600cb79a..694ae4e07070d4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx @@ -107,6 +107,7 @@ const fieldsConfig: FieldsConfig = { prefix: { type: FIELD_TYPES.TEXT, deserializer: String, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixFieldLabel', { defaultMessage: 'Prefix', }), @@ -118,6 +119,7 @@ const fieldsConfig: FieldsConfig = { trim_key: { type: FIELD_TYPES.TEXT, deserializer: String, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyFieldLabel', { defaultMessage: 'Trim key', }), @@ -129,6 +131,7 @@ const fieldsConfig: FieldsConfig = { trim_value: { type: FIELD_TYPES.TEXT, deserializer: String, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimValueFieldLabel', { defaultMessage: 'Trim value', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx index 60871fa7ba4ab5..3c662793578439 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx @@ -81,7 +81,7 @@ const fieldsConfig: FieldsConfig = { lang: { type: FIELD_TYPES.TEXT, deserializer: String, - serializer: from.undefinedIfValue('painless'), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldLabel', { defaultMessage: 'Language (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx index 9ccfe580a53aed..89ca373b9e6539 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx @@ -10,40 +10,30 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCode } from '@elastic/eui'; -import { - FIELD_TYPES, - fieldValidators, - ToggleField, - UseField, - Field, -} from '../../../../../../shared_imports'; +import { FIELD_TYPES, ToggleField, UseField, Field } from '../../../../../../shared_imports'; import { FieldsConfig, to, from } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; -const { emptyField } = fieldValidators; - const fieldsConfig: FieldsConfig = { /* Required fields config */ + // This is a required field, but we exclude validation because we accept empty values as '' value: { type: FIELD_TYPES.TEXT, deserializer: String, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', { defaultMessage: 'Value', }), - helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText', { - defaultMessage: 'Value for the field.', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { - defaultMessage: 'A value is required.', - }) - ), - }, - ], + helpText: ( + {'""'}, + }} + /> + ), }, /* Optional fields config */ override: { @@ -101,7 +91,16 @@ export const SetProcessor: FunctionComponent = () => { })} /> - + diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts index daa0e548ab7280..399da3c05c7831 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts @@ -64,9 +64,11 @@ export const from = { // Ignore } } + return undefined; }, optionalArrayOfStrings: (v: string[]) => (v.length ? v : undefined), - undefinedIfValue: (value: any) => (v: boolean) => (v === value ? undefined : v), + undefinedIfValue: (value: unknown) => (v: boolean) => (v === value ? undefined : v), + emptyStringToUndefined: (v: unknown) => (v === '' ? undefined : v), }; export const EDITOR_PX_HEIGHT = { @@ -78,4 +80,6 @@ export const EDITOR_PX_HEIGHT = { export type FieldsConfig = Record>; -export type FormFieldsComponent = FunctionComponent<{ initialFieldValues?: Record }>; +export type FormFieldsComponent = FunctionComponent<{ + initialFieldValues?: Record; +}>; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx index 82589e8777589f..3239f546820417 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx @@ -12,7 +12,7 @@ import { FIELD_TYPES, UseField, SelectField } from '../../../../../../shared_imp import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; -import { FieldsConfig, from } from './shared'; +import { FieldsConfig } from './shared'; const fieldsConfig: FieldsConfig = { /* Optional fields config */ @@ -20,7 +20,7 @@ const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.SELECT, defaultValue: 'asc', deserializer: (v) => (v === 'asc' || v === 'desc' ? v : 'asc'), - serializer: from.undefinedIfValue('asc'), + serializer: (v) => (v === 'asc' || v === '' ? undefined : v), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldLabel', { defaultMessage: 'Order', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx index d14048c4e00dce..893e52bcc0073e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption } from '@elastic/eui'; import { FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; -import { FieldsConfig } from './shared'; +import { FieldsConfig, from } from './shared'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; @@ -31,6 +31,7 @@ const fieldsConfig: FieldsConfig = { /* Optional fields config */ regex_file: { type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, deserializer: String, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel', diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx index 0302ff017f09f9..0c43297e811d3c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { omit } from 'lodash'; import React, { createContext, FunctionComponent, @@ -150,12 +150,21 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ }); break; case 'managingProcessor': + // These are the option names we get back from our UI + const knownOptionNames = Object.keys(processorTypeAndOptions.options); + // The processor that we are updating may have options configured the UI does not know about + const unknownOptions = omit(mode.arg.processor.options, knownOptionNames); + // In order to keep the options we don't get back from our UI, we merge the known and unknown options + const updatedProcessorOptions = { + ...processorTypeAndOptions.options, + ...unknownOptions, + }; processorsDispatch({ type: 'updateProcessor', payload: { processor: { ...mode.arg.processor, - ...processorTypeAndOptions, + options: updatedProcessorOptions, }, selector: mode.arg.selector, }, diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index d951e2f2a0768a..4afd434b893726 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -51,6 +51,7 @@ export { ValidationFunc, ValidationConfig, useFormData, + FormOptions, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d60729aeb055f8..3196bcc6031c9e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10832,7 +10832,6 @@ "xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError": "値が必要です。", "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldHelpText": "一致の置換テキスト。", "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldLabel": "置換", - "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError": "値が必要です。", "xpack.ingestPipelines.pipelineEditor.htmlStripForm.fieldNameHelpText": "HTMLタグを削除するフィールド。", "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapHelpText": "ドキュメントフィールド名をモデルの既知のフィールド名にマッピングします。モデルのどのマッピングよりも優先されます。", "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapInvalidJSONError": "無効なJSON", @@ -10930,9 +10929,7 @@ "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText": "有効にすると、既存のフィールド値を上書きします。無効にすると、{nullValue}フィールドのみを更新します。", "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "無効化", "xpack.ingestPipelines.pipelineEditor.setForm.propertiesFieldHelpText": "追加するユーザー詳細情報。フォルトは{value}です。", - "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText": "フィールドの値。", "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "値", - "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "値が必要です。", "xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.fieldNameField": "出力フィールド。", "xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.propertiesFieldLabel": "プロパティ(任意)", "xpack.ingestPipelines.pipelineEditor.settingsForm.learnMoreLabelLink.processor": "{processorLabel}ドキュメント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4643e64eb6b10b..7292769cdbd0fa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10860,7 +10860,6 @@ "xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError": "需要值。", "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldHelpText": "匹配项的替换文本。", "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldLabel": "替换", - "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError": "需要值。", "xpack.ingestPipelines.pipelineEditor.htmlStripForm.fieldNameHelpText": "从其中移除 HTML 标记的字段。", "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapHelpText": "将文档字段名称映射到模型的已知字段名称。优先于模型中的任何映射。", "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapInvalidJSONError": "JSON 无效", @@ -10958,9 +10957,7 @@ "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText": "如果启用,则覆盖现有字段值。如果禁用,则仅更新 {nullValue} 字段。", "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "覆盖", "xpack.ingestPipelines.pipelineEditor.setForm.propertiesFieldHelpText": "要添加的用户详情。默认为 {value}", - "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText": "字段的值。", "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "值", - "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "需要值。", "xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.fieldNameField": "输出字段。", "xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.propertiesFieldLabel": "属性(可选)", "xpack.ingestPipelines.pipelineEditor.settingsForm.learnMoreLabelLink.processor": "{processorLabel}文档", From 98bd8e02114e1ca356d2bbd3a0fadd526878c9d4 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 17 Feb 2021 14:41:15 +0200 Subject: [PATCH 3/3] Report telemetry for autocomplete (#91428) * Report telemetry about autocomplete * error cound * position of selected suggestion * length of query used to generate the suggestions Also added a couple if handly telemetry events: * query language switch * filter bar bulk actions * Fix ts * Report the value suggestions funnel * docs * docs * fix jest * test * code review Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...plugins-data-public.idatapluginservices.md | 1 + ...lic.idatapluginservices.usagecollection.md | 11 ++++ ...na-plugin-plugins-data-public.searchbar.md | 4 +- .../autocomplete/autocomplete_service.ts | 14 ++++- .../collectors/create_usage_collector.ts | 57 +++++++++++++++++++ .../public/autocomplete/collectors/index.ts | 10 ++++ .../public/autocomplete/collectors/types.ts | 21 +++++++ .../value_suggestion_provider.test.ts | 2 +- .../providers/value_suggestion_provider.ts | 49 +++++++++++----- src/plugins/data/public/plugin.ts | 10 ++-- src/plugins/data/public/public.api.md | 9 ++- src/plugins/data/public/types.ts | 3 +- .../data/public/ui/filter_bar/filter_bar.tsx | 36 ++++++------ .../query_string_input/query_string_input.tsx | 30 ++++++++-- .../ui/search_bar/create_search_bar.tsx | 8 +-- .../data/public/ui/search_bar/search_bar.tsx | 13 ++--- .../suggestions_component.test.tsx.snap | 4 ++ .../typeahead/suggestion_component.test.tsx | 7 ++- .../ui/typeahead/suggestion_component.tsx | 6 +- .../typeahead/suggestions_component.test.tsx | 2 +- .../ui/typeahead/suggestions_component.tsx | 4 +- src/plugins/data/public/ui/typeahead/types.ts | 11 ++++ 22 files changed, 244 insertions(+), 68 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md create mode 100644 src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts create mode 100644 src/plugins/data/public/autocomplete/collectors/index.ts create mode 100644 src/plugins/data/public/autocomplete/collectors/types.ts create mode 100644 src/plugins/data/public/ui/typeahead/types.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.md index 5f940bf70a12bd..44cfb0c65e3875 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.md @@ -21,4 +21,5 @@ export interface IDataPluginServices extends Partial | [savedObjects](./kibana-plugin-plugins-data-public.idatapluginservices.savedobjects.md) | CoreStart['savedObjects'] | | | [storage](./kibana-plugin-plugins-data-public.idatapluginservices.storage.md) | IStorageWrapper | | | [uiSettings](./kibana-plugin-plugins-data-public.idatapluginservices.uisettings.md) | CoreStart['uiSettings'] | | +| [usageCollection](./kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md) | UsageCollectionStart | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md new file mode 100644 index 00000000000000..b803dca76203f4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) > [usageCollection](./kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md) + +## IDataPluginServices.usageCollection property + +Signature: + +```typescript +usageCollection?: UsageCollectionStart; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index 83fbc00860ca5e..786ac4f9d61a90 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index a99943c6cd8784..6b288c4507f066 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -16,6 +16,8 @@ import { } from './providers/value_suggestion_provider'; import { ConfigSchema } from '../../config'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { createUsageCollector } from './collectors'; export class AutocompleteService { autocompleteConfig: ConfigSchema['autocomplete']; @@ -47,9 +49,17 @@ export class AutocompleteService { private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language); /** @public **/ - public setup(core: CoreSetup, { timefilter }: { timefilter: TimefilterSetup }) { + public setup( + core: CoreSetup, + { + timefilter, + usageCollection, + }: { timefilter: TimefilterSetup; usageCollection?: UsageCollectionSetup } + ) { + const usageCollector = createUsageCollector(core.getStartServices, usageCollection); + this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled - ? setupValueSuggestionProvider(core, { timefilter }) + ? setupValueSuggestionProvider(core, { timefilter, usageCollector }) : getEmptyValueSuggestions; return { diff --git a/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts b/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts new file mode 100644 index 00000000000000..fc0cea2fdbc520 --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { first } from 'rxjs/operators'; +import { StartServicesAccessor } from '../../../../../core/public'; +import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public'; +import { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types'; + +export const createUsageCollector = ( + getStartServices: StartServicesAccessor, + usageCollection?: UsageCollectionSetup +): AutocompleteUsageCollector => { + const getCurrentApp = async () => { + const [{ application }] = await getStartServices(); + return application.currentAppId$.pipe(first()).toPromise(); + }; + + return { + trackCall: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.CALL + ); + }, + trackRequest: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.REQUEST + ); + }, + trackResult: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.RESULT + ); + }, + trackError: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.ERROR + ); + }, + }; +}; diff --git a/src/plugins/data/public/autocomplete/collectors/index.ts b/src/plugins/data/public/autocomplete/collectors/index.ts new file mode 100644 index 00000000000000..5cfaab19787dab --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { createUsageCollector } from './create_usage_collector'; +export { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types'; diff --git a/src/plugins/data/public/autocomplete/collectors/types.ts b/src/plugins/data/public/autocomplete/collectors/types.ts new file mode 100644 index 00000000000000..60eb9103dc44ee --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export enum AUTOCOMPLETE_EVENT_TYPE { + CALL = 'autocomplete:call', + REQUEST = 'autocomplete:req', + RESULT = 'autocomplete:res', + ERROR = 'autocomplete:err', +} + +export interface AutocompleteUsageCollector { + trackCall: () => Promise; + trackRequest: () => Promise; + trackResult: () => Promise; + trackError: () => Promise; +} diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts index 23fc9f5405aea6..a7b1bd2c7839de 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -18,7 +18,7 @@ describe('FieldSuggestions', () => { beforeEach(() => { const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient; - http = { fetch: jest.fn() }; + http = { fetch: jest.fn().mockResolvedValue([]) }; getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, { timefilter: ({ diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index d8c6d16174d143..b8af6ad3a99e58 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -11,12 +11,7 @@ import { memoize } from 'lodash'; import { CoreSetup } from 'src/core/public'; import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common'; import { TimefilterSetup } from '../../query'; - -function resolver(title: string, field: IFieldType, query: string, filters: any[]) { - // Only cache results for a minute - const ttl = Math.floor(Date.now() / 1000 / 60); - return [ttl, query, title, field.name, JSON.stringify(filters)].join('|'); -} +import { AutocompleteUsageCollector } from '../collectors'; export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise; @@ -47,15 +42,31 @@ export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSugg export const setupValueSuggestionProvider = ( core: CoreSetup, - { timefilter }: { timefilter: TimefilterSetup } + { + timefilter, + usageCollector, + }: { timefilter: TimefilterSetup; usageCollector?: AutocompleteUsageCollector } ): ValueSuggestionsGetFn => { + function resolver(title: string, field: IFieldType, query: string, filters: any[]) { + // Only cache results for a minute + const ttl = Math.floor(Date.now() / 1000 / 60); + return [ttl, query, title, field.name, JSON.stringify(filters)].join('|'); + } + const requestSuggestions = memoize( - (index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => - core.http.fetch(`/api/kibana/suggestions/values/${index}`, { - method: 'POST', - body: JSON.stringify({ query, field: field.name, filters }), - signal, - }), + (index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => { + usageCollector?.trackRequest(); + return core.http + .fetch(`/api/kibana/suggestions/values/${index}`, { + method: 'POST', + body: JSON.stringify({ query, field: field.name, filters }), + signal, + }) + .then((r) => { + usageCollector?.trackResult(); + return r; + }); + }, resolver ); @@ -85,6 +96,16 @@ export const setupValueSuggestionProvider = ( : undefined; const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : []; const filters = [...(boolFilter ? boolFilter : []), ...filterQuery]; - return await requestSuggestions(title, field, query, filters, signal); + try { + usageCollector?.trackCall(); + return await requestSuggestions(title, field, query, filters, signal); + } catch (e) { + if (!signal?.aborted) { + usageCollector?.trackError(); + } + // Remove rejected results from memoize cache + requestSuggestions.cache.delete(resolver(title, field, query, filters)); + return []; + } }; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 39d3ca57215b74..862dd63948a224 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -115,7 +115,10 @@ export class DataPublicPlugin ); return { - autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }), + autocomplete: this.autocomplete.setup(core, { + timefilter: queryService.timefilter, + usageCollection, + }), search: searchService, fieldFormats: this.fieldFormatsService.setup(core), query: queryService, @@ -195,10 +198,7 @@ export class DataPublicPlugin core, data: dataServices, storage: this.storage, - trackUiMetric: this.usageCollection?.reportUiCounter.bind( - this.usageCollection, - 'data_plugin' - ), + usageCollection: this.usageCollection, }); return { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0a3e4666da7473..745f4a7d29d224 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -105,7 +105,6 @@ import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; -import { UiCounterMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; import { URL } from 'url'; @@ -1094,6 +1093,10 @@ export interface IDataPluginServices extends Partial { storage: IStorageWrapper; // (undocumented) uiSettings: CoreStart_2['uiSettings']; + // Warning: (ae-forgotten-export) The symbol "UsageCollectionStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + usageCollection?: UsageCollectionStart; } // Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2250,8 +2253,8 @@ export const SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions"; // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index acf9a4b084c0f3..8686823ef05683 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -19,7 +19,7 @@ import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternsContract } from './index_patterns'; import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; -import { UsageCollectionSetup } from '../../usage_collection/public'; +import { UsageCollectionSetup, UsageCollectionStart } from '../../usage_collection/public'; import { Setup as InspectorSetup } from '../../inspector/public'; import { NowProviderPublicContract } from './now_provider'; @@ -120,4 +120,5 @@ export interface IDataPluginServices extends Partial { http: CoreStart['http']; storage: IStorageWrapper; data: DataPublicPluginStart; + usageCollection?: UsageCollectionStart; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 5b52a529ae7aa7..9605eba9c1c282 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -11,12 +11,12 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { useState } from 'react'; -import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FilterEditor } from './filter_editor'; import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; import { useKibana } from '../../../../kibana_react/public'; -import { IIndexPattern } from '../..'; +import { IDataPluginServices, IIndexPattern } from '../..'; import { buildEmptyFilter, Filter, @@ -36,17 +36,16 @@ interface Props { indexPatterns: IIndexPattern[]; intl: InjectedIntl; appName: string; - // Track UI Metrics - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } function FilterBarUI(props: Props) { const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); - const kibana = useKibana(); - - const uiSettings = kibana.services.uiSettings; + const kibana = useKibana(); + const { appName, usageCollection, uiSettings } = kibana.services; if (!uiSettings) return null; + const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); + function onFiltersUpdated(filters: Filter[]) { if (props.onFiltersUpdated) { props.onFiltersUpdated(filters); @@ -119,66 +118,65 @@ function FilterBarUI(props: Props) { } function onAdd(filter: Filter) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`); setIsAddFilterPopoverOpen(false); - if (props.trackUiMetric) { - props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_added`); - } + const filters = [...props.filters, filter]; onFiltersUpdated(filters); } function onRemove(i: number) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`); const filters = [...props.filters]; filters.splice(i, 1); onFiltersUpdated(filters); } function onUpdate(i: number, filter: Filter) { - if (props.trackUiMetric) { - props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_edited`); - } + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`); const filters = [...props.filters]; filters[i] = filter; onFiltersUpdated(filters); } function onEnableAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`); const filters = props.filters.map(enableFilter); onFiltersUpdated(filters); } function onDisableAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`); const filters = props.filters.map(disableFilter); onFiltersUpdated(filters); } function onPinAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`); const filters = props.filters.map(pinFilter); onFiltersUpdated(filters); } function onUnpinAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`); const filters = props.filters.map(unpinFilter); onFiltersUpdated(filters); } function onToggleAllNegated() { - if (props.trackUiMetric) { - props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_invertInclusion`); - } + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`); const filters = props.filters.map(toggleFilterNegated); onFiltersUpdated(filters); } function onToggleAllDisabled() { - if (props.trackUiMetric) { - props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_toggleAllDisabled`); - } + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:toggle_all`); const filters = props.filters.map(toggleFilterDisabled); onFiltersUpdated(filters); } function onRemoveAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`); onFiltersUpdated([]); } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index aa42e11d318543..aa2fc9e6314361 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -26,6 +26,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { debounce, compact, isEqual, isFunction } from 'lodash'; import { Toast } from 'src/core/public'; +import { METRIC_TYPE } from '@kbn/analytics'; import { IDataPluginServices, IIndexPattern, Query } from '../..'; import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; @@ -105,6 +106,10 @@ export default class QueryStringInputUI extends Component { private abortController?: AbortController; private fetchIndexPatternsAbortController?: AbortController; private services = this.props.kibana.services; + private reportUiCounter = this.services.usageCollection?.reportUiCounter.bind( + this.services.usageCollection, + this.services.appName + ); private componentIsUnmounting = false; private queryBarInputDivRefInstance: RefObject = createRef(); @@ -178,12 +183,14 @@ export default class QueryStringInputUI extends Component { selectionEnd, signal: this.abortController.signal, })) || []; - return [...suggestions, ...recentSearchSuggestions]; } catch (e) { // TODO: Waiting on https://github.com/elastic/kibana/issues/51406 for a properly typed error // Ignore aborted requests if (e.message === 'The user aborted a request.') return; + + this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:suggestions_error`); + throw e; } }; @@ -302,7 +309,7 @@ export default class QueryStringInputUI extends Component { } if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) { event.preventDefault(); - this.selectSuggestion(this.state.suggestions[index]); + this.selectSuggestion(this.state.suggestions[index], index); } else { this.onSubmit(this.props.query); this.setState({ @@ -335,7 +342,7 @@ export default class QueryStringInputUI extends Component { } }; - private selectSuggestion = (suggestion: QuerySuggestion) => { + private selectSuggestion = (suggestion: QuerySuggestion, listIndex: number) => { if (!this.inputRef) { return; } @@ -352,6 +359,17 @@ export default class QueryStringInputUI extends Component { const value = query.substr(0, selectionStart) + query.substr(selectionEnd); const newQueryString = value.substr(0, start) + text + value.substr(end); + this.reportUiCounter?.( + METRIC_TYPE.LOADED, + `query_string:${type}:suggestions_select_position`, + listIndex + ); + this.reportUiCounter?.( + METRIC_TYPE.LOADED, + `query_string:${type}:suggestions_select_q_length`, + end - start + ); + this.onQueryStringChange(newQueryString); this.setState({ @@ -458,6 +476,7 @@ export default class QueryStringInputUI extends Component { const newQuery = { query: '', language }; this.onChange(newQuery); this.onSubmit(newQuery); + this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:language:${language}`); }; private onOutsideClick = () => { @@ -480,11 +499,11 @@ export default class QueryStringInputUI extends Component { } }; - private onClickSuggestion = (suggestion: QuerySuggestion) => { + private onClickSuggestion = (suggestion: QuerySuggestion, index: number) => { if (!this.inputRef) { return; } - this.selectSuggestion(suggestion); + this.selectSuggestion(suggestion, index); this.inputRef.focus(); }; @@ -588,6 +607,7 @@ export default class QueryStringInputUI extends Component { if (this.props.onChangeQueryInputFocus) { this.props.onChangeQueryInputFocus(true); } + requestAnimationFrame(() => { this.handleAutoHeight(); }); diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 8f34ec1912cb44..4aba59442d204f 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -10,7 +10,6 @@ import _ from 'lodash'; import React, { useEffect, useRef } from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { UiCounterMetricType } from '@kbn/analytics'; import { KibanaContextProvider } from '../../../../kibana_react/public'; import { QueryStart, SavedQuery } from '../../query'; import { SearchBar, SearchBarOwnProps } from './'; @@ -20,12 +19,13 @@ import { useSavedQuery } from './lib/use_saved_query'; import { DataPublicPluginStart } from '../../types'; import { Filter, Query, TimeRange } from '../../../common'; import { useQueryStringManager } from './lib/use_query_string_manager'; +import { UsageCollectionSetup } from '../../../../usage_collection/public'; interface StatefulSearchBarDeps { core: CoreStart; data: Omit; storage: IStorageWrapper; - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; + usageCollection?: UsageCollectionSetup; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -110,7 +110,7 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => { return props.useDefaultBehaviors ? {} : props; }; -export function createSearchBar({ core, storage, data, trackUiMetric }: StatefulSearchBarDeps) { +export function createSearchBar({ core, storage, data, usageCollection }: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. return (props: StatefulSearchBarProps) => { @@ -161,6 +161,7 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful appName: props.appName, data, storage, + usageCollection, ...core, }} > @@ -188,7 +189,6 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)} onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)} onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)} - trackUiMetric={trackUiMetric} {...overrideDefaultBehaviors(props)} /> diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index c87fcc3d950410..fe8165a12714a6 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -13,7 +13,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; -import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; +import { METRIC_TYPE } from '@kbn/analytics'; import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; import QueryBarTopRow from '../query_string_input/query_bar_top_row'; @@ -68,8 +68,6 @@ export interface SearchBarOwnProps { onRefresh?: (payload: { dateRange: TimeRange }) => void; indicateNoData?: boolean; - // Track UI Metrics - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -323,9 +321,11 @@ class SearchBarUI extends Component { }, }); } - if (this.props.trackUiMetric) { - this.props.trackUiMetric(METRIC_TYPE.CLICK, `${this.services.appName}:query_submitted`); - } + this.services.usageCollection?.reportUiCounter( + this.services.appName, + METRIC_TYPE.CLICK, + 'query_submitted' + ); } ); }; @@ -428,7 +428,6 @@ class SearchBarUI extends Component { onFiltersUpdated={this.props.onFiltersUpdated} indexPatterns={this.props.indexPatterns!} appName={this.services.appName} - trackUiMetric={this.props.trackUiMetric} /> diff --git a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap index 2fa7834872f6b1..9185e6a77d1026 100644 --- a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap +++ b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap @@ -22,6 +22,7 @@ exports[`SuggestionsComponent Passing the index should control which suggestion > { it('Should display the suggestion and use the provided ariaId', () => { const component = shallow( { it('Should make the element active if the selected prop is true', () => { const component = shallow( { mount( { const component = shallow( { component.simulate('click'); expect(mockHandler).toHaveBeenCalledTimes(1); - expect(mockHandler).toHaveBeenCalledWith(mockSuggestion); + expect(mockHandler).toHaveBeenCalledWith(mockSuggestion, 0); }); it('Should call onMouseEnter when user mouses over the element', () => { @@ -100,6 +104,7 @@ describe('SuggestionComponent', () => { const component = shallow( void; + onClick: SuggestionOnClick; onMouseEnter: () => void; selected: boolean; + index: number; suggestion: QuerySuggestion; innerRef: (node: HTMLDivElement) => void; ariaId: string; @@ -48,7 +50,7 @@ export function SuggestionComponent(props: Props) { active: props.selected, })} role="option" - onClick={() => props.onClick(props.suggestion)} + onClick={() => props.onClick(props.suggestion, props.index)} onMouseEnter={props.onMouseEnter} ref={props.innerRef} id={props.ariaId} diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index ebbdc7fc55e3af..dce8d5bdcfcd14 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -115,7 +115,7 @@ describe('SuggestionsComponent', () => { component.find(SuggestionComponent).at(1).simulate('click'); expect(mockCallback).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1]); + expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1], 1); }); it('Should call onMouseEnter with the index of the suggestion that was entered', () => { diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index fa1f4aa6a8ce8a..6bc91619fe8683 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -17,11 +17,12 @@ import { SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET, SUGGESTIONS_LIST_REQUIRED_WIDTH, } from './constants'; +import { SuggestionOnClick } from './types'; // @internal export interface SuggestionsComponentProps { index: number | null; - onClick: (suggestion: QuerySuggestion) => void; + onClick: SuggestionOnClick; onMouseEnter: (index: number) => void; show: boolean; suggestions: QuerySuggestion[]; @@ -50,6 +51,7 @@ export default class SuggestionsComponent extends Component (this.childNodes[index] = node)} selected={index === this.props.index} + index={index} suggestion={suggestion} onClick={this.props.onClick} onMouseEnter={() => this.props.onMouseEnter(index)} diff --git a/src/plugins/data/public/ui/typeahead/types.ts b/src/plugins/data/public/ui/typeahead/types.ts new file mode 100644 index 00000000000000..d0be717b2bf9b9 --- /dev/null +++ b/src/plugins/data/public/ui/typeahead/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { QuerySuggestion } from '../../autocomplete'; + +export type SuggestionOnClick = (suggestion: QuerySuggestion, index: number) => void;