From 8bfd5e2f956dc3bde006c7e76d7a174c02bdd00d Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 17 Aug 2021 09:48:04 -0400 Subject: [PATCH 01/11] [Uptime][Synthetics Integration] Add browser monitors configuration options (#102928) * update types * add browser context * update validation * add browser fields to custom fields * add browser simple fields and source field * add browser context to create and edit wrappers * update content * add formatters for formatting javascript values to integration policy yaml * fix policy name bug * adjust tests * adjust types * add normalizers for converting yaml to javascript * update default values * add params field to browser monitors * adjust types, formatters and normalizers to account for browser advanced fields * add browser advanced fields context and components * adjust http and tcp providers * adjust use_update_policy and wrappers * update types * update content * adjust timeout content * adjust zip url content * adjust content * remove unused content * hide monitor options that do not have corresponding data streams from the integration package * Update x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx Co-authored-by: Justin Kambic * Update x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx Co-authored-by: Justin Kambic * Update x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx Co-authored-by: Justin Kambic * Update x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx Co-authored-by: Justin Kambic * adjust content * adjust validation * adjust tests * adjust normalizers and formatters and add tests * resolves validation error with inline scripts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Justin Kambic --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../browser/advanced_fields.test.tsx | 49 ++ .../fleet_package/browser/advanced_fields.tsx | 117 ++++ .../fleet_package/browser/formatters.ts | 24 + .../fleet_package/browser/normalizers.ts | 43 ++ .../fleet_package/browser/simple_fields.tsx | 183 ++++++ .../fleet_package/browser/source_field.tsx | 248 ++++++++ .../fleet_package/common/default_values.ts | 19 + .../fleet_package/common/formatters.test.ts | 36 ++ .../fleet_package/common/formatters.ts | 32 + .../fleet_package/common/normalizers.test.ts | 28 + .../fleet_package/common/normalizers.ts | 79 +++ .../contexts/advanced_fields_http_context.tsx | 2 +- .../contexts/advanced_fields_tcp_context.tsx | 2 +- .../contexts/browser_context.tsx | 59 ++ .../contexts/browser_context_advanced.tsx | 52 ++ .../contexts/browser_provider.tsx | 52 ++ .../fleet_package/contexts/http_context.tsx | 13 +- .../fleet_package/contexts/http_provider.tsx | 60 +- .../fleet_package/contexts/icmp_context.tsx | 14 +- .../fleet_package/contexts/index.ts | 24 +- .../fleet_package/contexts/tcp_context.tsx | 14 +- .../fleet_package/contexts/tcp_provider.tsx | 59 +- .../contexts/tls_fields_context.tsx | 2 +- .../fleet_package/custom_fields.test.tsx | 73 ++- .../fleet_package/custom_fields.tsx | 39 +- .../fleet_package/helpers/context_helpers.ts | 17 + .../fleet_package/helpers/formatters.ts | 38 ++ .../fleet_package/helpers/normalizers.ts | 42 ++ .../fleet_package/http/advanced_fields.tsx | 44 +- .../fleet_package/http/formatters.ts | 44 ++ .../fleet_package/http/normalizers.ts | 85 +++ .../fleet_package/http/simple_fields.tsx | 29 +- .../fleet_package/icmp/formatters.ts | 17 + .../fleet_package/icmp/normalizers.ts | 31 + .../fleet_package/icmp/simple_fields.tsx | 29 +- .../synthetics_policy_create_extension.tsx | 125 ++-- ...s_policy_create_extension_wrapper.test.tsx | 146 ++++- ...hetics_policy_create_extension_wrapper.tsx | 5 +- .../synthetics_policy_edit_extension.tsx | 81 +-- ...ics_policy_edit_extension_wrapper.test.tsx | 237 +++++++- ...nthetics_policy_edit_extension_wrapper.tsx | 201 +++---- .../fleet_package/tcp/formatters.ts | 22 + .../fleet_package/tcp/normalizers.ts | 32 + .../fleet_package/tcp/simple_fields.tsx | 27 +- .../fleet_package/tls/formatters.ts | 34 ++ .../fleet_package/tls/normalizers.ts | 49 ++ .../public/components/fleet_package/types.tsx | 50 +- .../fleet_package/use_update_policy.test.tsx | 550 ++++++++++++------ .../fleet_package/use_update_policy.ts | 84 +-- .../components/fleet_package/validation.tsx | 81 ++- 52 files changed, 2650 insertions(+), 775 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/formatters.test.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_provider.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/helpers/context_helpers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/helpers/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/helpers/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/http/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/icmp/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/icmp/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/tcp/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/tcp/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/tls/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/tls/normalizers.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9c6dc3b591a9f..621af3e92de8d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25168,7 +25168,6 @@ "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tags.label": "タグ", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts": "ホスト:ポート", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts.error": "ホストとポートは必須です", - "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.error": "タイムアウトは0以上で、スケジュール間隔未満でなければなりません", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.helpText": "接続のテストとデータの交換に許可された合計時間。", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.label": "タイムアウト (秒) ", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.URL": "URL", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1465e6d0a1952..c2831d0aa7518 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25723,7 +25723,6 @@ "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tags.label": "标签", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts": "主机:端口", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts.error": "主机和端口必填", - "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.error": "超时必须等于或大于 0 且小于计划时间间隔", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.helpText": "允许用于测试连接并交换数据的总时间。", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.label": "超时(秒)", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.URL": "URL", diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx new file mode 100644 index 0000000000000..aa1f7ca07e3d8 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { BrowserAdvancedFields } from './advanced_fields'; +import { ConfigKeys, IBrowserAdvancedFields } from '../types'; +import { + BrowserAdvancedFieldsContextProvider, + defaultBrowserAdvancedFields as defaultConfig, +} from '../contexts'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('', () => { + const WrappedComponent = ({ defaultValues }: { defaultValues?: IBrowserAdvancedFields }) => { + return ( + + + + ); + }; + + it('renders BrowserAdvancedFields', () => { + const { getByLabelText } = render(); + + const syntheticsArgs = getByLabelText('Synthetics args'); + const screenshots = getByLabelText('Screenshot options') as HTMLInputElement; + expect(screenshots.value).toEqual(defaultConfig[ConfigKeys.SCREENSHOTS]); + expect(syntheticsArgs).toBeInTheDocument(); + }); + + it('handles changing fields', () => { + const { getByLabelText } = render(); + + const screenshots = getByLabelText('Screenshot options') as HTMLInputElement; + + fireEvent.change(screenshots, { target: { value: 'off' } }); + + expect(screenshots.value).toEqual('off'); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx new file mode 100644 index 0000000000000..28e2e39c79554 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiAccordion, + EuiSelect, + EuiFormRow, + EuiDescribedFormGroup, + EuiSpacer, +} from '@elastic/eui'; +import { ComboBox } from '../combo_box'; + +import { useBrowserAdvancedFieldsContext } from '../contexts'; + +import { ConfigKeys, ScreenshotOption } from '../types'; + +import { OptionalLabel } from '../optional_label'; + +export const BrowserAdvancedFields = () => { + const { fields, setFields } = useBrowserAdvancedFieldsContext(); + + const handleInputChange = useCallback( + ({ value, configKey }: { value: unknown; configKey: ConfigKeys }) => { + setFields((prevFields) => ({ ...prevFields, [configKey]: value })); + }, + [setFields] + ); + + return ( + + + + + + } + description={ + + } + > + + + } + labelAppend={} + helpText={ + + } + > + + handleInputChange({ + value: event.target.value, + configKey: ConfigKeys.SCREENSHOTS, + }) + } + data-test-subj="syntheticsBrowserScreenshots" + /> + + + } + labelAppend={} + helpText={ + + } + > + + handleInputChange({ value, configKey: ConfigKeys.SYNTHETICS_ARGS }) + } + data-test-subj="syntheticsBrowserSyntheticsArgs" + /> + + + + ); +}; + +const requestMethodOptions = Object.values(ScreenshotOption).map((option) => ({ + value: option, + text: option.replace(/-/g, ' '), +})); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts new file mode 100644 index 0000000000000..722b1625f023d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BrowserFields, ConfigKeys } from '../types'; +import { Formatter, commonFormatters, arrayToJsonFormatter } from '../common/formatters'; + +export type BrowserFormatMap = Record; + +export const browserFormatters: BrowserFormatMap = { + [ConfigKeys.SOURCE_ZIP_URL]: null, + [ConfigKeys.SOURCE_ZIP_USERNAME]: null, + [ConfigKeys.SOURCE_ZIP_PASSWORD]: null, + [ConfigKeys.SOURCE_ZIP_FOLDER]: null, + [ConfigKeys.SOURCE_INLINE]: null, + [ConfigKeys.PARAMS]: null, + [ConfigKeys.SCREENSHOTS]: null, + [ConfigKeys.SYNTHETICS_ARGS]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.SYNTHETICS_ARGS]), + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts new file mode 100644 index 0000000000000..2b742a188782a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BrowserFields, ConfigKeys } from '../types'; +import { + Normalizer, + commonNormalizers, + getNormalizer, + getJsonToArrayOrObjectNormalizer, +} from '../common/normalizers'; + +import { defaultBrowserSimpleFields, defaultBrowserAdvancedFields } from '../contexts'; + +export type BrowserNormalizerMap = Record; + +const defaultBrowserFields = { + ...defaultBrowserSimpleFields, + ...defaultBrowserAdvancedFields, +}; + +export const getBrowserNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, defaultBrowserFields); +}; + +export const getBrowserJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { + return getJsonToArrayOrObjectNormalizer(key, defaultBrowserFields); +}; + +export const browserNormalizers: BrowserNormalizerMap = { + [ConfigKeys.SOURCE_ZIP_URL]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_URL), + [ConfigKeys.SOURCE_ZIP_USERNAME]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_USERNAME), + [ConfigKeys.SOURCE_ZIP_PASSWORD]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_PASSWORD), + [ConfigKeys.SOURCE_ZIP_FOLDER]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_FOLDER), + [ConfigKeys.SOURCE_INLINE]: getBrowserNormalizer(ConfigKeys.SOURCE_INLINE), + [ConfigKeys.PARAMS]: getBrowserNormalizer(ConfigKeys.PARAMS), + [ConfigKeys.SCREENSHOTS]: getBrowserNormalizer(ConfigKeys.SCREENSHOTS), + [ConfigKeys.SYNTHETICS_ARGS]: getBrowserJsonToArrayOrObjectNormalizer(ConfigKeys.SYNTHETICS_ARGS), + ...commonNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx new file mode 100644 index 0000000000000..34f56a65df3e8 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui'; +import { ConfigKeys, Validation } from '../types'; +import { useBrowserSimpleFieldsContext } from '../contexts'; +import { ComboBox } from '../combo_box'; +import { OptionalLabel } from '../optional_label'; +import { ScheduleField } from '../schedule_field'; +import { SourceField } from './source_field'; + +interface Props { + validate: Validation; +} + +export const BrowserSimpleFields = memo(({ validate }) => { + const { fields, setFields, defaultValues } = useBrowserSimpleFieldsContext(); + const handleInputChange = ({ value, configKey }: { value: unknown; configKey: ConfigKeys }) => { + setFields((prevFields) => ({ ...prevFields, [configKey]: value })); + }; + const onChangeSourceField = useCallback( + ({ zipUrl, folder, username, password, inlineScript, params }) => { + setFields((prevFields) => ({ + ...prevFields, + [ConfigKeys.SOURCE_ZIP_URL]: zipUrl, + [ConfigKeys.SOURCE_ZIP_FOLDER]: folder, + [ConfigKeys.SOURCE_ZIP_USERNAME]: username, + [ConfigKeys.SOURCE_ZIP_PASSWORD]: password, + [ConfigKeys.SOURCE_INLINE]: inlineScript, + [ConfigKeys.PARAMS]: params, + })); + }, + [setFields] + ); + + return ( + <> + + } + isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields)} + error={ + + } + > + + handleInputChange({ + value: schedule, + configKey: ConfigKeys.SCHEDULE, + }) + } + number={fields[ConfigKeys.SCHEDULE].number} + unit={fields[ConfigKeys.SCHEDULE].unit} + /> + + + } + > + ({ + zipUrl: defaultValues[ConfigKeys.SOURCE_ZIP_URL], + folder: defaultValues[ConfigKeys.SOURCE_ZIP_FOLDER], + username: defaultValues[ConfigKeys.SOURCE_ZIP_USERNAME], + password: defaultValues[ConfigKeys.SOURCE_ZIP_PASSWORD], + inlineScript: defaultValues[ConfigKeys.SOURCE_INLINE], + params: defaultValues[ConfigKeys.PARAMS], + }), + [defaultValues] + )} + /> + + + } + labelAppend={} + helpText={ + + } + > + + handleInputChange({ + value: event.target.value, + configKey: ConfigKeys.APM_SERVICE_NAME, + }) + } + data-test-subj="syntheticsAPMServiceName" + /> + + + } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} + error={ + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) + } + helpText={ + + } + > + + handleInputChange({ + value: event.target.value, + configKey: ConfigKeys.TIMEOUT, + }) + } + step={'any'} + /> + + + } + labelAppend={} + helpText={ + + } + > + handleInputChange({ value, configKey: ConfigKeys.TAGS })} + data-test-subj="syntheticsTags" + /> + + + ); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx new file mode 100644 index 0000000000000..eca354f30c973 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiTabbedContent, + EuiFormRow, + EuiFieldText, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; +import { OptionalLabel } from '../optional_label'; +import { CodeEditor } from '../code_editor'; +import { MonacoEditorLangId } from '../types'; + +enum SourceType { + INLINE = 'syntheticsBrowserInlineConfig', + ZIP = 'syntheticsBrowserZipURLConfig', +} + +interface SourceConfig { + zipUrl: string; + folder: string; + username: string; + password: string; + inlineScript: string; + params: string; +} + +interface Props { + onChange: (sourceConfig: SourceConfig) => void; + defaultConfig: SourceConfig; +} + +const defaultValues = { + zipUrl: '', + folder: '', + username: '', + password: '', + inlineScript: '', + params: '', +}; + +export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) => { + const [sourceType, setSourceType] = useState( + defaultConfig.inlineScript ? SourceType.INLINE : SourceType.ZIP + ); + const [config, setConfig] = useState(defaultConfig); + + useEffect(() => { + onChange(config); + }, [config, onChange]); + + const zipUrlLabel = ( + + ); + + const tabs = [ + { + id: 'syntheticsBrowserZipURLConfig', + name: zipUrlLabel, + content: ( + <> + + + } + helpText={ + + } + > + + setConfig((prevConfig) => ({ ...prevConfig, zipUrl: value })) + } + value={config.zipUrl} + /> + + + } + labelAppend={} + helpText={ + + } + > + + setConfig((prevConfig) => ({ ...prevConfig, folder: value })) + } + value={config.folder} + /> + + + } + labelAppend={} + helpText={ + + } + > + setConfig((prevConfig) => ({ ...prevConfig, params: code }))} + value={config.params} + /> + + + } + labelAppend={} + helpText={ + + } + > + + setConfig((prevConfig) => ({ ...prevConfig, username: value })) + } + value={config.username} + /> + + + } + labelAppend={} + helpText={ + + } + > + + setConfig((prevConfig) => ({ ...prevConfig, password: value })) + } + value={config.password} + /> + + + ), + }, + { + id: 'syntheticsBrowserInlineConfig', + name: ( + + ), + content: ( + + } + helpText={ + + } + > + setConfig((prevConfig) => ({ ...prevConfig, inlineScript: code }))} + value={config.inlineScript} + /> + + ), + }, + ]; + + return ( + tab.id === sourceType)} + autoFocus="selected" + onTabClick={(tab) => { + setSourceType(tab.id as SourceType); + if (tab.id !== sourceType) { + setConfig(defaultValues); + } + }} + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts new file mode 100644 index 0000000000000..bba8cefd749ee --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICommonFields, ConfigKeys, ScheduleUnit, DataStream } from '../types'; + +export const defaultValues: ICommonFields = { + [ConfigKeys.MONITOR_TYPE]: DataStream.HTTP, + [ConfigKeys.SCHEDULE]: { + number: '3', + unit: ScheduleUnit.MINUTES, + }, + [ConfigKeys.APM_SERVICE_NAME]: '', + [ConfigKeys.TAGS]: [], + [ConfigKeys.TIMEOUT]: '16', +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.test.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.test.ts new file mode 100644 index 0000000000000..9f4d8320e4048 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { arrayToJsonFormatter, objectToJsonFormatter, secondsToCronFormatter } from './formatters'; + +describe('formatters', () => { + describe('cronToSecondsNormalizer', () => { + it('takes a number of seconds and converts it to cron format', () => { + expect(secondsToCronFormatter('3')).toEqual('3s'); + }); + }); + + describe('arrayToJsonFormatter', () => { + it('takes an array and converts it to json', () => { + expect(arrayToJsonFormatter(['tag1', 'tag2'])).toEqual('["tag1","tag2"]'); + }); + + it('returns null if the array has length of 0', () => { + expect(arrayToJsonFormatter([])).toEqual(null); + }); + }); + + describe('objectToJsonFormatter', () => { + it('takes a json object string and returns an object', () => { + expect(objectToJsonFormatter({ key: 'value' })).toEqual('{"key":"value"}'); + }); + + it('returns null if the object has no keys', () => { + expect(objectToJsonFormatter({})).toEqual(null); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts new file mode 100644 index 0000000000000..311fa7da13498 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICommonFields, ICustomFields, ConfigKeys } from '../types'; + +export type Formatter = null | ((fields: Partial) => string | null); + +export type CommonFormatMap = Record; + +export const commonFormatters: CommonFormatMap = { + [ConfigKeys.NAME]: null, + [ConfigKeys.MONITOR_TYPE]: null, + [ConfigKeys.SCHEDULE]: (fields) => + JSON.stringify( + `@every ${fields[ConfigKeys.SCHEDULE]?.number}${fields[ConfigKeys.SCHEDULE]?.unit}` + ), + [ConfigKeys.APM_SERVICE_NAME]: null, + [ConfigKeys.TAGS]: (fields) => arrayToJsonFormatter(fields[ConfigKeys.TAGS]), + [ConfigKeys.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKeys.TIMEOUT]), +}; + +export const arrayToJsonFormatter = (value: string[] = []) => + value.length ? JSON.stringify(value) : null; + +export const secondsToCronFormatter = (value: string = '') => (value ? `${value}s` : null); + +export const objectToJsonFormatter = (value: Record = {}) => + Object.keys(value).length ? JSON.stringify(value) : null; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts new file mode 100644 index 0000000000000..055e829858a16 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cronToSecondsNormalizer, jsonToArrayOrObjectNormalizer } from './normalizers'; + +describe('normalizers', () => { + describe('cronToSecondsNormalizer', () => { + it('returns number of seconds from cron formatted seconds', () => { + expect(cronToSecondsNormalizer('3s')).toEqual('3'); + }); + }); + + describe('jsonToArrayOrObjectNormalizer', () => { + it('takes a json object string and returns an object', () => { + expect(jsonToArrayOrObjectNormalizer('{\n "key": "value"\n}')).toEqual({ + key: 'value', + }); + }); + + it('takes a json array string and returns an array', () => { + expect(jsonToArrayOrObjectNormalizer('["tag1","tag2"]')).toEqual(['tag1', 'tag2']); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts new file mode 100644 index 0000000000000..69121ca4bd70e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICommonFields, ConfigKeys } from '../types'; +import { NewPackagePolicyInput } from '../../../../../fleet/common'; +import { defaultValues as commonDefaultValues } from './default_values'; + +// TO DO: create a standard input format that all fields resolve to +export type Normalizer = (fields: NewPackagePolicyInput['vars']) => unknown; + +// create a type of all the common policy fields, as well as the fleet managed 'name' field +export type CommonNormalizerMap = Record; + +/** + * Takes a cron formatted seconds and returns just the number of seconds. Assumes that cron is already in seconds format. + * @params {string} value (Ex '3s') + * @return {string} (Ex '3') + */ +export const cronToSecondsNormalizer = (value: string) => + value ? value.slice(0, value.length - 1) : null; + +export const jsonToArrayOrObjectNormalizer = (value: string) => (value ? JSON.parse(value) : null); + +export function getNormalizer(key: string, defaultValues: Fields): Normalizer { + return (fields: NewPackagePolicyInput['vars']) => + fields?.[key]?.value ?? defaultValues[key as keyof Fields]; +} + +export function getJsonToArrayOrObjectNormalizer( + key: string, + defaultValues: Fields +): Normalizer { + return (fields: NewPackagePolicyInput['vars']) => + jsonToArrayOrObjectNormalizer(fields?.[key]?.value) ?? defaultValues[key as keyof Fields]; +} + +export function getCronNormalizer(key: string, defaultValues: Fields): Normalizer { + return (fields: NewPackagePolicyInput['vars']) => + cronToSecondsNormalizer(fields?.[key]?.value) ?? defaultValues[key as keyof Fields]; +} + +export const getCommonNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, commonDefaultValues); +}; + +export const getCommonJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { + return getJsonToArrayOrObjectNormalizer(key, commonDefaultValues); +}; + +export const getCommonCronToSecondsNormalizer = (key: ConfigKeys) => { + return getCronNormalizer(key, commonDefaultValues); +}; + +export const commonNormalizers: CommonNormalizerMap = { + [ConfigKeys.NAME]: (fields) => fields?.[ConfigKeys.NAME]?.value ?? '', + [ConfigKeys.MONITOR_TYPE]: getCommonNormalizer(ConfigKeys.MONITOR_TYPE), + [ConfigKeys.SCHEDULE]: (fields) => { + const value = fields?.[ConfigKeys.SCHEDULE]?.value; + if (value) { + const fullString = JSON.parse(fields?.[ConfigKeys.SCHEDULE]?.value); + const fullSchedule = fullString.replace('@every ', ''); + const unit = fullSchedule.slice(-1); + const number = fullSchedule.slice(0, fullSchedule.length - 1); + return { + unit, + number, + }; + } else { + return commonDefaultValues[ConfigKeys.SCHEDULE]; + } + }, + [ConfigKeys.APM_SERVICE_NAME]: getCommonNormalizer(ConfigKeys.APM_SERVICE_NAME), + [ConfigKeys.TAGS]: getCommonJsonToArrayOrObjectNormalizer(ConfigKeys.TAGS), + [ConfigKeys.TIMEOUT]: getCommonCronToSecondsNormalizer(ConfigKeys.TIMEOUT), +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx index b51aa6cbf3a7c..11796050a545b 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx @@ -25,7 +25,7 @@ interface IHTTPAdvancedFieldsContextProvider { defaultValues?: IHTTPAdvancedFields; } -export const initialValues = { +export const initialValues: IHTTPAdvancedFields = { [ConfigKeys.PASSWORD]: '', [ConfigKeys.PROXY_URL]: '', [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: [], diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx index 6e4f46111c283..ef821b7e39dca 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx @@ -19,7 +19,7 @@ interface ITCPAdvancedFieldsContextProvider { defaultValues?: ITCPAdvancedFields; } -export const initialValues = { +export const initialValues: ITCPAdvancedFields = { [ConfigKeys.PROXY_URL]: '', [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: false, [ConfigKeys.RESPONSE_RECEIVE_CHECK]: '', diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx new file mode 100644 index 0000000000000..1d1493178b944 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { IBrowserSimpleFields, ConfigKeys, DataStream } from '../types'; +import { defaultValues as commonDefaultValues } from '../common/default_values'; + +interface IBrowserSimpleFieldsContext { + setFields: React.Dispatch>; + fields: IBrowserSimpleFields; + defaultValues: IBrowserSimpleFields; +} + +interface IBrowserSimpleFieldsContextProvider { + children: React.ReactNode; + defaultValues?: IBrowserSimpleFields; +} + +export const initialValues: IBrowserSimpleFields = { + ...commonDefaultValues, + [ConfigKeys.MONITOR_TYPE]: DataStream.BROWSER, + [ConfigKeys.SOURCE_ZIP_URL]: '', + [ConfigKeys.SOURCE_ZIP_USERNAME]: '', + [ConfigKeys.SOURCE_ZIP_PASSWORD]: '', + [ConfigKeys.SOURCE_ZIP_FOLDER]: '', + [ConfigKeys.SOURCE_INLINE]: '', + [ConfigKeys.PARAMS]: '', +}; + +const defaultContext: IBrowserSimpleFieldsContext = { + setFields: (_fields: React.SetStateAction) => { + throw new Error( + 'setFields was not initialized for Browser Simple Fields, set it when you invoke the context' + ); + }, + fields: initialValues, // mutable + defaultValues: initialValues, // immutable +}; + +export const BrowserSimpleFieldsContext = createContext(defaultContext); + +export const BrowserSimpleFieldsContextProvider = ({ + children, + defaultValues = initialValues, +}: IBrowserSimpleFieldsContextProvider) => { + const [fields, setFields] = useState(defaultValues); + + const value = useMemo(() => { + return { fields, setFields, defaultValues }; + }, [fields, defaultValues]); + + return ; +}; + +export const useBrowserSimpleFieldsContext = () => useContext(BrowserSimpleFieldsContext); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx new file mode 100644 index 0000000000000..3f3bb8f14c269 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { IBrowserAdvancedFields, ConfigKeys, ScreenshotOption } from '../types'; + +interface IBrowserAdvancedFieldsContext { + setFields: React.Dispatch>; + fields: IBrowserAdvancedFields; + defaultValues: IBrowserAdvancedFields; +} + +interface IBrowserAdvancedFieldsContextProvider { + children: React.ReactNode; + defaultValues?: IBrowserAdvancedFields; +} + +export const initialValues: IBrowserAdvancedFields = { + [ConfigKeys.SCREENSHOTS]: ScreenshotOption.ON, + [ConfigKeys.SYNTHETICS_ARGS]: [], +}; + +const defaultContext: IBrowserAdvancedFieldsContext = { + setFields: (_fields: React.SetStateAction) => { + throw new Error( + 'setFields was not initialized for Browser Advanced Fields, set it when you invoke the context' + ); + }, + fields: initialValues, // mutable + defaultValues: initialValues, // immutable +}; + +export const BrowserAdvancedFieldsContext = createContext(defaultContext); + +export const BrowserAdvancedFieldsContextProvider = ({ + children, + defaultValues = initialValues, +}: IBrowserAdvancedFieldsContextProvider) => { + const [fields, setFields] = useState(defaultValues); + + const value = useMemo(() => { + return { fields, setFields, defaultValues }; + }, [fields, defaultValues]); + + return ; +}; + +export const useBrowserAdvancedFieldsContext = () => useContext(BrowserAdvancedFieldsContext); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_provider.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_provider.tsx new file mode 100644 index 0000000000000..e2ce88f84f702 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_provider.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode } from 'react'; +import { BrowserFields, IBrowserSimpleFields, IBrowserAdvancedFields } from '../types'; +import { + BrowserSimpleFieldsContextProvider, + BrowserAdvancedFieldsContextProvider, + defaultBrowserSimpleFields, + defaultBrowserAdvancedFields, +} from '.'; +import { formatDefaultValues } from '../helpers/context_helpers'; + +interface BrowserContextProviderProps { + defaultValues?: BrowserFields; + children: ReactNode; +} + +export const BrowserContextProvider = ({ + defaultValues, + children, +}: BrowserContextProviderProps) => { + const simpleKeys = Object.keys(defaultBrowserSimpleFields) as Array; + const advancedKeys = Object.keys(defaultBrowserAdvancedFields) as Array< + keyof IBrowserAdvancedFields + >; + const formattedDefaultSimpleFields = formatDefaultValues( + simpleKeys, + defaultValues || {} + ); + const formattedDefaultAdvancedFields = formatDefaultValues( + advancedKeys, + defaultValues || {} + ); + const simpleFields: IBrowserSimpleFields | undefined = defaultValues + ? formattedDefaultSimpleFields + : undefined; + const advancedFields: IBrowserAdvancedFields | undefined = defaultValues + ? formattedDefaultAdvancedFields + : undefined; + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context.tsx index d1306836afa9c..d8b89a1dfc4d0 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context.tsx @@ -6,7 +6,8 @@ */ import React, { createContext, useContext, useMemo, useState } from 'react'; -import { IHTTPSimpleFields, ConfigKeys, ScheduleUnit, DataStream } from '../types'; +import { IHTTPSimpleFields, ConfigKeys, DataStream } from '../types'; +import { defaultValues as commonDefaultValues } from '../common/default_values'; interface IHTTPSimpleFieldsContext { setFields: React.Dispatch>; @@ -19,17 +20,11 @@ interface IHTTPSimpleFieldsContextProvider { defaultValues?: IHTTPSimpleFields; } -export const initialValues = { +export const initialValues: IHTTPSimpleFields = { + ...commonDefaultValues, [ConfigKeys.URLS]: '', [ConfigKeys.MAX_REDIRECTS]: '0', [ConfigKeys.MONITOR_TYPE]: DataStream.HTTP, - [ConfigKeys.SCHEDULE]: { - number: '3', - unit: ScheduleUnit.MINUTES, - }, - [ConfigKeys.APM_SERVICE_NAME]: '', - [ConfigKeys.TAGS]: [], - [ConfigKeys.TIMEOUT]: '16', }; const defaultContext: IHTTPSimpleFieldsContext = { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_provider.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_provider.tsx index e48de76862e24..ea577f3336936 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_provider.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_provider.tsx @@ -6,63 +6,39 @@ */ import React, { ReactNode } from 'react'; -import { IHTTPSimpleFields, IHTTPAdvancedFields, ITLSFields, ConfigKeys } from '../types'; +import { HTTPFields, IHTTPSimpleFields, IHTTPAdvancedFields } from '../types'; import { HTTPSimpleFieldsContextProvider, HTTPAdvancedFieldsContextProvider, - TLSFieldsContextProvider, + defaultHTTPSimpleFields, + defaultHTTPAdvancedFields, } from '.'; +import { formatDefaultValues } from '../helpers/context_helpers'; interface HTTPContextProviderProps { - defaultValues?: any; + defaultValues?: HTTPFields; children: ReactNode; } export const HTTPContextProvider = ({ defaultValues, children }: HTTPContextProviderProps) => { - const httpAdvancedFields: IHTTPAdvancedFields | undefined = defaultValues - ? { - [ConfigKeys.USERNAME]: defaultValues[ConfigKeys.USERNAME], - [ConfigKeys.PASSWORD]: defaultValues[ConfigKeys.PASSWORD], - [ConfigKeys.PROXY_URL]: defaultValues[ConfigKeys.PROXY_URL], - [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: - defaultValues[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE], - [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: - defaultValues[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE], - [ConfigKeys.RESPONSE_BODY_INDEX]: defaultValues[ConfigKeys.RESPONSE_BODY_INDEX], - [ConfigKeys.RESPONSE_HEADERS_CHECK]: defaultValues[ConfigKeys.RESPONSE_HEADERS_CHECK], - [ConfigKeys.RESPONSE_HEADERS_INDEX]: defaultValues[ConfigKeys.RESPONSE_HEADERS_INDEX], - [ConfigKeys.RESPONSE_STATUS_CHECK]: defaultValues[ConfigKeys.RESPONSE_STATUS_CHECK], - [ConfigKeys.REQUEST_BODY_CHECK]: defaultValues[ConfigKeys.REQUEST_BODY_CHECK], - [ConfigKeys.REQUEST_HEADERS_CHECK]: defaultValues[ConfigKeys.REQUEST_HEADERS_CHECK], - [ConfigKeys.REQUEST_METHOD_CHECK]: defaultValues[ConfigKeys.REQUEST_METHOD_CHECK], - } - : undefined; + const simpleKeys = Object.keys(defaultHTTPSimpleFields) as Array; + const advancedKeys = Object.keys(defaultHTTPAdvancedFields) as Array; + const formattedDefaultHTTPSimpleFields = formatDefaultValues( + simpleKeys, + defaultValues || {} + ); + const formattedDefaultHTTPAdvancedFields = formatDefaultValues( + advancedKeys, + defaultValues || {} + ); + const httpAdvancedFields = defaultValues ? formattedDefaultHTTPAdvancedFields : undefined; const httpSimpleFields: IHTTPSimpleFields | undefined = defaultValues - ? { - [ConfigKeys.APM_SERVICE_NAME]: defaultValues[ConfigKeys.APM_SERVICE_NAME], - [ConfigKeys.MAX_REDIRECTS]: defaultValues[ConfigKeys.MAX_REDIRECTS], - [ConfigKeys.MONITOR_TYPE]: defaultValues[ConfigKeys.MONITOR_TYPE], - [ConfigKeys.SCHEDULE]: defaultValues[ConfigKeys.SCHEDULE], - [ConfigKeys.TAGS]: defaultValues[ConfigKeys.TAGS], - [ConfigKeys.TIMEOUT]: defaultValues[ConfigKeys.TIMEOUT], - [ConfigKeys.URLS]: defaultValues[ConfigKeys.URLS], - } - : undefined; - const tlsFields: ITLSFields | undefined = defaultValues - ? { - [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: - defaultValues[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES], - [ConfigKeys.TLS_CERTIFICATE]: defaultValues[ConfigKeys.TLS_CERTIFICATE], - [ConfigKeys.TLS_KEY]: defaultValues[ConfigKeys.TLS_KEY], - [ConfigKeys.TLS_KEY_PASSPHRASE]: defaultValues[ConfigKeys.TLS_KEY_PASSPHRASE], - [ConfigKeys.TLS_VERIFICATION_MODE]: defaultValues[ConfigKeys.TLS_VERIFICATION_MODE], - [ConfigKeys.TLS_VERSION]: defaultValues[ConfigKeys.TLS_VERSION], - } + ? formattedDefaultHTTPSimpleFields : undefined; return ( - {children} + {children} ); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/icmp_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/icmp_context.tsx index 93c67c6133ce9..eb7227ebceb07 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/icmp_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/icmp_context.tsx @@ -6,7 +6,8 @@ */ import React, { createContext, useContext, useMemo, useState } from 'react'; -import { IICMPSimpleFields, ConfigKeys, ScheduleUnit, DataStream } from '../types'; +import { IICMPSimpleFields, ConfigKeys, DataStream } from '../types'; +import { defaultValues as commonDefaultValues } from '../common/default_values'; interface IICMPSimpleFieldsContext { setFields: React.Dispatch>; @@ -19,17 +20,10 @@ interface IICMPSimpleFieldsContextProvider { defaultValues?: IICMPSimpleFields; } -export const initialValues = { +export const initialValues: IICMPSimpleFields = { + ...commonDefaultValues, [ConfigKeys.HOSTS]: '', - [ConfigKeys.MAX_REDIRECTS]: '0', [ConfigKeys.MONITOR_TYPE]: DataStream.ICMP, - [ConfigKeys.SCHEDULE]: { - number: '3', - unit: ScheduleUnit.MINUTES, - }, - [ConfigKeys.APM_SERVICE_NAME]: '', - [ConfigKeys.TAGS]: [], - [ConfigKeys.TIMEOUT]: '16', [ConfigKeys.WAIT]: '1', }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts index f84a4e75df922..e955d2d7d4d50 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - export { MonitorTypeContext, MonitorTypeContextProvider, @@ -17,6 +16,12 @@ export { initialValues as defaultHTTPSimpleFields, useHTTPSimpleFieldsContext, } from './http_context'; +export { + HTTPAdvancedFieldsContext, + HTTPAdvancedFieldsContextProvider, + initialValues as defaultHTTPAdvancedFields, + useHTTPAdvancedFieldsContext, +} from './advanced_fields_http_context'; export { TCPSimpleFieldsContext, TCPSimpleFieldsContextProvider, @@ -36,11 +41,17 @@ export { useTCPAdvancedFieldsContext, } from './advanced_fields_tcp_context'; export { - HTTPAdvancedFieldsContext, - HTTPAdvancedFieldsContextProvider, - initialValues as defaultHTTPAdvancedFields, - useHTTPAdvancedFieldsContext, -} from './advanced_fields_http_context'; + BrowserSimpleFieldsContext, + BrowserSimpleFieldsContextProvider, + initialValues as defaultBrowserSimpleFields, + useBrowserSimpleFieldsContext, +} from './browser_context'; +export { + BrowserAdvancedFieldsContext, + BrowserAdvancedFieldsContextProvider, + initialValues as defaultBrowserAdvancedFields, + useBrowserAdvancedFieldsContext, +} from './browser_context_advanced'; export { TLSFieldsContext, TLSFieldsContextProvider, @@ -49,3 +60,4 @@ export { } from './tls_fields_context'; export { HTTPContextProvider } from './http_provider'; export { TCPContextProvider } from './tcp_provider'; +export { BrowserContextProvider } from './browser_provider'; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context.tsx index 6020a7ff2bff8..a1e01cb7faab7 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context.tsx @@ -6,7 +6,8 @@ */ import React, { createContext, useContext, useMemo, useState } from 'react'; -import { ITCPSimpleFields, ConfigKeys, ScheduleUnit, DataStream } from '../types'; +import { ITCPSimpleFields, ConfigKeys, DataStream } from '../types'; +import { defaultValues as commonDefaultValues } from '../common/default_values'; interface ITCPSimpleFieldsContext { setFields: React.Dispatch>; @@ -19,17 +20,10 @@ interface ITCPSimpleFieldsContextProvider { defaultValues?: ITCPSimpleFields; } -export const initialValues = { +export const initialValues: ITCPSimpleFields = { + ...commonDefaultValues, [ConfigKeys.HOSTS]: '', - [ConfigKeys.MAX_REDIRECTS]: '0', [ConfigKeys.MONITOR_TYPE]: DataStream.TCP, - [ConfigKeys.SCHEDULE]: { - number: '3', - unit: ScheduleUnit.MINUTES, - }, - [ConfigKeys.APM_SERVICE_NAME]: '', - [ConfigKeys.TAGS]: [], - [ConfigKeys.TIMEOUT]: '16', }; const defaultContext: ITCPSimpleFieldsContext = { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_provider.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_provider.tsx index 666839803f4d6..b62e87a566b97 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_provider.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_provider.tsx @@ -6,56 +6,41 @@ */ import React, { ReactNode } from 'react'; -import { ConfigKeys, ITCPSimpleFields, ITCPAdvancedFields, ITLSFields } from '../types'; +import { TCPFields, ITCPSimpleFields, ITCPAdvancedFields } from '../types'; import { TCPSimpleFieldsContextProvider, TCPAdvancedFieldsContextProvider, - TLSFieldsContextProvider, + defaultTCPSimpleFields, + defaultTCPAdvancedFields, } from '.'; +import { formatDefaultValues } from '../helpers/context_helpers'; interface TCPContextProviderProps { - defaultValues?: any; + defaultValues?: TCPFields; children: ReactNode; } -/** - * Exports Synthetics-specific package policy instructions - * for use in the Ingest app create / edit package policy - */ export const TCPContextProvider = ({ defaultValues, children }: TCPContextProviderProps) => { - const tcpSimpleFields: ITCPSimpleFields | undefined = defaultValues - ? { - [ConfigKeys.APM_SERVICE_NAME]: defaultValues[ConfigKeys.APM_SERVICE_NAME], - [ConfigKeys.HOSTS]: defaultValues[ConfigKeys.HOSTS], - [ConfigKeys.MONITOR_TYPE]: defaultValues[ConfigKeys.MONITOR_TYPE], - [ConfigKeys.SCHEDULE]: defaultValues[ConfigKeys.SCHEDULE], - [ConfigKeys.TAGS]: defaultValues[ConfigKeys.TAGS], - [ConfigKeys.TIMEOUT]: defaultValues[ConfigKeys.TIMEOUT], - } - : undefined; - const tcpAdvancedFields: ITCPAdvancedFields | undefined = defaultValues - ? { - [ConfigKeys.PROXY_URL]: defaultValues[ConfigKeys.PROXY_URL], - [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: defaultValues[ConfigKeys.PROXY_USE_LOCAL_RESOLVER], - [ConfigKeys.RESPONSE_RECEIVE_CHECK]: defaultValues[ConfigKeys.RESPONSE_RECEIVE_CHECK], - [ConfigKeys.REQUEST_SEND_CHECK]: defaultValues[ConfigKeys.REQUEST_SEND_CHECK], - } + const simpleKeys = Object.keys(defaultTCPSimpleFields) as Array; + const advancedKeys = Object.keys(defaultTCPAdvancedFields) as Array; + const formattedDefaultSimpleFields = formatDefaultValues( + simpleKeys, + defaultValues || {} + ); + const formattedDefaultAdvancedFields = formatDefaultValues( + advancedKeys, + defaultValues || {} + ); + const simpleFields: ITCPSimpleFields | undefined = defaultValues + ? formattedDefaultSimpleFields : undefined; - const tlsFields: ITLSFields | undefined = defaultValues - ? { - [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: - defaultValues[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES], - [ConfigKeys.TLS_CERTIFICATE]: defaultValues[ConfigKeys.TLS_CERTIFICATE], - [ConfigKeys.TLS_KEY]: defaultValues[ConfigKeys.TLS_KEY], - [ConfigKeys.TLS_KEY_PASSPHRASE]: defaultValues[ConfigKeys.TLS_KEY_PASSPHRASE], - [ConfigKeys.TLS_VERIFICATION_MODE]: defaultValues[ConfigKeys.TLS_VERIFICATION_MODE], - [ConfigKeys.TLS_VERSION]: defaultValues[ConfigKeys.TLS_VERSION], - } + const advancedFields: ITCPAdvancedFields | undefined = defaultValues + ? formattedDefaultAdvancedFields : undefined; return ( - - - {children} + + + {children} ); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tls_fields_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tls_fields_context.tsx index eaeb995654448..2a88b8c88e96c 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tls_fields_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tls_fields_context.tsx @@ -19,7 +19,7 @@ interface ITLSFieldsContextProvider { defaultValues?: ITLSFields; } -export const initialValues = { +export const initialValues: ITLSFields = { [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: { value: '', isEnabled: false, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx index e114ea72b8f49..5bcd235b9b60e 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import 'jest-canvas-mock'; import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; @@ -11,8 +12,10 @@ import { render } from '../../lib/helper/rtl_helpers'; import { TCPContextProvider, HTTPContextProvider, + BrowserContextProvider, ICMPSimpleFieldsContextProvider, MonitorTypeContextProvider, + TLSFieldsContextProvider, } from './contexts'; import { CustomFields } from './custom_fields'; import { ConfigKeys, DataStream, ScheduleUnit } from './types'; @@ -30,14 +33,26 @@ const defaultHTTPConfig = defaultConfig[DataStream.HTTP]; const defaultTCPConfig = defaultConfig[DataStream.TCP]; describe('', () => { - const WrappedComponent = ({ validate = defaultValidation, typeEditable = false }) => { + const WrappedComponent = ({ + validate = defaultValidation, + typeEditable = false, + dataStreams = [DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER], + }) => { return ( - - - + + + + + + + @@ -149,7 +164,7 @@ describe('', () => { }); it('handles switching monitor type', () => { - const { getByText, getByLabelText, queryByLabelText } = render( + const { getByText, getByLabelText, queryByLabelText, getAllByLabelText } = render( ); const monitorType = getByLabelText('Monitor Type') as HTMLInputElement; @@ -168,7 +183,7 @@ describe('', () => { expect(queryByLabelText('Max redirects')).not.toBeInTheDocument(); // ensure at least one tcp advanced option is present - const advancedOptionsButton = getByText('Advanced TCP options'); + let advancedOptionsButton = getByText('Advanced TCP options'); fireEvent.click(advancedOptionsButton); expect(queryByLabelText('Request method')).not.toBeInTheDocument(); @@ -181,6 +196,21 @@ describe('', () => { // expect TCP fields not to be in the DOM expect(queryByLabelText('Proxy URL')).not.toBeInTheDocument(); + + fireEvent.change(monitorType, { target: { value: DataStream.BROWSER } }); + + // expect browser fields to be in the DOM + getAllByLabelText('Zip URL').forEach((node) => { + expect(node).toBeInTheDocument(); + }); + + // ensure at least one browser advanced option is present + advancedOptionsButton = getByText('Advanced Browser options'); + fireEvent.click(advancedOptionsButton); + expect(getByLabelText('Screenshot options')).toBeInTheDocument(); + + // expect ICMP fields not to be in the DOM + expect(queryByLabelText('Wait in seconds')).not.toBeInTheDocument(); }); it('shows resolve hostnames locally field when proxy url is filled for tcp monitors', () => { @@ -213,7 +243,7 @@ describe('', () => { const urlError = getByText('URL is required'); const monitorIntervalError = getByText('Monitor interval is required'); const maxRedirectsError = getByText('Max redirects must be 0 or greater'); - const timeoutError = getByText('Timeout must be 0 or greater and less than schedule interval'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(urlError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -229,16 +259,35 @@ describe('', () => { expect(queryByText('URL is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); // create more errors fireEvent.change(monitorIntervalNumber, { target: { value: '1' } }); // 1 minute - fireEvent.change(timeout, { target: { value: '61' } }); // timeout cannot be more than monitor interval + fireEvent.change(timeout, { target: { value: '611' } }); // timeout cannot be more than monitor interval - const timeoutError2 = getByText('Timeout must be 0 or greater and less than schedule interval'); + const timeoutError2 = getByText('Timeout must be less than the monitor interval'); expect(timeoutError2).toBeInTheDocument(); }); + + it('does not show monitor options that are not contained in datastreams', async () => { + const { getByText, queryByText, queryByLabelText } = render( + + ); + + const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement; + + // resolve errors + fireEvent.click(monitorType); + + waitFor(() => { + expect(getByText('http')).toBeInTheDocument(); + expect(getByText('tcp')).toBeInTheDocument(); + expect(getByText('icmp')).toBeInTheDocument(); + expect(queryByText('browser')).not.toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index 0d9291261b82d..87f7a98aa4a6f 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import React, { useState, memo } from 'react'; +import React, { useState, useMemo, memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, @@ -25,20 +24,39 @@ import { HTTPAdvancedFields } from './http/advanced_fields'; import { TCPSimpleFields } from './tcp/simple_fields'; import { TCPAdvancedFields } from './tcp/advanced_fields'; import { ICMPSimpleFields } from './icmp/simple_fields'; +import { BrowserSimpleFields } from './browser/simple_fields'; +import { BrowserAdvancedFields } from './browser/advanced_fields'; interface Props { typeEditable?: boolean; isTLSEnabled?: boolean; validate: Validation; + dataStreams?: DataStream[]; } export const CustomFields = memo( - ({ typeEditable = false, isTLSEnabled: defaultIsTLSEnabled = false, validate }) => { + ({ + typeEditable = false, + isTLSEnabled: defaultIsTLSEnabled = false, + validate, + dataStreams = [], + }) => { const [isTLSEnabled, setIsTLSEnabled] = useState(defaultIsTLSEnabled); const { monitorType, setMonitorType } = useMonitorTypeContext(); const isHTTP = monitorType === DataStream.HTTP; const isTCP = monitorType === DataStream.TCP; + const isBrowser = monitorType === DataStream.BROWSER; + + const dataStreamOptions = useMemo(() => { + const dataStreamToString = [ + { value: DataStream.HTTP, text: 'HTTP' }, + { value: DataStream.TCP, text: 'TCP' }, + { value: DataStream.ICMP, text: 'ICMP' }, + { value: DataStream.BROWSER, text: 'Browser' }, + ]; + return dataStreamToString.filter((dataStream) => dataStreams.includes(dataStream.value)); + }, [dataStreams]); const renderSimpleFields = (type: DataStream) => { switch (type) { @@ -48,6 +66,8 @@ export const CustomFields = memo( return ; case DataStream.TCP: return ; + case DataStream.BROWSER: + return ; default: return null; } @@ -82,7 +102,11 @@ export const CustomFields = memo( defaultMessage="Monitor Type" /> } - isInvalid={!!validate[ConfigKeys.MONITOR_TYPE]?.(monitorType)} + isInvalid={ + !!validate[ConfigKeys.MONITOR_TYPE]?.({ + [ConfigKeys.MONITOR_TYPE]: monitorType, + }) + } error={ ( {isHTTP && } {isTCP && } + {isBrowser && } ); } ); - -const dataStreamOptions = [ - { value: DataStream.HTTP, text: 'HTTP' }, - { value: DataStream.TCP, text: 'TCP' }, - { value: DataStream.ICMP, text: 'ICMP' }, -]; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/helpers/context_helpers.ts b/x-pack/plugins/uptime/public/components/fleet_package/helpers/context_helpers.ts new file mode 100644 index 0000000000000..acd8bdf95ce85 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/helpers/context_helpers.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function formatDefaultValues( + keys: Array, + defaultValues: Partial +) { + return keys.reduce((acc: any, currentValue) => { + const key = currentValue as keyof Fields; + acc[key] = defaultValues?.[key]; + return acc; + }, {}) as Fields; +} diff --git a/x-pack/plugins/uptime/public/components/fleet_package/helpers/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/helpers/formatters.ts new file mode 100644 index 0000000000000..8ca10516a6200 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/helpers/formatters.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataStream } from '../types'; + +import { httpFormatters, HTTPFormatMap } from '../http/formatters'; +import { tcpFormatters, TCPFormatMap } from '../tcp/formatters'; +import { icmpFormatters, ICMPFormatMap } from '../icmp/formatters'; +import { browserFormatters, BrowserFormatMap } from '../browser/formatters'; +import { commonFormatters, CommonFormatMap } from '../common/formatters'; + +type Formatters = HTTPFormatMap & TCPFormatMap & ICMPFormatMap & BrowserFormatMap & CommonFormatMap; + +interface FormatterMap { + [DataStream.HTTP]: HTTPFormatMap; + [DataStream.ICMP]: ICMPFormatMap; + [DataStream.TCP]: TCPFormatMap; + [DataStream.BROWSER]: BrowserFormatMap; +} + +export const formattersMap: FormatterMap = { + [DataStream.HTTP]: httpFormatters, + [DataStream.ICMP]: icmpFormatters, + [DataStream.TCP]: tcpFormatters, + [DataStream.BROWSER]: browserFormatters, +}; + +export const formatters: Formatters = { + ...httpFormatters, + ...icmpFormatters, + ...tcpFormatters, + ...browserFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/helpers/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/helpers/normalizers.ts new file mode 100644 index 0000000000000..60aa607aebe61 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/helpers/normalizers.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataStream } from '../types'; + +import { httpNormalizers, HTTPNormalizerMap } from '../http/normalizers'; +import { tcpNormalizers, TCPNormalizerMap } from '../tcp/normalizers'; +import { icmpNormalizers, ICMPNormalizerMap } from '../icmp/normalizers'; +import { browserNormalizers, BrowserNormalizerMap } from '../browser/normalizers'; +import { commonNormalizers, CommonNormalizerMap } from '../common/normalizers'; + +type Normalizers = HTTPNormalizerMap & + ICMPNormalizerMap & + TCPNormalizerMap & + BrowserNormalizerMap & + CommonNormalizerMap; + +interface NormalizerMap { + [DataStream.HTTP]: HTTPNormalizerMap; + [DataStream.ICMP]: ICMPNormalizerMap; + [DataStream.TCP]: TCPNormalizerMap; + [DataStream.BROWSER]: BrowserNormalizerMap; +} + +export const normalizersMap: NormalizerMap = { + [DataStream.HTTP]: httpNormalizers, + [DataStream.ICMP]: icmpNormalizers, + [DataStream.TCP]: tcpNormalizers, + [DataStream.BROWSER]: browserNormalizers, +}; + +export const normalizers: Normalizers = { + ...httpNormalizers, + ...icmpNormalizers, + ...tcpNormalizers, + ...browserNormalizers, + ...commonNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx index 267ccd678ddad..c38ac509e377e 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx @@ -186,18 +186,12 @@ export const HTTPAdvancedFields = memo(({ validate }) => { /> } labelAppend={} - isInvalid={ - !!validate[ConfigKeys.REQUEST_HEADERS_CHECK]?.(fields[ConfigKeys.REQUEST_HEADERS_CHECK]) - } + isInvalid={!!validate[ConfigKeys.REQUEST_HEADERS_CHECK]?.(fields)} error={ - !!validate[ConfigKeys.REQUEST_HEADERS_CHECK]?.( - fields[ConfigKeys.REQUEST_HEADERS_CHECK] - ) ? ( - - ) : undefined + } helpText={ (({ validate }) => { /> } labelAppend={} - isInvalid={ - !!validate[ConfigKeys.RESPONSE_STATUS_CHECK]?.(fields[ConfigKeys.RESPONSE_STATUS_CHECK]) - } + isInvalid={!!validate[ConfigKeys.RESPONSE_STATUS_CHECK]?.(fields)} error={ (({ validate }) => { /> } labelAppend={} - isInvalid={ - !!validate[ConfigKeys.RESPONSE_HEADERS_CHECK]?.( - fields[ConfigKeys.RESPONSE_HEADERS_CHECK] - ) - } - error={ - !!validate[ConfigKeys.RESPONSE_HEADERS_CHECK]?.( - fields[ConfigKeys.RESPONSE_HEADERS_CHECK] - ) - ? [ - , - ] - : undefined - } + isInvalid={!!validate[ConfigKeys.RESPONSE_HEADERS_CHECK]?.(fields)} + error={[ + , + ]} helpText={ ; + +export const httpFormatters: HTTPFormatMap = { + [ConfigKeys.URLS]: null, + [ConfigKeys.MAX_REDIRECTS]: null, + [ConfigKeys.USERNAME]: null, + [ConfigKeys.PASSWORD]: null, + [ConfigKeys.PROXY_URL]: null, + [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]), + [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]), + [ConfigKeys.RESPONSE_BODY_INDEX]: null, + [ConfigKeys.RESPONSE_HEADERS_CHECK]: (fields) => + objectToJsonFormatter(fields[ConfigKeys.RESPONSE_HEADERS_CHECK]), + [ConfigKeys.RESPONSE_HEADERS_INDEX]: null, + [ConfigKeys.RESPONSE_STATUS_CHECK]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.RESPONSE_STATUS_CHECK]), + [ConfigKeys.REQUEST_BODY_CHECK]: (fields) => + fields[ConfigKeys.REQUEST_BODY_CHECK]?.value + ? JSON.stringify(fields[ConfigKeys.REQUEST_BODY_CHECK]?.value) + : null, + [ConfigKeys.REQUEST_HEADERS_CHECK]: (fields) => + objectToJsonFormatter(fields[ConfigKeys.REQUEST_HEADERS_CHECK]), + [ConfigKeys.REQUEST_METHOD_CHECK]: null, + ...tlsFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts new file mode 100644 index 0000000000000..10c52c295c9c4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HTTPFields, ConfigKeys, ContentType, contentTypesToMode } from '../types'; +import { + Normalizer, + commonNormalizers, + getNormalizer, + getJsonToArrayOrObjectNormalizer, +} from '../common/normalizers'; +import { tlsNormalizers } from '../tls/normalizers'; +import { defaultHTTPSimpleFields, defaultHTTPAdvancedFields } from '../contexts'; + +export type HTTPNormalizerMap = Record; + +const defaultHTTPValues = { + ...defaultHTTPSimpleFields, + ...defaultHTTPAdvancedFields, +}; + +export const getHTTPNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, defaultHTTPValues); +}; + +export const getHTTPJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { + return getJsonToArrayOrObjectNormalizer(key, defaultHTTPValues); +}; + +export const httpNormalizers: HTTPNormalizerMap = { + [ConfigKeys.URLS]: getHTTPNormalizer(ConfigKeys.URLS), + [ConfigKeys.MAX_REDIRECTS]: getHTTPNormalizer(ConfigKeys.MAX_REDIRECTS), + [ConfigKeys.USERNAME]: getHTTPNormalizer(ConfigKeys.USERNAME), + [ConfigKeys.PASSWORD]: getHTTPNormalizer(ConfigKeys.PASSWORD), + [ConfigKeys.PROXY_URL]: getHTTPNormalizer(ConfigKeys.PROXY_URL), + [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE + ), + [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE + ), + [ConfigKeys.RESPONSE_BODY_INDEX]: getHTTPNormalizer(ConfigKeys.RESPONSE_BODY_INDEX), + [ConfigKeys.RESPONSE_HEADERS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.RESPONSE_HEADERS_CHECK + ), + [ConfigKeys.RESPONSE_HEADERS_INDEX]: getHTTPNormalizer(ConfigKeys.RESPONSE_HEADERS_INDEX), + [ConfigKeys.RESPONSE_STATUS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.RESPONSE_STATUS_CHECK + ), + [ConfigKeys.REQUEST_BODY_CHECK]: (fields) => { + const requestBody = fields?.[ConfigKeys.REQUEST_BODY_CHECK]?.value; + const requestHeaders = fields?.[ConfigKeys.REQUEST_HEADERS_CHECK]?.value; + if (requestBody) { + const headers = requestHeaders + ? JSON.parse(fields?.[ConfigKeys.REQUEST_HEADERS_CHECK]?.value) + : defaultHTTPAdvancedFields[ConfigKeys.REQUEST_HEADERS_CHECK]; + const requestBodyValue = + requestBody !== null && requestBody !== undefined + ? JSON.parse(requestBody) + : defaultHTTPAdvancedFields[ConfigKeys.REQUEST_BODY_CHECK]?.value; + let requestBodyType = defaultHTTPAdvancedFields[ConfigKeys.REQUEST_BODY_CHECK]?.type; + Object.keys(headers || []).some((headerKey) => { + if (headerKey === 'Content-Type' && contentTypesToMode[headers[headerKey] as ContentType]) { + requestBodyType = contentTypesToMode[headers[headerKey] as ContentType]; + return true; + } + }); + return { + value: requestBodyValue, + type: requestBodyType, + }; + } else { + return defaultHTTPAdvancedFields[ConfigKeys.REQUEST_BODY_CHECK]; + } + }, + [ConfigKeys.REQUEST_HEADERS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.REQUEST_HEADERS_CHECK + ), + [ConfigKeys.REQUEST_METHOD_CHECK]: getHTTPNormalizer(ConfigKeys.REQUEST_METHOD_CHECK), + ...commonNormalizers, + ...tlsNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx index d17b8c997e9e8..8eb81eb92f7b4 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx @@ -33,7 +33,7 @@ export const HTTPSimpleFields = memo(({ validate }) => { defaultMessage="URL" /> } - isInvalid={!!validate[ConfigKeys.URLS]?.(fields[ConfigKeys.URLS])} + isInvalid={!!validate[ConfigKeys.URLS]?.(fields)} error={ (({ validate }) => { defaultMessage="Monitor interval" /> } - isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields[ConfigKeys.SCHEDULE])} + isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields)} error={ (({ validate }) => { defaultMessage="Max redirects" /> } - isInvalid={!!validate[ConfigKeys.MAX_REDIRECTS]?.(fields[ConfigKeys.MAX_REDIRECTS])} + isInvalid={!!validate[ConfigKeys.MAX_REDIRECTS]?.(fields)} error={ (({ validate }) => { defaultMessage="Timeout in seconds" /> } - isInvalid={ - !!validate[ConfigKeys.TIMEOUT]?.( - fields[ConfigKeys.TIMEOUT], - fields[ConfigKeys.SCHEDULE].number, - fields[ConfigKeys.SCHEDULE].unit - ) - } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} error={ - + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) } helpText={ ; + +export const icmpFormatters: ICMPFormatMap = { + [ConfigKeys.HOSTS]: null, + [ConfigKeys.WAIT]: (fields) => secondsToCronFormatter(fields[ConfigKeys.WAIT]), + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/icmp/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/icmp/normalizers.ts new file mode 100644 index 0000000000000..18ce1da00e117 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/icmp/normalizers.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICMPFields, ConfigKeys } from '../types'; +import { + Normalizer, + commonNormalizers, + getNormalizer, + getCronNormalizer, +} from '../common/normalizers'; +import { defaultICMPSimpleFields } from '../contexts'; + +export type ICMPNormalizerMap = Record; + +export const getICMPNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, defaultICMPSimpleFields); +}; + +export const getICMPCronToSecondsNormalizer = (key: ConfigKeys) => { + return getCronNormalizer(key, defaultICMPSimpleFields); +}; + +export const icmpNormalizers: ICMPNormalizerMap = { + [ConfigKeys.HOSTS]: getICMPNormalizer(ConfigKeys.HOSTS), + [ConfigKeys.WAIT]: getICMPCronToSecondsNormalizer(ConfigKeys.WAIT), + ...commonNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx index 3ca07c7067367..420f218429e40 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx @@ -33,7 +33,7 @@ export const ICMPSimpleFields = memo(({ validate }) => { defaultMessage="Host" /> } - isInvalid={!!validate[ConfigKeys.HOSTS]?.(fields[ConfigKeys.HOSTS])} + isInvalid={!!validate[ConfigKeys.HOSTS]?.(fields)} error={ (({ validate }) => { defaultMessage="Monitor interval" /> } - isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields[ConfigKeys.SCHEDULE])} + isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields)} error={ (({ validate }) => { defaultMessage="Wait in seconds" /> } - isInvalid={!!validate[ConfigKeys.WAIT]?.(fields[ConfigKeys.WAIT])} + isInvalid={!!validate[ConfigKeys.WAIT]?.(fields)} error={ (({ validate }) => { defaultMessage="Timeout in seconds" /> } - isInvalid={ - !!validate[ConfigKeys.TIMEOUT]?.( - fields[ConfigKeys.TIMEOUT], - fields[ConfigKeys.SCHEDULE].number, - fields[ConfigKeys.SCHEDULE].unit - ) - } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} error={ - + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) } helpText={ ( ({ newPolicy, onChange }) => { - const { monitorType } = useContext(MonitorTypeContext); - const { fields: httpSimpleFields } = useContext(HTTPSimpleFieldsContext); - const { fields: tcpSimpleFields } = useContext(TCPSimpleFieldsContext); - const { fields: icmpSimpleFields } = useContext(ICMPSimpleFieldsContext); - const { fields: httpAdvancedFields } = useContext(HTTPAdvancedFieldsContext); - const { fields: tcpAdvancedFields } = useContext(TCPAdvancedFieldsContext); - const { fields: tlsFields } = useContext(TLSFieldsContext); + const { monitorType } = useMonitorTypeContext(); + const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext(); + const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext(); + const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext(); + const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext(); + const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext(); + const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext(); + const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext(); + const { fields: tlsFields } = useTLSFieldsContext(); + + const policyConfig: PolicyConfig = { + [DataStream.HTTP]: { + ...httpSimpleFields, + ...httpAdvancedFields, + ...tlsFields, + [ConfigKeys.NAME]: newPolicy.name, + } as HTTPFields, + [DataStream.TCP]: { + ...tcpSimpleFields, + ...tcpAdvancedFields, + ...tlsFields, + [ConfigKeys.NAME]: newPolicy.name, + } as TCPFields, + [DataStream.ICMP]: { + ...icmpSimpleFields, + [ConfigKeys.NAME]: newPolicy.name, + } as ICMPFields, + [DataStream.BROWSER]: { + ...browserSimpleFields, + ...browserAdvancedFields, + [ConfigKeys.NAME]: newPolicy.name, + } as BrowserFields, + }; + useTrackPageview({ app: 'fleet', path: 'syntheticsCreate' }); useTrackPageview({ app: 'fleet', path: 'syntheticsCreate', delay: 15000 }); - const { setConfig } = useUpdatePolicy({ + + const dataStreams: DataStream[] = useMemo(() => { + return newPolicy.inputs.map((input) => { + return input.type.replace(/synthetics\//g, '') as DataStream; + }); + }, [newPolicy]); + + useUpdatePolicy({ monitorType, - defaultConfig, + defaultConfig: defaultConfig[monitorType], + config: policyConfig[monitorType], newPolicy, onChange, validate, @@ -80,42 +130,7 @@ export const SyntheticsPolicyCreateExtension = memo { - setConfig(() => { - switch (monitorType) { - case DataStream.HTTP: - return { - ...httpSimpleFields, - ...httpAdvancedFields, - ...tlsFields, - }; - case DataStream.TCP: - return { - ...tcpSimpleFields, - ...tcpAdvancedFields, - ...tlsFields, - }; - case DataStream.ICMP: - return { - ...icmpSimpleFields, - }; - } - }); - }, - 250, - [ - setConfig, - httpSimpleFields, - tcpSimpleFields, - icmpSimpleFields, - httpAdvancedFields, - tcpAdvancedFields, - tlsFields, - ] - ); - - return ; + return ; } ); SyntheticsPolicyCreateExtension.displayName = 'SyntheticsPolicyCreateExtension'; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx index 395b5d67abeb0..e642ea44ab58d 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import 'jest-canvas-mock'; + import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; import { render } from '../../lib/helper/rtl_helpers'; @@ -18,6 +20,10 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); +jest.mock('./code_editor', () => ({ + CodeEditor: () =>
code editor mock
, +})); + const defaultNewPolicy: NewPackagePolicy = { name: 'samplePolicyName', description: '', @@ -148,6 +154,7 @@ const defaultNewPolicy: NewPackagePolicy = { type: 'text', }, name: { + value: 'Sample name', type: 'text', }, schedule: { @@ -217,6 +224,7 @@ const defaultNewPolicy: NewPackagePolicy = { type: 'text', }, name: { + value: 'Sample name', type: 'text', }, schedule: { @@ -236,8 +244,53 @@ const defaultNewPolicy: NewPackagePolicy = { timeout: { type: 'text', }, - max_redirects: { - type: 'integer', + tags: { + type: 'yaml', + }, + }, + }, + ], + }, + { + type: 'synthetics/browser', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { + type: 'synthetics', + dataset: 'browser', + }, + vars: { + type: { + value: 'browser', + type: 'text', + }, + name: { + value: 'Sample name', + type: 'text', + }, + schedule: { + value: '10s', + type: 'text', + }, + 'source.zip_url.url': { + type: 'text', + }, + 'source.zip_url.username': { + type: 'text', + }, + 'source.zip_url.password': { + type: 'password', + }, + 'source.zip_url.folder': { + type: 'text', + }, + 'source.inline.script': { + type: 'yaml', + }, + timeout: { + type: 'text', }, tags: { type: 'yaml', @@ -263,6 +316,10 @@ describe('', () => { return ; }; + beforeEach(() => { + onChange.mockClear(); + }); + it('renders SyntheticsPolicyCreateExtension', async () => { const { getByText, getByLabelText, queryByLabelText } = render(); const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement; @@ -371,6 +428,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -406,6 +464,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -434,6 +493,7 @@ describe('', () => { enabled: true, }, defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -481,7 +541,7 @@ describe('', () => { const urlError = getByText('URL is required'); const monitorIntervalError = getByText('Monitor interval is required'); const maxRedirectsError = getByText('Max redirects must be 0 or greater'); - const timeoutError = getByText('Timeout must be 0 or greater and less than schedule interval'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(urlError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -508,9 +568,7 @@ describe('', () => { expect(queryByText('URL is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ isValid: true, @@ -537,9 +595,7 @@ describe('', () => { await waitFor(() => { const hostError = getByText('Host and port are required'); const monitorIntervalError = getByText('Monitor interval is required'); - const timeoutError = getByText( - 'Timeout must be 0 or greater and less than schedule interval' - ); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(hostError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -560,9 +616,7 @@ describe('', () => { expect(queryByText('Host and port are required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ isValid: true, @@ -591,9 +645,7 @@ describe('', () => { await waitFor(() => { const hostError = getByText('Host is required'); const monitorIntervalError = getByText('Monitor interval is required'); - const timeoutError = getByText( - 'Timeout must be 0 or greater and less than schedule interval' - ); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); const waitError = getByText('Wait must be 0 or greater'); expect(hostError).toBeInTheDocument(); @@ -616,9 +668,7 @@ describe('', () => { await waitFor(() => { expect(queryByText('Host is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(queryByText('Wait must be 0 or greater')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ @@ -628,13 +678,67 @@ describe('', () => { }); }); + it('handles browser validation', async () => { + const { getByText, getByLabelText, queryByText, getByRole } = render(); + + const monitorType = getByLabelText('Monitor Type') as HTMLInputElement; + fireEvent.change(monitorType, { target: { value: DataStream.BROWSER } }); + + const zipUrl = getByRole('textbox', { name: 'Zip URL' }) as HTMLInputElement; + const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement; + const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement; + + // create errors + fireEvent.change(zipUrl, { target: { value: '' } }); + fireEvent.change(monitorIntervalNumber, { target: { value: '-1' } }); + fireEvent.change(timeout, { target: { value: '-1' } }); + + await waitFor(() => { + const hostError = getByText('Zip URL is required'); + const monitorIntervalError = getByText('Monitor interval is required'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); + + expect(hostError).toBeInTheDocument(); + expect(monitorIntervalError).toBeInTheDocument(); + expect(timeoutError).toBeInTheDocument(); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: false, + }) + ); + }); + + // resolve errors + fireEvent.change(zipUrl, { target: { value: 'http://github.com/tests.zip' } }); + fireEvent.change(monitorIntervalNumber, { target: { value: '1' } }); + fireEvent.change(timeout, { target: { value: '1' } }); + + await waitFor(() => { + expect(queryByText('Zip URL is required')).not.toBeInTheDocument(); + expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: true, + }) + ); + }); + + // test inline script validation + fireEvent.click(getByText('Inline script')); + + await waitFor(() => { + expect(getByText('Script is required')).toBeInTheDocument(); + }); + }); + it('handles changing TLS fields', async () => { const { findByLabelText, queryByLabelText } = render(); const enableSSL = queryByLabelText('Enable TLS configuration') as HTMLInputElement; await waitFor(() => { expect(onChange).toBeCalledWith({ - isValid: true, + isValid: false, updatedPolicy: { ...defaultNewPolicy, inputs: [ @@ -671,6 +775,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -714,7 +819,7 @@ describe('', () => { await waitFor(() => { expect(onChange).toBeCalledWith({ - isValid: true, + isValid: false, updatedPolicy: { ...defaultNewPolicy, inputs: [ @@ -751,6 +856,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx index 88bb8e7871459..0bc8f31f3d6cf 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx @@ -13,6 +13,7 @@ import { TCPContextProvider, ICMPSimpleFieldsContextProvider, HTTPContextProvider, + BrowserContextProvider, TLSFieldsContextProvider, } from './contexts'; @@ -28,7 +29,9 @@ export const SyntheticsPolicyCreateExtensionWrapper = memo - + + + diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx index 8a3c42c10bc14..ec135e4e914a7 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx @@ -6,7 +6,6 @@ */ import React, { memo } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; import { PackagePolicyEditExtensionComponentProps } from '../../../../fleet/public'; import { useTrackPageview } from '../../../../observability/public'; import { @@ -17,8 +16,19 @@ import { useHTTPSimpleFieldsContext, useHTTPAdvancedFieldsContext, useTLSFieldsContext, + useBrowserSimpleFieldsContext, + useBrowserAdvancedFieldsContext, } from './contexts'; -import { PolicyConfig, DataStream } from './types'; +import { + ICustomFields, + DataStream, + HTTPFields, + TCPFields, + ICMPFields, + BrowserFields, + ConfigKeys, + PolicyConfig, +} from './types'; import { CustomFields } from './custom_fields'; import { useUpdatePolicy } from './use_update_policy'; import { validate } from './validation'; @@ -26,7 +36,7 @@ import { validate } from './validation'; interface SyntheticsPolicyEditExtensionProps { newPolicy: PackagePolicyEditExtensionComponentProps['newPolicy']; onChange: PackagePolicyEditExtensionComponentProps['onChange']; - defaultConfig: PolicyConfig; + defaultConfig: Partial; isTLSEnabled: boolean; } /** @@ -44,49 +54,42 @@ export const SyntheticsPolicyEditExtension = memo { - setConfig(() => { - switch (monitorType) { - case DataStream.HTTP: - return { - ...httpSimpleFields, - ...httpAdvancedFields, - ...tlsFields, - }; - case DataStream.TCP: - return { - ...tcpSimpleFields, - ...tcpAdvancedFields, - ...tlsFields, - }; - case DataStream.ICMP: - return { - ...icmpSimpleFields, - }; - } - }); - }, - 250, - [ - setConfig, - httpSimpleFields, - httpAdvancedFields, - tcpSimpleFields, - tcpAdvancedFields, - icmpSimpleFields, - tlsFields, - ] - ); - return ; } ); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx index fec6c504a445f..d3c9030e85597 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import 'jest-canvas-mock'; + import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; import { render } from '../../lib/helper/rtl_helpers'; @@ -18,6 +20,10 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); +jest.mock('./code_editor', () => ({ + CodeEditor: () =>
code editor mock
, +})); + const defaultNewPolicy: NewPackagePolicy = { name: 'samplePolicyName', description: '', @@ -247,6 +253,54 @@ const defaultNewPolicy: NewPackagePolicy = { }, ], }, + { + type: 'synthetics/browser', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { + type: 'synthetics', + dataset: 'browser', + }, + vars: { + type: { + value: 'browser', + type: 'text', + }, + name: { + value: 'Sample name', + type: 'text', + }, + schedule: { + value: '"@every 5s"', + type: 'text', + }, + 'source.zip_url.url': { + type: 'text', + }, + 'source.zip_url.username': { + type: 'text', + }, + 'source.zip_url.password': { + type: 'password', + }, + 'source.zip_url.folder': { + type: 'text', + }, + 'source.inline.script': { + type: 'yaml', + }, + timeout: { + type: 'text', + }, + tags: { + type: 'yaml', + }, + }, + }, + ], + }, ], package: { name: 'synthetics', @@ -268,6 +322,7 @@ const defaultCurrentPolicy: any = { const defaultHTTPConfig = defaultConfig[DataStream.HTTP]; const defaultICMPConfig = defaultConfig[DataStream.ICMP]; const defaultTCPConfig = defaultConfig[DataStream.TCP]; +const defaultBrowserConfig = defaultConfig[DataStream.BROWSER]; describe('', () => { const onChange = jest.fn(); @@ -281,6 +336,10 @@ describe('', () => { ); }; + beforeEach(() => { + onChange.mockClear(); + }); + it('renders SyntheticsPolicyEditExtension', async () => { const { getByText, getByLabelText, queryByLabelText } = render(); const url = getByLabelText('URL') as HTMLInputElement; @@ -400,6 +459,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -435,6 +495,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -458,7 +519,7 @@ describe('', () => { const urlError = getByText('URL is required'); const monitorIntervalError = getByText('Monitor interval is required'); const maxRedirectsError = getByText('Max redirects must be 0 or greater'); - const timeoutError = getByText('Timeout must be 0 or greater and less than schedule interval'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(urlError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -485,9 +546,7 @@ describe('', () => { expect(queryByText('URL is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ isValid: true, @@ -496,6 +555,82 @@ describe('', () => { }); }); + it('handles browser validation', async () => { + const currentPolicy = { + ...defaultCurrentPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[1], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[2], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[3], + enabled: true, + }, + ], + }; + const { getByText, getByLabelText, queryByText, getByRole } = render( + + ); + + const zipUrl = getByRole('textbox', { name: 'Zip URL' }) as HTMLInputElement; + const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement; + const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement; + + // create errors + fireEvent.change(zipUrl, { target: { value: '' } }); + fireEvent.change(monitorIntervalNumber, { target: { value: '-1' } }); + fireEvent.change(timeout, { target: { value: '-1' } }); + + await waitFor(() => { + const hostError = getByText('Zip URL is required'); + const monitorIntervalError = getByText('Monitor interval is required'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); + + expect(hostError).toBeInTheDocument(); + expect(monitorIntervalError).toBeInTheDocument(); + expect(timeoutError).toBeInTheDocument(); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: false, + }) + ); + }); + + await waitFor(() => { + fireEvent.change(zipUrl, { target: { value: 'http://github.com/tests.zip' } }); + fireEvent.change(monitorIntervalNumber, { target: { value: '2' } }); + fireEvent.change(timeout, { target: { value: '1' } }); + expect(zipUrl.value).toEqual('http://github.com/tests.zip'); + expect(monitorIntervalNumber.value).toEqual('2'); + expect(timeout.value).toEqual('1'); + expect(queryByText('Zip URL is required')).not.toBeInTheDocument(); + expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: true, + }) + ); + }); + + await waitFor(() => { + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: true, + }) + ); + }); + }, 10000); + it('handles tcp validation', async () => { const currentPolicy = { ...defaultCurrentPolicy, @@ -509,6 +644,7 @@ describe('', () => { enabled: true, }, defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }; const { getByText, getByLabelText, queryByText } = render( @@ -527,9 +663,7 @@ describe('', () => { await waitFor(() => { const hostError = getByText('Host and port are required'); const monitorIntervalError = getByText('Monitor interval is required'); - const timeoutError = getByText( - 'Timeout must be 0 or greater and less than schedule interval' - ); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(hostError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -549,9 +683,7 @@ describe('', () => { await waitFor(() => { expect(queryByText('Host is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ isValid: true, @@ -576,6 +708,7 @@ describe('', () => { ...defaultNewPolicy.inputs[2], enabled: true, }, + defaultNewPolicy.inputs[3], ], }; const { getByText, getByLabelText, queryByText } = render( @@ -596,9 +729,7 @@ describe('', () => { await waitFor(() => { const hostError = getByText('Host is required'); const monitorIntervalError = getByText('Monitor interval is required'); - const timeoutError = getByText( - 'Timeout must be 0 or greater and less than schedule interval' - ); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); const waitError = getByText('Wait must be 0 or greater'); expect(hostError).toBeInTheDocument(); @@ -621,9 +752,7 @@ describe('', () => { await waitFor(() => { expect(queryByText('Host is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(queryByText('Wait must be 0 or greater')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ @@ -640,6 +769,7 @@ describe('', () => { inputs: [ { ...defaultNewPolicy.inputs[0], + enabled: true, streams: [ { ...defaultNewPolicy.inputs[0].streams[0], @@ -663,6 +793,7 @@ describe('', () => { }, defaultCurrentPolicy.inputs[1], defaultCurrentPolicy.inputs[2], + defaultCurrentPolicy.inputs[3], ], }; const { getByText, getByLabelText, queryByLabelText, queryByText } = render( @@ -782,7 +913,7 @@ describe('', () => { }); it('handles null values for icmp', async () => { - const tcpVars = defaultNewPolicy.inputs[1].streams[0].vars; + const icmpVars = defaultNewPolicy.inputs[2].streams[0].vars; const currentPolicy: NewPackagePolicy = { ...defaultCurrentPolicy, inputs: [ @@ -801,12 +932,12 @@ describe('', () => { { ...defaultNewPolicy.inputs[2].streams[0], vars: { - ...Object.keys(tcpVars || []).reduce< + ...Object.keys(icmpVars || []).reduce< Record >((acc, key) => { acc[key] = { value: undefined, - type: `${tcpVars?.[key].type}`, + type: `${icmpVars?.[key].type}`, }; return acc; }, {}), @@ -846,4 +977,72 @@ describe('', () => { expect(queryByLabelText('Url')).not.toBeInTheDocument(); expect(queryByLabelText('Proxy URL')).not.toBeInTheDocument(); }); + + it('handles null values for browser', async () => { + const browserVars = defaultNewPolicy.inputs[3].streams[0].vars; + const currentPolicy: NewPackagePolicy = { + ...defaultCurrentPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[1], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[2], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[3], + enabled: true, + streams: [ + { + ...defaultNewPolicy.inputs[3].streams[0], + vars: { + ...Object.keys(browserVars || []).reduce< + Record + >((acc, key) => { + acc[key] = { + value: undefined, + type: `${browserVars?.[key].type}`, + }; + return acc; + }, {}), + [ConfigKeys.MONITOR_TYPE]: { + value: DataStream.BROWSER, + type: 'text', + }, + }, + }, + ], + }, + ], + }; + const { getByLabelText, queryByLabelText, getByRole } = render( + + ); + const zipUrl = getByRole('textbox', { name: 'Zip URL' }) as HTMLInputElement; + const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement; + const monitorIntervalUnit = getByLabelText('Unit') as HTMLInputElement; + const apmServiceName = getByLabelText('APM service name') as HTMLInputElement; + const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement; + expect(zipUrl).toBeInTheDocument(); + expect(zipUrl.value).toEqual(defaultBrowserConfig[ConfigKeys.SOURCE_ZIP_URL]); + expect(monitorIntervalNumber).toBeInTheDocument(); + expect(monitorIntervalNumber.value).toEqual(defaultBrowserConfig[ConfigKeys.SCHEDULE].number); + expect(monitorIntervalUnit).toBeInTheDocument(); + expect(monitorIntervalUnit.value).toEqual(defaultBrowserConfig[ConfigKeys.SCHEDULE].unit); + expect(apmServiceName).toBeInTheDocument(); + expect(apmServiceName.value).toEqual(defaultBrowserConfig[ConfigKeys.APM_SERVICE_NAME]); + expect(timeout).toBeInTheDocument(); + expect(timeout.value).toEqual(`${defaultBrowserConfig[ConfigKeys.TIMEOUT]}`); + + // ensure other monitor type options are not in the DOM + expect(queryByLabelText('Url')).not.toBeInTheDocument(); + expect(queryByLabelText('Proxy URL')).not.toBeInTheDocument(); + expect(queryByLabelText('Host')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.tsx index 0bafef61166d2..d83130b21a0f1 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.tsx @@ -7,27 +7,17 @@ import React, { memo, useMemo } from 'react'; import { PackagePolicyEditExtensionComponentProps } from '../../../../fleet/public'; -import { - PolicyConfig, - ConfigKeys, - ContentType, - DataStream, - ICustomFields, - contentTypesToMode, -} from './types'; +import { PolicyConfig, ConfigKeys, DataStream, ITLSFields, ICustomFields } from './types'; import { SyntheticsPolicyEditExtension } from './synthetics_policy_edit_extension'; import { MonitorTypeContextProvider, HTTPContextProvider, TCPContextProvider, - defaultTCPSimpleFields, - defaultHTTPSimpleFields, - defaultICMPSimpleFields, - defaultHTTPAdvancedFields, - defaultTCPAdvancedFields, - defaultTLSFields, ICMPSimpleFieldsContextProvider, + BrowserContextProvider, + TLSFieldsContextProvider, } from './contexts'; +import { normalizers } from './helpers/normalizers'; /** * Exports Synthetics-specific package policy instructions @@ -35,123 +25,64 @@ import { */ export const SyntheticsPolicyEditExtensionWrapper = memo( ({ policy: currentPolicy, newPolicy, onChange }) => { - const { enableTLS: isTLSEnabled, config: defaultConfig, monitorType } = useMemo(() => { - const fallbackConfig: PolicyConfig = { - [DataStream.HTTP]: { - ...defaultHTTPSimpleFields, - ...defaultHTTPAdvancedFields, - ...defaultTLSFields, - }, - [DataStream.TCP]: { - ...defaultTCPSimpleFields, - ...defaultTCPAdvancedFields, - ...defaultTLSFields, - }, - [DataStream.ICMP]: defaultICMPSimpleFields, - }; + const { + enableTLS: isTLSEnabled, + fullConfig: fullDefaultConfig, + monitorTypeConfig: defaultConfig, + monitorType, + tlsConfig: defaultTLSConfig, + } = useMemo(() => { let enableTLS = false; const getDefaultConfig = () => { + // find the enabled input to identify the current monitor type const currentInput = currentPolicy.inputs.find((input) => input.enabled === true); - const vars = currentInput?.streams[0]?.vars; + /* Inputs can have multiple data streams. This is true of the `synthetics/browser` input, which includes the browser.network and browser.screenshot + * data streams. The `browser.network` and `browser.screenshot` data streams are used to store metadata and mappings. + * However, the `browser` data stream is where the variables for the policy are stored. For this reason, we only want + * to grab the data stream that exists within our explicitly defined list, which is the browser data stream */ + const vars = currentInput?.streams.find((stream) => + Object.values(DataStream).includes(stream.data_stream.dataset as DataStream) + )?.vars; + const type: DataStream = vars?.[ConfigKeys.MONITOR_TYPE].value as DataStream; - const fallbackConfigForMonitorType = fallbackConfig[type] as Partial; - const configKeys: ConfigKeys[] = Object.values(ConfigKeys); - const formatttedDefaultConfigForMonitorType = configKeys.reduce( - (acc: Record, key: ConfigKeys) => { - const value = vars?.[key]?.value; - switch (key) { - case ConfigKeys.NAME: - acc[key] = currentPolicy.name; - break; - case ConfigKeys.SCHEDULE: - // split unit and number - if (value) { - const fullString = JSON.parse(value); - const fullSchedule = fullString.replace('@every ', ''); - const unit = fullSchedule.slice(-1); - const number = fullSchedule.slice(0, fullSchedule.length - 1); - acc[key] = { - unit, - number, - }; - } else { - acc[key] = fallbackConfigForMonitorType[key]; - } - break; - case ConfigKeys.TIMEOUT: - case ConfigKeys.WAIT: - acc[key] = value - ? value.slice(0, value.length - 1) - : fallbackConfigForMonitorType[key]; // remove unit - break; - case ConfigKeys.TAGS: - case ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE: - case ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE: - case ConfigKeys.RESPONSE_STATUS_CHECK: - case ConfigKeys.RESPONSE_HEADERS_CHECK: - case ConfigKeys.REQUEST_HEADERS_CHECK: - acc[key] = value ? JSON.parse(value) : fallbackConfigForMonitorType[key]; - break; - case ConfigKeys.REQUEST_BODY_CHECK: - const headers = value - ? JSON.parse(vars?.[ConfigKeys.REQUEST_HEADERS_CHECK].value) - : fallbackConfigForMonitorType[ConfigKeys.REQUEST_HEADERS_CHECK]; - const requestBodyValue = - value !== null && value !== undefined - ? JSON.parse(value) - : fallbackConfigForMonitorType[key]?.value; - let requestBodyType = fallbackConfigForMonitorType[key]?.type; - Object.keys(headers || []).some((headerKey) => { - if ( - headerKey === 'Content-Type' && - contentTypesToMode[headers[headerKey] as ContentType] - ) { - requestBodyType = contentTypesToMode[headers[headerKey] as ContentType]; - return true; - } - }); - acc[key] = { - value: requestBodyValue, - type: requestBodyType, - }; - break; - case ConfigKeys.TLS_KEY_PASSPHRASE: - case ConfigKeys.TLS_VERIFICATION_MODE: - acc[key] = { - value: value ?? fallbackConfigForMonitorType[key]?.value, - isEnabled: !!value, - }; - if (!!value) { - enableTLS = true; - } - break; - case ConfigKeys.TLS_CERTIFICATE: - case ConfigKeys.TLS_CERTIFICATE_AUTHORITIES: - case ConfigKeys.TLS_KEY: - case ConfigKeys.TLS_VERSION: - acc[key] = { - value: value ? JSON.parse(value) : fallbackConfigForMonitorType[key]?.value, - isEnabled: !!value, - }; - if (!!value) { - enableTLS = true; - } - break; - default: - acc[key] = value ?? fallbackConfigForMonitorType[key]; - } - return acc; + const configKeys: ConfigKeys[] = Object.values(ConfigKeys) || ([] as ConfigKeys[]); + const formattedDefaultConfigForMonitorType: ICustomFields = configKeys.reduce( + (acc: ICustomFields, key: ConfigKeys) => { + return { + ...acc, + [key]: normalizers[key]?.(vars), + }; }, - {} + {} as ICustomFields ); - const formattedDefaultConfig: PolicyConfig = { - ...fallbackConfig, - [type]: formatttedDefaultConfigForMonitorType, + const tlsConfig: ITLSFields = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES], + [ConfigKeys.TLS_CERTIFICATE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_CERTIFICATE], + [ConfigKeys.TLS_KEY]: formattedDefaultConfigForMonitorType[ConfigKeys.TLS_KEY], + [ConfigKeys.TLS_KEY_PASSPHRASE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_KEY_PASSPHRASE], + [ConfigKeys.TLS_VERIFICATION_MODE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_VERIFICATION_MODE], + [ConfigKeys.TLS_VERSION]: formattedDefaultConfigForMonitorType[ConfigKeys.TLS_VERSION], }; - return { config: formattedDefaultConfig, enableTLS, monitorType: type }; + enableTLS = Object.values(tlsConfig).some((value) => value?.isEnabled); + + const formattedDefaultConfig: Partial = { + [type]: formattedDefaultConfigForMonitorType, + }; + + return { + fullConfig: formattedDefaultConfig, + monitorTypeConfig: formattedDefaultConfigForMonitorType, + tlsConfig, + enableTLS, + monitorType: type, + }; }; return getDefaultConfig(); @@ -159,18 +90,22 @@ export const SyntheticsPolicyEditExtensionWrapper = memo - - - - - - - + + + + + + + + + + + ); } diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/tcp/formatters.ts new file mode 100644 index 0000000000000..2f4a43ee6becf --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/formatters.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TCPFields, ConfigKeys } from '../types'; +import { Formatter, commonFormatters } from '../common/formatters'; +import { tlsFormatters } from '../tls/formatters'; + +export type TCPFormatMap = Record; + +export const tcpFormatters: TCPFormatMap = { + [ConfigKeys.HOSTS]: null, + [ConfigKeys.PROXY_URL]: null, + [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: null, + [ConfigKeys.RESPONSE_RECEIVE_CHECK]: null, + [ConfigKeys.REQUEST_SEND_CHECK]: null, + ...tlsFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/tcp/normalizers.ts new file mode 100644 index 0000000000000..d19aea55addf2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/normalizers.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TCPFields, ConfigKeys } from '../types'; +import { Normalizer, commonNormalizers, getNormalizer } from '../common/normalizers'; +import { tlsNormalizers } from '../tls/normalizers'; +import { defaultTCPSimpleFields, defaultTCPAdvancedFields } from '../contexts'; + +const defaultTCPFields = { + ...defaultTCPSimpleFields, + ...defaultTCPAdvancedFields, +}; + +export type TCPNormalizerMap = Record; + +export const getTCPNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, defaultTCPFields); +}; + +export const tcpNormalizers: TCPNormalizerMap = { + [ConfigKeys.HOSTS]: getTCPNormalizer(ConfigKeys.HOSTS), + [ConfigKeys.PROXY_URL]: getTCPNormalizer(ConfigKeys.PROXY_URL), + [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: getTCPNormalizer(ConfigKeys.PROXY_USE_LOCAL_RESOLVER), + [ConfigKeys.RESPONSE_RECEIVE_CHECK]: getTCPNormalizer(ConfigKeys.RESPONSE_RECEIVE_CHECK), + [ConfigKeys.REQUEST_SEND_CHECK]: getTCPNormalizer(ConfigKeys.REQUEST_SEND_CHECK), + ...tlsNormalizers, + ...commonNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx index 82c77a63611f2..8bc017a51cfa9 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx @@ -33,7 +33,7 @@ export const TCPSimpleFields = memo(({ validate }) => { defaultMessage="Host:Port" /> } - isInvalid={!!validate[ConfigKeys.HOSTS]?.(fields[ConfigKeys.HOSTS])} + isInvalid={!!validate[ConfigKeys.HOSTS]?.(fields)} error={ (({ validate }) => { defaultMessage="Monitor interval" /> } - isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields[ConfigKeys.SCHEDULE])} + isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields)} error={ (({ validate }) => { defaultMessage="Timeout in seconds" /> } - isInvalid={ - !!validate[ConfigKeys.TIMEOUT]?.( - fields[ConfigKeys.TIMEOUT], - fields[ConfigKeys.SCHEDULE].number, - fields[ConfigKeys.SCHEDULE].unit - ) - } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} error={ - + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) } helpText={ ; + +export const tlsFormatters: TLSFormatMap = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: (fields) => + tlsValueToYamlFormatter(fields[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]), + [ConfigKeys.TLS_CERTIFICATE]: (fields) => + tlsValueToYamlFormatter(fields[ConfigKeys.TLS_CERTIFICATE]), + [ConfigKeys.TLS_KEY]: (fields) => tlsValueToYamlFormatter(fields[ConfigKeys.TLS_KEY]), + [ConfigKeys.TLS_KEY_PASSPHRASE]: (fields) => + tlsValueToStringFormatter(fields[ConfigKeys.TLS_KEY_PASSPHRASE]), + [ConfigKeys.TLS_VERIFICATION_MODE]: (fields) => + tlsValueToStringFormatter(fields[ConfigKeys.TLS_VERIFICATION_MODE]), + [ConfigKeys.TLS_VERSION]: (fields) => tlsArrayToYamlFormatter(fields[ConfigKeys.TLS_VERSION]), +}; + +// only add tls settings if they are enabled by the user and isEnabled is true +export const tlsValueToYamlFormatter = (tlsValue: { value?: string; isEnabled?: boolean } = {}) => + tlsValue.isEnabled && tlsValue.value ? JSON.stringify(tlsValue.value) : null; + +export const tlsValueToStringFormatter = (tlsValue: { value?: string; isEnabled?: boolean } = {}) => + tlsValue.isEnabled && tlsValue.value ? tlsValue.value : null; + +export const tlsArrayToYamlFormatter = (tlsValue: { value?: string[]; isEnabled?: boolean } = {}) => + tlsValue.isEnabled && tlsValue.value?.length ? JSON.stringify(tlsValue.value) : null; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tls/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/tls/normalizers.ts new file mode 100644 index 0000000000000..2344e599d6c01 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/tls/normalizers.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ITLSFields, ConfigKeys } from '../types'; +import { Normalizer } from '../common/normalizers'; +import { defaultTLSFields } from '../contexts'; + +type TLSNormalizerMap = Record; + +export const tlsNormalizers: TLSNormalizerMap = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: (fields) => + tlsYamlToObjectNormalizer( + fields?.[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]?.value, + ConfigKeys.TLS_CERTIFICATE_AUTHORITIES + ), + [ConfigKeys.TLS_CERTIFICATE]: (fields) => + tlsYamlToObjectNormalizer( + fields?.[ConfigKeys.TLS_CERTIFICATE]?.value, + ConfigKeys.TLS_CERTIFICATE + ), + [ConfigKeys.TLS_KEY]: (fields) => + tlsYamlToObjectNormalizer(fields?.[ConfigKeys.TLS_KEY]?.value, ConfigKeys.TLS_KEY), + [ConfigKeys.TLS_KEY_PASSPHRASE]: (fields) => + tlsStringToObjectNormalizer( + fields?.[ConfigKeys.TLS_KEY_PASSPHRASE]?.value, + ConfigKeys.TLS_KEY_PASSPHRASE + ), + [ConfigKeys.TLS_VERIFICATION_MODE]: (fields) => + tlsStringToObjectNormalizer( + fields?.[ConfigKeys.TLS_VERIFICATION_MODE]?.value, + ConfigKeys.TLS_VERIFICATION_MODE + ), + [ConfigKeys.TLS_VERSION]: (fields) => + tlsYamlToObjectNormalizer(fields?.[ConfigKeys.TLS_VERSION]?.value, ConfigKeys.TLS_VERSION), +}; + +// only add tls settings if they are enabled by the user and isEnabled is true +export const tlsStringToObjectNormalizer = (value: string = '', key: keyof ITLSFields) => ({ + value: value ?? defaultTLSFields[key]?.value, + isEnabled: Boolean(value), +}); +export const tlsYamlToObjectNormalizer = (value: string = '', key: keyof ITLSFields) => ({ + value: value ? JSON.parse(value) : defaultTLSFields[key]?.value, + isEnabled: Boolean(value), +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx index 7a16d1352c40a..89581bf993339 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx @@ -9,6 +9,7 @@ export enum DataStream { HTTP = 'http', TCP = 'tcp', ICMP = 'icmp', + BROWSER = 'browser', } export enum HTTPMethod { @@ -65,6 +66,12 @@ export enum TLSVersion { ONE_THREE = 'TLSv1.3', } +export enum ScreenshotOption { + ON = 'on', + OFF = 'off', + ONLY_ON_FAILURE = 'only-on-failure', +} + // values must match keys in the integration package export enum ConfigKeys { APM_SERVICE_NAME = 'service.name', @@ -87,6 +94,14 @@ export enum ConfigKeys { REQUEST_METHOD_CHECK = 'check.request.method', REQUEST_SEND_CHECK = 'check.send', SCHEDULE = 'schedule', + SCREENSHOTS = 'screenshots', + SOURCE_INLINE = 'source.inline.script', + SOURCE_ZIP_URL = 'source.zip_url.url', + SOURCE_ZIP_USERNAME = 'source.zip_url.username', + SOURCE_ZIP_PASSWORD = 'source.zip_url.password', + SOURCE_ZIP_FOLDER = 'source.zip_url.folder', + SYNTHETICS_ARGS = 'synthetics_args', + PARAMS = 'params', TLS_CERTIFICATE_AUTHORITIES = 'ssl.certificate_authorities', TLS_CERTIFICATE = 'ssl.certificate', TLS_KEY = 'ssl.key', @@ -100,18 +115,6 @@ export enum ConfigKeys { WAIT = 'wait', } -export interface ISimpleFields { - [ConfigKeys.HOSTS]: string; - [ConfigKeys.MAX_REDIRECTS]: string; - [ConfigKeys.MONITOR_TYPE]: DataStream; - [ConfigKeys.SCHEDULE]: { number: string; unit: ScheduleUnit }; - [ConfigKeys.APM_SERVICE_NAME]: string; - [ConfigKeys.TIMEOUT]: string; - [ConfigKeys.URLS]: string; - [ConfigKeys.TAGS]: string[]; - [ConfigKeys.WAIT]: string; -} - export interface ICommonFields { [ConfigKeys.MONITOR_TYPE]: DataStream; [ConfigKeys.SCHEDULE]: { number: string; unit: ScheduleUnit }; @@ -183,13 +186,29 @@ export interface ITCPAdvancedFields { [ConfigKeys.REQUEST_SEND_CHECK]: string; } +export type IBrowserSimpleFields = { + [ConfigKeys.SOURCE_INLINE]: string; + [ConfigKeys.SOURCE_ZIP_URL]: string; + [ConfigKeys.SOURCE_ZIP_FOLDER]: string; + [ConfigKeys.SOURCE_ZIP_USERNAME]: string; + [ConfigKeys.SOURCE_ZIP_PASSWORD]: string; + [ConfigKeys.PARAMS]: string; +} & ICommonFields; + +export interface IBrowserAdvancedFields { + [ConfigKeys.SYNTHETICS_ARGS]: string[]; + [ConfigKeys.SCREENSHOTS]: string; +} + export type HTTPFields = IHTTPSimpleFields & IHTTPAdvancedFields & ITLSFields; export type TCPFields = ITCPSimpleFields & ITCPAdvancedFields & ITLSFields; export type ICMPFields = IICMPSimpleFields; +export type BrowserFields = IBrowserSimpleFields & IBrowserAdvancedFields; export type ICustomFields = HTTPFields & TCPFields & - ICMPFields & { + ICMPFields & + BrowserFields & { [ConfigKeys.NAME]: string; }; @@ -197,9 +216,12 @@ export interface PolicyConfig { [DataStream.HTTP]: HTTPFields; [DataStream.TCP]: TCPFields; [DataStream.ICMP]: ICMPFields; + [DataStream.BROWSER]: BrowserFields; } -export type Validation = Partial boolean>>; +export type Validator = (config: Partial) => boolean; + +export type Validation = Partial>; export const contentTypesToMode = { [ContentType.FORM]: Mode.FORM, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx index 5a62aec90032d..d57a69860311c 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx @@ -6,10 +6,21 @@ */ import { useUpdatePolicy } from './use_update_policy'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; import { NewPackagePolicy } from '../../../../fleet/public'; import { validate } from './validation'; -import { ConfigKeys, DataStream, TLSVersion } from './types'; +import { + ConfigKeys, + DataStream, + TLSVersion, + ICommonFields, + ScheduleUnit, + ICMPFields, + TCPFields, + ITLSFields, + HTTPFields, + BrowserFields, +} from './types'; import { defaultConfig } from './synthetics_policy_create_extension'; describe('useBarChartsHooks', () => { @@ -245,6 +256,63 @@ describe('useBarChartsHooks', () => { }, ], }, + { + type: 'synthetics/browser', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { + type: 'synthetics', + dataset: 'browser', + }, + vars: { + type: { + value: 'browser', + type: 'text', + }, + name: { + value: 'Sample name', + type: 'text', + }, + schedule: { + value: '10s', + type: 'text', + }, + 'source.zip_url.url': { + type: 'text', + }, + 'source.zip_url.username': { + type: 'text', + }, + 'source.zip_url.password': { + type: 'password', + }, + 'source.zip_url.folder': { + type: 'text', + }, + 'source.inline.script': { + type: 'yaml', + }, + 'service.name': { + type: 'text', + }, + screenshots: { + type: 'text', + }, + synthetics_args: { + type: 'yaml', + }, + timeout: { + type: 'text', + }, + tags: { + type: 'yaml', + }, + }, + }, + ], + }, ], package: { name: 'synthetics', @@ -253,84 +321,117 @@ describe('useBarChartsHooks', () => { }, }; - it('handles http data stream', () => { + const defaultCommonFields: Partial = { + [ConfigKeys.APM_SERVICE_NAME]: 'APM Service name', + [ConfigKeys.TAGS]: ['some', 'tags'], + [ConfigKeys.SCHEDULE]: { + number: '5', + unit: ScheduleUnit.MINUTES, + }, + [ConfigKeys.TIMEOUT]: '17', + }; + + const defaultTLSFields: Partial = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: { + isEnabled: true, + value: 'ca', + }, + [ConfigKeys.TLS_CERTIFICATE]: { + isEnabled: true, + value: 'cert', + }, + [ConfigKeys.TLS_KEY]: { + isEnabled: true, + value: 'key', + }, + [ConfigKeys.TLS_KEY_PASSPHRASE]: { + isEnabled: true, + value: 'password', + }, + }; + + it('handles http data stream', async () => { const onChange = jest.fn(); - const { result } = renderHook((props) => useUpdatePolicy(props), { - initialProps: { defaultConfig, newPolicy, onChange, validate, monitorType: DataStream.HTTP }, + const initialProps = { + defaultConfig: defaultConfig[DataStream.HTTP], + config: defaultConfig[DataStream.HTTP], + newPolicy, + onChange, + validate, + monitorType: DataStream.HTTP, + }; + const { result, rerender, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, }); expect(result.current.config).toMatchObject({ ...defaultConfig[DataStream.HTTP] }); + const config: HTTPFields = { + ...defaultConfig[DataStream.HTTP], + ...defaultCommonFields, + ...defaultTLSFields, + [ConfigKeys.URLS]: 'url', + [ConfigKeys.PROXY_URL]: 'proxyUrl', + }; + // expect only http to be enabled expect(result.current.updatedPolicy.inputs[0].enabled).toBe(true); expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(false); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.MONITOR_TYPE].value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.MONITOR_TYPE]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.URLS].value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.URLS]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.SCHEDULE].value - ).toEqual( - JSON.stringify( - `@every ${defaultConfig[DataStream.HTTP][ConfigKeys.SCHEDULE].number}${ - defaultConfig[DataStream.HTTP][ConfigKeys.SCHEDULE].unit - }` - ) - ); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.PROXY_URL].value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.PROXY_URL]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.APM_SERVICE_NAME].value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.APM_SERVICE_NAME]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TIMEOUT].value - ).toEqual(`${defaultConfig[DataStream.HTTP][ConfigKeys.TIMEOUT]}s`); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE - ].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE - ].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_STATUS_CHECK] - .value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.REQUEST_HEADERS_CHECK] - .value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_HEADERS_CHECK] - .value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_BODY_INDEX] - .value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.RESPONSE_BODY_INDEX]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_HEADERS_INDEX] - .value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.RESPONSE_HEADERS_INDEX]); + rerender({ + ...initialProps, + config, + }); + + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[0]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.MONITOR_TYPE].value).toEqual(config[ConfigKeys.MONITOR_TYPE]); + expect(vars?.[ConfigKeys.URLS].value).toEqual(config[ConfigKeys.URLS]); + expect(vars?.[ConfigKeys.SCHEDULE].value).toEqual( + JSON.stringify( + `@every ${config[ConfigKeys.SCHEDULE].number}${config[ConfigKeys.SCHEDULE].unit}` + ) + ); + expect(vars?.[ConfigKeys.PROXY_URL].value).toEqual(config[ConfigKeys.PROXY_URL]); + expect(vars?.[ConfigKeys.APM_SERVICE_NAME].value).toEqual( + config[ConfigKeys.APM_SERVICE_NAME] + ); + expect(vars?.[ConfigKeys.TIMEOUT].value).toEqual(`${config[ConfigKeys.TIMEOUT]}s`); + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_STATUS_CHECK].value).toEqual(null); + expect(vars?.[ConfigKeys.REQUEST_HEADERS_CHECK].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_HEADERS_CHECK].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_BODY_INDEX].value).toEqual( + config[ConfigKeys.RESPONSE_BODY_INDEX] + ); + expect(vars?.[ConfigKeys.RESPONSE_HEADERS_INDEX].value).toEqual( + config[ConfigKeys.RESPONSE_HEADERS_INDEX] + ); + }); }); - it('stringifies array values and returns null for empty array values', () => { + it('stringifies array values and returns null for empty array values', async () => { const onChange = jest.fn(); - const { result } = renderHook((props) => useUpdatePolicy(props), { - initialProps: { defaultConfig, newPolicy, onChange, validate, monitorType: DataStream.HTTP }, + const initialProps = { + defaultConfig: defaultConfig[DataStream.HTTP], + config: defaultConfig[DataStream.HTTP], + newPolicy, + onChange, + validate, + monitorType: DataStream.HTTP, + }; + const { rerender, result, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, }); - act(() => { - result.current.setConfig({ - ...defaultConfig, + rerender({ + ...initialProps, + config: { + ...defaultConfig[DataStream.HTTP], [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: ['test'], [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: ['test'], [ConfigKeys.RESPONSE_STATUS_CHECK]: ['test'], @@ -339,38 +440,29 @@ describe('useBarChartsHooks', () => { value: [TLSVersion.ONE_ONE], isEnabled: true, }, - }); + }, }); - // expect only http to be enabled - expect(result.current.updatedPolicy.inputs[0].enabled).toBe(true); - expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); - expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + await waitFor(() => { + // expect only http to be enabled + expect(result.current.updatedPolicy.inputs[0].enabled).toBe(true); + expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(false); + + const vars = result.current.updatedPolicy.inputs[0]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE].value).toEqual('["test"]'); + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE].value).toEqual('["test"]'); + expect(vars?.[ConfigKeys.RESPONSE_STATUS_CHECK].value).toEqual('["test"]'); + expect(vars?.[ConfigKeys.TAGS].value).toEqual('["test"]'); + expect(vars?.[ConfigKeys.TLS_VERSION].value).toEqual('["TLSv1.1"]'); + }); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE - ].value - ).toEqual('["test"]'); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE - ].value - ).toEqual('["test"]'); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_STATUS_CHECK] - .value - ).toEqual('["test"]'); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TAGS].value - ).toEqual('["test"]'); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TLS_VERSION].value - ).toEqual('["TLSv1.1"]'); - - act(() => { - result.current.setConfig({ - ...defaultConfig, + rerender({ + ...initialProps, + config: { + ...defaultConfig[DataStream.HTTP], [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: [], [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: [], [ConfigKeys.RESPONSE_STATUS_CHECK]: [], @@ -379,125 +471,207 @@ describe('useBarChartsHooks', () => { value: [], isEnabled: true, }, - }); + }, }); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE - ].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE - ].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_STATUS_CHECK] - .value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TAGS].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TLS_VERSION].value - ).toEqual(null); + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[0]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_STATUS_CHECK].value).toEqual(null); + expect(vars?.[ConfigKeys.TAGS].value).toEqual(null); + expect(vars?.[ConfigKeys.TLS_VERSION].value).toEqual(null); + }); }); - it('handles tcp data stream', () => { + it('handles tcp data stream', async () => { const onChange = jest.fn(); - const { result } = renderHook((props) => useUpdatePolicy(props), { - initialProps: { defaultConfig, newPolicy, onChange, validate, monitorType: DataStream.TCP }, + const initialProps = { + defaultConfig: defaultConfig[DataStream.TCP], + config: defaultConfig[DataStream.TCP], + newPolicy, + onChange, + validate, + monitorType: DataStream.TCP, + }; + const { result, rerender, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, }); // expect only tcp to be enabled expect(result.current.updatedPolicy.inputs[0].enabled).toBe(false); expect(result.current.updatedPolicy.inputs[1].enabled).toBe(true); expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(false); + + const config: TCPFields = { + ...defaultConfig[DataStream.TCP], + ...defaultCommonFields, + ...defaultTLSFields, + [ConfigKeys.HOSTS]: 'sampleHost', + [ConfigKeys.PROXY_URL]: 'proxyUrl', + [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: true, + [ConfigKeys.RESPONSE_RECEIVE_CHECK]: 'response', + [ConfigKeys.REQUEST_SEND_CHECK]: 'request', + }; - expect(onChange).toBeCalledWith({ - isValid: false, - updatedPolicy: result.current.updatedPolicy, + rerender({ + ...initialProps, + config, }); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.MONITOR_TYPE].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.MONITOR_TYPE]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.HOSTS].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.HOSTS]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.SCHEDULE].value - ).toEqual( - JSON.stringify( - `@every ${defaultConfig[DataStream.TCP][ConfigKeys.SCHEDULE].number}${ - defaultConfig[DataStream.TCP][ConfigKeys.SCHEDULE].unit - }` - ) - ); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.PROXY_URL].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.PROXY_URL]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.APM_SERVICE_NAME].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.APM_SERVICE_NAME]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.TIMEOUT].value - ).toEqual(`${defaultConfig[DataStream.TCP][ConfigKeys.TIMEOUT]}s`); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ - ConfigKeys.PROXY_USE_LOCAL_RESOLVER - ].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.PROXY_USE_LOCAL_RESOLVER]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_RECEIVE_CHECK] - .value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.RESPONSE_RECEIVE_CHECK]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.REQUEST_SEND_CHECK] - .value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.REQUEST_SEND_CHECK]); + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[1]?.streams[0]?.vars; + + expect(onChange).toBeCalledWith({ + isValid: false, + updatedPolicy: result.current.updatedPolicy, + }); + + expect(vars?.[ConfigKeys.MONITOR_TYPE].value).toEqual(config[ConfigKeys.MONITOR_TYPE]); + expect(vars?.[ConfigKeys.HOSTS].value).toEqual(config[ConfigKeys.HOSTS]); + expect(vars?.[ConfigKeys.SCHEDULE].value).toEqual( + JSON.stringify( + `@every ${config[ConfigKeys.SCHEDULE].number}${config[ConfigKeys.SCHEDULE].unit}` + ) + ); + expect(vars?.[ConfigKeys.PROXY_URL].value).toEqual(config[ConfigKeys.PROXY_URL]); + expect(vars?.[ConfigKeys.APM_SERVICE_NAME].value).toEqual( + config[ConfigKeys.APM_SERVICE_NAME] + ); + expect(vars?.[ConfigKeys.TIMEOUT].value).toEqual(`${config[ConfigKeys.TIMEOUT]}s`); + expect(vars?.[ConfigKeys.PROXY_USE_LOCAL_RESOLVER].value).toEqual( + config[ConfigKeys.PROXY_USE_LOCAL_RESOLVER] + ); + expect(vars?.[ConfigKeys.RESPONSE_RECEIVE_CHECK].value).toEqual( + config[ConfigKeys.RESPONSE_RECEIVE_CHECK] + ); + expect(vars?.[ConfigKeys.REQUEST_SEND_CHECK].value).toEqual( + config[ConfigKeys.REQUEST_SEND_CHECK] + ); + }); }); - it('handles icmp data stream', () => { + it('handles icmp data stream', async () => { const onChange = jest.fn(); - const { result } = renderHook((props) => useUpdatePolicy(props), { - initialProps: { defaultConfig, newPolicy, onChange, validate, monitorType: DataStream.ICMP }, + const initialProps = { + defaultConfig: defaultConfig[DataStream.ICMP], + config: defaultConfig[DataStream.ICMP], + newPolicy, + onChange, + validate, + monitorType: DataStream.ICMP, + }; + const { rerender, result, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, }); + const config: ICMPFields = { + ...defaultConfig[DataStream.ICMP], + ...defaultCommonFields, + [ConfigKeys.WAIT]: '2', + [ConfigKeys.HOSTS]: 'sampleHost', + }; // expect only icmp to be enabled expect(result.current.updatedPolicy.inputs[0].enabled).toBe(false); expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); expect(result.current.updatedPolicy.inputs[2].enabled).toBe(true); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(false); + + // only call onChange when the policy is changed + rerender({ + ...initialProps, + config, + }); + + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[2]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.MONITOR_TYPE].value).toEqual(config[ConfigKeys.MONITOR_TYPE]); + expect(vars?.[ConfigKeys.HOSTS].value).toEqual(config[ConfigKeys.HOSTS]); + expect(vars?.[ConfigKeys.SCHEDULE].value).toEqual( + JSON.stringify( + `@every ${config[ConfigKeys.SCHEDULE].number}${config[ConfigKeys.SCHEDULE].unit}` + ) + ); + expect(vars?.[ConfigKeys.APM_SERVICE_NAME].value).toEqual( + config[ConfigKeys.APM_SERVICE_NAME] + ); + expect(vars?.[ConfigKeys.TIMEOUT].value).toEqual(`${config[ConfigKeys.TIMEOUT]}s`); + expect(vars?.[ConfigKeys.WAIT].value).toEqual(`${config[ConfigKeys.WAIT]}s`); + + expect(onChange).toBeCalledWith({ + isValid: false, + updatedPolicy: result.current.updatedPolicy, + }); + }); + }); - expect(onChange).toBeCalledWith({ - isValid: false, - updatedPolicy: result.current.updatedPolicy, + it('handles browser data stream', async () => { + const onChange = jest.fn(); + const initialProps = { + defaultConfig: defaultConfig[DataStream.BROWSER], + config: defaultConfig[DataStream.BROWSER], + newPolicy, + onChange, + validate, + monitorType: DataStream.BROWSER, + }; + const { result, rerender, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, + }); + + // expect only browser to be enabled + expect(result.current.updatedPolicy.inputs[0].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(true); + + const config: BrowserFields = { + ...defaultConfig[DataStream.BROWSER], + ...defaultCommonFields, + [ConfigKeys.SOURCE_INLINE]: 'inlineScript', + [ConfigKeys.SOURCE_ZIP_URL]: 'zipFolder', + [ConfigKeys.SOURCE_ZIP_FOLDER]: 'zipFolder', + [ConfigKeys.SOURCE_ZIP_USERNAME]: 'username', + [ConfigKeys.SOURCE_ZIP_PASSWORD]: 'password', + [ConfigKeys.SCREENSHOTS]: 'off', + [ConfigKeys.SYNTHETICS_ARGS]: ['args'], + }; + + rerender({ + ...initialProps, + config, }); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.MONITOR_TYPE].value - ).toEqual(defaultConfig[DataStream.ICMP][ConfigKeys.MONITOR_TYPE]); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.HOSTS].value - ).toEqual(defaultConfig[DataStream.ICMP][ConfigKeys.HOSTS]); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.SCHEDULE].value - ).toEqual( - JSON.stringify( - `@every ${defaultConfig[DataStream.ICMP][ConfigKeys.SCHEDULE].number}${ - defaultConfig[DataStream.ICMP][ConfigKeys.SCHEDULE].unit - }` - ) - ); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.APM_SERVICE_NAME].value - ).toEqual(defaultConfig[DataStream.ICMP][ConfigKeys.APM_SERVICE_NAME]); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.TIMEOUT].value - ).toEqual(`${defaultConfig[DataStream.ICMP][ConfigKeys.TIMEOUT]}s`); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.WAIT].value - ).toEqual(`${defaultConfig[DataStream.ICMP][ConfigKeys.WAIT]}s`); + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[3]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.SOURCE_ZIP_FOLDER].value).toEqual( + config[ConfigKeys.SOURCE_ZIP_FOLDER] + ); + expect(vars?.[ConfigKeys.SOURCE_ZIP_PASSWORD].value).toEqual( + config[ConfigKeys.SOURCE_ZIP_PASSWORD] + ); + expect(vars?.[ConfigKeys.SOURCE_ZIP_URL].value).toEqual(config[ConfigKeys.SOURCE_ZIP_URL]); + expect(vars?.[ConfigKeys.SOURCE_INLINE].value).toEqual(config[ConfigKeys.SOURCE_INLINE]); + expect(vars?.[ConfigKeys.SOURCE_ZIP_PASSWORD].value).toEqual( + config[ConfigKeys.SOURCE_ZIP_PASSWORD] + ); + expect(vars?.[ConfigKeys.SCREENSHOTS].value).toEqual(config[ConfigKeys.SCREENSHOTS]); + expect(vars?.[ConfigKeys.SYNTHETICS_ARGS].value).toEqual( + JSON.stringify(config[ConfigKeys.SYNTHETICS_ARGS]) + ); + expect(vars?.[ConfigKeys.APM_SERVICE_NAME].value).toEqual( + config[ConfigKeys.APM_SERVICE_NAME] + ); + expect(vars?.[ConfigKeys.TIMEOUT].value).toEqual(`${config[ConfigKeys.TIMEOUT]}s`); + + expect(onChange).toBeCalledWith({ + isValid: false, + updatedPolicy: result.current.updatedPolicy, + }); + }); }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts index 2b2fb22866463..145a86c6bd50d 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts @@ -6,11 +6,13 @@ */ import { useEffect, useRef, useState } from 'react'; import { NewPackagePolicy } from '../../../../fleet/public'; -import { ConfigKeys, PolicyConfig, DataStream, Validation, ICustomFields } from './types'; +import { ConfigKeys, DataStream, Validation, ICustomFields } from './types'; +import { formatters } from './helpers/formatters'; interface Props { monitorType: DataStream; - defaultConfig: PolicyConfig; + defaultConfig: Partial; + config: Partial; newPolicy: NewPackagePolicy; onChange: (opts: { /** is current form state is valid */ @@ -24,85 +26,45 @@ interface Props { export const useUpdatePolicy = ({ monitorType, defaultConfig, + config, newPolicy, onChange, validate, }: Props) => { const [updatedPolicy, setUpdatedPolicy] = useState(newPolicy); // Update the integration policy with our custom fields - const [config, setConfig] = useState>(defaultConfig[monitorType]); - const currentConfig = useRef>(defaultConfig[monitorType]); + const currentConfig = useRef>(defaultConfig); useEffect(() => { const configKeys = Object.keys(config) as ConfigKeys[]; const validationKeys = Object.keys(validate[monitorType]) as ConfigKeys[]; const configDidUpdate = configKeys.some((key) => config[key] !== currentConfig.current[key]); const isValid = - !!newPolicy.name && !validationKeys.find((key) => validate[monitorType][key]?.(config[key])); + !!newPolicy.name && !validationKeys.find((key) => validate[monitorType]?.[key]?.(config)); const formattedPolicy = { ...newPolicy }; const currentInput = formattedPolicy.inputs.find( (input) => input.type === `synthetics/${monitorType}` ); - const dataStream = currentInput?.streams[0]; - - // prevent an infinite loop of updating the policy - if (currentInput && dataStream && configDidUpdate) { + const dataStream = currentInput?.streams.find( + (stream) => stream.data_stream.dataset === monitorType + ); + formattedPolicy.inputs.forEach((input) => (input.enabled = false)); + if (currentInput && dataStream) { // reset all data streams to enabled false formattedPolicy.inputs.forEach((input) => (input.enabled = false)); // enable only the input type and data stream that matches the monitor type. currentInput.enabled = true; dataStream.enabled = true; + } + + // prevent an infinite loop of updating the policy + if (currentInput && dataStream && configDidUpdate) { configKeys.forEach((key) => { const configItem = dataStream.vars?.[key]; - if (configItem) { - switch (key) { - case ConfigKeys.SCHEDULE: - configItem.value = JSON.stringify( - `@every ${config[key]?.number}${config[key]?.unit}` - ); // convert to cron - break; - case ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE: - case ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE: - case ConfigKeys.RESPONSE_STATUS_CHECK: - case ConfigKeys.TAGS: - configItem.value = config[key]?.length ? JSON.stringify(config[key]) : null; - break; - case ConfigKeys.RESPONSE_HEADERS_CHECK: - case ConfigKeys.REQUEST_HEADERS_CHECK: - configItem.value = Object.keys(config?.[key] || []).length - ? JSON.stringify(config[key]) - : null; - break; - case ConfigKeys.TIMEOUT: - case ConfigKeys.WAIT: - configItem.value = config[key] ? `${config[key]}s` : null; // convert to cron - break; - case ConfigKeys.REQUEST_BODY_CHECK: - configItem.value = config[key]?.value ? JSON.stringify(config[key]?.value) : null; // only need value of REQUEST_BODY_CHECK for outputted policy - break; - case ConfigKeys.TLS_CERTIFICATE: - case ConfigKeys.TLS_CERTIFICATE_AUTHORITIES: - case ConfigKeys.TLS_KEY: - configItem.value = - config[key]?.isEnabled && config[key]?.value - ? JSON.stringify(config[key]?.value) - : null; // only add tls settings if they are enabled by the user - break; - case ConfigKeys.TLS_VERSION: - configItem.value = - config[key]?.isEnabled && config[key]?.value.length - ? JSON.stringify(config[key]?.value) - : null; // only add tls settings if they are enabled by the user - break; - case ConfigKeys.TLS_KEY_PASSPHRASE: - case ConfigKeys.TLS_VERIFICATION_MODE: - configItem.value = - config[key]?.isEnabled && config[key]?.value ? config[key]?.value : null; // only add tls settings if they are enabled by the user - break; - default: - configItem.value = - config[key] === undefined || config[key] === null ? null : config[key]; - } + if (configItem && formatters[key]) { + configItem.value = formatters[key]?.(config); + } else if (configItem) { + configItem.value = config[key] === undefined || config[key] === null ? null : config[key]; } }); currentConfig.current = config; @@ -114,14 +76,8 @@ export const useUpdatePolicy = ({ } }, [config, currentConfig, newPolicy, onChange, validate, monitorType]); - // update our local config state ever time name, which is managed by fleet, changes - useEffect(() => { - setConfig((prevConfig) => ({ ...prevConfig, name: newPolicy.name })); - }, [newPolicy.name, setConfig]); - return { config, - setConfig, updatedPolicy, }; }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/validation.tsx b/x-pack/plugins/uptime/public/components/fleet_package/validation.tsx index f3057baf10381..0ce5dc6f9f02d 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/validation.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/validation.tsx @@ -4,11 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ConfigKeys, DataStream, ICustomFields, Validation, ScheduleUnit } from './types'; +import { + ConfigKeys, + DataStream, + ICustomFields, + Validator, + Validation, + ScheduleUnit, +} from './types'; export const digitsOnly = /^[0-9]*$/g; export const includesValidPort = /[^\:]+:[0-9]{1,5}$/g; +type ValidationLibrary = Record; + // returns true if invalid function validateHeaders(headers: T): boolean { return Object.keys(headers).some((key) => { @@ -22,7 +31,7 @@ function validateHeaders(headers: T): boolean { } // returns true if invalid -function validateTimeout({ +const validateTimeout = ({ scheduleNumber, scheduleUnit, timeout, @@ -30,7 +39,7 @@ function validateTimeout({ scheduleNumber: string; scheduleUnit: ScheduleUnit; timeout: string; -}): boolean { +}): boolean => { let schedule: number; switch (scheduleUnit) { case ScheduleUnit.SECONDS: @@ -44,69 +53,83 @@ function validateTimeout({ } return parseFloat(timeout) > schedule; -} +}; // validation functions return true when invalid -const validateCommon = { - [ConfigKeys.SCHEDULE]: (value: unknown) => { +const validateCommon: ValidationLibrary = { + [ConfigKeys.SCHEDULE]: ({ [ConfigKeys.SCHEDULE]: value }) => { const { number, unit } = value as ICustomFields[ConfigKeys.SCHEDULE]; const parsedFloat = parseFloat(number); return !parsedFloat || !unit || parsedFloat < 1; }, - [ConfigKeys.TIMEOUT]: ( - timeoutValue: unknown, - scheduleNumber: string, - scheduleUnit: ScheduleUnit - ) => - !timeoutValue || - parseFloat(timeoutValue as ICustomFields[ConfigKeys.TIMEOUT]) < 0 || - validateTimeout({ - timeout: timeoutValue as ICustomFields[ConfigKeys.TIMEOUT], - scheduleNumber, - scheduleUnit, - }), + [ConfigKeys.TIMEOUT]: ({ [ConfigKeys.TIMEOUT]: timeout, [ConfigKeys.SCHEDULE]: schedule }) => { + const { number, unit } = schedule as ICustomFields[ConfigKeys.SCHEDULE]; + + return ( + !timeout || + parseFloat(timeout) < 0 || + validateTimeout({ + timeout, + scheduleNumber: number, + scheduleUnit: unit, + }) + ); + }, }; -const validateHTTP = { - [ConfigKeys.RESPONSE_STATUS_CHECK]: (value: unknown) => { +const validateHTTP: ValidationLibrary = { + [ConfigKeys.RESPONSE_STATUS_CHECK]: ({ [ConfigKeys.RESPONSE_STATUS_CHECK]: value }) => { const statusCodes = value as ICustomFields[ConfigKeys.RESPONSE_STATUS_CHECK]; return statusCodes.length ? statusCodes.some((code) => !`${code}`.match(digitsOnly)) : false; }, - [ConfigKeys.RESPONSE_HEADERS_CHECK]: (value: unknown) => { + [ConfigKeys.RESPONSE_HEADERS_CHECK]: ({ [ConfigKeys.RESPONSE_HEADERS_CHECK]: value }) => { const headers = value as ICustomFields[ConfigKeys.RESPONSE_HEADERS_CHECK]; return validateHeaders(headers); }, - [ConfigKeys.REQUEST_HEADERS_CHECK]: (value: unknown) => { + [ConfigKeys.REQUEST_HEADERS_CHECK]: ({ [ConfigKeys.REQUEST_HEADERS_CHECK]: value }) => { const headers = value as ICustomFields[ConfigKeys.REQUEST_HEADERS_CHECK]; return validateHeaders(headers); }, - [ConfigKeys.MAX_REDIRECTS]: (value: unknown) => + [ConfigKeys.MAX_REDIRECTS]: ({ [ConfigKeys.MAX_REDIRECTS]: value }) => (!!value && !`${value}`.match(digitsOnly)) || parseFloat(value as ICustomFields[ConfigKeys.MAX_REDIRECTS]) < 0, - [ConfigKeys.URLS]: (value: unknown) => !value, + [ConfigKeys.URLS]: ({ [ConfigKeys.URLS]: value }) => !value, ...validateCommon, }; -const validateTCP = { - [ConfigKeys.HOSTS]: (value: unknown) => { +const validateTCP: Record = { + [ConfigKeys.HOSTS]: ({ [ConfigKeys.HOSTS]: value }) => { return !value || !`${value}`.match(includesValidPort); }, ...validateCommon, }; -const validateICMP = { - [ConfigKeys.HOSTS]: (value: unknown) => !value, - [ConfigKeys.WAIT]: (value: unknown) => +const validateICMP: ValidationLibrary = { + [ConfigKeys.HOSTS]: ({ [ConfigKeys.HOSTS]: value }) => !value, + [ConfigKeys.WAIT]: ({ [ConfigKeys.WAIT]: value }) => !!value && !digitsOnly.test(`${value}`) && parseFloat(value as ICustomFields[ConfigKeys.WAIT]) < 0, ...validateCommon, }; +const validateBrowser: ValidationLibrary = { + ...validateCommon, + [ConfigKeys.SOURCE_ZIP_URL]: ({ + [ConfigKeys.SOURCE_ZIP_URL]: zipUrl, + [ConfigKeys.SOURCE_INLINE]: inlineScript, + }) => !zipUrl && !inlineScript, + [ConfigKeys.SOURCE_INLINE]: ({ + [ConfigKeys.SOURCE_ZIP_URL]: zipUrl, + [ConfigKeys.SOURCE_INLINE]: inlineScript, + }) => !zipUrl && !inlineScript, +}; + export type ValidateDictionary = Record; export const validate: ValidateDictionary = { [DataStream.HTTP]: validateHTTP, [DataStream.TCP]: validateTCP, [DataStream.ICMP]: validateICMP, + [DataStream.BROWSER]: validateBrowser, }; From 0640a11feb1f92237c3ce4ba3f82b6fa924ed472 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 17 Aug 2021 16:54:36 +0300 Subject: [PATCH 02/11] Import EuiCodeEditor from kibana (#108846) --- src/plugins/advanced_settings/kibana.json | 2 +- .../public/management_app/components/field/field.tsx | 3 +-- src/plugins/advanced_settings/tsconfig.json | 1 + src/plugins/vis_type_vega/kibana.json | 2 +- .../vis_type_vega/public/components/vega_vis_editor.tsx | 2 +- src/plugins/vis_type_vega/tsconfig.json | 1 + 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index e524d78a53e80..cf00241ee2766 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["management"], "optionalPlugins": ["home", "usageCollection"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "home"], + "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "esUiShared"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 759e1f992808f..745452a31ff9c 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -8,7 +8,6 @@ import React, { PureComponent, Fragment } from 'react'; import classNames from 'classnames'; - import 'brace/theme/textmate'; import 'brace/mode/markdown'; import 'brace/mode/json'; @@ -19,7 +18,6 @@ import { EuiCodeBlock, EuiColorPicker, EuiScreenReaderOnly, - EuiCodeEditor, EuiDescribedFormGroup, EuiFieldNumber, EuiFieldText, @@ -40,6 +38,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { FieldSetting, FieldState } from '../../types'; import { isDefaultValue } from '../../lib'; import { UiSettingsType, DocLinksStart, ToastsStart } from '../../../../../../core/public'; +import { EuiCodeEditor } from '../../../../../es_ui_shared/public'; interface FieldProps { setting: FieldSetting; diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index b207f600cbd4e..5bf4ce3d6248b 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -16,5 +16,6 @@ { "path": "../home/tsconfig.json" }, { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, ] } diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index 407e20fe0688a..a579e85c0caf2 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["data", "visualizations", "mapsEms", "expressions", "inspector"], "optionalPlugins": ["home","usageCollection"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor", "esUiShared"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx index 148c630ad94e5..9150b31343799 100644 --- a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx @@ -7,13 +7,13 @@ */ import React, { useCallback } from 'react'; -import { EuiCodeEditor } from '@elastic/eui'; import compactStringify from 'json-stringify-pretty-compact'; import hjson from 'hjson'; import 'brace/mode/hjson'; import { i18n } from '@kbn/i18n'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { EuiCodeEditor } from '../../../es_ui_shared/public'; import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index e1b8b5d9d4bac..62bdd0262b4a5 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -26,5 +26,6 @@ { "path": "../kibana_utils/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, { "path": "../vis_default_editor/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, ] } From 9258ba5147026fb27786a5b9c1b60858659032bd Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Tue, 17 Aug 2021 10:21:06 -0400 Subject: [PATCH 03/11] Adding owners to kibana plugins (#108407) * Adding owners to kibana plugins * Fix ui actions enhanced owner * Account for virtual RAC team owning a plugin * Fix empty githubTeam for ui actions --- .github/CODEOWNERS | 5 ++++ examples/bfetch_explorer/kibana.json | 4 +++ examples/developer_examples/kibana.json | 4 +++ examples/expressions_explorer/kibana.json | 4 +++ src/plugins/console/kibana.json | 4 +++ src/plugins/dev_tools/kibana.json | 4 +++ src/plugins/embeddable/kibana.json | 19 +++++-------- src/plugins/es_ui_shared/kibana.json | 8 +++--- src/plugins/expressions/kibana.json | 9 ++++--- src/plugins/inspector/kibana.json | 4 +++ src/plugins/kibana_react/kibana.json | 4 +++ src/plugins/kibana_utils/kibana.json | 10 +++---- src/plugins/maps_ems/kibana.json | 4 +++ src/plugins/maps_legacy/kibana.json | 4 +++ src/plugins/navigation/kibana.json | 4 +++ src/plugins/region_map/kibana.json | 10 +++---- src/plugins/screenshot_mode/kibana.json | 4 +++ src/plugins/share/kibana.json | 4 +++ src/plugins/tile_map/kibana.json | 10 +++---- src/plugins/ui_actions/kibana.json | 9 ++++--- src/plugins/url_forwarding/kibana.json | 4 +++ x-pack/examples/alerting_example/kibana.json | 15 ++++++++++- .../ui_actions_enhanced_examples/kibana.json | 11 ++++---- x-pack/plugins/actions/kibana.json | 4 +++ x-pack/plugins/alerting/kibana.json | 4 +++ .../cross_cluster_replication/kibana.json | 14 +++++----- x-pack/plugins/data_enhanced/kibana.json | 4 +++ .../drilldowns/url_drilldown/kibana.json | 4 +++ .../plugins/embeddable_enhanced/kibana.json | 4 +++ x-pack/plugins/event_log/kibana.json | 4 +++ x-pack/plugins/grokdebugger/kibana.json | 15 +++++------ .../index_lifecycle_management/kibana.json | 25 +++++------------ x-pack/plugins/index_management/kibana.json | 10 +++---- x-pack/plugins/ingest_pipelines/kibana.json | 4 +++ x-pack/plugins/license_api_guard/kibana.json | 4 +++ x-pack/plugins/license_management/kibana.json | 10 +++---- x-pack/plugins/lists/kibana.json | 4 +++ x-pack/plugins/logstash/kibana.json | 16 +++++------ x-pack/plugins/maps/kibana.json | 27 ++++++------------- x-pack/plugins/metrics_entities/kibana.json | 4 +++ x-pack/plugins/monitoring/kibana.json | 4 +++ x-pack/plugins/osquery/kibana.json | 27 ++++++------------- x-pack/plugins/painless_lab/kibana.json | 19 +++++-------- x-pack/plugins/remote_clusters/kibana.json | 26 ++++++------------ x-pack/plugins/rollup/kibana.json | 25 +++++------------ x-pack/plugins/rule_registry/kibana.json | 11 ++------ x-pack/plugins/runtime_fields/kibana.json | 15 +++++------ x-pack/plugins/searchprofiler/kibana.json | 4 +++ x-pack/plugins/security_solution/kibana.json | 4 +++ x-pack/plugins/snapshot_restore/kibana.json | 23 +++++----------- x-pack/plugins/stack_alerts/kibana.json | 13 ++++++++- x-pack/plugins/task_manager/kibana.json | 4 +++ x-pack/plugins/timelines/kibana.json | 4 +++ .../plugins/triggers_actions_ui/kibana.json | 4 +++ .../plugins/ui_actions_enhanced/kibana.json | 16 +++++------ x-pack/plugins/upgrade_assistant/kibana.json | 4 +++ x-pack/plugins/watcher/kibana.json | 10 +++---- x-pack/plugins/xpack_legacy/kibana.json | 8 +++--- 58 files changed, 298 insertions(+), 238 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7ee84b6bc9e8d..d116b1d3a41fc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,6 +9,9 @@ /dev_docs @elastic/kibana-tech-leads /packages/kbn-docs-utils/ @elastic/kibana-tech-leads @elastic/kibana-operations +# Virtual teams +/x-pack/plugins/rule_registry/ @elastic/rac + # App /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app @@ -31,6 +34,7 @@ /src/plugins/vis_type_pie/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/visualizations/ @elastic/kibana-app +/src/plugins/url_forwarding/ @elastic/kibana-app /packages/kbn-tinymath/ @elastic/kibana-app # Application Services @@ -369,6 +373,7 @@ /x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/security-solution /x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution /x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/plugins/metrics_entities/ @elastic/security-solution /x-pack/test/detection_engine_api_integration @elastic/security-solution /x-pack/test/lists_api_integration @elastic/security-solution /x-pack/test/api_integration/apis/security_solution @elastic/security-solution diff --git a/examples/bfetch_explorer/kibana.json b/examples/bfetch_explorer/kibana.json index 4bd4492611812..0eda11670034c 100644 --- a/examples/bfetch_explorer/kibana.json +++ b/examples/bfetch_explorer/kibana.json @@ -4,6 +4,10 @@ "version": "0.0.1", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["bfetch", "developerExamples"], "optionalPlugins": [], "requiredBundles": ["kibanaReact"] diff --git a/examples/developer_examples/kibana.json b/examples/developer_examples/kibana.json index 9e6b54c7af67c..a744b53137dc7 100644 --- a/examples/developer_examples/kibana.json +++ b/examples/developer_examples/kibana.json @@ -1,5 +1,9 @@ { "id": "developerExamples", + "owner": { + "name": "Kibana Core", + "githubTeam": "kibana-core" + }, "kibanaVersion": "kibana", "version": "0.0.1", "ui": true diff --git a/examples/expressions_explorer/kibana.json b/examples/expressions_explorer/kibana.json index 7e2062ff0a588..770ce91143d99 100644 --- a/examples/expressions_explorer/kibana.json +++ b/examples/expressions_explorer/kibana.json @@ -4,6 +4,10 @@ "version": "0.0.1", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["expressions", "inspector", "uiActions", "developerExamples"], "optionalPlugins": [], "requiredBundles": [] diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json index ca43e4f258add..9452f43647a19 100644 --- a/src/plugins/console/kibana.json +++ b/src/plugins/console/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["devTools"], "optionalPlugins": ["usageCollection", "home"], "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"] diff --git a/src/plugins/dev_tools/kibana.json b/src/plugins/dev_tools/kibana.json index f1c6c9ecf87e6..75a1e82f1d910 100644 --- a/src/plugins/dev_tools/kibana.json +++ b/src/plugins/dev_tools/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["urlForwarding"] } diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 1ecf76dbbd5c2..42dc716fe64e9 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -3,16 +3,11 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "inspector", - "uiActions" - ], - "extraPublicDirs": [ - "public/lib/test_samples" - ], - "requiredBundles": [ - "savedObjects", - "kibanaReact", - "kibanaUtils" - ] + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredPlugins": ["inspector", "uiActions"], + "extraPublicDirs": ["public/lib/test_samples"], + "requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json index d442bfb93d5af..2735b153f738c 100644 --- a/src/plugins/es_ui_shared/kibana.json +++ b/src/plugins/es_ui_shared/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "ui": true, "server": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "extraPublicDirs": [ "static/validators/string", "static/forms/hook_form_lib", @@ -10,7 +14,5 @@ "static/forms/components", "static/forms/helpers/field_validators/types" ], - "requiredBundles": [ - "data" - ] + "requiredBundles": ["data"] } diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json index 23c7fe722fdb3..46e6ef8b4ea75 100644 --- a/src/plugins/expressions/kibana.json +++ b/src/plugins/expressions/kibana.json @@ -3,9 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common", "common/fonts"], - "requiredBundles": [ - "kibanaUtils", - "inspector" - ] + "requiredBundles": ["kibanaUtils", "inspector"] } diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json index 90e5d60250728..66c6617924a7e 100644 --- a/src/plugins/inspector/kibana.json +++ b/src/plugins/inspector/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common", "common/adapters/request"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/kibana_react/kibana.json b/src/plugins/kibana_react/kibana.json index 6bf7ff1d82070..210b15897cfad 100644 --- a/src/plugins/kibana_react/kibana.json +++ b/src/plugins/kibana_react/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "ui": true, "server": false, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common"] } diff --git a/src/plugins/kibana_utils/kibana.json b/src/plugins/kibana_utils/kibana.json index 3e20b68bca431..7c0f73a160970 100644 --- a/src/plugins/kibana_utils/kibana.json +++ b/src/plugins/kibana_utils/kibana.json @@ -3,9 +3,9 @@ "version": "kibana", "ui": true, "server": false, - "extraPublicDirs": [ - "common", - "demos/state_containers/todomvc", - "common/state_containers" - ] + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "extraPublicDirs": ["common", "demos/state_containers/todomvc", "common/state_containers"] } diff --git a/src/plugins/maps_ems/kibana.json b/src/plugins/maps_ems/kibana.json index a7cf580becfd5..0807867e9dcb3 100644 --- a/src/plugins/maps_ems/kibana.json +++ b/src/plugins/maps_ems/kibana.json @@ -1,5 +1,9 @@ { "id": "mapsEms", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["map"], diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index f321274791a3b..fde5ad7b7adf5 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -1,5 +1,9 @@ { "id": "mapsLegacy", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "ui": true, diff --git a/src/plugins/navigation/kibana.json b/src/plugins/navigation/kibana.json index 85d2049a34be0..aa1294847cef8 100644 --- a/src/plugins/navigation/kibana.json +++ b/src/plugins/navigation/kibana.json @@ -1,5 +1,9 @@ { "id": "navigation", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "version": "kibana", "server": false, "ui": true, diff --git a/src/plugins/region_map/kibana.json b/src/plugins/region_map/kibana.json index 18ae8ec7eec8c..1a24b6f8f05e8 100644 --- a/src/plugins/region_map/kibana.json +++ b/src/plugins/region_map/kibana.json @@ -1,5 +1,9 @@ { "id": "regionMap", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "ui": true, @@ -13,9 +17,5 @@ "data", "share" ], - "requiredBundles": [ - "kibanaUtils", - "charts", - "visDefaultEditor" - ] + "requiredBundles": ["kibanaUtils", "charts", "visDefaultEditor"] } diff --git a/src/plugins/screenshot_mode/kibana.json b/src/plugins/screenshot_mode/kibana.json index 67c40b20be525..98942569dfac8 100644 --- a/src/plugins/screenshot_mode/kibana.json +++ b/src/plugins/screenshot_mode/kibana.json @@ -1,5 +1,9 @@ { "id": "screenshotMode", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "version": "1.0.0", "kibanaVersion": "kibana", "ui": true, diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index 8b1d28b1606d4..5580b723a095a 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredBundles": ["kibanaUtils"], "optionalPlugins": ["securityOss"] } diff --git a/src/plugins/tile_map/kibana.json b/src/plugins/tile_map/kibana.json index 16be04b5189de..48ed613b72cd3 100644 --- a/src/plugins/tile_map/kibana.json +++ b/src/plugins/tile_map/kibana.json @@ -1,5 +1,9 @@ { "id": "tileMap", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "ui": true, @@ -13,9 +17,5 @@ "data", "share" ], - "requiredBundles": [ - "kibanaUtils", - "charts", - "visDefaultEditor" - ] + "requiredBundles": ["kibanaUtils", "charts", "visDefaultEditor"] } diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json index ca979aa021026..d112f6310a1fe 100644 --- a/src/plugins/ui_actions/kibana.json +++ b/src/plugins/ui_actions/kibana.json @@ -3,8 +3,9 @@ "version": "kibana", "server": false, "ui": true, - "requiredBundles": [ - "kibanaUtils", - "kibanaReact" - ] + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/url_forwarding/kibana.json b/src/plugins/url_forwarding/kibana.json index 4f534c1219b34..253466631f2e2 100644 --- a/src/plugins/url_forwarding/kibana.json +++ b/src/plugins/url_forwarding/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + }, "requiredPlugins": ["kibanaLegacy"] } diff --git a/x-pack/examples/alerting_example/kibana.json b/x-pack/examples/alerting_example/kibana.json index f2950db96ba2c..13117713a9a7e 100644 --- a/x-pack/examples/alerting_example/kibana.json +++ b/x-pack/examples/alerting_example/kibana.json @@ -2,9 +2,22 @@ "id": "alertingExample", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "server": true, "ui": true, - "requiredPlugins": ["triggersActionsUi", "charts", "data", "alerting", "actions", "kibanaReact", "features", "developerExamples"], + "requiredPlugins": [ + "triggersActionsUi", + "charts", + "data", + "alerting", + "actions", + "kibanaReact", + "features", + "developerExamples" + ], "optionalPlugins": [], "requiredBundles": ["kibanaReact"] } diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json index 59a0926118962..4fa62668dd557 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -5,6 +5,10 @@ "configPath": ["ui_actions_enhanced_examples"], "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": [ "uiActions", "uiActionsEnhanced", @@ -15,10 +19,5 @@ "developerExamples" ], "optionalPlugins": [], - "requiredBundles": [ - "dashboardEnhanced", - "embeddable", - "kibanaUtils", - "kibanaReact" - ] + "requiredBundles": ["dashboardEnhanced", "embeddable", "kibanaUtils", "kibanaReact"] } diff --git a/x-pack/plugins/actions/kibana.json b/x-pack/plugins/actions/kibana.json index ef604a9cf6417..aa3a9f3f6c34c 100644 --- a/x-pack/plugins/actions/kibana.json +++ b/x-pack/plugins/actions/kibana.json @@ -1,5 +1,9 @@ { "id": "actions", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "server": true, "version": "8.0.0", "kibanaVersion": "kibana", diff --git a/x-pack/plugins/alerting/kibana.json b/x-pack/plugins/alerting/kibana.json index af2d08e69f597..82d8de0daf14a 100644 --- a/x-pack/plugins/alerting/kibana.json +++ b/x-pack/plugins/alerting/kibana.json @@ -2,6 +2,10 @@ "id": "alerting", "server": true, "ui": true, + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "alerting"], diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json index f130d0173cc89..0a594cf1cc2ac 100644 --- a/x-pack/plugins/cross_cluster_replication/kibana.json +++ b/x-pack/plugins/cross_cluster_replication/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": [ "home", "licensing", @@ -12,13 +16,7 @@ "indexManagement", "features" ], - "optionalPlugins": [ - "usageCollection" - ], + "optionalPlugins": ["usageCollection"], "configPath": ["xpack", "ccr"], - "requiredBundles": [ - "kibanaReact", - "esUiShared", - "data" - ] + "requiredBundles": ["kibanaReact", "esUiShared", "data"] } diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index da83ded471d0b..d678921e9ac7b 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -3,6 +3,10 @@ "id": "dataEnhanced", "version": "8.0.0", "kibanaVersion": "kibana", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "configPath": ["xpack", "data_enhanced"], "requiredPlugins": ["bfetch", "data", "features", "management", "share", "taskManager"], "optionalPlugins": ["kibanaUtils", "usageCollection", "security"], diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.json b/x-pack/plugins/drilldowns/url_drilldown/kibana.json index 9bdd13fbfea26..a4552d201f263 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/kibana.json +++ b/x-pack/plugins/drilldowns/url_drilldown/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["embeddable", "uiActions", "uiActionsEnhanced"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index 8d49e3e26eb7b..09416ce18aecb 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/event_log/kibana.json b/x-pack/plugins/event_log/kibana.json index 0231bb6234471..5223549a2e4fb 100644 --- a/x-pack/plugins/event_log/kibana.json +++ b/x-pack/plugins/event_log/kibana.json @@ -2,6 +2,10 @@ "id": "eventLog", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "configPath": ["xpack", "eventLog"], "optionalPlugins": ["spaces"], "server": true, diff --git a/x-pack/plugins/grokdebugger/kibana.json b/x-pack/plugins/grokdebugger/kibana.json index 5f288e0cf3bdb..692aa16329d54 100644 --- a/x-pack/plugins/grokdebugger/kibana.json +++ b/x-pack/plugins/grokdebugger/kibana.json @@ -2,16 +2,13 @@ "id": "grokdebugger", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": [ - "licensing", - "home", - "devTools" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "home", "devTools"], "server": true, "ui": true, "configPath": ["xpack", "grokdebugger"], - "requiredBundles": [ - "kibanaReact", - "esUiShared" - ] + "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json index 21e7e7888acb9..bccb3cd78e78d 100644 --- a/x-pack/plugins/index_lifecycle_management/kibana.json +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -3,23 +3,12 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "licensing", - "management", - "features", - "share" - ], - "optionalPlugins": [ - "cloud", - "usageCollection", - "indexManagement", - "home" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "management", "features", "share"], + "optionalPlugins": ["cloud", "usageCollection", "indexManagement", "home"], "configPath": ["xpack", "ilm"], - "requiredBundles": [ - "indexManagement", - "kibanaReact", - "esUiShared", - "home" - ] + "requiredBundles": ["indexManagement", "kibanaReact", "esUiShared", "home"] } diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index cd29e7b9ee1cd..456ce830f6b57 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -1,14 +1,14 @@ { "id": "indexManagement", + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "version": "kibana", "server": true, "ui": true, "requiredPlugins": ["home", "management", "features", "share"], "optionalPlugins": ["security", "usageCollection", "fleet"], "configPath": ["xpack", "index_management"], - "requiredBundles": [ - "kibanaReact", - "esUiShared", - "runtimeFields" - ] + "requiredBundles": ["kibanaReact", "esUiShared", "runtimeFields"] } diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 7c54d18fbd382..800d92b5c9748 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -3,6 +3,10 @@ "version": "8.0.0", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["management", "features", "share"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], diff --git a/x-pack/plugins/license_api_guard/kibana.json b/x-pack/plugins/license_api_guard/kibana.json index 0fdf7ffed8988..1b870810ccbed 100644 --- a/x-pack/plugins/license_api_guard/kibana.json +++ b/x-pack/plugins/license_api_guard/kibana.json @@ -2,6 +2,10 @@ "id": "licenseApiGuard", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "configPath": ["xpack", "licenseApiGuard"], "server": true, "ui": false diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json index be2e21c7eb41e..a06bfbb9409fc 100644 --- a/x-pack/plugins/license_management/kibana.json +++ b/x-pack/plugins/license_management/kibana.json @@ -3,13 +3,13 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["home", "licensing", "management", "features"], "optionalPlugins": ["telemetry"], "configPath": ["xpack", "license_management"], "extraPublicDirs": ["common/constants"], - "requiredBundles": [ - "telemetryManagementSection", - "esUiShared", - "kibanaReact" - ] + "requiredBundles": ["telemetryManagementSection", "esUiShared", "kibanaReact"] } diff --git a/x-pack/plugins/lists/kibana.json b/x-pack/plugins/lists/kibana.json index ae7b3e7679e0b..17a900b3f6fdc 100644 --- a/x-pack/plugins/lists/kibana.json +++ b/x-pack/plugins/lists/kibana.json @@ -2,6 +2,10 @@ "configPath": ["xpack", "lists"], "extraPublicDirs": ["common"], "id": "lists", + "owner": { + "name": "Security detections response", + "githubTeam": "security-detections-response" + }, "kibanaVersion": "kibana", "requiredPlugins": [], "optionalPlugins": ["spaces", "security"], diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json index 0d14312a154e0..2ff4aac9ba55b 100644 --- a/x-pack/plugins/logstash/kibana.json +++ b/x-pack/plugins/logstash/kibana.json @@ -1,18 +1,14 @@ { "id": "logstash", + "owner": { + "name": "Logstash", + "githubTeam": "logstash" + }, "version": "0.0.1", "kibanaVersion": "kibana", "configPath": ["xpack", "logstash"], - "requiredPlugins": [ - "licensing", - "management", - "features" - ], - "optionalPlugins": [ - "home", - "monitoring", - "security" - ], + "requiredPlugins": ["licensing", "management", "features"], + "optionalPlugins": ["home", "monitoring", "security"], "server": true, "ui": true, "requiredBundles": ["home"] diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 1cccfaa7748b1..8ee4315aa62e6 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -1,11 +1,12 @@ { "id": "maps", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "maps" - ], + "configPath": ["xpack", "maps"], "requiredPlugins": [ "licensing", "features", @@ -23,21 +24,9 @@ "share", "presentationUtil" ], - "optionalPlugins": [ - "home", - "savedObjectsTagging", - "charts", - "security" - ], + "optionalPlugins": ["home", "savedObjectsTagging", "charts", "security"], "ui": true, "server": true, - "extraPublicDirs": [ - "common/constants" - ], - "requiredBundles": [ - "kibanaReact", - "kibanaUtils", - "home", - "mapsEms" - ] + "extraPublicDirs": ["common/constants"], + "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "mapsEms"] } diff --git a/x-pack/plugins/metrics_entities/kibana.json b/x-pack/plugins/metrics_entities/kibana.json index 17484c2c243ce..9d3a4f7f66a8d 100644 --- a/x-pack/plugins/metrics_entities/kibana.json +++ b/x-pack/plugins/metrics_entities/kibana.json @@ -1,5 +1,9 @@ { "id": "metricsEntities", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "metricsEntities"], diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 7d82006c6b999..0e02812af28fb 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -2,6 +2,10 @@ "id": "monitoring", "version": "8.0.0", "kibanaVersion": "kibana", + "owner": { + "owner": "Stack Monitoring", + "githubTeam": "stack-monitoring-ui" + }, "configPath": ["monitoring"], "requiredPlugins": [ "licensing", diff --git a/x-pack/plugins/osquery/kibana.json b/x-pack/plugins/osquery/kibana.json index f947866641c28..a499b2b75ee68 100644 --- a/x-pack/plugins/osquery/kibana.json +++ b/x-pack/plugins/osquery/kibana.json @@ -1,25 +1,14 @@ { - "configPath": [ - "xpack", - "osquery" - ], - "extraPublicDirs": [ - "common" - ], + "configPath": ["xpack", "osquery"], + "extraPublicDirs": ["common"], "id": "osquery", + "owner": { + "name": "Security asset management", + "githubTeam": "security-asset-management" + }, "kibanaVersion": "kibana", - "optionalPlugins": [ - "fleet", - "home", - "usageCollection", - "lens" - ], - "requiredBundles": [ - "esUiShared", - "fleet", - "kibanaUtils", - "kibanaReact" - ], + "optionalPlugins": ["fleet", "home", "usageCollection", "lens"], + "requiredBundles": ["esUiShared", "fleet", "kibanaUtils", "kibanaReact"], "requiredPlugins": [ "actions", "data", diff --git a/x-pack/plugins/painless_lab/kibana.json b/x-pack/plugins/painless_lab/kibana.json index ca97e73704e70..7c71d4bdb4b76 100644 --- a/x-pack/plugins/painless_lab/kibana.json +++ b/x-pack/plugins/painless_lab/kibana.json @@ -2,18 +2,13 @@ "id": "painlessLab", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": [ - "devTools", - "licensing", - "home" - ], - "configPath": [ - "xpack", - "painless_lab" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["devTools", "licensing", "home"], + "configPath": ["xpack", "painless_lab"], "server": true, "ui": true, - "requiredBundles": [ - "kibanaReact" - ] + "requiredBundles": ["kibanaReact"] } diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json index 0334af5a868f2..192a1308c265a 100644 --- a/x-pack/plugins/remote_clusters/kibana.json +++ b/x-pack/plugins/remote_clusters/kibana.json @@ -1,24 +1,14 @@ { "id": "remoteClusters", "version": "kibana", - "configPath": [ - "xpack", - "remote_clusters" - ], - "requiredPlugins": [ - "licensing", - "management", - "indexManagement", - "features" - ], - "optionalPlugins": [ - "usageCollection", - "cloud" - ], + "configPath": ["xpack", "remote_clusters"], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "management", "indexManagement", "features"], + "optionalPlugins": ["usageCollection", "cloud"], "server": true, "ui": true, - "requiredBundles": [ - "kibanaReact", - "esUiShared" - ] + "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index 10541d9a4ebdd..20f284686f3b5 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -4,23 +4,12 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "management", - "licensing", - "features" - ], - "optionalPlugins": [ - "home", - "indexManagement", - "usageCollection", - "visTypeTimeseries" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["management", "licensing", "features"], + "optionalPlugins": ["home", "indexManagement", "usageCollection", "visTypeTimeseries"], "configPath": ["xpack", "rollup"], - "requiredBundles": [ - "kibanaUtils", - "kibanaReact", - "home", - "esUiShared", - "data" - ] + "requiredBundles": ["kibanaUtils", "kibanaReact", "home", "esUiShared", "data"] } diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 360ea18df9ca1..a750c4a91072a 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -2,15 +2,8 @@ "id": "ruleRegistry", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "ruleRegistry" - ], - "requiredPlugins": [ - "alerting", - "data", - "triggersActionsUi" - ], + "configPath": ["xpack", "ruleRegistry"], + "requiredPlugins": ["alerting", "data", "triggersActionsUi"], "optionalPlugins": ["security"], "server": true } diff --git a/x-pack/plugins/runtime_fields/kibana.json b/x-pack/plugins/runtime_fields/kibana.json index 65932c723c474..ef5514a01b3cf 100644 --- a/x-pack/plugins/runtime_fields/kibana.json +++ b/x-pack/plugins/runtime_fields/kibana.json @@ -3,13 +3,12 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": [ - ], - "optionalPlugins": [ - ], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredPlugins": [], + "optionalPlugins": [], "configPath": ["xpack", "runtime_fields"], - "requiredBundles": [ - "kibanaReact", - "esUiShared" - ] + "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/searchprofiler/kibana.json b/x-pack/plugins/searchprofiler/kibana.json index 6c94701c0ec09..864e3880ae200 100644 --- a/x-pack/plugins/searchprofiler/kibana.json +++ b/x-pack/plugins/searchprofiler/kibana.json @@ -5,6 +5,10 @@ "configPath": ["xpack", "searchprofiler"], "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["devTools", "home", "licensing"], "requiredBundles": ["esUiShared"] } diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 990756f3da701..c8678a227510e 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -1,5 +1,9 @@ { "id": "securitySolution", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "8.0.0", "extraPublicDirs": ["common"], "kibanaVersion": "kibana", diff --git a/x-pack/plugins/snapshot_restore/kibana.json b/x-pack/plugins/snapshot_restore/kibana.json index a8a3881929f40..bd2a85126c0c6 100644 --- a/x-pack/plugins/snapshot_restore/kibana.json +++ b/x-pack/plugins/snapshot_restore/kibana.json @@ -3,21 +3,12 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "licensing", - "management", - "features" - ], - "optionalPlugins": [ - "usageCollection", - "security", - "cloud", - "home" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "management", "features"], + "optionalPlugins": ["usageCollection", "security", "cloud", "home"], "configPath": ["xpack", "snapshot_restore"], - "requiredBundles": [ - "esUiShared", - "kibanaReact", - "home" - ] + "requiredBundles": ["esUiShared", "kibanaReact", "home"] } diff --git a/x-pack/plugins/stack_alerts/kibana.json b/x-pack/plugins/stack_alerts/kibana.json index ed9f7d4e635f4..1b4271328c2f9 100644 --- a/x-pack/plugins/stack_alerts/kibana.json +++ b/x-pack/plugins/stack_alerts/kibana.json @@ -1,9 +1,20 @@ { "id": "stackAlerts", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerting", "features", "triggersActionsUi", "kibanaReact", "savedObjects", "data"], + "requiredPlugins": [ + "alerting", + "features", + "triggersActionsUi", + "kibanaReact", + "savedObjects", + "data" + ], "configPath": ["xpack", "stack_alerts"], "requiredBundles": ["esUiShared"], "ui": true diff --git a/x-pack/plugins/task_manager/kibana.json b/x-pack/plugins/task_manager/kibana.json index aab1cd0ab41a5..d0b847ce58d77 100644 --- a/x-pack/plugins/task_manager/kibana.json +++ b/x-pack/plugins/task_manager/kibana.json @@ -2,6 +2,10 @@ "id": "taskManager", "server": true, "version": "8.0.0", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "kibanaVersion": "kibana", "configPath": ["xpack", "task_manager"], "optionalPlugins": ["usageCollection"], diff --git a/x-pack/plugins/timelines/kibana.json b/x-pack/plugins/timelines/kibana.json index bc9fba2c4a1bb..0239dcdd8f166 100644 --- a/x-pack/plugins/timelines/kibana.json +++ b/x-pack/plugins/timelines/kibana.json @@ -1,5 +1,9 @@ { "id": "timelines", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "timelines"], diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index a302673c2ec08..4033889d9811e 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -1,5 +1,9 @@ { "id": "triggersActionsUi", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "version": "kibana", "server": true, "ui": true, diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json index dbc136a258884..6fcac67c5a66b 100644 --- a/x-pack/plugins/ui_actions_enhanced/kibana.json +++ b/x-pack/plugins/ui_actions_enhanced/kibana.json @@ -1,17 +1,13 @@ { "id": "uiActionsEnhanced", + "owner": { + "name": "Kibana App Services", + "githubTeam": "kibana-app-services" + }, "version": "kibana", "configPath": ["xpack", "ui_actions_enhanced"], "server": true, "ui": true, - "requiredPlugins": [ - "embeddable", - "uiActions", - "licensing" - ], - "requiredBundles": [ - "kibanaUtils", - "kibanaReact", - "data" - ] + "requiredPlugins": ["embeddable", "uiActions", "licensing"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "data"] } diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json index d013c16837b77..e69e352104f35 100644 --- a/x-pack/plugins/upgrade_assistant/kibana.json +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "configPath": ["xpack", "upgrade_assistant"], "requiredPlugins": ["management", "licensing", "features"], "optionalPlugins": ["usageCollection"], diff --git a/x-pack/plugins/watcher/kibana.json b/x-pack/plugins/watcher/kibana.json index 84fe2b509b263..6c9e46e0647af 100644 --- a/x-pack/plugins/watcher/kibana.json +++ b/x-pack/plugins/watcher/kibana.json @@ -2,6 +2,10 @@ "id": "watcher", "configPath": ["xpack", "watcher"], "version": "kibana", + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": [ "home", "licensing", @@ -13,9 +17,5 @@ ], "server": true, "ui": true, - "requiredBundles": [ - "esUiShared", - "kibanaReact", - "fieldFormats" - ] + "requiredBundles": ["esUiShared", "kibanaReact", "fieldFormats"] } diff --git a/x-pack/plugins/xpack_legacy/kibana.json b/x-pack/plugins/xpack_legacy/kibana.json index fc45b612d72cf..9dd0ac8340183 100644 --- a/x-pack/plugins/xpack_legacy/kibana.json +++ b/x-pack/plugins/xpack_legacy/kibana.json @@ -1,10 +1,12 @@ { "id": "xpackLegacy", + "owner": { + "name": "Kibana Core", + "githubTeam": "kibana-core" + }, "version": "8.0.0", "kibanaVersion": "kibana", "server": true, "ui": false, - "requiredPlugins": [ - "usageCollection" - ] + "requiredPlugins": ["usageCollection"] } From f243b0540d954a53b1b0371feb22c1cfd5593de9 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 17 Aug 2021 16:21:44 +0200 Subject: [PATCH 04/11] [ML] Check for error messages in the Anomaly Detection jobs health rule type (#108701) * [ML] retrieve job errors * [ML] account for previous execution time * [ML] update default message * [ML] update description * [ML] update unit tests * [ML] update unit tests * [ML] update action name * [ML] update errorMessages name * [ML] update a default message to avoid line breaks * [ML] update rule helper text * [ML] refactor getJobsErrors * [ML] perform errors check starting from the second execution --- x-pack/plugins/ml/common/constants/alerts.ts | 4 +- x-pack/plugins/ml/common/util/alerts.test.ts | 6 + x-pack/plugins/ml/common/util/alerts.ts | 2 +- .../register_jobs_health_alerting_rule.ts | 20 +- .../lib/alerts/jobs_health_service.test.ts | 177 ++++++++++++------ .../server/lib/alerts/jobs_health_service.ts | 54 +++++- .../register_jobs_monitoring_rule_type.ts | 21 ++- .../job_audit_messages/job_audit_messages.ts | 70 ++++++- 8 files changed, 268 insertions(+), 86 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/alerts.ts b/x-pack/plugins/ml/common/constants/alerts.ts index 2192b2b504b59..1b373b2ec435b 100644 --- a/x-pack/plugins/ml/common/constants/alerts.ts +++ b/x-pack/plugins/ml/common/constants/alerts.ts @@ -54,12 +54,12 @@ export const HEALTH_CHECK_NAMES: Record { enabled: true, timeInterval: null, }, + errorMessages: { + enabled: true, + }, }); }); test('returns config with overridden values based on provided configuration', () => { @@ -119,6 +122,9 @@ describe('getResultJobsHealthRuleConfig', () => { enabled: true, timeInterval: null, }, + errorMessages: { + enabled: true, + }, }); }); }); diff --git a/x-pack/plugins/ml/common/util/alerts.ts b/x-pack/plugins/ml/common/util/alerts.ts index 7328c2a4dcc71..6abc5333a1f73 100644 --- a/x-pack/plugins/ml/common/util/alerts.ts +++ b/x-pack/plugins/ml/common/util/alerts.ts @@ -54,7 +54,7 @@ export function getTopNBuckets(job: Job): number { return Math.ceil(narrowBucketLength / bucketSpan.asSeconds()); } -const implementedTests = ['datafeed', 'mml', 'delayedData'] as JobsHealthTests[]; +const implementedTests = ['datafeed', 'mml', 'delayedData', 'errorMessages'] as JobsHealthTests[]; /** * Returns tests configuration combined with default values. diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts index f6446b454a877..a5f433bcc3752 100644 --- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts @@ -21,7 +21,8 @@ export function registerJobsHealthAlertingRule( triggersActionsUi.ruleTypeRegistry.register({ id: ML_ALERT_TYPES.AD_JOBS_HEALTH, description: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.description', { - defaultMessage: 'Alert when anomaly detection jobs experience operational issues.', + defaultMessage: + 'Alert when anomaly detection jobs experience operational issues. Enable suitable alerts for critically important jobs.', }), iconClass: 'bell', documentationUrl(docLinks) { @@ -90,14 +91,15 @@ export function registerJobsHealthAlertingRule( \\{\\{context.message\\}\\} \\{\\{#context.results\\}\\} Job ID: \\{\\{job_id\\}\\} - \\{\\{#datafeed_id\\}\\}Datafeed ID: \\{\\{datafeed_id\\}\\} \\{\\{/datafeed_id\\}\\} - \\{\\{#datafeed_state\\}\\}Datafeed state: \\{\\{datafeed_state\\}\\} \\{\\{/datafeed_state\\}\\} - \\{\\{#memory_status\\}\\}Memory status: \\{\\{memory_status\\}\\} \\{\\{/memory_status\\}\\} - \\{\\{#log_time\\}\\}Memory logging time: \\{\\{log_time\\}\\} \\{\\{/log_time\\}\\} - \\{\\{#failed_category_count\\}\\}Failed category count: \\{\\{failed_category_count\\}\\} \\{\\{/failed_category_count\\}\\} - \\{\\{#annotation\\}\\}Annotation: \\{\\{annotation\\}\\} \\{\\{/annotation\\}\\} - \\{\\{#missed_docs_count\\}\\}Number of missed documents: \\{\\{missed_docs_count\\}\\} \\{\\{/missed_docs_count\\}\\} - \\{\\{#end_timestamp\\}\\}Latest finalized bucket with missing docs: \\{\\{end_timestamp\\}\\} \\{\\{/end_timestamp\\}\\} + \\{\\{#datafeed_id\\}\\}Datafeed ID: \\{\\{datafeed_id\\}\\} + \\{\\{/datafeed_id\\}\\} \\{\\{#datafeed_state\\}\\}Datafeed state: \\{\\{datafeed_state\\}\\} + \\{\\{/datafeed_state\\}\\} \\{\\{#memory_status\\}\\}Memory status: \\{\\{memory_status\\}\\} + \\{\\{/memory_status\\}\\} \\{\\{#log_time\\}\\}Memory logging time: \\{\\{log_time\\}\\} + \\{\\{/log_time\\}\\} \\{\\{#failed_category_count\\}\\}Failed category count: \\{\\{failed_category_count\\}\\} + \\{\\{/failed_category_count\\}\\} \\{\\{#annotation\\}\\}Annotation: \\{\\{annotation\\}\\} + \\{\\{/annotation\\}\\} \\{\\{#missed_docs_count\\}\\}Number of missed documents: \\{\\{missed_docs_count\\}\\} + \\{\\{/missed_docs_count\\}\\} \\{\\{#end_timestamp\\}\\}Latest finalized bucket with missing docs: \\{\\{end_timestamp\\}\\} + \\{\\{/end_timestamp\\}\\} \\{\\{#errors\\}\\}Error message: \\{\\{message\\}\\} \\{\\{/errors\\}\\} \\{\\{/context.results\\}\\} `, } diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts index b345cf8c1245c..ffaa26fc949ee 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts @@ -11,9 +11,39 @@ import type { Logger } from 'kibana/server'; import { MlClient } from '../ml_client'; import { MlJob, MlJobStats } from '@elastic/elasticsearch/api/types'; import { AnnotationService } from '../../models/annotation_service/annotation'; +import { JobsHealthExecutorOptions } from './register_jobs_monitoring_rule_type'; +import { JobAuditMessagesService } from '../../models/job_audit_messages/job_audit_messages'; +import { DeepPartial } from '../../../common/types/common'; const MOCK_DATE_NOW = 1487076708000; +function getDefaultExecutorOptions( + overrides: DeepPartial = {} +): JobsHealthExecutorOptions { + return ({ + state: {}, + startedAt: new Date('2021-08-12T13:13:39.396Z'), + previousStartedAt: new Date('2021-08-12T13:13:27.396Z'), + spaceId: 'default', + namespace: undefined, + name: 'ml-health-check', + tags: [], + createdBy: 'elastic', + updatedBy: 'elastic', + rule: { + name: 'ml-health-check', + tags: [], + consumer: 'alerts', + producer: 'ml', + ruleTypeId: 'xpack.ml.anomaly_detection_jobs_health', + ruleTypeName: 'Anomaly detection jobs health', + enabled: true, + schedule: { interval: '10s' }, + }, + ...overrides, + } as unknown) as JobsHealthExecutorOptions; +} + describe('JobsHealthService', () => { const mlClient = ({ getJobs: jest.fn().mockImplementation(({ job_id: jobIds = [] }) => { @@ -117,6 +147,12 @@ describe('JobsHealthService', () => { }), } as unknown) as jest.Mocked; + const jobAuditMessagesService = ({ + getJobsErrors: jest.fn().mockImplementation((jobIds: string) => { + return Promise.resolve({}); + }), + } as unknown) as jest.Mocked; + const logger = ({ warn: jest.fn(), info: jest.fn(), @@ -127,6 +163,7 @@ describe('JobsHealthService', () => { mlClient, datafeedsService, annotationService, + jobAuditMessagesService, logger ); @@ -143,42 +180,52 @@ describe('JobsHealthService', () => { test('returns empty results when no jobs provided', async () => { // act - const executionResult = await jobHealthService.getTestsResults('testRule', { - testsConfig: null, - includeJobs: { - jobIds: ['*'], - groupIds: [], - }, - excludeJobs: null, - }); + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule' }, + params: { + testsConfig: null, + includeJobs: { + jobIds: ['*'], + groupIds: [], + }, + excludeJobs: null, + }, + }) + ); expect(logger.warn).toHaveBeenCalledWith('Rule "testRule" does not have associated jobs.'); expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); expect(executionResult).toEqual([]); }); test('returns empty results and does not perform datafeed check when test is disabled', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule', { - testsConfig: { - datafeed: { - enabled: false, - }, - behindRealtime: null, - delayedData: { - enabled: false, - docsCount: null, - timeInterval: null, - }, - errorMessages: null, - mml: { - enabled: false, + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule' }, + params: { + testsConfig: { + datafeed: { + enabled: false, + }, + behindRealtime: null, + delayedData: { + enabled: false, + docsCount: null, + timeInterval: null, + }, + errorMessages: null, + mml: { + enabled: false, + }, + }, + includeJobs: { + jobIds: ['test_job_01'], + groupIds: [], + }, + excludeJobs: null, }, - }, - includeJobs: { - jobIds: ['test_job_01'], - groupIds: [], - }, - excludeJobs: null, - }); + }) + ); expect(logger.warn).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith(`Performing health checks for job IDs: test_job_01`); expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); @@ -186,27 +233,32 @@ describe('JobsHealthService', () => { }); test('takes into account delayed data params', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule_04', { - testsConfig: { - delayedData: { - enabled: true, - docsCount: 10, - timeInterval: '4h', + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule_04' }, + params: { + testsConfig: { + delayedData: { + enabled: true, + docsCount: 10, + timeInterval: '4h', + }, + behindRealtime: { enabled: false, timeInterval: null }, + mml: { enabled: false }, + datafeed: { enabled: false }, + errorMessages: { enabled: false }, + }, + includeJobs: { + jobIds: [], + groupIds: ['test_group'], + }, + excludeJobs: { + jobIds: ['test_job_03'], + groupIds: [], + }, }, - behindRealtime: { enabled: false, timeInterval: null }, - mml: { enabled: false }, - datafeed: { enabled: false }, - errorMessages: { enabled: false }, - }, - includeJobs: { - jobIds: [], - groupIds: ['test_group'], - }, - excludeJobs: { - jobIds: ['test_job_03'], - groupIds: [], - }, - }); + }) + ); expect(annotationService.getDelayedDataAnnotations).toHaveBeenCalledWith({ jobIds: ['test_job_01', 'test_job_02'], @@ -234,17 +286,22 @@ describe('JobsHealthService', () => { }); test('returns results based on provided selection', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule_03', { - testsConfig: null, - includeJobs: { - jobIds: [], - groupIds: ['test_group'], - }, - excludeJobs: { - jobIds: ['test_job_03'], - groupIds: [], - }, - }); + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule_03' }, + params: { + testsConfig: null, + includeJobs: { + jobIds: [], + groupIds: ['test_group'], + }, + excludeJobs: { + jobIds: ['test_job_03'], + groupIds: [], + }, + }, + }) + ); expect(logger.warn).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith( `Performing health checks for job IDs: test_job_01, test_job_02` diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts index 52e17fed7a414..bcae57e558573 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts @@ -11,10 +11,7 @@ import { i18n } from '@kbn/i18n'; import { Logger } from 'kibana/server'; import { MlJob } from '@elastic/elasticsearch/api/types'; import { MlClient } from '../ml_client'; -import { - AnomalyDetectionJobsHealthRuleParams, - JobSelection, -} from '../../routes/schemas/alerting_schema'; +import { JobSelection } from '../../routes/schemas/alerting_schema'; import { datafeedsProvider, DatafeedsService } from '../../models/job_service/datafeeds'; import { ALL_JOBS_SELECTION, HEALTH_CHECK_NAMES } from '../../../common/constants/alerts'; import { DatafeedStats } from '../../../common/types/anomaly_detection_jobs'; @@ -22,6 +19,7 @@ import { GetGuards } from '../../shared_services/shared_services'; import { AnomalyDetectionJobsHealthAlertContext, DelayedDataResponse, + JobsHealthExecutorOptions, MmlTestResponse, NotStartedDatafeedResponse, } from './register_jobs_monitoring_rule_type'; @@ -33,6 +31,10 @@ import { AnnotationService } from '../../models/annotation_service/annotation'; import { annotationServiceProvider } from '../../models/annotation_service'; import { parseInterval } from '../../../common/util/parse_interval'; import { isDefined } from '../../../common/types/guards'; +import { + jobAuditMessagesProvider, + JobAuditMessagesService, +} from '../../models/job_audit_messages/job_audit_messages'; interface TestResult { name: string; @@ -45,6 +47,7 @@ export function jobsHealthServiceProvider( mlClient: MlClient, datafeedsService: DatafeedsService, annotationService: AnnotationService, + jobAuditMessagesService: JobAuditMessagesService, logger: Logger ) { /** @@ -236,13 +239,25 @@ export function jobsHealthServiceProvider( return annotations; }, + /** + * Retrieves a list of the latest errors per jobs. + * @param jobIds List of job IDs. + * @param previousStartedAt Time of the previous rule execution. As we intend to notify + * about an error only once, limit the scope of the errors search. + */ + async getErrorsReport(jobIds: string[], previousStartedAt: Date) { + return await jobAuditMessagesService.getJobsErrors(jobIds, previousStartedAt.getTime()); + }, /** * Retrieves report grouped by test. */ - async getTestsResults( - ruleInstanceName: string, - { testsConfig, includeJobs, excludeJobs }: AnomalyDetectionJobsHealthRuleParams - ): Promise { + async getTestsResults(executorOptions: JobsHealthExecutorOptions): Promise { + const { + rule, + previousStartedAt, + params: { testsConfig, includeJobs, excludeJobs }, + } = executorOptions; + const config = getResultJobsHealthRuleConfig(testsConfig); const results: TestsResults = []; @@ -251,7 +266,7 @@ export function jobsHealthServiceProvider( const jobIds = getJobIds(jobs); if (jobIds.length === 0) { - logger.warn(`Rule "${ruleInstanceName}" does not have associated jobs.`); + logger.warn(`Rule "${rule.name}" does not have associated jobs.`); return results; } @@ -334,6 +349,26 @@ export function jobsHealthServiceProvider( } } + if (config.errorMessages.enabled && previousStartedAt) { + const response = await this.getErrorsReport(jobIds, previousStartedAt); + if (response.length > 0) { + results.push({ + name: HEALTH_CHECK_NAMES.errorMessages.name, + context: { + results: response, + message: i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesMessage', + { + defaultMessage: + '{jobsCount, plural, one {# job contains} other {# jobs contain}} errors in the messages.', + values: { jobsCount: response.length }, + } + ), + }, + }); + } + } + return results; }, }; @@ -360,6 +395,7 @@ export function getJobsHealthServiceProvider(getGuards: GetGuards) { mlClient, datafeedsProvider(scopedClient, mlClient), annotationServiceProvider(scopedClient), + jobAuditMessagesProvider(scopedClient, mlClient), logger ).getTestsResults(...args) ); diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 063d8ad5a8980..c49c169d3bd21 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -22,6 +22,8 @@ import { AlertInstanceState, AlertTypeState, } from '../../../../alerting/common'; +import { JobsErrorsResponse } from '../../models/job_audit_messages/job_audit_messages'; +import { AlertExecutorOptions } from '../../../../alerting/server'; type ModelSizeStats = MlJobStats['model_size_stats']; @@ -55,7 +57,8 @@ export interface DelayedDataResponse { export type AnomalyDetectionJobHealthResult = | MmlTestResponse | NotStartedDatafeedResponse - | DelayedDataResponse; + | DelayedDataResponse + | JobsErrorsResponse[number]; export type AnomalyDetectionJobsHealthAlertContext = { results: AnomalyDetectionJobHealthResult[]; @@ -69,10 +72,18 @@ export type AnomalyDetectionJobRealtimeIssue = typeof ANOMALY_DETECTION_JOB_REAL export const REALTIME_ISSUE_DETECTED: ActionGroup = { id: ANOMALY_DETECTION_JOB_REALTIME_ISSUE, name: i18n.translate('xpack.ml.jobsHealthAlertingRule.actionGroupName', { - defaultMessage: 'Real-time issue detected', + defaultMessage: 'Issue detected', }), }; +export type JobsHealthExecutorOptions = AlertExecutorOptions< + AnomalyDetectionJobsHealthRuleParams, + Record, + Record, + AnomalyDetectionJobsHealthAlertContext, + AnomalyDetectionJobRealtimeIssue +>; + export function registerJobsMonitoringRuleType({ alerting, mlServicesProviders, @@ -120,14 +131,16 @@ export function registerJobsMonitoringRuleType({ producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, - async executor({ services, params, alertId, state, previousStartedAt, startedAt, name, rule }) { + async executor(options) { + const { services, name } = options; + const fakeRequest = {} as KibanaRequest; const { getTestsResults } = mlServicesProviders.jobsHealthServiceProvider( services.savedObjectsClient, fakeRequest, logger ); - const executionResult = await getTestsResults(name, params); + const executionResult = await getTestsResults(options); if (executionResult.length > 0) { logger.info( diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts index 98ed76319a0f7..fcda1a2a3ea73 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts @@ -54,6 +54,10 @@ export function isClearable(index?: string): boolean { return false; } +export type JobsErrorsResponse = Array<{ job_id: string; errors: JobMessage[] }>; + +export type JobAuditMessagesService = ReturnType; + export function jobAuditMessagesProvider( { asInternalUser }: IScopedClusterClient, mlClient: MlClient @@ -178,7 +182,10 @@ export function jobAuditMessagesProvider( return { messages, notificationIndices }; } - // search highest, most recent audit messages for all jobs for the last 24hrs. + /** + * Search highest, most recent audit messages for all jobs for the last 24hrs. + * @param jobIds + */ async function getAuditMessagesSummary(jobIds: string[]): Promise { // TODO This is the current default value of the cluster setting `search.max_buckets`. // This should possibly consider the real settings in a future update. @@ -400,9 +407,70 @@ export function jobAuditMessagesProvider( return (Object.keys(LEVEL) as LevelName[])[Object.values(LEVEL).indexOf(level)]; } + /** + * Retrieve list of errors per job. + * @param jobIds + */ + async function getJobsErrors(jobIds: string[], earliestMs?: number): Promise { + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + size: 0, + body: { + query: { + bool: { + filter: [ + ...(earliestMs ? [{ range: { timestamp: { gte: earliestMs } } }] : []), + { terms: { job_id: jobIds } }, + { + term: { level: { value: MESSAGE_LEVEL.ERROR } }, + }, + ], + }, + }, + aggs: { + by_job: { + terms: { + field: 'job_id', + size: jobIds.length, + }, + aggs: { + latest_errors: { + top_hits: { + size: 10, + sort: [ + { + timestamp: { + order: 'desc', + }, + }, + ], + }, + }, + }, + }, + }, + }, + }); + + const errors = body.aggregations!.by_job as estypes.AggregationsTermsAggregate<{ + key: string; + doc_count: number; + latest_errors: Pick, 'hits'>; + }>; + + return errors.buckets.map((bucket) => { + return { + job_id: bucket.key, + errors: bucket.latest_errors.hits.hits.map((v) => v._source!), + }; + }); + } + return { getJobAuditMessages, getAuditMessagesSummary, clearJobAuditMessages, + getJobsErrors, }; } From 6f4a615860f52e7b2b5bf9735af0bc0ed7365b38 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 17 Aug 2021 16:47:43 +0200 Subject: [PATCH 05/11] Make indexPattern only read only in Discover permission (#108376) * Make indexPattern only read only in Discover permission * Fix test failures * Address review comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/doc_table/actions/columns.ts | 8 +-- .../components/layout/discover_layout.tsx | 4 +- .../context_app/context_app.test.tsx | 3 + .../components/context_app/context_app.tsx | 4 +- .../helpers/popularize_field.test.ts | 57 +++++++++++++++++-- .../application/helpers/popularize_field.ts | 6 +- .../__snapshots__/oss_features.test.ts.snap | 10 ++-- .../plugins/features/server/oss_features.ts | 4 +- 8 files changed, 74 insertions(+), 22 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts index 130b43539d9b5..9b69a98ca7996 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts @@ -78,9 +78,7 @@ export function getStateColumnActions({ state: DiscoverState | ContextState; }) { function onAddColumn(columnName: string) { - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } + popularizeField(indexPattern, columnName, indexPatterns, capabilities); const columns = addColumn(state.columns || [], columnName, useNewFieldsApi); const defaultOrder = config.get(SORT_DEFAULT_ORDER_SETTING); const sort = @@ -89,9 +87,7 @@ export function getStateColumnActions({ } function onRemoveColumn(columnName: string) { - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } + popularizeField(indexPattern, columnName, indexPatterns, capabilities); const columns = removeColumn(state.columns || [], columnName, useNewFieldsApi); // The state's sort property is an array of [sortByColumn,sortDirection] const sort = diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 94e28c3f1d54c..6d241468bdf74 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -122,7 +122,7 @@ export function DiscoverLayout({ const onAddFilter = useCallback( (field: IndexPatternField | string, values: string, operation: '+' | '-') => { const fieldName = typeof field === 'string' ? field : field.name; - popularizeField(indexPattern, fieldName, indexPatterns); + popularizeField(indexPattern, fieldName, indexPatterns, capabilities); const newFilters = esFilters.generateFilters( filterManager, field, @@ -135,7 +135,7 @@ export function DiscoverLayout({ } return filterManager.addFilters(newFilters); }, - [filterManager, indexPattern, indexPatterns, trackUiMetric] + [filterManager, indexPattern, indexPatterns, trackUiMetric, capabilities] ); const onEditRuntimeField = useCallback(() => { diff --git a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx index 7ac6a9d0e8de3..a21b035c335df 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx @@ -54,6 +54,9 @@ describe('ContextApp test', () => { discover: { save: true, }, + indexPatterns: { + save: true, + }, }, indexPatterns: indexPatternsMock, toastNotifications: { addDanger: () => {} }, diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 37963eb2dfa93..25590f331839e 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -109,10 +109,10 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp filterManager.addFilters(newFilters); if (indexPatterns) { const fieldName = typeof field === 'string' ? field : field.name; - await popularizeField(indexPattern, fieldName, indexPatterns); + await popularizeField(indexPattern, fieldName, indexPatterns, capabilities); } }, - [filterManager, indexPatternId, indexPatterns, indexPattern] + [filterManager, indexPatternId, indexPatterns, indexPattern, capabilities] ); const TopNavMenu = navigation.ui.TopNavMenu; diff --git a/src/plugins/discover/public/application/helpers/popularize_field.test.ts b/src/plugins/discover/public/application/helpers/popularize_field.test.ts index 8be23c4270438..7ae3994abd21a 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.test.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.test.ts @@ -6,15 +6,27 @@ * Side Public License, v 1. */ +import { Capabilities } from 'kibana/public'; import { IndexPattern, IndexPatternsService } from '../../../../data/public'; import { popularizeField } from './popularize_field'; +const capabilities = ({ + indexPatterns: { + save: true, + }, +} as unknown) as Capabilities; + describe('Popularize field', () => { test('returns undefined if index pattern lacks id', async () => { const indexPattern = ({} as unknown) as IndexPattern; const fieldName = '@timestamp'; const indexPatternsService = ({} as unknown) as IndexPatternsService; - const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + const result = await popularizeField( + indexPattern, + fieldName, + indexPatternsService, + capabilities + ); expect(result).toBeUndefined(); }); @@ -26,7 +38,12 @@ describe('Popularize field', () => { } as unknown) as IndexPattern; const fieldName = '@timestamp'; const indexPatternsService = ({} as unknown) as IndexPatternsService; - const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + const result = await popularizeField( + indexPattern, + fieldName, + indexPatternsService, + capabilities + ); expect(result).toBeUndefined(); }); @@ -44,7 +61,12 @@ describe('Popularize field', () => { const indexPatternsService = ({ updateSavedObject: async () => {}, } as unknown) as IndexPatternsService; - const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + const result = await popularizeField( + indexPattern, + fieldName, + indexPatternsService, + capabilities + ); expect(result).toBeUndefined(); expect(field.count).toEqual(1); }); @@ -65,7 +87,34 @@ describe('Popularize field', () => { throw new Error('unknown error'); }, } as unknown) as IndexPatternsService; - const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + const result = await popularizeField( + indexPattern, + fieldName, + indexPatternsService, + capabilities + ); + expect(result).toBeUndefined(); + }); + + test('should not try to update index pattern without permissions', async () => { + const field = { + count: 0, + }; + const indexPattern = ({ + id: 'id', + fields: { + getByName: () => field, + }, + } as unknown) as IndexPattern; + const fieldName = '@timestamp'; + const indexPatternsService = ({ + updateSavedObject: jest.fn(), + } as unknown) as IndexPatternsService; + const result = await popularizeField(indexPattern, fieldName, indexPatternsService, ({ + indexPatterns: { save: false }, + } as unknown) as Capabilities); expect(result).toBeUndefined(); + expect(indexPatternsService.updateSavedObject).not.toHaveBeenCalled(); + expect(field.count).toEqual(0); }); }); diff --git a/src/plugins/discover/public/application/helpers/popularize_field.ts b/src/plugins/discover/public/application/helpers/popularize_field.ts index 4ade7d1768419..90968dd7c3d58 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.ts @@ -6,14 +6,16 @@ * Side Public License, v 1. */ +import type { Capabilities } from 'kibana/public'; import { IndexPattern, IndexPatternsContract } from '../../../../data/public'; async function popularizeField( indexPattern: IndexPattern, fieldName: string, - indexPatternsService: IndexPatternsContract + indexPatternsService: IndexPatternsContract, + capabilities: Capabilities ) { - if (!indexPattern.id) return; + if (!indexPattern.id || !capabilities?.indexPatterns?.save) return; const field = indexPattern.fields.getByName(fieldName); if (!field) { return; diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index fe5e4fb4f1e0e..954701f0087f4 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -694,11 +694,12 @@ Array [ "all": Array [ "search", "query", - "index-pattern", "url", "search-session", ], - "read": Array [], + "read": Array [ + "index-pattern", + ], }, "ui": Array [ "show", @@ -1223,11 +1224,12 @@ Array [ "all": Array [ "search", "query", - "index-pattern", "url", "search-session", ], - "read": Array [], + "read": Array [ + "index-pattern", + ], }, "ui": Array [ "show", diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index f3fd934b4c3e8..14265948c2dee 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -40,8 +40,8 @@ export const buildOSSFeatures = ({ api: ['fileUpload:analyzeFile'], catalogue: ['discover'], savedObject: { - all: ['search', 'query', 'index-pattern'], - read: [], + all: ['search', 'query'], + read: ['index-pattern'], }, ui: ['show', 'save', 'saveQuery'], }, From d62ff559b8b287ff3569186b192f7a43548808b7 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 17 Aug 2021 10:03:54 -0500 Subject: [PATCH 06/11] [index pattern management] load index pattern list without loading field lists (#108823) * don't load field list for index pattern list --- ...ins-data-public.indexpatternlistitem.id.md | 11 ++++ ...lugins-data-public.indexpatternlistitem.md | 21 ++++++ ...-data-public.indexpatternlistitem.title.md | 11 ++++ ...s-data-public.indexpatternlistitem.type.md | 11 ++++ ...ta-public.indexpatternlistitem.typemeta.md | 11 ++++ ...ta-public.indexpatternsservice.getcache.md | 2 +- ...ic.indexpatternsservice.getidswithtitle.md | 5 +- ...lugins-data-public.indexpatternsservice.md | 4 +- .../kibana-plugin-plugins-data-public.md | 2 + ...lugin-plugins-data-public.typemeta.aggs.md | 11 ++++ ...ana-plugin-plugins-data-public.typemeta.md | 19 ++++++ ...gin-plugins-data-public.typemeta.params.md | 13 ++++ ...ta-server.indexpatternsservice.getcache.md | 2 +- ...er.indexpatternsservice.getidswithtitle.md | 5 +- ...lugins-data-server.indexpatternsservice.md | 4 +- .../data/common/index_patterns/index.ts | 2 +- .../index_patterns/index_patterns.test.ts | 4 +- .../index_patterns/index_patterns.ts | 26 ++++++-- src/plugins/data/public/index.ts | 3 +- .../data/public/index_patterns/index.ts | 2 +- src/plugins/data/public/public.api.md | 64 ++++++++++++------- src/plugins/data/server/server.api.md | 10 +-- .../public/components/utils.ts | 58 +++++++++-------- 23 files changed, 219 insertions(+), 82 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.id.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.title.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.type.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.aggs.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.params.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.id.md new file mode 100644 index 0000000000000..88c3a7d3654be --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) > [id](./kibana-plugin-plugins-data-public.indexpatternlistitem.id.md) + +## IndexPatternListItem.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.md new file mode 100644 index 0000000000000..609a5e0d9ef2c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) + +## IndexPatternListItem interface + +Signature: + +```typescript +export interface IndexPatternListItem +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-plugins-data-public.indexpatternlistitem.id.md) | string | | +| [title](./kibana-plugin-plugins-data-public.indexpatternlistitem.title.md) | string | | +| [type](./kibana-plugin-plugins-data-public.indexpatternlistitem.type.md) | string | | +| [typeMeta](./kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md) | TypeMeta | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.title.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.title.md new file mode 100644 index 0000000000000..26f292bf0d17b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) > [title](./kibana-plugin-plugins-data-public.indexpatternlistitem.title.md) + +## IndexPatternListItem.title property + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.type.md new file mode 100644 index 0000000000000..467e8bb81b159 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) > [type](./kibana-plugin-plugins-data-public.indexpatternlistitem.type.md) + +## IndexPatternListItem.type property + +Signature: + +```typescript +type?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md new file mode 100644 index 0000000000000..3b93c5111f8dd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) > [typeMeta](./kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md) + +## IndexPatternListItem.typeMeta property + +Signature: + +```typescript +typeMeta?: TypeMeta; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md index ad2a167bd8c74..1f0148df596af 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md @@ -7,5 +7,5 @@ Signature: ```typescript -getCache: () => Promise[] | null | undefined>; +getCache: () => Promise>[] | null | undefined>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md index 7d29ced66afa8..b2dcddce0457c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md @@ -9,8 +9,5 @@ Get list of index pattern ids with titles Signature: ```typescript -getIdsWithTitle: (refresh?: boolean) => Promise>; +getIdsWithTitle: (refresh?: boolean) => Promise; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 26b393a5fb5b6..572a122066868 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -25,13 +25,13 @@ export declare class IndexPatternsService | [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | | [find](./kibana-plugin-plugins-data-public.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | -| [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<Pick<IndexPatternAttributes, "type" | "title" | "typeMeta">>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | | [getDefaultId](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefaultid.md) | | () => Promise<string | null> | Get default index pattern id | | [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | | [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | -| [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | +| [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<IndexPatternListItem[]> | Get list of index pattern ids with titles | | [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | | [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | | [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 760f6d8651428..185dd771c4ace 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -66,6 +66,7 @@ | [IKibanaSearchRequest](./kibana-plugin-plugins-data-public.ikibanasearchrequest.md) | | | [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | | | [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | Interface for an index pattern saved object | +| [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) | | | [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) | Static index pattern format Serialized data object, representing index pattern attributes and state | | [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | @@ -84,6 +85,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | +| [TypeMeta](./kibana-plugin-plugins-data-public.typemeta.md) | | | [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) | Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | ## Variables diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.aggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.aggs.md new file mode 100644 index 0000000000000..d2ab7ef72a4a5 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.aggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TypeMeta](./kibana-plugin-plugins-data-public.typemeta.md) > [aggs](./kibana-plugin-plugins-data-public.typemeta.aggs.md) + +## TypeMeta.aggs property + +Signature: + +```typescript +aggs?: Record; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.md new file mode 100644 index 0000000000000..dcc6500d54c5e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TypeMeta](./kibana-plugin-plugins-data-public.typemeta.md) + +## TypeMeta interface + +Signature: + +```typescript +export interface TypeMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [aggs](./kibana-plugin-plugins-data-public.typemeta.aggs.md) | Record<string, AggregationRestrictions> | | +| [params](./kibana-plugin-plugins-data-public.typemeta.params.md) | {
rollup_index: string;
} | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.params.md new file mode 100644 index 0000000000000..6646f3c63ecc1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.params.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TypeMeta](./kibana-plugin-plugins-data-public.typemeta.md) > [params](./kibana-plugin-plugins-data-public.typemeta.params.md) + +## TypeMeta.params property + +Signature: + +```typescript +params?: { + rollup_index: string; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md index 821c06984e55e..db765cf54d048 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md @@ -7,5 +7,5 @@ Signature: ```typescript -getCache: () => Promise[] | null | undefined>; +getCache: () => Promise>[] | null | undefined>; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md index 6433c78483545..a047b056e0ed5 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md @@ -9,8 +9,5 @@ Get list of index pattern ids with titles Signature: ```typescript -getIdsWithTitle: (refresh?: boolean) => Promise>; +getIdsWithTitle: (refresh?: boolean) => Promise; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index f5e845ced3cd1..64c46fe4abbd8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -25,13 +25,13 @@ export declare class IndexPatternsService | [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | | [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | | [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | -| [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<Pick<IndexPatternAttributes, "type" | "title" | "typeMeta">>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | | [getDefaultId](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefaultid.md) | | () => Promise<string | null> | Get default index pattern id | | [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | | [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | -| [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | +| [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<IndexPatternListItem[]> | Get list of index pattern ids with titles | | [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | | [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | | [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 340162e8bda70..f493b417b47ef 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -10,6 +10,6 @@ export * from './constants'; export * from './fields'; export * from './types'; export { IndexPatternsService, IndexPatternsContract } from './index_patterns'; -export type { IndexPattern } from './index_patterns'; +export type { IndexPattern, IndexPatternListItem } from './index_patterns'; export * from './errors'; export * from './expressions'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index a80e97b4e2cab..c6715fac5d9af 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -137,11 +137,11 @@ describe('IndexPatterns', () => { expect((await indexPatterns.get(id)).fields.length).toBe(1); }); - test('savedObjectCache pre-fetches only title', async () => { + test('savedObjectCache pre-fetches title, type, typeMeta', async () => { expect(await indexPatterns.getIds()).toEqual(['id']); expect(savedObjectsClient.find).toHaveBeenCalledWith({ type: 'index-pattern', - fields: ['title'], + fields: ['title', 'type', 'typeMeta'], perPage: 10000, }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 64628f7165f27..d20cfc98ba059 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -28,6 +28,7 @@ import { FieldAttrs, FieldSpec, IndexPatternFieldMap, + TypeMeta, } from '../types'; import { FieldFormatsStartCommon, FORMATS_UI_SETTINGS } from '../../../../field_formats/common/'; import { UI_SETTINGS, SavedObject } from '../../../common'; @@ -39,8 +40,21 @@ import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; -export interface IndexPatternSavedObjectAttrs { +export type IndexPatternSavedObjectAttrs = Pick< + IndexPatternAttributes, + 'title' | 'type' | 'typeMeta' +>; + +export type IndexPatternListSavedObjectAttrs = Pick< + IndexPatternAttributes, + 'title' | 'type' | 'typeMeta' +>; + +export interface IndexPatternListItem { + id: string; title: string; + type?: string; + typeMeta?: TypeMeta; } interface IndexPatternsServiceDeps { @@ -94,7 +108,7 @@ export class IndexPatternsService { private async refreshSavedObjectsCache() { const so = await this.savedObjectsClient.find({ type: INDEX_PATTERN_SAVED_OBJECT_TYPE, - fields: ['title'], + fields: ['title', 'type', 'typeMeta'], perPage: 10000, }); this.savedObjectsCache = so; @@ -152,9 +166,7 @@ export class IndexPatternsService { * Get list of index pattern ids with titles * @param refresh Force refresh of index pattern list */ - getIdsWithTitle = async ( - refresh: boolean = false - ): Promise> => { + getIdsWithTitle = async (refresh: boolean = false): Promise => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); } @@ -164,6 +176,8 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => ({ id: obj?.id, title: obj?.attributes?.title, + type: obj?.attributes?.type, + typeMeta: obj?.attributes?.typeMeta && JSON.parse(obj?.attributes?.typeMeta), })); }; @@ -559,7 +573,7 @@ export class IndexPatternsService { const createdIndexPattern = await this.initFromSavedObject(response); this.indexPatternCache.set(createdIndexPattern.id!, Promise.resolve(createdIndexPattern)); if (this.savedObjectsCache) { - this.savedObjectsCache.push(response as SavedObject); + this.savedObjectsCache.push(response as SavedObject); } return createdIndexPattern; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 99c89ac69b795..0602f51889a6c 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -62,7 +62,7 @@ export const indexPatterns = { flattenHitWrapper, }; -export { IndexPatternsContract, IndexPattern, IndexPatternField } from './index_patterns'; +export { IndexPatternsContract, IndexPattern, IndexPatternField, TypeMeta } from './index_patterns'; export { IIndexPattern, @@ -79,6 +79,7 @@ export { INDEX_PATTERN_SAVED_OBJECT_TYPE, AggregationRestrictions, IndexPatternType, + IndexPatternListItem, } from '../common'; export { DuplicateIndexPatternError } from '../common/index_patterns/errors'; diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 1bdd17af2a78d..e23fc789656af 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -16,7 +16,7 @@ export { } from '../../common/index_patterns/lib'; export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './index_patterns'; -export { IndexPatternField, IIndexPatternFieldList } from '../../common/index_patterns'; +export { IndexPatternField, IIndexPatternFieldList, TypeMeta } from '../../common/index_patterns'; export { IndexPatternsService, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 7da9f0293ec6f..485dad1daea9d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1269,7 +1269,6 @@ export class IndexPattern implements IIndexPattern { title: string; toSpec(): IndexPatternSpec; type: string | undefined; - // Warning: (ae-forgotten-export) The symbol "TypeMeta" needs to be exported by the entry point index.d.ts typeMeta?: TypeMeta; version: string | undefined; } @@ -1369,6 +1368,20 @@ export class IndexPatternField implements IFieldType { get visualizable(): boolean; } +// Warning: (ae-missing-release-tag) "IndexPatternListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface IndexPatternListItem { + // (undocumented) + id: string; + // (undocumented) + title: string; + // (undocumented) + type?: string; + // (undocumented) + typeMeta?: TypeMeta; +} + // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts @@ -1457,19 +1470,14 @@ export class IndexPatternsService { fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; find: (search: string, size?: number) => Promise; get: (id: string) => Promise; - // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts - // // (undocumented) - getCache: () => Promise[] | null | undefined>; + getCache: () => Promise>[] | null | undefined>; getDefault: () => Promise; getDefaultId: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; getFieldsForWildcard: (options: GetFieldsOptions) => Promise; getIds: (refresh?: boolean) => Promise; - getIdsWithTitle: (refresh?: boolean) => Promise>; + getIdsWithTitle: (refresh?: boolean) => Promise; getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; @@ -2275,6 +2283,18 @@ export type TimeRange = { mode?: 'absolute' | 'relative'; }; +// Warning: (ae-missing-release-tag) "TypeMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface TypeMeta { + // (undocumented) + aggs?: Record; + // (undocumented) + params?: { + rollup_index: string; + }; +} + // Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2334,20 +2354,20 @@ export interface WaitUntilNextSessionCompletesOptions { // src/plugins/data/public/index.ts:53:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:53:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:53:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:210:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:210:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:210:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:212:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:213:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:222:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:223:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:224:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:225:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:229:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:230:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:233:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:237:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:211:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:211:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:211:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:213:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:214:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:223:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:224:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:225:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:226:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:230:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:231:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:234:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:235:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:62:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 71863ecb61341..f684586917fe7 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -517,20 +517,16 @@ class IndexPatternsService { fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; find: (search: string, size?: number) => Promise; get: (id: string) => Promise; - // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts - // // (undocumented) - getCache: () => Promise[] | null | undefined>; + getCache: () => Promise>[] | null | undefined>; getDefault: () => Promise; getDefaultId: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts getFieldsForWildcard: (options: GetFieldsOptions) => Promise; getIds: (refresh?: boolean) => Promise; - getIdsWithTitle: (refresh?: boolean) => Promise>; + // Warning: (ae-forgotten-export) The symbol "IndexPatternListItem" needs to be exported by the entry point index.d.ts + getIdsWithTitle: (refresh?: boolean) => Promise; getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts index 6520de95028c6..1273a1073fbbf 100644 --- a/src/plugins/index_pattern_management/public/components/utils.ts +++ b/src/plugins/index_pattern_management/public/components/utils.ts @@ -7,7 +7,7 @@ */ import { IndexPatternsContract } from 'src/plugins/data/public'; -import { IndexPattern, IFieldType } from 'src/plugins/data/public'; +import { IFieldType, IndexPattern, IndexPatternListItem } from 'src/plugins/data/public'; import { i18n } from '@kbn/i18n'; const defaultIndexPatternListName = i18n.translate( @@ -24,8 +24,8 @@ const rollupIndexPatternListName = i18n.translate( } ); -const isRollup = (indexPattern: IndexPattern) => { - return indexPattern.type === 'rollup'; +const isRollup = (indexPatternType: string = '') => { + return indexPatternType === 'rollup'; }; export async function getIndexPatterns( @@ -33,24 +33,22 @@ export async function getIndexPatterns( indexPatternsService: IndexPatternsContract ) { const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(true); - const indexPatternsListItems = await Promise.all( - existingIndexPatterns.map(async ({ id, title }) => { - const isDefault = defaultIndex === id; - const pattern = await indexPatternsService.get(id); - const tags = getTags(pattern, isDefault); + const indexPatternsListItems = existingIndexPatterns.map((idxPattern) => { + const { id, title } = idxPattern; + const isDefault = defaultIndex === id; + const tags = getTags(idxPattern, isDefault); - return { - id, - title, - default: isDefault, - tags, - // the prepending of 0 at the default pattern takes care of prioritization - // so the sorting will but the default index on top - // or on bottom of a the table - sort: `${isDefault ? '0' : '1'}${title}`, - }; - }) - ); + return { + id, + title, + default: isDefault, + tags, + // the prepending of 0 at the default pattern takes care of prioritization + // so the sorting will but the default index on top + // or on bottom of a the table + sort: `${isDefault ? '0' : '1'}${title}`, + }; + }); return ( indexPatternsListItems.sort((a, b) => { @@ -65,7 +63,7 @@ export async function getIndexPatterns( ); } -export const getTags = (indexPattern: IndexPattern, isDefault: boolean) => { +export const getTags = (indexPattern: IndexPatternListItem | IndexPattern, isDefault: boolean) => { const tags = []; if (isDefault) { tags.push({ @@ -73,7 +71,7 @@ export const getTags = (indexPattern: IndexPattern, isDefault: boolean) => { name: defaultIndexPatternListName, }); } - if (isRollup(indexPattern)) { + if (isRollup(indexPattern.type)) { tags.push({ key: 'rollup', name: rollupIndexPatternListName, @@ -82,17 +80,21 @@ export const getTags = (indexPattern: IndexPattern, isDefault: boolean) => { return tags; }; -export const areScriptedFieldsEnabled = (indexPattern: IndexPattern) => { - return !isRollup(indexPattern); +export const areScriptedFieldsEnabled = (indexPattern: IndexPatternListItem | IndexPattern) => { + return !isRollup(indexPattern.type); }; -export const getFieldInfo = (indexPattern: IndexPattern, field: IFieldType) => { - if (!isRollup(indexPattern)) { +export const getFieldInfo = ( + indexPattern: IndexPatternListItem | IndexPattern, + field: IFieldType +) => { + if (!isRollup(indexPattern.type)) { return []; } - const allAggs = indexPattern.typeMeta && indexPattern.typeMeta.aggs; - const fieldAggs = allAggs && Object.keys(allAggs).filter((agg) => allAggs[agg][field.name]); + const allAggs = indexPattern.typeMeta?.aggs; + const fieldAggs: string[] | undefined = + allAggs && Object.keys(allAggs).filter((agg) => allAggs[agg][field.name]); if (!fieldAggs || !fieldAggs.length) { return []; From d07f7a5d5e30e8ca272413761cdfe3be1508a34b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 17 Aug 2021 17:15:44 +0200 Subject: [PATCH 07/11] [ML] Move APM Latency Correlations from flyout to transactions page. (#107266) - Moves APM Latency Correlations from flyout to transactions page. - Introduces a tab based navigation for `Trace samples` / `Latency correlations` / `Failed transactions correlations` - For trace samples, the previous low detailed histogram chart gets replaced with the new log log based histogram chart. Users can drag select a range of lantency to filter trace samples. - Removes code related to the previous distribution chart. Renames `useTransactionDistributionFetcher` to `useTransactionTraceSamplesFetcher`. Instead of a histogram with top hits, this will now just return a sample of traces for the given filters. --- .../search_strategies/correlations/types.ts | 2 +- .../app/correlations/error_correlations.tsx | 12 +- .../components/app/correlations/index.tsx | 258 ------- .../app/correlations/latency_correlations.tsx | 662 ++++++++++-------- ... => latency_correlations_help_popover.tsx} | 0 .../correlations/ml_latency_correlations.tsx | 430 ------------ .../app/correlations/use_correlations.ts | 123 ---- .../Distribution/custom_tooltip.tsx | 68 -- .../Distribution/distribution.test.ts | 56 -- .../Distribution/index.tsx | 256 ------- .../distribution/index.tsx | 188 +++++ .../failed_transactions_correlations_tab.tsx | 94 +++ .../app/transaction_details/index.tsx | 95 +-- .../latency_correlations_tab.tsx | 60 ++ .../transaction_details/trace_samples_tab.tsx | 72 ++ .../transaction_details_tabs.tsx | 139 ++++ .../app/transaction_details/types.ts | 18 + .../waterfall_with_summary/index.tsx | 15 +- .../app/transaction_overview/index.tsx | 1 + .../templates/apm_service_template/index.tsx | 5 - .../components/shared/Links/url_helpers.ts | 2 + .../transaction_distribution_chart/index.tsx} | 137 +++- .../url_params_context/resolve_url_params.ts | 4 + .../context/url_params_context/types.ts | 2 + .../use_transaction_distribution_fetcher.ts | 232 +++--- ...ransaction_latency_correlations_fetcher.ts | 160 +++++ .../use_transaction_trace_samples_fetcher.ts | 101 +++ .../correlations/async_search_service.ts | 28 +- .../queries/get_query_with_params.test.ts | 8 - .../queries/get_query_with_params.ts | 19 - .../__snapshots__/queries.test.ts.snap | 28 +- .../distribution/get_buckets/index.ts | 217 ------ .../distribution/get_distribution_max.ts | 79 --- .../lib/transactions/distribution/index.ts | 80 --- .../server/lib/transactions/queries.test.ts | 7 +- .../trace_samples/get_trace_samples/index.ts | 107 +++ .../lib/transactions/trace_samples/index.ts | 49 ++ .../plugins/apm/server/routes/transactions.ts | 23 +- .../translations/translations/ja-JP.json | 25 - .../translations/translations/zh-CN.json | 26 - .../tests/feature_controls.ts | 2 +- .../test/apm_api_integration/tests/index.ts | 4 +- .../tests/transactions/distribution.ts | 95 --- .../tests/transactions/trace_samples.ts | 133 ++++ .../apm/correlations/latency_correlations.ts | 60 +- 45 files changed, 1871 insertions(+), 2311 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/app/correlations/index.tsx rename x-pack/plugins/apm/public/components/app/correlations/{ml_latency_correlations_help_popover.tsx => latency_correlations_help_popover.tsx} (100%) delete mode 100644 x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx delete mode 100644 x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts delete mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx delete mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts delete mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/types.ts rename x-pack/plugins/apm/public/components/{app/correlations/correlations_chart.tsx => shared/charts/transaction_distribution_chart/index.tsx} (61%) create mode 100644 x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts create mode 100644 x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/distribution/index.ts create mode 100644 x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts create mode 100644 x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts delete mode 100644 x-pack/test/apm_api_integration/tests/transactions/distribution.ts create mode 100644 x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts index 70c1c7524cfe9..703106628f561 100644 --- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts @@ -27,7 +27,7 @@ export interface SearchServiceParams { start?: string; end?: string; percentileThreshold?: number; - percentileThresholdValue?: number; + analyzeCorrelations?: boolean; } export interface SearchServiceFetchParams extends SearchServiceParams { diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index 298206f30d614..2e5887cab9918 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -42,11 +42,7 @@ type CorrelationsApiResponse = NonNullable< APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> >; -interface Props { - onClose: () => void; -} - -export function ErrorCorrelations({ onClose }: Props) { +export function ErrorCorrelations() { const [ selectedSignificantTerm, setSelectedSignificantTerm, @@ -130,7 +126,9 @@ export function ErrorCorrelations({ onClose }: Props) { ); const trackApmEvent = useUiTracker({ app: 'apm' }); - trackApmEvent({ metric: 'view_errors_correlations' }); + trackApmEvent({ metric: 'view_failed_transactions' }); + + const onFilter = () => {}; return ( <> @@ -175,7 +173,7 @@ export function ErrorCorrelations({ onClose }: Props) { } status={correlationsStatus} setSelectedSignificantTerm={setSelectedSignificantTerm} - onFilter={onClose} + onFilter={onFilter} /> diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx deleted file mode 100644 index 57ba75d945ee5..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useState } from 'react'; -import { - EuiButtonEmpty, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiPortal, - EuiCode, - EuiLink, - EuiCallOut, - EuiButton, - EuiTab, - EuiTabs, - EuiSpacer, - EuiBetaBadge, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; -import { MlLatencyCorrelations } from './ml_latency_correlations'; -import { ErrorCorrelations } from './error_correlations'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { createHref } from '../../shared/Links/url_helpers'; -import { - METRIC_TYPE, - useTrackMetric, -} from '../../../../../observability/public'; -import { isActivePlatinumLicense } from '../../../../common/license_check'; -import { useLicenseContext } from '../../../context/license/use_license_context'; -import { LicensePrompt } from '../../shared/license_prompt'; -import { IUrlParams } from '../../../context/url_params_context/types'; -import { - IStickyProperty, - StickyProperties, -} from '../../shared/sticky_properties'; -import { getEnvironmentLabel } from '../../../../common/environment_filter_values'; -import { - SERVICE_ENVIRONMENT, - SERVICE_NAME, - TRANSACTION_NAME, -} from '../../../../common/elasticsearch_fieldnames'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useApmParams } from '../../../hooks/use_apm_params'; - -const errorRateTab = { - key: 'errorRate', - label: i18n.translate('xpack.apm.correlations.tabs.errorRateLabel', { - defaultMessage: 'Failed transaction rate', - }), - component: ErrorCorrelations, -}; -const latencyCorrelationsTab = { - key: 'latencyCorrelations', - label: i18n.translate('xpack.apm.correlations.tabs.latencyLabel', { - defaultMessage: 'Latency', - }), - component: MlLatencyCorrelations, -}; -const tabs = [latencyCorrelationsTab, errorRateTab]; - -export function Correlations() { - const license = useLicenseContext(); - const hasActivePlatinumLicense = isActivePlatinumLicense(license); - const { urlParams } = useUrlParams(); - const { serviceName } = useApmServiceContext(); - - const { - query: { environment }, - } = useApmParams('/services/:serviceName'); - - const history = useHistory(); - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const [currentTab, setCurrentTab] = useState(latencyCorrelationsTab.key); - const { component: TabContent } = - tabs.find((tab) => tab.key === currentTab) ?? latencyCorrelationsTab; - const metric = { - app: 'apm' as const, - metric: hasActivePlatinumLicense - ? 'correlations_flyout_view' - : 'correlations_license_prompt', - metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, - }; - useTrackMetric(metric); - useTrackMetric({ ...metric, delay: 15000 }); - - const stickyProperties: IStickyProperty[] = useMemo(() => { - const properties: IStickyProperty[] = []; - if (serviceName !== undefined) { - properties.push({ - label: i18n.translate('xpack.apm.correlations.serviceLabel', { - defaultMessage: 'Service', - }), - fieldName: SERVICE_NAME, - val: serviceName, - width: '20%', - }); - } - - properties.push({ - label: i18n.translate('xpack.apm.correlations.environmentLabel', { - defaultMessage: 'Environment', - }), - fieldName: SERVICE_ENVIRONMENT, - val: getEnvironmentLabel(environment), - width: '20%', - }); - - if (urlParams.transactionName) { - properties.push({ - label: i18n.translate('xpack.apm.correlations.transactionLabel', { - defaultMessage: 'Transaction', - }), - fieldName: TRANSACTION_NAME, - val: urlParams.transactionName, - width: '20%', - }); - } - - return properties; - }, [serviceName, environment, urlParams.transactionName]); - - return ( - <> - { - setIsFlyoutVisible(true); - }} - > - {i18n.translate('xpack.apm.correlations.buttonLabel', { - defaultMessage: 'View correlations', - })} - - - {isFlyoutVisible && ( - - setIsFlyoutVisible(false)} - > - - -

- {CORRELATIONS_TITLE} -   - -

-
- {hasActivePlatinumLicense && ( - <> - - - - {urlParams.kuery ? ( - <> - - - - ) : ( - - )} - - {tabs.map(({ key, label }) => ( - { - setCurrentTab(key); - }} - > - {label} - - ))} - - - )} -
- - {hasActivePlatinumLicense ? ( - <> - setIsFlyoutVisible(false)} /> - - ) : ( - - )} - -
-
- )} - - ); -} - -function Filters({ - urlParams, - history, -}: { - urlParams: IUrlParams; - history: ReturnType; -}) { - if (!urlParams.kuery) { - return null; - } - - return ( - - - {i18n.translate('xpack.apm.correlations.filteringByLabel', { - defaultMessage: 'Filtering by', - })} - - {urlParams.kuery} - - - {i18n.translate('xpack.apm.correlations.clearFiltersLabel', { - defaultMessage: 'Clear', - })} - - - - ); -} - -const CORRELATIONS_TITLE = i18n.translate('xpack.apm.correlations.title', { - defaultMessage: 'Correlations', -}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 0871447337780..0a534ba1b945b 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -5,338 +5,426 @@ * 2.0. */ +import React, { useEffect, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { - ScaleType, - Chart, - Axis, - BarSeries, - Position, - Settings, -} from '@elastic/charts'; -import React, { useState } from 'react'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + EuiCallOut, + EuiCode, + EuiAccordion, + EuiPanel, + EuiIcon, + EuiBasicTableColumn, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getDurationFormatter } from '../../../../common/utils/formatters'; +import { FormattedMessage } from '@kbn/i18n/react'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useTransactionLatencyCorrelationsFetcher } from '../../../hooks/use_transaction_latency_correlations_fetcher'; +import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; import { CorrelationsTable, SelectedSignificantTerm, } from './correlations_table'; -import { ChartContainer } from '../../shared/charts/chart_container'; -import { useTheme } from '../../../hooks/use_theme'; -import { CustomFields, PercentileOption } from './custom_fields'; -import { useFieldNames } from './use_field_names'; -import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useUiTracker } from '../../../../../observability/public'; +import { push } from '../../shared/Links/url_helpers'; +import { + enableInspectEsQueries, + useUiTracker, +} from '../../../../../observability/public'; +import { asPreciseDecimal } from '../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; import { useApmParams } from '../../../hooks/use_apm_params'; -type OverallLatencyApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'> ->; - -type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'> ->; +const DEFAULT_PERCENTILE_THRESHOLD = 95; +const isErrorMessage = (arg: unknown): arg is Error => { + return arg instanceof Error; +}; -interface Props { - onClose: () => void; +interface MlCorrelationsTerms { + correlation: number; + ksTest: number; + fieldName: string; + fieldValue: string; + duplicatedFields?: string[]; } -export function LatencyCorrelations({ onClose }: Props) { - const [ - selectedSignificantTerm, - setSelectedSignificantTerm, - ] = useState(null); +export function LatencyCorrelations() { + const { + core: { notifications, uiSettings }, + } = useApmPluginContext(); - const { serviceName } = useApmServiceContext(); + const { serviceName, transactionType } = useApmServiceContext(); const { query: { kuery, environment }, } = useApmParams('/services/:serviceName'); const { urlParams } = useUrlParams(); - const { transactionName, transactionType, start, end } = urlParams; - const { defaultFieldNames } = useFieldNames(); - const [fieldNames, setFieldNames] = useLocalStorage( - `apm.correlations.latency.fields:${serviceName}`, - defaultFieldNames - ); - const hasFieldNames = fieldNames.length > 0; + + const { transactionName, start, end } = urlParams; + + const displayLog = uiSettings.get(enableInspectEsQueries); + + const { + ccsWarning, + log, + error, + histograms, + percentileThresholdValue, + isRunning, + progress, + startFetch, + cancelFetch, + overallHistogram, + } = useTransactionLatencyCorrelationsFetcher({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }); + + // start fetching on load + // we want this effect to execute exactly once after the component mounts + useEffect(() => { + startFetch(); + + return () => { + // cancel any running async partial request when unmounting the component + // we want this effect to execute exactly once after the component mounts + cancelFetch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (isErrorMessage(error)) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.errorTitle', + { + defaultMessage: 'An error occurred fetching correlations', + } + ), + text: error.toString(), + }); + } + }, [error, notifications.toasts]); const [ - durationPercentile, - setDurationPercentile, - ] = useLocalStorage( - `apm.correlations.latency.threshold:${serviceName}`, - 75 - ); + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); - const { data: overallData, status: overallStatus } = useFetcher( - (callApmApi) => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/latency/overall_distribution', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }, - }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - ] - ); + let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined; - const maxLatency = overallData?.maxLatency; - const distributionInterval = overallData?.distributionInterval; - const fieldNamesCommaSeparated = fieldNames.join(','); + if (histograms.length > 0 && selectedSignificantTerm !== null) { + selectedHistogram = histograms.find( + (h) => + h.field === selectedSignificantTerm.fieldName && + h.value === selectedSignificantTerm.fieldValue + ); + } + const history = useHistory(); + const trackApmEvent = useUiTracker({ app: 'apm' }); - const { data: correlationsData, status: correlationsStatus } = useFetcher( - (callApmApi) => { - if (start && end && hasFieldNames && maxLatency && distributionInterval) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/latency/slow_transactions', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - durationPercentile: durationPercentile.toString(10), - fieldNames: fieldNamesCommaSeparated, - maxLatency: maxLatency.toString(10), - distributionInterval: distributionInterval.toString(10), + const mlCorrelationColumns: Array< + EuiBasicTableColumn + > = useMemo( + () => [ + { + width: '116px', + field: 'correlation', + name: ( + + <> + {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel', + { + defaultMessage: 'Correlation', + } + )} + + + + ), + render: (correlation: number) => { + return
{asPreciseDecimal(correlation, 2)}
; + }, + }, + { + field: 'fieldName', + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel', + { defaultMessage: 'Field name' } + ), + }, + { + field: 'fieldValue', + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel', + { defaultMessage: 'Field value' } + ), + render: (fieldValue: string) => String(fieldValue).slice(0, 50), + }, + { + width: '100px', + actions: [ + { + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel', + { defaultMessage: 'Filter' } + ), + description: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription', + { defaultMessage: 'Filter by value' } + ), + icon: 'plusInCircle', + type: 'icon', + onClick: (term: MlCorrelationsTerms) => { + push(history, { + query: { + kuery: `${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_include_filter' }); }, }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - durationPercentile, - fieldNamesCommaSeparated, - hasFieldNames, - maxLatency, - distributionInterval, - ] + { + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeLabel', + { defaultMessage: 'Exclude' } + ), + description: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeDescription', + { defaultMessage: 'Filter out value' } + ), + icon: 'minusInCircle', + type: 'icon', + onClick: (term: MlCorrelationsTerms) => { + push(history, { + query: { + kuery: `not ${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + trackApmEvent({ metric: 'correlations_term_exclude_filter' }); + }, + }, + ], + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel', + { defaultMessage: 'Filter' } + ), + }, + ], + [history, trackApmEvent] ); - const trackApmEvent = useUiTracker({ app: 'apm' }); - trackApmEvent({ metric: 'view_latency_correlations' }); + const histogramTerms: MlCorrelationsTerms[] = useMemo(() => { + return histograms.map((d) => { + return { + fieldName: d.field, + fieldValue: d.value, + ksTest: d.ksTest, + correlation: d.correlation, + duplicatedFields: d.duplicatedFields, + }; + }); + }, [histograms]); return ( <> - - - -

- {i18n.translate('xpack.apm.correlations.latency.description', { - defaultMessage: - 'What is slowing down my service? Correlations will help discover a slower performance in a particular cohort of your data. Either by host, version, or other custom fields.', - })} -

-
-
- - - - -

- {i18n.translate( - 'xpack.apm.correlations.latency.chart.title', - { defaultMessage: 'Latency distribution' } - )} -

-
- + + +
+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.panelTitle', + { + defaultMessage: 'Latency distribution', } - status={overallStatus} - selectedSignificantTerm={selectedSignificantTerm} - /> - - - - - - - - + )} +
+
- - ); -} -function getAxisMaxes(data?: OverallLatencyApiResponse) { - if (!data?.overallDistribution) { - return { xMax: 0, yMax: 0 }; - } - const { overallDistribution } = data; - const xValues = overallDistribution.map((p) => p.x ?? 0); - const yValues = overallDistribution.map((p) => p.y ?? 0); - return { - xMax: Math.max(...xValues), - yMax: Math.max(...yValues), - }; -} + -function getSelectedDistribution( - significantTerms: CorrelationsApiResponse['significantTerms'], - selectedSignificantTerm: SelectedSignificantTerm -) { - if (!significantTerms) { - return []; - } - return ( - significantTerms.find( - ({ fieldName, fieldValue }) => - selectedSignificantTerm.fieldName === fieldName && - selectedSignificantTerm.fieldValue === fieldValue - )?.distribution || [] - ); -} - -function LatencyDistributionChart({ - overallData, - correlationsData, - selectedSignificantTerm, - status, -}: { - overallData?: OverallLatencyApiResponse; - correlationsData?: CorrelationsApiResponse['significantTerms']; - selectedSignificantTerm: SelectedSignificantTerm | null; - status: FETCH_STATUS; -}) { - const theme = useTheme(); - const { xMax, yMax } = getAxisMaxes(overallData); - const durationFormatter = getDurationFormatter(xMax); + - return ( - - - { - const start = durationFormatter(obj.value); - const end = durationFormatter( - obj.value + overallData?.distributionInterval - ); + + + + {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.tableTitle', + { + defaultMessage: 'Correlations', + } + )} + + - return `${start.value} - ${end.formatted}`; - }, - }} - /> - durationFormatter(d).formatted} - /> - `${d}%`} - domain={{ min: 0, max: yMax }} - /> + - + + {!isRunning && ( + + + )} - xScaleType={ScaleType.Linear} - yScaleType={ScaleType.Linear} - xAccessor={'x'} - yAccessors={['y']} - color={theme.eui.euiColorVis1} - data={overallData?.overallDistribution || []} - minBarHeight={5} - tickFormat={(d) => `${roundFloat(d)}%`} - /> - - {correlationsData && selectedSignificantTerm ? ( - + + + )} + + + + + + + + + + + + + + + + +
+ {ccsWarning && ( + <> + + `${roundFloat(d)}%`} + color="warning" + > +

+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody', + { + defaultMessage: + 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.', + } + )} +

+
+ + )} + +
+ {histograms.length > 0 && selectedHistogram !== undefined && ( + + )} + {histograms.length < 1 && progress > 0.99 ? ( + <> + + + + + ) : null} - - +
+ {log.length > 0 && displayLog && ( + + + {log.map((d, i) => { + const splitItem = d.split(': '); + return ( +

+ + {splitItem[0]} {splitItem[1]} + +

+ ); + })} +
+
+ )} + ); } - -function roundFloat(n: number, digits = 2) { - const factor = Math.pow(10, digits); - return Math.round(n * factor) / factor; -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx rename to x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx deleted file mode 100644 index bbd6648ccaf6e..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useMemo, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { - EuiCallOut, - EuiCode, - EuiAccordion, - EuiPanel, - EuiIcon, - EuiBasicTableColumn, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiSpacer, - EuiText, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { - CorrelationsChart, - replaceHistogramDotsWithBars, -} from './correlations_chart'; -import { - CorrelationsTable, - SelectedSignificantTerm, -} from './correlations_table'; -import { useCorrelations } from './use_correlations'; -import { push } from '../../shared/Links/url_helpers'; -import { - enableInspectEsQueries, - useUiTracker, -} from '../../../../../observability/public'; -import { asPreciseDecimal } from '../../../../common/utils/formatters'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { LatencyCorrelationsHelpPopover } from './ml_latency_correlations_help_popover'; - -const DEFAULT_PERCENTILE_THRESHOLD = 95; -const isErrorMessage = (arg: unknown): arg is Error => { - return arg instanceof Error; -}; - -interface Props { - onClose: () => void; -} - -interface MlCorrelationsTerms { - correlation: number; - ksTest: number; - fieldName: string; - fieldValue: string; - duplicatedFields?: string[]; -} - -export function MlLatencyCorrelations({ onClose }: Props) { - const { - core: { notifications, uiSettings }, - } = useApmPluginContext(); - - const { serviceName, transactionType } = useApmServiceContext(); - const { urlParams } = useUrlParams(); - - const { environment, kuery, transactionName, start, end } = urlParams; - - const displayLog = uiSettings.get(enableInspectEsQueries); - - const { - ccsWarning, - log, - error, - histograms, - percentileThresholdValue, - isRunning, - progress, - startFetch, - cancelFetch, - overallHistogram: originalOverallHistogram, - } = useCorrelations({ - ...{ - ...{ - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }, - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - }, - }); - - const overallHistogram = useMemo( - () => replaceHistogramDotsWithBars(originalOverallHistogram), - [originalOverallHistogram] - ); - - // start fetching on load - // we want this effect to execute exactly once after the component mounts - useEffect(() => { - startFetch(); - - return () => { - // cancel any running async partial request when unmounting the component - // we want this effect to execute exactly once after the component mounts - cancelFetch(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (isErrorMessage(error)) { - notifications.toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.errorTitle', - { - defaultMessage: 'An error occurred fetching correlations', - } - ), - text: error.toString(), - }); - } - }, [error, notifications.toasts]); - - const [ - selectedSignificantTerm, - setSelectedSignificantTerm, - ] = useState(null); - - let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined; - - if (histograms.length > 0 && selectedSignificantTerm !== null) { - selectedHistogram = histograms.find( - (h) => - h.field === selectedSignificantTerm.fieldName && - h.value === selectedSignificantTerm.fieldValue - ); - } - const history = useHistory(); - const trackApmEvent = useUiTracker({ app: 'apm' }); - - const mlCorrelationColumns: Array< - EuiBasicTableColumn - > = useMemo( - () => [ - { - width: '116px', - field: 'correlation', - name: ( - - <> - {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel', - { - defaultMessage: 'Correlation', - } - )} - - - - ), - render: (correlation: number) => { - return
{asPreciseDecimal(correlation, 2)}
; - }, - }, - { - field: 'fieldName', - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel', - { defaultMessage: 'Field name' } - ), - }, - { - field: 'fieldValue', - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel', - { defaultMessage: 'Field value' } - ), - render: (fieldValue: string) => String(fieldValue).slice(0, 50), - }, - { - width: '100px', - actions: [ - { - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel', - { defaultMessage: 'Filter' } - ), - description: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription', - { defaultMessage: 'Filter by value' } - ), - icon: 'plusInCircle', - type: 'icon', - onClick: (term: MlCorrelationsTerms) => { - push(history, { - query: { - kuery: `${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onClose(); - trackApmEvent({ metric: 'correlations_term_include_filter' }); - }, - }, - { - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeLabel', - { defaultMessage: 'Exclude' } - ), - description: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeDescription', - { defaultMessage: 'Filter out value' } - ), - icon: 'minusInCircle', - type: 'icon', - onClick: (term: MlCorrelationsTerms) => { - push(history, { - query: { - kuery: `not ${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onClose(); - trackApmEvent({ metric: 'correlations_term_exclude_filter' }); - }, - }, - ], - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel', - { defaultMessage: 'Filter' } - ), - }, - ], - [history, onClose, trackApmEvent] - ); - - const histogramTerms: MlCorrelationsTerms[] = useMemo(() => { - return histograms.map((d) => { - return { - fieldName: d.field, - fieldValue: d.value, - ksTest: d.ksTest, - correlation: d.correlation, - duplicatedFields: d.duplicatedFields, - }; - }); - }, [histograms]); - - return ( - <> - - - {!isRunning && ( - - - - )} - {isRunning && ( - - - - )} - - - - - - - - - - - - - - - - - - - {ccsWarning && ( - <> - - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody', - { - defaultMessage: - 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.', - } - )} -

-
- - )} - - - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.chartTitle', - { - defaultMessage: 'Latency distribution for {name} (Log-Log Plot)', - values: { - name: transactionName ?? serviceName, - }, - } - )} -

-
- - - - - -
- {histograms.length > 0 && selectedHistogram !== undefined && ( - - )} - {histograms.length < 1 && progress > 0.99 ? ( - <> - - - - - - ) : null} -
- {log.length > 0 && displayLog && ( - - - {log.map((d, i) => { - const splitItem = d.split(': '); - return ( -

- - {splitItem[0]} {splitItem[1]} - -

- ); - })} -
-
- )} - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts deleted file mode 100644 index 05cb367a9fde7..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useRef, useState } from 'react'; -import type { Subscription } from 'rxjs'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - isCompleteResponse, - isErrorResponse, -} from '../../../../../../../src/plugins/data/public'; -import type { - HistogramItem, - SearchServiceValue, -} from '../../../../common/search_strategies/correlations/types'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmPluginStartDeps } from '../../../plugin'; - -interface CorrelationsOptions { - environment?: string; - kuery?: string; - serviceName?: string; - transactionName?: string; - transactionType?: string; - start?: string; - end?: string; -} - -interface RawResponse { - percentileThresholdValue?: number; - took: number; - values: SearchServiceValue[]; - overallHistogram: HistogramItem[]; - log: string[]; - ccsWarning: boolean; -} - -export const useCorrelations = (params: CorrelationsOptions) => { - const { - services: { data }, - } = useKibana(); - - const [error, setError] = useState(); - const [isComplete, setIsComplete] = useState(false); - const [isRunning, setIsRunning] = useState(false); - const [loaded, setLoaded] = useState(0); - const [rawResponse, setRawResponse] = useState(); - const [timeTook, setTimeTook] = useState(); - const [total, setTotal] = useState(100); - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(); - - function setResponse(response: IKibanaSearchResponse) { - // @TODO: optimize rawResponse.overallHistogram if histogram is the same - setIsRunning(response.isRunning || false); - setRawResponse(response.rawResponse); - setLoaded(response.loaded!); - setTotal(response.total!); - setTimeTook(response.rawResponse.took); - } - - const startFetch = () => { - setError(undefined); - setIsComplete(false); - searchSubscription$.current?.unsubscribe(); - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - - const req = { params }; - - // Submit the search request using the `data.search` service. - searchSubscription$.current = data.search - .search>(req, { - strategy: 'apmCorrelationsSearchStrategy', - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (res: IKibanaSearchResponse) => { - setResponse(res); - if (isCompleteResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setIsRunning(false); - setIsComplete(true); - } else if (isErrorResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setError((res as unknown) as Error); - setIsRunning(false); - } - }, - error: (e: Error) => { - setError(e); - setIsRunning(false); - }, - }); - }; - - const cancelFetch = () => { - searchSubscription$.current?.unsubscribe(); - searchSubscription$.current = undefined; - abortCtrl.current.abort(); - setIsRunning(false); - }; - - return { - ccsWarning: rawResponse?.ccsWarning ?? false, - log: rawResponse?.log ?? [], - error, - histograms: rawResponse?.values ?? [], - percentileThresholdValue: - rawResponse?.percentileThresholdValue ?? undefined, - overallHistogram: rawResponse?.overallHistogram, - isComplete, - isRunning, - progress: loaded / total, - timeTook, - startFetch, - cancelFetch, - }; -}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx deleted file mode 100644 index ba007015b25f8..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { TooltipInfo } from '@elastic/charts'; -import { EuiIcon, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { TimeFormatter } from '../../../../../common/utils/formatters'; -import { useTheme } from '../../../../hooks/use_theme'; -import { formatYLong, IChartPoint } from './'; - -export function CustomTooltip( - props: TooltipInfo & { - serie?: IChartPoint; - isSamplesEmpty: boolean; - timeFormatter: TimeFormatter; - } -) { - const theme = useTheme(); - const { values, header, serie, isSamplesEmpty, timeFormatter } = props; - const { color, value } = values[0]; - - let headerTitle = `${timeFormatter(header?.value)}`; - if (serie) { - const xFormatted = timeFormatter(serie.x); - const x0Formatted = timeFormatter(serie.x0); - headerTitle = `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; - } - - return ( -
- <> -
{headerTitle}
-
-
-
-
-
-
- {formatYLong(value)} - {value} -
-
-
- - {isSamplesEmpty && ( -
- - - {i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable', - { defaultMessage: 'No samples available' } - )} - -
- )} -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts deleted file mode 100644 index 5d6d73f36fac1..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getFormattedBuckets } from './index'; - -describe('Distribution', () => { - it('getFormattedBuckets', () => { - const buckets = [ - { key: 0, count: 0, samples: [] }, - { key: 20, count: 0, samples: [] }, - { key: 40, count: 0, samples: [] }, - { - key: 60, - count: 5, - samples: [ - { - transactionId: 'someTransactionId', - traceId: 'someTraceId', - }, - ], - }, - { - key: 80, - count: 100, - samples: [ - { - transactionId: 'anotherTransactionId', - traceId: 'anotherTraceId', - }, - ], - }, - ]; - - expect(getFormattedBuckets(buckets, 20)).toEqual([ - { x: 20, x0: 0, y: 0, style: { cursor: 'default' } }, - { x: 40, x0: 20, y: 0, style: { cursor: 'default' } }, - { x: 60, x0: 40, y: 0, style: { cursor: 'default' } }, - { - x: 80, - x0: 60, - y: 5, - style: { cursor: 'pointer' }, - }, - { - x: 100, - x0: 80, - y: 100, - style: { cursor: 'pointer' }, - }, - ]); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx deleted file mode 100644 index 4ff094c025451..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Axis, - Chart, - HistogramBarSeries, - Position, - ProjectionClickListener, - RectAnnotation, - ScaleType, - Settings, - SettingsSpec, - TooltipInfo, - XYChartSeriesIdentifier, -} from '@elastic/charts'; -import { EuiIconTip, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import d3 from 'd3'; -import { isEmpty, keyBy } from 'lodash'; -import React from 'react'; -import { ValuesType } from 'utility-types'; -import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { useTheme } from '../../../../hooks/use_theme'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { unit } from '../../../../utils/style'; -import { ChartContainer } from '../../../shared/charts/chart_container'; -import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { CustomTooltip } from './custom_tooltip'; - -type TransactionDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; - -type DistributionBucket = TransactionDistributionAPIResponse['buckets'][0]; - -export interface IChartPoint { - x0: number; - x: number; - y: number; - style: { - cursor: string; - }; -} - -export function getFormattedBuckets( - buckets?: DistributionBucket[], - bucketSize?: number -) { - if (!buckets || !bucketSize) { - return []; - } - - return buckets.map( - ({ samples, count, key }): IChartPoint => { - return { - x0: key, - x: key + bucketSize, - y: count, - style: { - cursor: isEmpty(samples) ? 'default' : 'pointer', - }, - }; - } - ); -} - -const formatYShort = (t: number) => { - return i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel', - { - defaultMessage: '{transCount} trans.', - values: { transCount: t }, - } - ); -}; - -export const formatYLong = (t: number) => { - return i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel', - { - defaultMessage: - '{transCount, plural, =0 {transactions} one {transaction} other {transactions}}', - values: { - transCount: t, - }, - } - ); -}; - -interface Props { - distribution?: TransactionDistributionAPIResponse; - fetchStatus: FETCH_STATUS; - bucketIndex: number; - onBucketClick: ( - bucket: ValuesType - ) => void; -} - -export function TransactionDistribution({ - distribution, - fetchStatus, - bucketIndex, - onBucketClick, -}: Props) { - const theme = useTheme(); - - // no data in response - if ( - (!distribution || distribution.noHits) && - fetchStatus !== FETCH_STATUS.LOADING - ) { - return ( - - ); - } - - const buckets = getFormattedBuckets( - distribution?.buckets, - distribution?.bucketSize - ); - - const xMin = d3.min(buckets, (d) => d.x0) || 0; - const xMax = d3.max(buckets, (d) => d.x0) || 0; - const timeFormatter = getDurationFormatter(xMax); - - const distributionMap = keyBy(distribution?.buckets, 'key'); - const bucketsMap = keyBy(buckets, 'x0'); - - const tooltip: SettingsSpec['tooltip'] = { - stickTo: 'top', - customTooltip: (props: TooltipInfo) => { - const datum = props.header?.datum as IChartPoint; - const selectedDistribution = distributionMap[datum?.x0]; - const serie = bucketsMap[datum?.x0]; - return ( - - ); - }, - }; - - const onBarClick: ProjectionClickListener = ({ x }) => { - const clickedBucket = distribution?.buckets.find((bucket) => { - return bucket.key === x; - }); - if (clickedBucket) { - onBucketClick(clickedBucket); - } - }; - - const selectedBucket = buckets[bucketIndex]; - - return ( -
- -
- {i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle', - { - defaultMessage: 'Latency distribution', - } - )}{' '} - -
-
- - - - {selectedBucket && ( - - )} - timeFormatter(time).formatted} - /> - formatYShort(value)} - /> - value} - minBarHeight={2} - id="transactionDurationDistribution" - name={(series: XYChartSeriesIdentifier) => { - const bucketCount = series.splitAccessors.get( - series.yAccessor - ) as number; - return formatYLong(bucketCount); - }} - splitSeriesAccessors={['y']} - xScaleType={ScaleType.Linear} - yScaleType={ScaleType.Linear} - xAccessor="x0" - yAccessors={['y']} - data={buckets} - color={theme.eui.euiColorVis1} - /> - - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx new file mode 100644 index 0000000000000..c21d292c05c85 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { BrushEndListener, XYBrushArea } from '@elastic/charts'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useTransactionDistributionFetcher } from '../../../../hooks/use_transaction_distribution_fetcher'; +import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart'; +import { useUiTracker } from '../../../../../../observability/public'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useApmParams } from '../../../../hooks/use_apm_params'; + +const DEFAULT_PERCENTILE_THRESHOLD = 95; +const isErrorMessage = (arg: unknown): arg is Error => { + return arg instanceof Error; +}; + +interface Props { + markerCurrentTransaction?: number; + onChartSelection: BrushEndListener; + onClearSelection: () => void; + selection?: [number, number]; +} + +export function TransactionDistribution({ + markerCurrentTransaction, + onChartSelection, + onClearSelection, + selection, +}: Props) { + const { + core: { notifications }, + } = useApmPluginContext(); + + const { serviceName, transactionType } = useApmServiceContext(); + + const { + query: { kuery, environment }, + } = useApmParams('/services/:serviceName'); + + const { urlParams } = useUrlParams(); + + const { transactionName, start, end } = urlParams; + + const clearSelectionButtonLabel = i18n.translate( + 'xpack.apm.transactionDetails.clearSelectionButtonLabel', + { + defaultMessage: 'Clear selection', + } + ); + + const { + error, + percentileThresholdValue, + startFetch, + cancelFetch, + transactionDistribution, + } = useTransactionDistributionFetcher({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }); + + // start fetching on load + // we want this effect to execute exactly once after the component mounts + useEffect(() => { + startFetch(); + + return () => { + // cancel any running async partial request when unmounting the component + // we want this effect to execute exactly once after the component mounts + cancelFetch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (isErrorMessage(error)) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.transactionDetails.distribution.errorTitle', + { + defaultMessage: 'An error occurred fetching the distribution', + } + ), + text: error.toString(), + }); + } + }, [error, notifications.toasts]); + + const trackApmEvent = useUiTracker({ app: 'apm' }); + + const onTrackedChartSelection: BrushEndListener = ( + brushArea: XYBrushArea + ) => { + onChartSelection(brushArea); + trackApmEvent({ metric: 'transaction_distribution_chart_selection' }); + }; + + const onTrackedClearSelection = () => { + onClearSelection(); + trackApmEvent({ metric: 'transaction_distribution_chart_clear_selection' }); + }; + + return ( + <> + + + +
+ {i18n.translate( + 'xpack.apm.transactionDetails.distribution.panelTitle', + { + defaultMessage: 'Latency distribution', + } + )} +
+
+
+ {selection && ( + + + + + {i18n.translate( + 'xpack.apm.transactionDetails.distribution.selectionText', + { + defaultMessage: `Selection: {selectionFrom} - {selectionTo}ms`, + values: { + selectionFrom: Math.round(selection[0] / 1000), + selectionTo: Math.round(selection[1] / 1000), + }, + } + )} + + + + + {clearSelectionButtonLabel} + + + + + )} +
+ + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx new file mode 100644 index 0000000000000..e727aa4dfc5fd --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiBetaBadge } from '@elastic/eui'; + +import { + METRIC_TYPE, + useTrackMetric, +} from '../../../../../observability/public'; + +import { isActivePlatinumLicense } from '../../../../common/license_check'; + +import { useLicenseContext } from '../../../context/license/use_license_context'; + +import { LicensePrompt } from '../../shared/license_prompt'; + +import { ErrorCorrelations } from '../correlations/error_correlations'; + +import type { TabContentProps } from './types'; + +function FailedTransactionsCorrelationsTab({}: TabContentProps) { + const license = useLicenseContext(); + + const hasActivePlatinumLicense = isActivePlatinumLicense(license); + + const metric = { + app: 'apm' as const, + metric: hasActivePlatinumLicense + ? 'failed_transactions_tab_view' + : 'failed_transactions_license_prompt', + metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, + }; + useTrackMetric(metric); + useTrackMetric({ ...metric, delay: 15000 }); + + return hasActivePlatinumLicense ? ( + + ) : ( + + ); +} + +export const failedTransactionsCorrelationsTab = { + dataTestSubj: 'apmFailedTransactionsCorrelationsTabButton', + key: 'failedTransactionsCorrelations', + label: ( + <> + {i18n.translate( + 'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel', + { + defaultMessage: 'Failed transaction correlations', + } + )}{' '} + + + ), + component: FailedTransactionsCorrelationsTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 143e82649facd..0c6f03047dc7d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -5,55 +5,23 @@ * 2.0. */ -import { EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { flatten, isEmpty } from 'lodash'; +import { EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; -import { useHistory } from 'react-router-dom'; import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; -import { HeightRetainer } from '../../shared/HeightRetainer'; -import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { TransactionDistribution } from './Distribution'; -import { useWaterfallFetcher } from './use_waterfall_fetcher'; -import { WaterfallWithSummary } from './waterfall_with_summary'; -interface Sample { - traceId: string; - transactionId: string; -} +import { TransactionDetailsTabs } from './transaction_details_tabs'; export function TransactionDetails() { - const { urlParams } = useUrlParams(); - const history = useHistory(); - - const { - waterfall, - exceedsMax, - status: waterfallStatus, - } = useWaterfallFetcher(); - const { path, query } = useApmParams( '/services/:serviceName/transactions/view' ); - - const apmRouter = useApmRouter(); - const { transactionName } = query; - const { - distributionData, - distributionStatus, - } = useTransactionDistributionFetcher({ - transactionName, - environment: query.environment, - kuery: query.kuery, - }); + const apmRouter = useApmRouter(); useBreadcrumb({ title: transactionName, @@ -63,36 +31,6 @@ export function TransactionDetails() { }), }); - const selectedSample = flatten( - distributionData.buckets.map((bucket) => bucket.samples) - ).find( - (sample) => - sample.transactionId === urlParams.transactionId && - sample.traceId === urlParams.traceId - ); - - const bucketWithSample = - selectedSample && - distributionData.buckets.find((bucket) => - bucket.samples.includes(selectedSample) - ); - - const traceSamples = bucketWithSample?.samples ?? []; - const bucketIndex = bucketWithSample - ? distributionData.buckets.indexOf(bucketWithSample) - : -1; - - const selectSampleFromBucketClick = (sample: Sample) => { - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - transactionId: sample.transactionId, - traceId: sample.traceId, - }), - }); - }; - return ( <> @@ -110,32 +48,9 @@ export function TransactionDetails() { /> - - - - { - if (!isEmpty(bucket.samples)) { - selectSampleFromBucketClick(bucket.samples[0]); - } - }} - /> - - - + - - - + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx new file mode 100644 index 0000000000000..c396b6317c311 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + METRIC_TYPE, + useTrackMetric, +} from '../../../../../observability/public'; + +import { isActivePlatinumLicense } from '../../../../common/license_check'; + +import { useLicenseContext } from '../../../context/license/use_license_context'; + +import { LicensePrompt } from '../../shared/license_prompt'; + +import { LatencyCorrelations } from '../correlations/latency_correlations'; + +import type { TabContentProps } from './types'; + +function LatencyCorrelationsTab({}: TabContentProps) { + const license = useLicenseContext(); + + const hasActivePlatinumLicense = isActivePlatinumLicense(license); + + const metric = { + app: 'apm' as const, + metric: hasActivePlatinumLicense + ? 'correlations_tab_view' + : 'correlations_license_prompt', + metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, + }; + useTrackMetric(metric); + useTrackMetric({ ...metric, delay: 15000 }); + + return hasActivePlatinumLicense ? ( + + ) : ( + + ); +} + +export const latencyCorrelationsTab = { + dataTestSubj: 'apmLatencyCorrelationsTabButton', + key: 'latencyCorrelations', + label: i18n.translate('xpack.apm.transactionDetails.tabs.latencyLabel', { + defaultMessage: 'Latency correlations', + }), + component: LatencyCorrelationsTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx new file mode 100644 index 0000000000000..0421fcd055d6c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; + +import { TransactionDistribution } from './distribution'; +import { useWaterfallFetcher } from './use_waterfall_fetcher'; +import type { TabContentProps } from './types'; +import { WaterfallWithSummary } from './waterfall_with_summary'; + +function TraceSamplesTab({ + selectSampleFromChartSelection, + clearChartSelection, + sampleRangeFrom, + sampleRangeTo, + traceSamples, +}: TabContentProps) { + const { urlParams } = useUrlParams(); + + const { + waterfall, + exceedsMax, + status: waterfallStatus, + } = useWaterfallFetcher(); + + return ( + <> + + + + + + + ); +} + +export const traceSamplesTab = { + dataTestSubj: 'apmTraceSamplesTabButton', + key: 'traceSamples', + label: i18n.translate('xpack.apm.transactionDetails.tabs.traceSamplesLabel', { + defaultMessage: 'Trace samples', + }), + component: TraceSamplesTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx new file mode 100644 index 0000000000000..8cdfd44c7581a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; + +import { omit } from 'lodash'; +import { useHistory } from 'react-router-dom'; + +import { XYBrushArea } from '@elastic/charts'; +import { EuiPanel, EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; + +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useTransactionTraceSamplesFetcher } from '../../../hooks/use_transaction_trace_samples_fetcher'; + +import { maybe } from '../../../../common/utils/maybe'; +import { HeightRetainer } from '../../shared/HeightRetainer'; +import { fromQuery, push, toQuery } from '../../shared/Links/url_helpers'; + +import { failedTransactionsCorrelationsTab } from './failed_transactions_correlations_tab'; +import { latencyCorrelationsTab } from './latency_correlations_tab'; +import { traceSamplesTab } from './trace_samples_tab'; + +const tabs = [ + traceSamplesTab, + latencyCorrelationsTab, + failedTransactionsCorrelationsTab, +]; + +export function TransactionDetailsTabs() { + const { query } = useApmParams('/services/:serviceName/transactions/view'); + + const { urlParams } = useUrlParams(); + const history = useHistory(); + + const [currentTab, setCurrentTab] = useState(traceSamplesTab.key); + const { component: TabContent } = + tabs.find((tab) => tab.key === currentTab) ?? traceSamplesTab; + + const { environment, kuery, transactionName } = query; + const { traceSamplesData } = useTransactionTraceSamplesFetcher({ + transactionName, + kuery, + environment, + }); + + const selectSampleFromChartSelection = (selection: XYBrushArea) => { + if (selection !== undefined) { + const { x } = selection; + if (Array.isArray(x)) { + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + sampleRangeFrom: Math.round(x[0]), + sampleRangeTo: Math.round(x[1]), + }), + }); + } + } + }; + + const { sampleRangeFrom, sampleRangeTo, transactionId, traceId } = urlParams; + const { traceSamples } = traceSamplesData; + + const clearChartSelection = () => { + // enforces a reset of the current sample to be highlighted in the chart + // and selected in waterfall section below, otherwise we end up with + // stale data for the selected sample + push(history, { + query: { + sampleRangeFrom: '', + sampleRangeTo: '', + traceId: '', + transactionId: '', + }, + }); + }; + + useEffect(() => { + const selectedSample = traceSamples.find( + (sample) => + sample.transactionId === transactionId && sample.traceId === traceId + ); + + if (!selectedSample) { + // selected sample was not found. select a new one: + const preferredSample = maybe(traceSamples[0]); + + history.replace({ + ...history.location, + search: fromQuery({ + ...omit(toQuery(history.location.search), [ + 'traceId', + 'transactionId', + ]), + ...preferredSample, + }), + }); + } + }, [history, traceSamples, transactionId, traceId]); + + return ( + <> + + {tabs.map(({ dataTestSubj, key, label }) => ( + { + setCurrentTab(key); + }} + > + {label} + + ))} + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/types.ts b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts new file mode 100644 index 0000000000000..5396d5a8a538d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { XYBrushArea } from '@elastic/charts'; + +import type { TraceSample } from '../../../hooks/use_transaction_trace_samples_fetcher'; + +export interface TabContentProps { + selectSampleFromChartSelection: (selection: XYBrushArea) => void; + clearChartSelection: () => void; + sampleRangeFrom?: number; + sampleRangeTo?: number; + traceSamples: TraceSample[]; +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index 64c4e7dcb42b9..19199cda9495e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -10,34 +10,29 @@ import { EuiFlexGroup, EuiFlexItem, EuiPagination, - EuiPanel, EuiSpacer, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import type { IUrlParams } from '../../../../context/url_params_context/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; import { TransactionActionMenu } from '../../../shared/transaction_action_menu/TransactionActionMenu'; +import type { TraceSample } from '../../../../hooks/use_transaction_trace_samples_fetcher'; import { MaybeViewTraceLink } from './MaybeViewTraceLink'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; import { useApmParams } from '../../../../hooks/use_apm_params'; -type DistributionApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; - -type DistributionBucket = DistributionApiResponse['buckets'][0]; - interface Props { urlParams: IUrlParams; waterfall: IWaterfall; exceedsMax: boolean; isLoading: boolean; - traceSamples: DistributionBucket['samples']; + traceSamples: TraceSample[]; } export function WaterfallWithSummary({ @@ -88,13 +83,13 @@ export function WaterfallWithSummary({ /> ); - return {content}; + return content; } const entryTransaction = entryWaterfallTransaction.doc; return ( - + <> @@ -142,6 +137,6 @@ export function WaterfallWithSummary({ waterfall={waterfall} exceedsMax={exceedsMax} /> - + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index e53ca324eac0a..137e0ea7b2d29 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -18,6 +18,7 @@ import { AggregatedTransactionsCallout } from '../../shared/aggregated_transacti import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { TransactionsTable } from '../../shared/transactions_table'; + import { useRedirect } from './useRedirect'; function getRedirectLocation({ diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index d332048338cc0..b01c45437f430 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -26,7 +26,6 @@ import { useApmServiceContext } from '../../../../context/apm_service/use_apm_se import { useBreadcrumb } from '../../../../context/breadcrumbs/use_breadcrumb'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; -import { Correlations } from '../../../app/correlations'; import { SearchBar } from '../../../shared/search_bar'; import { ServiceIcons } from '../../../shared/service_icons'; import { ApmMainTemplate } from '../apm_main_template'; @@ -108,10 +107,6 @@ function TemplateWithContext({ - - - - ), }} diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index 9acc04f18f187..b0cadd50b3d61 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -65,6 +65,8 @@ export function createHref( } export type APMQueryParams = { + sampleRangeFrom?: number; + sampleRangeTo?: number; transactionId?: string; transactionName?: string; transactionType?: string; diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx rename to x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index e3ff631ae1a6f..51250818a2269 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -5,38 +5,42 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { AnnotationDomainType, - Chart, - CurveType, - Settings, - Axis, - ScaleType, - Position, AreaSeries, - RecursivePartial, + Axis, AxisStyle, - PartialTheme, + BrushEndListener, + Chart, + CurveType, LineAnnotation, LineAnnotationDatum, + PartialTheme, + Position, + RectAnnotation, + RecursivePartial, + ScaleType, + Settings, } from '@elastic/charts'; import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiPaletteColorBlind } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; import { getDurationUnitKey, getUnitLabelAndConvertedValue, -} from '../../../../common/utils/formatters'; +} from '../../../../../common/utils/formatters'; -import { HistogramItem } from '../../../../common/search_strategies/correlations/types'; +import { HistogramItem } from '../../../../../common/search_strategies/correlations/types'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useTheme } from '../../../hooks/use_theme'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../hooks/use_theme'; -import { ChartContainer } from '../../shared/charts/chart_container'; +import { ChartContainer } from '../chart_container'; const { euiColorMediumShade } = euiVars; const axisColor = euiColorMediumShade; @@ -79,25 +83,28 @@ interface CorrelationsChartProps { field?: string; value?: string; histogram?: HistogramItem[]; + markerCurrentTransaction?: number; markerValue: number; markerPercentile: number; overallHistogram?: HistogramItem[]; + onChartSelection?: BrushEndListener; + selection?: [number, number]; } -const annotationsStyle = { +const getAnnotationsStyle = (color = 'gray') => ({ line: { strokeWidth: 1, - stroke: 'gray', + stroke: color, opacity: 0.8, }, details: { fontSize: 8, fontFamily: 'Arial', fontStyle: 'normal', - fill: 'gray', + fill: color, padding: 0, }, -}; +}); const CHART_PLACEHOLDER_VALUE = 0.0001; @@ -123,21 +130,29 @@ export const replaceHistogramDotsWithBars = ( } }; -export function CorrelationsChart({ +export function TransactionDistributionChart({ field, value, histogram: originalHistogram, + markerCurrentTransaction, markerValue, markerPercentile, overallHistogram, + onChartSelection, + selection, }: CorrelationsChartProps) { const euiTheme = useTheme(); + const patchedOverallHistogram = useMemo( + () => replaceHistogramDotsWithBars(overallHistogram), + [overallHistogram] + ); + const annotationsDataValues: LineAnnotationDatum[] = [ { dataValue: markerValue, details: i18n.translate( - 'xpack.apm.correlations.latency.chart.percentileMarkerLabel', + 'xpack.apm.transactionDistribution.chart.percentileMarkerLabel', { defaultMessage: '{markerPercentile}th percentile', values: { @@ -159,6 +174,21 @@ export function CorrelationsChart({ const histogram = replaceHistogramDotsWithBars(originalHistogram); + const selectionAnnotation = + selection !== undefined + ? [ + { + coordinates: { + x0: selection[0], + x1: selection[1], + y0: 0, + y1: 100000, + }, + details: 'selection', + }, + ] + : undefined; + return (
0} + hasData={ + Array.isArray(patchedOverallHistogram) && + patchedOverallHistogram.length > 0 + } status={ - Array.isArray(overallHistogram) + Array.isArray(patchedOverallHistogram) ? FETCH_STATUS.SUCCESS : FETCH_STATUS.LOADING } @@ -179,12 +212,51 @@ export function CorrelationsChart({ theme={chartTheme} showLegend legendPosition={Position.Bottom} + onBrushEnd={onChartSelection} /> + {selectionAnnotation !== undefined && ( + + )} + {typeof markerCurrentTransaction === 'number' && ( + + )} @@ -208,7 +280,7 @@ export function CorrelationsChart({ id="y-axis" domain={yAxisDomain} title={i18n.translate( - 'xpack.apm.correlations.latency.chart.numberOfTransactionsLabel', + 'xpack.apm.transactionDistribution.chart.numberOfTransactionsLabel', { defaultMessage: '# transactions' } )} position={Position.Left} @@ -216,12 +288,12 @@ export function CorrelationsChart({ /> ; - -const INITIAL_DATA = { - buckets: [] as APIResponse['buckets'], - noHits: true, - bucketSize: 0, -}; +interface RawResponse { + percentileThresholdValue?: number; + took: number; + values: SearchServiceValue[]; + overallHistogram: HistogramItem[]; + log: string[]; + ccsWarning: boolean; +} -export function useTransactionDistributionFetcher({ - transactionName, - kuery, - environment, -}: { - transactionName: string; - kuery: string; - environment: string; -}) { - const { serviceName, transactionType } = useApmServiceContext(); +interface TransactionDistributionFetcherState { + error?: Error; + isComplete: boolean; + isRunning: boolean; + loaded: number; + ccsWarning: RawResponse['ccsWarning']; + log: RawResponse['log']; + transactionDistribution?: RawResponse['overallHistogram']; + percentileThresholdValue?: RawResponse['percentileThresholdValue']; + timeTook?: number; + total: number; +} +export function useTransactionDistributionFetcher( + params: Omit +) { const { - urlParams: { start, end, transactionId, traceId }, - } = useUrlParams(); + services: { data }, + } = useKibana(); - const history = useHistory(); - const { data = INITIAL_DATA, status, error } = useFetcher( - async (callApmApi) => { - if (serviceName && start && end && transactionType && transactionName) { - const response = await callApmApi({ - endpoint: - 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', - params: { - path: { - serviceName, - }, - query: { - environment, - kuery, - start, - end, - transactionType, - transactionName, - transactionId, - traceId, - }, - }, - }); + const [ + fetchState, + setFetchState, + ] = useState({ + isComplete: false, + isRunning: false, + loaded: 0, + ccsWarning: false, + log: [], + total: 100, + }); - const selectedSample = - transactionId && traceId - ? flatten(response.buckets.map((bucket) => bucket.samples)).find( - (sample) => - sample.transactionId === transactionId && - sample.traceId === traceId - ) - : undefined; + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); - if (!selectedSample) { - // selected sample was not found. select a new one: - // sorted by total number of requests, but only pick - // from buckets that have samples - const bucketsSortedByCount = response.buckets - .filter((bucket) => !isEmpty(bucket.samples)) - .sort((bucket) => bucket.count); + function setResponse(response: IKibanaSearchResponse) { + setFetchState((prevState) => ({ + ...prevState, + isRunning: response.isRunning || false, + ccsWarning: response.rawResponse?.ccsWarning ?? false, + histograms: response.rawResponse?.values ?? [], + log: response.rawResponse?.log ?? [], + loaded: response.loaded!, + total: response.total!, + timeTook: response.rawResponse.took, + // only set percentileThresholdValue and overallHistogram once it's repopulated on a refresh, + // otherwise the consuming chart would flicker with an empty state on reload. + ...(response.rawResponse?.percentileThresholdValue !== undefined && + response.rawResponse?.overallHistogram !== undefined + ? { + transactionDistribution: response.rawResponse?.overallHistogram, + percentileThresholdValue: + response.rawResponse?.percentileThresholdValue, + } + : {}), + })); + } - const preferredSample = maybe(bucketsSortedByCount[0]?.samples[0]); + const startFetch = () => { + setFetchState((prevState) => ({ + ...prevState, + error: undefined, + isComplete: false, + })); + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); - history.replace({ - ...history.location, - search: fromQuery({ - ...omit(toQuery(history.location.search), [ - 'traceId', - 'transactionId', - ]), - ...preferredSample, - }), - }); - } + const searchServiceParams: SearchServiceParams = { + ...params, + analyzeCorrelations: false, + }; + const req = { params: searchServiceParams }; - return response; - } - }, - // the histogram should not be refetched if the transactionId or traceId changes - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - environment, - kuery, - serviceName, - start, - end, - transactionType, - transactionName, - ] - ); + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search>(req, { + strategy: 'apmCorrelationsSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (res: IKibanaSearchResponse) => { + setResponse(res); + if (isCompleteResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + isRunnning: false, + isComplete: true, + })); + } else if (isErrorResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + error: (res as unknown) as Error, + setIsRunning: false, + })); + } + }, + error: (error: Error) => { + setFetchState((prevState) => ({ + ...prevState, + error, + setIsRunning: false, + })); + }, + }); + }; + + const cancelFetch = () => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState((prevState) => ({ + ...prevState, + setIsRunning: false, + })); + }; return { - distributionData: data, - distributionStatus: status, - distributionError: error, + ...fetchState, + progress: fetchState.loaded / fetchState.total, + startFetch, + cancelFetch, }; } diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts new file mode 100644 index 0000000000000..538792bbf23a8 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useState } from 'react'; +import type { Subscription } from 'rxjs'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + isCompleteResponse, + isErrorResponse, +} from '../../../../../src/plugins/data/public'; +import type { + HistogramItem, + SearchServiceParams, + SearchServiceValue, +} from '../../common/search_strategies/correlations/types'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../plugin'; + +interface RawResponse { + percentileThresholdValue?: number; + took: number; + values: SearchServiceValue[]; + overallHistogram: HistogramItem[]; + log: string[]; + ccsWarning: boolean; +} + +interface TransactionLatencyCorrelationsFetcherState { + error?: Error; + isComplete: boolean; + isRunning: boolean; + loaded: number; + ccsWarning: RawResponse['ccsWarning']; + histograms: RawResponse['values']; + log: RawResponse['log']; + overallHistogram?: RawResponse['overallHistogram']; + percentileThresholdValue?: RawResponse['percentileThresholdValue']; + timeTook?: number; + total: number; +} + +export const useTransactionLatencyCorrelationsFetcher = ( + params: Omit +) => { + const { + services: { data }, + } = useKibana(); + + const [ + fetchState, + setFetchState, + ] = useState({ + isComplete: false, + isRunning: false, + loaded: 0, + ccsWarning: false, + histograms: [], + log: [], + total: 100, + }); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + + function setResponse(response: IKibanaSearchResponse) { + setFetchState((prevState) => ({ + ...prevState, + isRunning: response.isRunning || false, + ccsWarning: response.rawResponse?.ccsWarning ?? false, + histograms: response.rawResponse?.values ?? [], + log: response.rawResponse?.log ?? [], + loaded: response.loaded!, + total: response.total!, + timeTook: response.rawResponse.took, + // only set percentileThresholdValue and overallHistogram once it's repopulated on a refresh, + // otherwise the consuming chart would flicker with an empty state on reload. + ...(response.rawResponse?.percentileThresholdValue !== undefined && + response.rawResponse?.overallHistogram !== undefined + ? { + overallHistogram: response.rawResponse?.overallHistogram, + percentileThresholdValue: + response.rawResponse?.percentileThresholdValue, + } + : {}), + })); + } + + const startFetch = () => { + setFetchState((prevState) => ({ + ...prevState, + error: undefined, + isComplete: false, + })); + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + const searchServiceParams: SearchServiceParams = { + ...params, + analyzeCorrelations: true, + }; + const req = { params: searchServiceParams }; + + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search>(req, { + strategy: 'apmCorrelationsSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (res: IKibanaSearchResponse) => { + setResponse(res); + if (isCompleteResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + isRunnning: false, + isComplete: true, + })); + } else if (isErrorResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + error: (res as unknown) as Error, + setIsRunning: false, + })); + } + }, + error: (error: Error) => { + setFetchState((prevState) => ({ + ...prevState, + error, + setIsRunning: false, + })); + }, + }); + }; + + const cancelFetch = () => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState((prevState) => ({ + ...prevState, + setIsRunning: false, + })); + }; + + return { + ...fetchState, + progress: fetchState.loaded / fetchState.total, + startFetch, + cancelFetch, + }; +}; diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts new file mode 100644 index 0000000000000..673c1086033b5 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useFetcher } from './use_fetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; + +export interface TraceSample { + traceId: string; + transactionId: string; +} + +const INITIAL_DATA = { + noHits: true, + traceSamples: [] as TraceSample[], +}; + +export function useTransactionTraceSamplesFetcher({ + transactionName, + kuery, + environment, +}: { + transactionName: string; + kuery: string; + environment: string; +}) { + const { serviceName, transactionType } = useApmServiceContext(); + + const { + urlParams: { + start, + end, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + }, + } = useUrlParams(); + + const { data = INITIAL_DATA, status, error } = useFetcher( + async (callApmApi) => { + if (serviceName && start && end && transactionType && transactionName) { + const response = await callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/traces/samples', + params: { + path: { + serviceName, + }, + query: { + environment, + kuery, + start, + end, + transactionType, + transactionName, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + }, + }, + }); + + if (response.noHits) { + return response; + } + + const { traceSamples } = response; + + return { + noHits: false, + traceSamples, + }; + } + }, + // the samples should not be refetched if the transactionId or traceId changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + environment, + kuery, + serviceName, + start, + end, + transactionType, + transactionName, + sampleRangeFrom, + sampleRangeTo, + ] + ); + + return { + traceSamplesData: data, + traceSamplesStatus: status, + traceSamplesError: error, + }; +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts index ae42a0c94fe9c..e9986bd9f0cf5 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts @@ -49,14 +49,14 @@ export const asyncSearchServiceProvider = ( // 95th percentile to be displayed as a marker in the log log chart const { totalDocs, - percentiles: percentileThreshold, + percentiles: percentilesResponseThresholds, } = await fetchTransactionDurationPercentiles( esClient, params, params.percentileThreshold ? [params.percentileThreshold] : undefined ); const percentileThresholdValue = - percentileThreshold[`${params.percentileThreshold}.0`]; + percentilesResponseThresholds[`${params.percentileThreshold}.0`]; state.setPercentileThresholdValue(percentileThresholdValue); addLogMessage( @@ -107,11 +107,31 @@ export const asyncSearchServiceProvider = ( return; } + // finish early if correlation analysis is not required. + if (params.analyzeCorrelations === false) { + addLogMessage( + `Finish service since correlation analysis wasn't requested.` + ); + state.setProgress({ + loadedHistogramStepsize: 1, + loadedOverallHistogram: 1, + loadedFieldCanditates: 1, + loadedFieldValuePairs: 1, + loadedHistograms: 1, + }); + state.setIsRunning(false); + return; + } + // Create an array of ranges [2, 4, 6, ..., 98] - const percents = Array.from(range(2, 100, 2)); + const percentileAggregationPercents = range(2, 100, 2); const { percentiles: percentilesRecords, - } = await fetchTransactionDurationPercentiles(esClient, params, percents); + } = await fetchTransactionDurationPercentiles( + esClient, + params, + percentileAggregationPercents + ); const percentiles = Object.values(percentilesRecords); addLogMessage(`Loaded percentiles.`); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts index 4b10ceb035e15..3be3438b2d18f 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts @@ -49,7 +49,6 @@ describe('correlations', () => { end: '2021', environment: 'dev', kuery: '', - percentileThresholdValue: 75, includeFrozen: false, }, }); @@ -85,13 +84,6 @@ describe('correlations', () => { 'transaction.name': 'actualTransactionName', }, }, - { - range: { - 'transaction.duration.us': { - gte: 75, - }, - }, - }, ], }, }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts index f28556f7a90b5..8bd9f3d4e582c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts @@ -10,28 +10,11 @@ import { getOrElse } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { rangeRt } from '../../../../routes/default_api_types'; import { getCorrelationsFilters } from '../../../correlations/get_filters'; import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; -const getPercentileThresholdValueQuery = ( - percentileThresholdValue: number | undefined -): estypes.QueryDslQueryContainer[] => { - return percentileThresholdValue - ? [ - { - range: { - [TRANSACTION_DURATION]: { - gte: percentileThresholdValue, - }, - }, - }, - ] - : []; -}; - export const getTermsQuery = ( fieldName: string | undefined, fieldValue: string | undefined @@ -55,7 +38,6 @@ export const getQueryWithParams = ({ serviceName, start, end, - percentileThresholdValue, transactionType, transactionName, } = params; @@ -82,7 +64,6 @@ export const getQueryWithParams = ({ filter: [ ...filters, ...getTermsQuery(fieldName, fieldValue), - ...getPercentileThresholdValueQuery(percentileThresholdValue), ] as estypes.QueryDslQueryContainer[], }, }; diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index baa9b3ae230fe..44125d557dcc8 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -335,7 +335,7 @@ Object { } `; -exports[`transaction queries fetches transaction distribution 1`] = ` +exports[`transaction queries fetches transaction trace samples 1`] = ` Object { "apm": Object { "events": Array [ @@ -343,13 +343,6 @@ Object { ], }, "body": Object { - "aggs": Object { - "stats": Object { - "max": Object { - "field": "transaction.duration.us", - }, - }, - }, "query": Object { "bool": Object { "filter": Array [ @@ -377,10 +370,27 @@ Object { }, }, }, + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "trace.id": "qux", + }, + }, + Object { + "term": Object { + "transaction.id": "quz", + }, + }, ], }, }, - "size": 0, + "size": 500, }, } `; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts deleted file mode 100644 index e868f7de049f9..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; -import { withApmSpan } from '../../../../utils/with_apm_span'; -import { - SERVICE_NAME, - TRACE_ID, - TRANSACTION_DURATION, - TRANSACTION_ID, - TRANSACTION_NAME, - TRANSACTION_SAMPLED, - TRANSACTION_TYPE, -} from '../../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../../common/processor_event'; -import { joinByKey } from '../../../../../common/utils/join_by_key'; -import { rangeQuery, kqlQuery } from '../../../../../../observability/server'; -import { environmentQuery } from '../../../../../common/utils/environment_query'; -import { - getDocumentTypeFilterForAggregatedTransactions, - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../../helpers/aggregated_transactions'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; - -function getHistogramAggOptions({ - bucketSize, - field, - distributionMax, -}: { - bucketSize: number; - field: string; - distributionMax: number; -}) { - return { - field, - interval: bucketSize, - min_doc_count: 0, - extended_bounds: { - min: 0, - max: distributionMax, - }, - }; -} - -export async function getBuckets({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - distributionMax, - bucketSize, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - transactionId: string; - traceId: string; - distributionMax: number; - bucketSize: number; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - return withApmSpan( - 'get_latency_distribution_buckets_with_samples', - async () => { - const { start, end, apmEventClient } = setup; - - const commonFilters = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ] as QueryDslQueryContainer[]; - - async function getSamplesForDistributionBuckets() { - const response = await apmEventClient.search( - 'get_samples_for_latency_distribution_buckets', - { - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - query: { - bool: { - filter: [ - ...commonFilters, - { term: { [TRANSACTION_SAMPLED]: true } }, - ], - should: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [TRANSACTION_ID]: transactionId } }, - ] as QueryDslQueryContainer[], - }, - }, - aggs: { - distribution: { - histogram: getHistogramAggOptions({ - bucketSize, - field: TRANSACTION_DURATION, - distributionMax, - }), - aggs: { - samples: { - top_hits: { - _source: [TRANSACTION_ID, TRACE_ID], - size: 10, - sort: { - _score: 'desc' as const, - }, - }, - }, - }, - }, - }, - }, - } - ); - - return ( - response.aggregations?.distribution.buckets.map((bucket) => { - const samples = bucket.samples.hits.hits; - return { - key: bucket.key, - samples: samples.map(({ _source: sample }) => ({ - traceId: sample.trace.id, - transactionId: sample.transaction.id, - })), - }; - }) ?? [] - ); - } - - async function getDistributionBuckets() { - const response = await apmEventClient.search( - 'get_latency_distribution_buckets', - { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - query: { - bool: { - filter: [ - ...commonFilters, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - }, - aggs: { - distribution: { - histogram: getHistogramAggOptions({ - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - bucketSize, - distributionMax, - }), - }, - }, - }, - } - ); - - return ( - response.aggregations?.distribution.buckets.map((bucket) => { - return { - key: bucket.key, - count: bucket.doc_count, - }; - }) ?? [] - ); - } - - const [ - samplesForDistributionBuckets, - distributionBuckets, - ] = await Promise.all([ - getSamplesForDistributionBuckets(), - getDistributionBuckets(), - ]); - - const buckets = joinByKey( - [...samplesForDistributionBuckets, ...distributionBuckets], - 'key' - ).map((bucket) => ({ - ...bucket, - samples: bucket.samples ?? [], - count: bucket.count ?? 0, - })); - - return { - noHits: buckets.length === 0, - bucketSize, - buckets, - }; - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts deleted file mode 100644 index 9c056bc506e92..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; -import { rangeQuery, kqlQuery } from '../../../../../observability/server'; -import { environmentQuery } from '../../../../common/utils/environment_query'; - -export async function getDistributionMax({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - const { start, end, apmEventClient } = setup; - - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], - }, - }, - aggs: { - stats: { - max: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }; - - const resp = await apmEventClient.search( - 'get_latency_distribution_max', - params - ); - return resp.aggregations?.stats.value ?? null; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts deleted file mode 100644 index ef72f2434fde2..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getBuckets } from './get_buckets'; -import { getDistributionMax } from './get_distribution_max'; -import { roundToNearestFiveOrTen } from '../../helpers/round_to_nearest_five_or_ten'; -import { MINIMUM_BUCKET_SIZE, BUCKET_TARGET_COUNT } from '../constants'; -import { withApmSpan } from '../../../utils/with_apm_span'; - -function getBucketSize(max: number) { - const bucketSize = max / BUCKET_TARGET_COUNT; - return roundToNearestFiveOrTen( - bucketSize > MINIMUM_BUCKET_SIZE ? bucketSize : MINIMUM_BUCKET_SIZE - ); -} - -export async function getTransactionDistribution({ - kuery, - environment, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - transactionId: string; - traceId: string; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - return withApmSpan('get_transaction_latency_distribution', async () => { - const distributionMax = await getDistributionMax({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - setup, - searchAggregatedTransactions, - }); - - if (distributionMax == null) { - return { noHits: true, buckets: [], bucketSize: 0 }; - } - - const bucketSize = getBucketSize(distributionMax); - - const { buckets, noHits } = await getBuckets({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - distributionMax, - bucketSize, - setup, - searchAggregatedTransactions, - }); - - return { - noHits, - buckets, - bucketSize, - }; - }); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index b1d942a261387..b6b727d2273a1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -11,7 +11,7 @@ import { SearchParamsMock, } from '../../utils/test_helpers'; import { getTransactionBreakdown } from './breakdown'; -import { getTransactionDistribution } from './distribution'; +import { getTransactionTraceSamples } from './trace_samples'; import { getTransaction } from './get_transaction'; describe('transaction queries', () => { @@ -50,16 +50,15 @@ describe('transaction queries', () => { expect(mock.params).toMatchSnapshot(); }); - it('fetches transaction distribution', async () => { + it('fetches transaction trace samples', async () => { mock = await inspectSearchParams((setup) => - getTransactionDistribution({ + getTransactionTraceSamples({ serviceName: 'foo', transactionName: 'bar', transactionType: 'baz', traceId: 'qux', transactionId: 'quz', setup, - searchAggregatedTransactions: false, environment: ENVIRONMENT_ALL.value, kuery: '', }) diff --git a/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts b/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts new file mode 100644 index 0000000000000..98ef9ecaf346f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import { withApmSpan } from '../../../../utils/with_apm_span'; +import { + SERVICE_NAME, + TRACE_ID, + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_SAMPLED, + TRANSACTION_TYPE, +} from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { rangeQuery, kqlQuery } from '../../../../../../observability/server'; +import { environmentQuery } from '../../../../../common/utils/environment_query'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; + +const TRACE_SAMPLES_SIZE = 500; + +export async function getTraceSamples({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, +}: { + environment: string; + kuery: string; + serviceName: string; + transactionName: string; + transactionType: string; + transactionId: string; + traceId: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; + setup: Setup & SetupTimeRange; +}) { + return withApmSpan('get_trace_samples', async () => { + const { start, end, apmEventClient } = setup; + + const commonFilters = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { term: { [TRANSACTION_NAME]: transactionName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ] as QueryDslQueryContainer[]; + + if (sampleRangeFrom !== undefined && sampleRangeTo !== undefined) { + commonFilters.push({ + range: { + 'transaction.duration.us': { + gte: sampleRangeFrom, + lte: sampleRangeTo, + }, + }, + }); + } + + async function getTraceSamplesHits() { + const response = await apmEventClient.search('get_trace_samples_hits', { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + ...commonFilters, + { term: { [TRANSACTION_SAMPLED]: true } }, + ], + should: [ + { term: { [TRACE_ID]: traceId } }, + { term: { [TRANSACTION_ID]: transactionId } }, + ] as QueryDslQueryContainer[], + }, + }, + size: TRACE_SAMPLES_SIZE, + }, + }); + + return response.hits.hits; + } + + const samplesForDistributionHits = await getTraceSamplesHits(); + + const traceSamples = samplesForDistributionHits.map((hit) => ({ + transactionId: hit._source.transaction.id, + traceId: hit._source.trace.id, + })); + + return { + noHits: samplesForDistributionHits.length === 0, + traceSamples, + }; + }); +} diff --git a/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts b/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts new file mode 100644 index 0000000000000..95548cd2afadf --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getTraceSamples } from './get_trace_samples'; +import { withApmSpan } from '../../../utils/with_apm_span'; + +export async function getTransactionTraceSamples({ + kuery, + environment, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, +}: { + environment: string; + kuery: string; + serviceName: string; + transactionName: string; + transactionType: string; + transactionId: string; + traceId: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; + setup: Setup & SetupTimeRange; +}) { + return withApmSpan('get_transaction_trace_samples', async () => { + return await getTraceSamples({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, + }); + }); +} diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index f211e722958c5..c267487cd36b7 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -16,7 +16,7 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; import { getServiceTransactionGroupDetailedStatisticsPeriods } from '../lib/services/get_service_transaction_group_detailed_statistics'; import { getTransactionBreakdown } from '../lib/transactions/breakdown'; -import { getTransactionDistribution } from '../lib/transactions/distribution'; +import { getTransactionTraceSamples } from '../lib/transactions/trace_samples'; import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; import { getLatencyPeriods } from '../lib/transactions/get_latency_charts'; import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate'; @@ -204,9 +204,8 @@ const transactionLatencyChartsRoute = createApmServerRoute({ }, }); -const transactionChartsDistributionRoute = createApmServerRoute({ - endpoint: - 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', +const transactionTraceSamplesRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transactions/traces/samples', params: t.type({ path: t.type({ serviceName: t.string, @@ -219,6 +218,8 @@ const transactionChartsDistributionRoute = createApmServerRoute({ t.partial({ transactionId: t.string, traceId: t.string, + sampleRangeFrom: toNumberRt, + sampleRangeTo: toNumberRt, }), environmentRt, kueryRt, @@ -237,14 +238,11 @@ const transactionChartsDistributionRoute = createApmServerRoute({ transactionName, transactionId = '', traceId = '', + sampleRangeFrom, + sampleRangeTo, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions({ - ...setup, - kuery, - }); - - return getTransactionDistribution({ + return getTransactionTraceSamples({ environment, kuery, serviceName, @@ -252,8 +250,9 @@ const transactionChartsDistributionRoute = createApmServerRoute({ transactionName, transactionId, traceId, + sampleRangeFrom, + sampleRangeTo, setup, - searchAggregatedTransactions, }); }, }); @@ -347,6 +346,6 @@ export const transactionRouteRepository = createApmServerRouteRepository() .add(transactionGroupsMainStatisticsRoute) .add(transactionGroupsDetailedStatisticsRoute) .add(transactionLatencyChartsRoute) - .add(transactionChartsDistributionRoute) + .add(transactionTraceSamplesRoute) .add(transactionChartsBreakdownRoute) .add(transactionChartsErrorRateRoute); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 621af3e92de8d..673b2b331b2a2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5499,10 +5499,6 @@ "xpack.apm.chart.memorySeries.systemAverageLabel": "平均", "xpack.apm.chart.memorySeries.systemMaxLabel": "最高", "xpack.apm.clearFilters": "フィルターを消去", - "xpack.apm.correlations.betaDescription": "相関関係がGAではありません。不具合が発生したら報告してください。", - "xpack.apm.correlations.betaLabel": "ベータ", - "xpack.apm.correlations.buttonLabel": "相関関係を表示", - "xpack.apm.correlations.clearFiltersLabel": "クリア", "xpack.apm.correlations.correlationsTable.actionsLabel": "フィルター", "xpack.apm.correlations.correlationsTable.excludeDescription": "値を除外", "xpack.apm.correlations.correlationsTable.excludeLabel": "除外", @@ -5522,21 +5518,12 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "オプションを選択または作成", "xpack.apm.correlations.customize.thresholdLabel": "しきい値", "xpack.apm.correlations.customize.thresholdPercentile": "{percentile}パーセンタイル", - "xpack.apm.correlations.environmentLabel": "環境", "xpack.apm.correlations.error.chart.overallErrorRateLabel": "全体のエラー率", "xpack.apm.correlations.error.chart.selectedTermErrorRateLabel": "{fieldName}:{fieldValue}", "xpack.apm.correlations.error.chart.title": "経時的なエラー率", "xpack.apm.correlations.error.description": "一部のトランザクションが失敗してエラーが返される理由。相関関係は、データの特定のコホートで想定される原因を検出するのに役立ちます。ホスト、バージョン、または他のカスタムフィールドのいずれか。", "xpack.apm.correlations.error.percentageColumnName": "失敗したトランザクションの%", - "xpack.apm.correlations.filteringByLabel": "フィルタリング条件", - "xpack.apm.correlations.latency.chart.numberOfTransactionsLabel": "# トランザクション", - "xpack.apm.correlations.latency.chart.overallLatencyDistributionLabel": "全体のレイテンシ分布", - "xpack.apm.correlations.latency.chart.selectedTermLatencyDistributionLabel": "{fieldName}:{fieldValue}", - "xpack.apm.correlations.latency.chart.title": "レイテンシ分布", - "xpack.apm.correlations.latency.description": "サービスが低速になっている原因。相関関係は、データの特定のコホートにあるパフォーマンス低下を特定するのに役立ちます。ホスト、バージョン、または他のカスタムフィールドのいずれか。", - "xpack.apm.correlations.latency.percentageColumnName": "低速なトランザクションの%", "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "キャンセル", - "xpack.apm.correlations.latencyCorrelations.chartTitle": "{name}の遅延分布", "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "フィルター", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "サービスの遅延に対するフィールドの影響。0~1の範囲。", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel": "相関関係", @@ -5551,12 +5538,6 @@ "xpack.apm.correlations.latencyCorrelations.progressAriaLabel": "進捗", "xpack.apm.correlations.latencyCorrelations.progressTitle": "進捗状況: {progress}%", "xpack.apm.correlations.latencyCorrelations.refreshButtonTitle": "更新", - "xpack.apm.correlations.licenseCheckText": "相関関係を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。使用すると、パフォーマンスの低下に関連しているフィールドを検出できます。", - "xpack.apm.correlations.serviceLabel": "サービス", - "xpack.apm.correlations.tabs.errorRateLabel": "エラー率", - "xpack.apm.correlations.tabs.latencyLabel": "レイテンシ", - "xpack.apm.correlations.title": "相関関係", - "xpack.apm.correlations.transactionLabel": "トランザクション", "xpack.apm.csm.breakdownFilter.browser": "ブラウザー", "xpack.apm.csm.breakdownFilter.device": "デバイス", "xpack.apm.csm.breakdownFilter.location": "場所", @@ -6049,7 +6030,6 @@ "xpack.apm.transactionCardinalityWarning.body": "一意のトランザクション名の数が構成された値{bucketSize}を超えています。エージェントを再構成し、類似したトランザクションをグループ化するか、{codeBlock}の値を増やしてください。", "xpack.apm.transactionCardinalityWarning.docsLink": "詳細はドキュメントをご覧ください", "xpack.apm.transactionCardinalityWarning.title": "このビューには、報告されたトランザクションのサブセットが表示されます。", - "xpack.apm.transactionDetails.notFoundLabel": "トランザクションが見つかりませんでした。", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "トレースの親が見つかりませんでした", "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {トランザクション} trace {トレース} }の割合が100%を超えています。これは、この{childType, select, span {スパン} transaction {トランザクション} }がルートトランザクションよりも時間がかかるためです。", "xpack.apm.transactionDetails.requestMethodLabel": "リクエストメソッド", @@ -6072,11 +6052,6 @@ "xpack.apm.transactionDetails.traceNotFound": "選択されたトレースが見つかりません", "xpack.apm.transactionDetails.traceSampleTitle": "トレースのサンプル", "xpack.apm.transactionDetails.transactionLabel": "トランザクション", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable": "サンプルがありません", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} 件のトランザクション", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle": "レイテンシ分布", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingDescription": "各バケットはサンプルトランザクションを示します。利用可能なサンプルがない場合、おそらくエージェントの構成で設定されたサンプリング制限が原因です。", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingLabel": "サンプリング", "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "このトランザクションを報告した APM エージェントが、構成に基づき {dropped} 個以上のスパンをドロップしました。", "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "ドロップされたスパンの詳細。", "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "トランザクションの詳細", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c2831d0aa7518..f51e9b2de0007 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5524,10 +5524,6 @@ "xpack.apm.chart.memorySeries.systemAverageLabel": "平均值", "xpack.apm.chart.memorySeries.systemMaxLabel": "最大值", "xpack.apm.clearFilters": "清除筛选", - "xpack.apm.correlations.betaDescription": "相关性不是 GA 版。请通过报告错误来帮助我们。", - "xpack.apm.correlations.betaLabel": "公测版", - "xpack.apm.correlations.buttonLabel": "查看相关性", - "xpack.apm.correlations.clearFiltersLabel": "清除", "xpack.apm.correlations.correlationsTable.actionsLabel": "筛选", "xpack.apm.correlations.correlationsTable.excludeDescription": "筛除值", "xpack.apm.correlations.correlationsTable.excludeLabel": "排除", @@ -5547,21 +5543,12 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "选择或创建选项", "xpack.apm.correlations.customize.thresholdLabel": "阈值", "xpack.apm.correlations.customize.thresholdPercentile": "第 {percentile} 个百分位数", - "xpack.apm.correlations.environmentLabel": "环境", "xpack.apm.correlations.error.chart.overallErrorRateLabel": "总错误率", "xpack.apm.correlations.error.chart.selectedTermErrorRateLabel": "{fieldName}:{fieldValue}", "xpack.apm.correlations.error.chart.title": "时移错误率", "xpack.apm.correlations.error.description": "为什么某些事务失败并返回错误?相关性将有助于在您数据的特定群组中发现可能的原因。按主机、版本或其他定制字段。", "xpack.apm.correlations.error.percentageColumnName": "失败事务 %", - "xpack.apm.correlations.filteringByLabel": "筛选依据", - "xpack.apm.correlations.latency.chart.numberOfTransactionsLabel": "事务数", - "xpack.apm.correlations.latency.chart.overallLatencyDistributionLabel": "总体延迟分布", - "xpack.apm.correlations.latency.chart.selectedTermLatencyDistributionLabel": "{fieldName}:{fieldValue}", - "xpack.apm.correlations.latency.chart.title": "延迟分布", - "xpack.apm.correlations.latency.description": "什么在拖慢我的服务?相关性将有助于在您数据的特定群组中发现较慢的性能。按主机、版本或其他定制字段。", - "xpack.apm.correlations.latency.percentageColumnName": "缓慢事务 %", "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "取消", - "xpack.apm.correlations.latencyCorrelations.chartTitle": "{name} 的延迟分布", "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "筛选", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "字段对服务延迟的影响,范围从 0 到 1。", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel": "相关性", @@ -5576,12 +5563,6 @@ "xpack.apm.correlations.latencyCorrelations.progressAriaLabel": "进度", "xpack.apm.correlations.latencyCorrelations.progressTitle": "进度:{progress}%", "xpack.apm.correlations.latencyCorrelations.refreshButtonTitle": "刷新", - "xpack.apm.correlations.licenseCheckText": "要使用相关性,必须订阅 Elastic 白金级许可证。使用相关性,将能够发现哪些字段与性能差相关。", - "xpack.apm.correlations.serviceLabel": "服务", - "xpack.apm.correlations.tabs.errorRateLabel": "错误率", - "xpack.apm.correlations.tabs.latencyLabel": "延迟", - "xpack.apm.correlations.title": "相关性", - "xpack.apm.correlations.transactionLabel": "事务", "xpack.apm.csm.breakdownFilter.browser": "浏览器", "xpack.apm.csm.breakdownFilter.device": "设备", "xpack.apm.csm.breakdownFilter.location": "位置", @@ -6082,7 +6063,6 @@ "xpack.apm.transactionCardinalityWarning.title": "此视图显示已报告事务的子集。", "xpack.apm.transactionDetails.errorCount": "{errorCount, number} 个 {errorCount, plural, other {错误}}", "xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {查看 1 个相关错误} other {查看 # 个相关错误}}", - "xpack.apm.transactionDetails.notFoundLabel": "未找到任何事务。", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯", "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {事务} trace {追溯} }的百分比超过 100%,因为此{childType, select, span {跨度} transaction {事务} }比根事务花费更长的时间。", "xpack.apm.transactionDetails.requestMethodLabel": "请求方法", @@ -6105,12 +6085,6 @@ "xpack.apm.transactionDetails.traceNotFound": "找不到所选跟踪", "xpack.apm.transactionDetails.traceSampleTitle": "跟踪样例", "xpack.apm.transactionDetails.transactionLabel": "事务", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable": "没有可用样本", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel": "{transCount, plural, other {事务}}", - "xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} 个事务", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle": "延迟分布", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingDescription": "每个存储桶将显示一个样例事务。如果没有可用的样例,很可能是在代理配置设置了采样限制。", - "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingLabel": "采样", "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "报告此事务的 APM 代理基于其配置丢弃了 {dropped} 个跨度。", "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "详细了解丢弃的跨度。", "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "事务详情", diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.ts b/x-pack/test/apm_api_integration/tests/feature_controls.ts index 589fba8561ae6..11c16fd87483c 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.ts @@ -124,7 +124,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }, { req: { - url: `/api/apm/services/foo/transactions/charts/distribution?start=${start}&end=${end}&transactionType=bar&transactionName=baz&environment=ENVIRONMENT_ALL&kuery=`, + url: `/api/apm/services/foo/transactions/traces/samples?start=${start}&end=${end}&transactionType=bar&transactionName=baz&environment=ENVIRONMENT_ALL&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 12b21ad17bf2f..0c1f695d4395b 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -158,8 +158,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./transactions/breakdown')); }); - describe('transactions/distribution', function () { - loadTestFile(require.resolve('./transactions/distribution')); + describe('transactions/trace_samples', function () { + loadTestFile(require.resolve('./transactions/trace_samples')); }); describe('transactions/error_rate', function () { diff --git a/x-pack/test/apm_api_integration/tests/transactions/distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/distribution.ts deleted file mode 100644 index 3c322a727d1f0..0000000000000 --- a/x-pack/test/apm_api_integration/tests/transactions/distribution.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import qs from 'querystring'; -import { isEmpty } from 'lodash'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - const url = `/api/apm/services/opbeans-java/transactions/charts/distribution?${qs.stringify({ - start: metadata.start, - end: metadata.end, - transactionName: 'APIRestController#stats', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - kuery: '', - })}`; - - registry.when( - 'Transaction groups distribution when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - - expect(response.body.noHits).to.be(true); - expect(response.body.buckets.length).to.be(0); - }); - } - ); - - registry.when( - 'Transaction groups distribution when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - let response: any; - before(async () => { - response = await supertest.get(url); - }); - - it('returns the correct metadata', () => { - expect(response.status).to.be(200); - expect(response.body.noHits).to.be(false); - expect(response.body.buckets.length).to.be.greaterThan(0); - }); - - it('returns groups with some hits', () => { - expect(response.body.buckets.some((bucket: any) => bucket.count > 0)).to.be(true); - }); - - it('returns groups with some samples', () => { - expect(response.body.buckets.some((bucket: any) => !isEmpty(bucket.samples))).to.be(true); - }); - - it('returns the correct number of buckets', () => { - expectSnapshot(response.body.buckets.length).toMatchInline(`26`); - }); - - it('returns the correct bucket size', () => { - expectSnapshot(response.body.bucketSize).toMatchInline(`1000`); - }); - - it('returns the correct buckets', () => { - const bucketWithSamples = response.body.buckets.find( - (bucket: any) => !isEmpty(bucket.samples) - ); - - expectSnapshot(bucketWithSamples.count).toMatchInline(`1`); - - expectSnapshot(bucketWithSamples.samples.sort((sample: any) => sample.traceId)) - .toMatchInline(` - Array [ - Object { - "traceId": "6d85d8f1bc4bbbfdb19cdba59d2fc164", - "transactionId": "d0a16f0f52f25d6b", - }, - ] - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts b/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts new file mode 100644 index 0000000000000..73b1bbfd781d0 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import qs from 'querystring'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + const url = `/api/apm/services/opbeans-java/transactions/traces/samples?${qs.stringify({ + environment: 'ENVIRONMENT_ALL', + kuery: '', + start: metadata.start, + end: metadata.end, + transactionName: 'APIRestController#stats', + transactionType: 'request', + })}`; + + registry.when( + 'Transaction trace samples response structure when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + + expect(response.body.noHits).to.be(true); + expect(response.body.traceSamples.length).to.be(0); + }); + } + ); + + registry.when( + 'Transaction trace samples response structure when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + let response: any; + before(async () => { + response = await supertest.get(url); + }); + + it('returns the correct metadata', () => { + expect(response.status).to.be(200); + expect(response.body.noHits).to.be(false); + expect(response.body.traceSamples.length).to.be.greaterThan(0); + }); + + it('returns the correct number of samples', () => { + expectSnapshot(response.body.traceSamples.length).toMatchInline(`15`); + }); + + it('returns the correct samples', () => { + const { traceSamples } = response.body; + + expectSnapshot(traceSamples.sort((sample: any) => sample.traceId)).toMatchInline(` + Array [ + Object { + "traceId": "5267685738bf75b68b16bf3426ba858c", + "transactionId": "5223f43bc3154c5a", + }, + Object { + "traceId": "9a84d15e5a0e32098d569948474e8e2f", + "transactionId": "b85db78a9824107b", + }, + Object { + "traceId": "e123f0466fa092f345d047399db65aa2", + "transactionId": "c0af16286229d811", + }, + Object { + "traceId": "4943691f87b7eb97d442d1ef33ca65c7", + "transactionId": "f6f4677d731e57c5", + }, + Object { + "traceId": "66bd97c457f5675665397ac9201cc050", + "transactionId": "592b60cc9ddabb15", + }, + Object { + "traceId": "10d882b7118870015815a27c37892375", + "transactionId": "0cf9db0b1e321239", + }, + Object { + "traceId": "6d85d8f1bc4bbbfdb19cdba59d2fc164", + "transactionId": "d0a16f0f52f25d6b", + }, + Object { + "traceId": "0996b09e42ad4dbfaaa6a069326c6e66", + "transactionId": "5721364b179716d0", + }, + Object { + "traceId": "d9415d102c0634e1e8fa53ceef07be70", + "transactionId": "fab91c68c9b1c42b", + }, + Object { + "traceId": "ca7a2072e7974ae84b5096706c6b6255", + "transactionId": "92ab7f2ef11685dd", + }, + Object { + "traceId": "d250e2a1bad40f78653d8858db65326b", + "transactionId": "6fcd12599c1b57fa", + }, + Object { + "traceId": "2ca82e99453c58584c4b8de9a8ba4ec3", + "transactionId": "8fa2ca73976ce1e7", + }, + Object { + "traceId": "45b3d1a86003938687a55e49bf3610b8", + "transactionId": "a707456bda99ee98", + }, + Object { + "traceId": "7483bd52150d1c93a858c60bfdd0c138", + "transactionId": "e20e701ff93bdb55", + }, + Object { + "traceId": "a21ea39b41349a4614a86321d965c957", + "transactionId": "338bd7908cbf7f2d", + }, + ] + `); + }); + } + ); +} diff --git a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts index 616402098acec..c2b24e87266af 100644 --- a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts +++ b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts @@ -17,7 +17,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); - const testData = { serviceName: 'opbeans-go' }; + const testData = { + latencyCorrelationsTab: 'Latency correlations', + logLogChartTitle: 'Latency distribution', + serviceName: 'opbeans-go', + transactionsTab: 'Transactions', + transaction: 'GET /api/stats', + }; describe('latency correlations', () => { describe('space with no features disabled', () => { @@ -90,23 +96,48 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const apmMainTemplateHeaderServiceName = await testSubjects.getVisibleTextAll( 'apmMainTemplateHeaderServiceName' ); - expect(apmMainTemplateHeaderServiceName).to.contain('opbeans-go'); + expect(apmMainTemplateHeaderServiceName).to.contain(testData.serviceName); }); }); - it('shows the correlations flyout', async function () { - await testSubjects.click('apmViewCorrelationsButton'); + it('navigates to the transactions tab', async function () { + await find.clickByDisplayedLinkText(testData.transactionsTab); await retry.try(async () => { - await testSubjects.existOrFail('apmCorrelationsFlyout', { - timeout: 10000, - }); + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + + expect(apmMainContainerTextItems).to.contain(testData.transaction); + }); + }); + + it(`navigates to the 'GET /api/stats' transactions`, async function () { + await find.clickByDisplayedLinkText(testData.transaction); + + await retry.try(async () => { + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + + expect(apmMainContainerTextItems).to.contain(testData.transaction); + expect(apmMainContainerTextItems).to.contain(testData.latencyCorrelationsTab); - const apmCorrelationsFlyoutHeader = await testSubjects.getVisibleText( - 'apmCorrelationsFlyoutHeader' + // The default tab 'Trace samples' should show the log log chart without the correlations analysis part. + // First assert that the log log chart and its header are present + const apmCorrelationsLatencyCorrelationsChartTitle = await testSubjects.getVisibleText( + 'apmCorrelationsLatencyCorrelationsChartTitle' ); + expect(apmCorrelationsLatencyCorrelationsChartTitle).to.be(testData.logLogChartTitle); + await testSubjects.existOrFail('apmCorrelationsChart'); + // Then assert that the correlation analysis part is not present + await testSubjects.missingOrFail('apmCorrelationsLatencyCorrelationsTablePanelTitle'); + }); + }); - expect(apmCorrelationsFlyoutHeader).to.contain('Correlations BETA'); + it('shows the correlations tab', async function () { + await testSubjects.click('apmLatencyCorrelationsTabButton'); + + await retry.try(async () => { + await testSubjects.existOrFail('apmCorrelationsTabContent'); }); }); @@ -122,12 +153,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const apmCorrelationsLatencyCorrelationsChartTitle = await testSubjects.getVisibleText( 'apmCorrelationsLatencyCorrelationsChartTitle' ); - expect(apmCorrelationsLatencyCorrelationsChartTitle).to.be( - `Latency distribution for ${testData.serviceName} (Log-Log Plot)` - ); - await testSubjects.existOrFail('apmCorrelationsChart', { - timeout: 10000, - }); + expect(apmCorrelationsLatencyCorrelationsChartTitle).to.be(testData.logLogChartTitle); + await testSubjects.existOrFail('apmCorrelationsChart'); + await testSubjects.existOrFail('apmCorrelationsLatencyCorrelationsTablePanelTitle'); // Assert that results for the given service didn't find any correlations const apmCorrelationsTable = await testSubjects.getVisibleText('apmCorrelationsTable'); From 720a6092664a72ff255b8d2f2d482ee10491f489 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Tue, 17 Aug 2021 11:59:51 -0400 Subject: [PATCH 08/11] [App Search] Migrate duplicate document handling UX for Crawler domains (#108623) --- .../components/crawl_requests_table.test.tsx | 3 + .../deduplication_panel.scss | 14 ++ .../deduplication_panel.test.tsx | 170 ++++++++++++++ .../deduplication_panel.tsx | 214 ++++++++++++++++++ .../components/deduplication_panel/index.ts | 8 + .../deduplication_panel/utils.test.ts | 69 ++++++ .../components/deduplication_panel/utils.ts | 39 ++++ .../components/delete_domain_panel.tsx | 10 +- .../crawler/components/domains_table.test.tsx | 6 + .../components/entry_points_table.test.tsx | 7 +- .../components/sitemaps_table.test.tsx | 3 + .../crawler/crawler_overview.test.tsx | 6 + .../crawler/crawler_overview_logic.test.ts | 6 + .../crawler/crawler_single_domain.test.tsx | 23 +- .../crawler/crawler_single_domain.tsx | 20 +- .../crawler_single_domain_logic.test.ts | 57 +++++ .../crawler/crawler_single_domain_logic.ts | 29 +++ .../app_search/components/crawler/types.ts | 6 + .../components/crawler/utils.test.ts | 23 +- .../app_search/components/crawler/utils.ts | 6 + .../test_helpers/enzyme_rerender.ts | 4 +- .../server/routes/app_search/crawler.test.ts | 23 +- .../server/routes/app_search/crawler.ts | 14 +- 23 files changed, 722 insertions(+), 38 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx index 80c72235f7a4a..c9a540b9bf72b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx @@ -43,6 +43,9 @@ const values: { domains: CrawlerDomain[]; crawlRequests: CrawlRequest[] } = { rule: CrawlerRules.regex, pattern: '.*', }, + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }, ], crawlRequests: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.scss new file mode 100644 index 0000000000000..6190a0beb91bc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.scss @@ -0,0 +1,14 @@ +.deduplicationPanel { + .selectableWrapper { + padding: $euiSize; + border-radius: $euiSize *.675; + border: $euiBorderThin solid $euiColorLightestShade; + } + + .showAllFieldsPopoverToggle { + .euiButtonEmpty__content { + padding-left: $euiSizeM; + padding-right: $euiSizeM; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx new file mode 100644 index 0000000000000..eba4ee9423ae9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { act } from 'react-dom/test-utils'; + +import { + EuiButton, + EuiButtonEmpty, + EuiContextMenuItem, + EuiPopover, + EuiSelectable, + EuiSelectableList, + EuiSelectableSearch, + EuiSwitch, +} from '@elastic/eui'; + +import { mountWithIntl, rerender } from '../../../../../test_helpers'; + +import { DeduplicationPanel } from './deduplication_panel'; + +const MOCK_ACTIONS = { + submitDeduplicationUpdate: jest.fn(), +}; + +const MOCK_VALUES = { + domain: { + deduplicationEnabled: true, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], + }, +}; + +describe('DeduplicationPanel', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + setMockValues(MOCK_VALUES); + }); + + it('renders an empty component if no domain', () => { + setMockValues({ + ...MOCK_VALUES, + domain: null, + }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('contains a button to reset to defaults', () => { + const wrapper = shallow(); + + wrapper.find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenCalledWith(MOCK_VALUES.domain, { + fields: [], + }); + }); + + it('contains a switch to enable and disable deduplication', () => { + setMockValues({ + ...MOCK_VALUES, + domain: { + ...MOCK_VALUES.domain, + deduplicationEnabled: false, + }, + }); + const wrapper = shallow(); + + wrapper.find(EuiSwitch).simulate('change'); + + expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenNthCalledWith( + 1, + { + ...MOCK_VALUES.domain, + deduplicationEnabled: false, + }, + { + enabled: true, + } + ); + + setMockValues({ + ...MOCK_VALUES, + domain: { + ...MOCK_VALUES.domain, + deduplicationEnabled: true, + }, + }); + rerender(wrapper); + + wrapper.find(EuiSwitch).simulate('change'); + + expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenNthCalledWith( + 2, + { + ...MOCK_VALUES.domain, + deduplicationEnabled: true, + }, + { + enabled: false, + fields: [], + } + ); + }); + + it('contains a popover to switch between displaying all fields or only selected ones', () => { + const fullRender = mountWithIntl(); + + expect(fullRender.find(EuiButtonEmpty).text()).toEqual('All fields'); + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(false); + + // Open the popover + fullRender.find(EuiButtonEmpty).simulate('click'); + rerender(fullRender); + + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(true); + + // Click "Show selected fields" + fullRender.find(EuiContextMenuItem).at(1).simulate('click'); + rerender(fullRender); + + expect(fullRender.find(EuiButtonEmpty).text()).toEqual('Selected fields'); + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(false); + + // Open the popover and click "show all fields" + fullRender.find(EuiButtonEmpty).simulate('click'); + fullRender.find(EuiContextMenuItem).at(0).simulate('click'); + rerender(fullRender); + + expect(fullRender.find(EuiButtonEmpty).text()).toEqual('All fields'); + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(false); + + // Open the popover then simulate closing the popover + fullRender.find(EuiButtonEmpty).simulate('click'); + act(() => { + fullRender.find(EuiPopover).prop('closePopover')(); + }); + rerender(fullRender); + + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(false); + }); + + it('contains a selectable to toggle fields for deduplication', () => { + const wrapper = shallow(); + + wrapper + .find(EuiSelectable) + .simulate('change', [{ label: 'title' }, { label: 'description', checked: 'on' }]); + + expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenCalledWith(MOCK_VALUES.domain, { + fields: ['description'], + }); + + const fullRender = mountWithIntl(); + + expect(fullRender.find(EuiSelectableSearch)).toHaveLength(1); + expect(fullRender.find(EuiSelectableList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx new file mode 100644 index 0000000000000..1686b5e25e8f0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPopover, + EuiSelectable, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DOCS_PREFIX } from '../../../../routes'; +import { CrawlerSingleDomainLogic } from '../../crawler_single_domain_logic'; + +import { getCheckedOptionLabels, getSelectableOptions } from './utils'; + +import './deduplication_panel.scss'; + +export const DeduplicationPanel: React.FC = () => { + const { domain } = useValues(CrawlerSingleDomainLogic); + const { submitDeduplicationUpdate } = useActions(CrawlerSingleDomainLogic); + + const [showAllFields, setShowAllFields] = useState(true); + const [showAllFieldsPopover, setShowAllFieldsPopover] = useState(false); + + if (!domain) { + return null; + } + + const { deduplicationEnabled, deduplicationFields } = domain; + + const selectableOptions = getSelectableOptions(domain, showAllFields); + + return ( +
+ + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.title', { + defaultMessage: 'Duplicate document handling', + })} +

+
+
+ + submitDeduplicationUpdate(domain, { fields: [] })} + disabled={deduplicationFields.length === 0} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.resetToDefaultsButtonLabel', + { + defaultMessage: 'Reset to defaults', + } + )} + + +
+ + +

+ + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.learnMoreMessage', + { + defaultMessage: 'Learn more about content hashing', + } + )} + + ), + }} + /> +

+
+ + + deduplicationEnabled + ? submitDeduplicationUpdate(domain, { enabled: false, fields: [] }) + : submitDeduplicationUpdate(domain, { enabled: true }) + } + /> + + +
+ + submitDeduplicationUpdate(domain, { + fields: getCheckedOptionLabels(options as Array>), + }) + } + searchable + searchProps={{ + disabled: !deduplicationEnabled, + append: ( + setShowAllFieldsPopover(!showAllFieldsPopover)} + className="showAllFieldsPopoverToggle" + disabled={!deduplicationEnabled} + > + {showAllFields + ? i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.allFieldsLabel', + { + defaultMessage: 'All fields', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.selectedFieldsLabel', + { + defaultMessage: 'Selected fields', + } + )} + + } + isOpen={showAllFieldsPopover} + closePopover={() => setShowAllFieldsPopover(false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + setShowAllFields(true); + setShowAllFieldsPopover(false); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.showAllFieldsButtonLabel', + { + defaultMessage: 'Show all fields', + } + )} + , + { + setShowAllFields(false); + setShowAllFieldsPopover(false); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.showSelectedFieldsButtonLabel', + { + defaultMessage: 'Show only selected fields', + } + )} + , + ]} + /> + + ), + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/index.ts new file mode 100644 index 0000000000000..23545e91a7a69 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { DeduplicationPanel } from './deduplication_panel'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.test.ts new file mode 100644 index 0000000000000..58d8e1effa159 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; + +import { CrawlerDomain } from '../../types'; + +import { getCheckedOptionLabels, getSelectableOptions } from './utils'; + +describe('getCheckedOptionLabels', () => { + it('returns the labels of selected options', () => { + const options = [{ label: 'title' }, { label: 'description', checked: 'on' }] as Array< + EuiSelectableLIOption + >; + + expect(getCheckedOptionLabels(options)).toEqual(['description']); + }); +}); + +describe('getSelectableOptions', () => { + it('returns all available fields when we want all fields', () => { + expect( + getSelectableOptions( + { + availableDeduplicationFields: ['title', 'description'], + deduplicationFields: ['title'], + deduplicationEnabled: true, + } as CrawlerDomain, + true + ) + ).toEqual([ + { label: 'title', checked: 'on' }, + { label: 'description', checked: undefined }, + ]); + }); + + it('can returns only selected fields', () => { + expect( + getSelectableOptions( + { + availableDeduplicationFields: ['title', 'description'], + deduplicationFields: ['title'], + deduplicationEnabled: true, + } as CrawlerDomain, + false + ) + ).toEqual([{ label: 'title', checked: 'on' }]); + }); + + it('disables all options when deduplication is disabled', () => { + expect( + getSelectableOptions( + { + availableDeduplicationFields: ['title', 'description'], + deduplicationFields: ['title'], + deduplicationEnabled: false, + } as CrawlerDomain, + true + ) + ).toEqual([ + { label: 'title', checked: 'on', disabled: true }, + { label: 'description', checked: undefined, disabled: true }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.ts new file mode 100644 index 0000000000000..f0ef7ece0c6a6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; + +import { CrawlerDomain } from '../../types'; + +export const getSelectableOptions = ( + domain: CrawlerDomain, + showAllFields: boolean +): Array> => { + const { availableDeduplicationFields, deduplicationFields, deduplicationEnabled } = domain; + + let selectableOptions: Array>; + + if (showAllFields) { + selectableOptions = availableDeduplicationFields.map((field) => ({ + label: field, + checked: deduplicationFields.includes(field) ? 'on' : undefined, + })); + } else { + selectableOptions = availableDeduplicationFields + .filter((field) => deduplicationFields.includes(field)) + .map((field) => ({ label: field, checked: 'on' })); + } + + if (!deduplicationEnabled) { + selectableOptions = selectableOptions.map((option) => ({ ...option, disabled: true })); + } + + return selectableOptions; +}; + +export const getCheckedOptionLabels = (options: Array>): string[] => { + return options.filter((option) => option.checked).map((option) => option.label); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx index 084d9693fe279..6b8377775021c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -27,6 +27,14 @@ export const DeleteDomainPanel: React.FC = ({}) => { return ( <> + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.deleteDomainPanel.title', { + defaultMessage: 'Delete domain', + })} +

+
+

{ @@ -23,7 +25,7 @@ describe('EntryPointsTable', () => { { id: '1', value: '/whatever' }, { id: '2', value: '/foo' }, ]; - const domain = { + const domain: CrawlerDomain = { createdOn: '2018-01-01T00:00:00.000Z', documentCount: 10, id: '6113e1407a2f2e6f42489794', @@ -31,6 +33,9 @@ describe('EntryPointsTable', () => { crawlRules: [], entryPoints, sitemaps: [], + deduplicationEnabled: true, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }; beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx index 8d7aa83cd2ec6..8bfc5cdc45e4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx @@ -34,6 +34,9 @@ describe('SitemapsTable', () => { crawlRules: [], entryPoints: [], sitemaps, + deduplicationEnabled: true, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }; beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 8e8ed0d4c9258..97c7a3e47ae59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -47,6 +47,9 @@ const domains: CrawlerDomainFromServer[] = [ rule: CrawlerRules.regex, pattern: '.*', }, + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, { id: 'y', @@ -57,6 +60,9 @@ const domains: CrawlerDomainFromServer[] = [ sitemaps: [], entry_points: [], crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts index 86f6e14631329..97a050152a543 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts @@ -52,6 +52,9 @@ const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = { sitemaps: [], entry_points: [], crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, ], }; @@ -112,6 +115,9 @@ describe('CrawlerOverviewLogic', () => { entryPoints: [], crawlRules: [], defaultCrawlRule: DEFAULT_CRAWL_RULE, + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }, ], }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index 903068e28c39a..76612ee913c48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -13,15 +13,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCode } from '@elastic/eui'; - import { getPageHeaderActions } from '../../../test_helpers'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; +import { DeduplicationPanel } from './components/deduplication_panel'; import { DeleteDomainPanel } from './components/delete_domain_panel'; import { ManageCrawlsPopover } from './components/manage_crawls_popover/manage_crawls_popover'; -import { CrawlerOverview } from './crawler_overview'; import { CrawlerSingleDomain } from './crawler_single_domain'; const MOCK_VALUES = { @@ -53,7 +51,6 @@ describe('CrawlerSingleDomain', () => { const wrapper = shallow(); expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); - expect(wrapper.find(EuiCode).render().text()).toContain('https://elastic.co'); expect(wrapper.prop('pageHeader').pageTitle).toEqual('https://elastic.co'); }); @@ -71,20 +68,32 @@ describe('CrawlerSingleDomain', () => { }); it('contains a crawler status banner', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(CrawlerStatusBanner)).toHaveLength(1); }); it('contains a crawler status indicator', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(getPageHeaderActions(wrapper).find(CrawlerStatusIndicator)).toHaveLength(1); }); it('contains a popover to manage crawls', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(getPageHeaderActions(wrapper).find(ManageCrawlsPopover)).toHaveLength(1); }); + + it('contains a panel to manage deduplication settings', () => { + const wrapper = shallow(); + + expect(wrapper.find(DeduplicationPanel)).toHaveLength(1); + }); + + it('contains a panel to delete the domain', () => { + const wrapper = shallow(); + + expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index b93fb8592cff8..a4b2a9709cd62 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -11,9 +11,7 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiCode, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; @@ -21,6 +19,7 @@ import { AppSearchPageTemplate } from '../layout'; import { CrawlRulesTable } from './components/crawl_rules_table'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; +import { DeduplicationPanel } from './components/deduplication_panel'; import { DeleteDomainPanel } from './components/delete_domain_panel'; import { EntryPointsTable } from './components/entry_points_table'; import { ManageCrawlsPopover } from './components/manage_crawls_popover/manage_crawls_popover'; @@ -76,20 +75,9 @@ export const CrawlerSingleDomain: React.FC = () => { )} - -

- {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.singleDomain.deleteDomainTitle', - { - defaultMessage: 'Delete domain', - } - )} -

- - - + - {JSON.stringify(domain, null, 2)} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index 492bd363a5f2d..bf0add6df5cfe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -216,5 +216,62 @@ describe('CrawlerSingleDomainLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); + + describe('submitDeduplicationUpdate', () => { + it('updates logic with data that has been converted from server to client', async () => { + jest.spyOn(CrawlerSingleDomainLogic.actions, 'onReceiveDomainData'); + http.put.mockReturnValueOnce( + Promise.resolve({ + id: '507f1f77bcf86cd799439011', + name: 'https://elastic.co', + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + document_count: 13, + sitemaps: [], + entry_points: [], + crawl_rules: [], + deduplication_enabled: true, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], + }) + ); + + CrawlerSingleDomainLogic.actions.submitDeduplicationUpdate( + { id: '507f1f77bcf86cd799439011' } as CrawlerDomain, + { fields: ['title'], enabled: true } + ); + await nextTick(); + + expect(http.put).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/crawler/domains/507f1f77bcf86cd799439011', + { + body: JSON.stringify({ deduplication_enabled: true, deduplication_fields: ['title'] }), + } + ); + expect(CrawlerSingleDomainLogic.actions.onReceiveDomainData).toHaveBeenCalledWith({ + id: '507f1f77bcf86cd799439011', + createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', + url: 'https://elastic.co', + documentCount: 13, + sitemaps: [], + entryPoints: [], + crawlRules: [], + deduplicationEnabled: true, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], + }); + }); + + it('displays any errors to the user', async () => { + http.put.mockReturnValueOnce(Promise.reject('error')); + + CrawlerSingleDomainLogic.actions.submitDeduplicationUpdate( + { id: '507f1f77bcf86cd799439011' } as CrawlerDomain, + { fields: ['title'], enabled: true } + ); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts index 78912f736926d..e9c74c864b1b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts @@ -29,6 +29,10 @@ interface CrawlerSingleDomainActions { updateCrawlRules(crawlRules: CrawlRule[]): { crawlRules: CrawlRule[] }; updateEntryPoints(entryPoints: EntryPoint[]): { entryPoints: EntryPoint[] }; updateSitemaps(entryPoints: Sitemap[]): { sitemaps: Sitemap[] }; + submitDeduplicationUpdate( + domain: CrawlerDomain, + payload: { fields?: string[]; enabled?: boolean } + ): { domain: CrawlerDomain; fields: string[]; enabled: boolean }; } export const CrawlerSingleDomainLogic = kea< @@ -42,6 +46,7 @@ export const CrawlerSingleDomainLogic = kea< updateCrawlRules: (crawlRules) => ({ crawlRules }), updateEntryPoints: (entryPoints) => ({ entryPoints }), updateSitemaps: (sitemaps) => ({ sitemaps }), + submitDeduplicationUpdate: (domain, { fields, enabled }) => ({ domain, fields, enabled }), }, reducers: { dataLoading: [ @@ -88,6 +93,30 @@ export const CrawlerSingleDomainLogic = kea< const domainData = crawlerDomainServerToClient(response); + actions.onReceiveDomainData(domainData); + } catch (e) { + flashAPIErrors(e); + } + }, + submitDeduplicationUpdate: async ({ domain, fields, enabled }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const payload = { + deduplication_enabled: enabled, + deduplication_fields: fields, + }; + + try { + const response = await http.put( + `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}`, + { + body: JSON.stringify(payload), + } + ); + + const domainData = crawlerDomainServerToClient(response); + actions.onReceiveDomainData(domainData); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 1b46e21dbcb72..932af7a6ac93b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -98,6 +98,9 @@ export interface CrawlerDomain { defaultCrawlRule?: CrawlRule; entryPoints: EntryPoint[]; sitemaps: Sitemap[]; + deduplicationEnabled: boolean; + deduplicationFields: string[]; + availableDeduplicationFields: string[]; } export interface CrawlerDomainFromServer { @@ -110,6 +113,9 @@ export interface CrawlerDomainFromServer { default_crawl_rule?: CrawlRule; entry_points: EntryPoint[]; sitemaps: Sitemap[]; + deduplication_enabled: boolean; + deduplication_fields: string[]; + available_deduplication_fields: string[]; } export interface CrawlerData { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts index e356fae46f30e..1844932bac926 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts @@ -16,6 +16,7 @@ import { CrawlerStatus, CrawlerData, CrawlRequest, + CrawlerDomain, } from './types'; import { @@ -39,7 +40,7 @@ describe('crawlerDomainServerToClient', () => { const id = '507f1f77bcf86cd799439011'; const name = 'moviedatabase.com'; - const defaultServerPayload = { + const defaultServerPayload: CrawlerDomainFromServer = { id, name, created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', @@ -47,9 +48,12 @@ describe('crawlerDomainServerToClient', () => { sitemaps: [], entry_points: [], crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }; - const defaultClientPayload = { + const defaultClientPayload: CrawlerDomain = { id, createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', url: name, @@ -57,6 +61,9 @@ describe('crawlerDomainServerToClient', () => { sitemaps: [], entryPoints: [], crawlRules: [], + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }; expect(crawlerDomainServerToClient(defaultServerPayload)).toStrictEqual(defaultClientPayload); @@ -124,6 +131,9 @@ describe('crawlerDataServerToClient', () => { entry_points: [], crawl_rules: [], default_crawl_rule: DEFAULT_CRAWL_RULE, + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, { id: 'y', @@ -134,6 +144,9 @@ describe('crawlerDataServerToClient', () => { sitemaps: [], entry_points: [], crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, ]; @@ -154,6 +167,9 @@ describe('crawlerDataServerToClient', () => { entryPoints: [], crawlRules: [], defaultCrawlRule: DEFAULT_CRAWL_RULE, + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }, { id: 'y', @@ -164,6 +180,9 @@ describe('crawlerDataServerToClient', () => { sitemaps: [], entryPoints: [], crawlRules: [], + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }, ]); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts index a25025dc08522..1f54db12a0217 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts @@ -29,6 +29,9 @@ export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): C crawl_rules: crawlRules, default_crawl_rule: defaultCrawlRule, entry_points: entryPoints, + deduplication_enabled: deduplicationEnabled, + deduplication_fields: deduplicationFields, + available_deduplication_fields: availableDeduplicationFields, } = payload; const clientPayload: CrawlerDomain = { @@ -39,6 +42,9 @@ export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): C crawlRules, sitemaps, entryPoints, + deduplicationEnabled, + deduplicationFields, + availableDeduplicationFields, }; if (lastCrawl) { diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/enzyme_rerender.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/enzyme_rerender.ts index 70703b7017667..68b8791a0d087 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/enzyme_rerender.ts +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/enzyme_rerender.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { ShallowWrapper } from 'enzyme'; +import { CommonWrapper } from 'enzyme'; /** * Quick and easy helper for re-rendering a React component in Enzyme * after (e.g.) updating Kea values */ -export const rerender = (wrapper: ShallowWrapper) => { +export const rerender = (wrapper: CommonWrapper) => { wrapper.setProps({}); // Re-renders wrapper.update(); // Just in case }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index 38cae6d5d7f7c..d50d7b7cee225 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -266,7 +266,7 @@ describe('crawler routes', () => { }); }); - it('validates correctly with required params', () => { + it('validates correctly with crawl rules', () => { const request = { params: { name: 'some-engine', id: '1234' }, body: { @@ -281,9 +281,24 @@ describe('crawler routes', () => { mockRouter.shouldValidate(request); }); - it('fails otherwise', () => { - const request = { params: {}, body: {} }; - mockRouter.shouldThrow(request); + it('validates correctly with deduplication enabled', () => { + const request = { + params: { name: 'some-engine', id: '1234' }, + body: { + deduplication_enabled: true, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('validates correctly with deduplication fields', () => { + const request = { + params: { name: 'some-engine', id: '1234' }, + body: { + deduplication_fields: ['title', 'description'], + }, + }; + mockRouter.shouldValidate(request); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index 79664d45dbbd8..cf90ffdea412a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -136,12 +136,16 @@ export function registerCrawlerRoutes({ id: schema.string(), }), body: schema.object({ - crawl_rules: schema.arrayOf( - schema.object({ - order: schema.number(), - id: schema.string(), - }) + crawl_rules: schema.maybe( + schema.arrayOf( + schema.object({ + order: schema.number(), + id: schema.string(), + }) + ) ), + deduplication_enabled: schema.maybe(schema.boolean()), + deduplication_fields: schema.maybe(schema.arrayOf(schema.string())), }), }, }, From 15494cd25f9c01fdb0e63cfd867ec842301b7fee Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 17 Aug 2021 12:10:00 -0400 Subject: [PATCH 09/11] uptime - index default severity of warning for tls and monitor status alerts (#108731) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/alerts_as_data_severity.ts | 11 +++++++++++ packages/kbn-rule-data-utils/src/index.ts | 1 + .../uptime/server/lib/alerts/status_check.test.ts | 4 +++- .../plugins/uptime/server/lib/alerts/status_check.ts | 2 ++ x-pack/plugins/uptime/server/lib/alerts/tls.test.ts | 2 ++ x-pack/plugins/uptime/server/lib/alerts/tls.ts | 3 ++- 6 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 packages/kbn-rule-data-utils/src/alerts_as_data_severity.ts diff --git a/packages/kbn-rule-data-utils/src/alerts_as_data_severity.ts b/packages/kbn-rule-data-utils/src/alerts_as_data_severity.ts new file mode 100644 index 0000000000000..c23af291fbefc --- /dev/null +++ b/packages/kbn-rule-data-utils/src/alerts_as_data_severity.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. + */ + +export const ALERT_SEVERITY_WARNING = 'warning'; +export const ALERT_SEVERITY_CRITICAL = 'critical'; +export type AlertSeverity = typeof ALERT_SEVERITY_WARNING | typeof ALERT_SEVERITY_CRITICAL; diff --git a/packages/kbn-rule-data-utils/src/index.ts b/packages/kbn-rule-data-utils/src/index.ts index f60ad31286c9c..ef06d5777b5ab 100644 --- a/packages/kbn-rule-data-utils/src/index.ts +++ b/packages/kbn-rule-data-utils/src/index.ts @@ -8,3 +8,4 @@ export * from './technical_field_names'; export * from './alerts_as_data_rbac'; +export * from './alerts_as_data_severity'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts index 73f4501ace591..4cf7a566454c4 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_SEVERITY_WARNING, ALERT_SEVERITY_LEVEL } from '@kbn/rule-data-utils'; import { generateFilterDSL, hasFilters, @@ -75,6 +75,7 @@ const mockStatusAlertDocument = ( [ALERT_REASON]: `Monitor first with url ${monitorInfo?.url?.full} is down from ${ monitorInfo.observer?.geo?.name }. The latest error message is ${monitorInfo.error?.message || ''}`, + [ALERT_SEVERITY_LEVEL]: ALERT_SEVERITY_WARNING, }, id: getInstanceId( monitorInfo, @@ -95,6 +96,7 @@ const mockAvailabilityAlertDocument = (monitor: GetMonitorAvailabilityResult) => )}% availability expected is 99.34% from ${ monitorInfo.observer?.geo?.name }. The latest error message is ${monitorInfo.error?.message || ''}`, + [ALERT_SEVERITY_LEVEL]: ALERT_SEVERITY_WARNING, }, id: getInstanceId(monitorInfo, `${monitorInfo?.monitor.id}-${monitorInfo.observer?.geo?.name}`), }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index bf8c0176122f0..4b00b7316b687 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -7,6 +7,7 @@ import { min } from 'lodash'; import datemath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; +import { ALERT_SEVERITY_WARNING, ALERT_SEVERITY_LEVEL } from '@kbn/rule-data-utils'; import { i18n } from '@kbn/i18n'; import { JsonObject } from '@kbn/utility-types'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; @@ -158,6 +159,7 @@ export const getMonitorAlertDocument = (monitorSummary: Record { 'tls.server.x509.not_after': cert.not_after, 'tls.server.x509.not_before': cert.not_before, 'tls.server.hash.sha256': cert.sha256, + [ALERT_SEVERITY_LEVEL]: ALERT_SEVERITY_WARNING, }), id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`, }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 5bb1e5ee3d903..88fa88b24d22e 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -6,7 +6,7 @@ */ import moment from 'moment'; import { schema } from '@kbn/config-schema'; -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_SEVERITY_WARNING, ALERT_SEVERITY_LEVEL } from '@kbn/rule-data-utils'; import { UptimeAlertTypeFactory } from './types'; import { updateState, generateAlertMessage } from './common'; import { TLS } from '../../../common/constants/alerts'; @@ -172,6 +172,7 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, 'tls.server.x509.not_after': cert.not_after, 'tls.server.x509.not_before': cert.not_before, 'tls.server.hash.sha256': cert.sha256, + [ALERT_SEVERITY_LEVEL]: ALERT_SEVERITY_WARNING, [ALERT_REASON]: generateAlertMessage(TlsTranslations.defaultActionMessage, summary), }, }); From 6a5a2150ea894f675e88fb818341fe190beb0f87 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Tue, 17 Aug 2021 18:18:00 +0200 Subject: [PATCH 10/11] [Security solution] [Timeline] Improve timeline title and move description to notes tab (#106544) * Improve timeline title and move description to the notes tab Truncate the title only in the UI When the user hover the title we display the full title Truncate the title if it appears in a table --- .../common/types/timeline/index.ts | 11 +++ .../integration/timelines/creation.spec.ts | 5 +- .../cypress/tasks/timeline.ts | 17 ++-- .../common/components/line_clamp/index.tsx | 23 +---- .../components/markdown_editor/editor.tsx | 11 ++- .../components/scroll_to_top/index.test.tsx | 18 ++++ .../common/components/scroll_to_top/index.tsx | 16 +++- .../public/common/hooks/use_is_overflow.tsx | 32 +++++++ .../public/common/mock/utils.ts | 2 + .../flyout/header/active_timelines.tsx | 8 +- .../components/flyout/header/index.tsx | 83 +++++++++++++++---- .../components/flyout/header/translations.ts | 4 + .../__snapshots__/new_note.test.tsx.snap | 1 + .../components/notes/add_note/index.tsx | 10 ++- .../components/notes/add_note/new_note.tsx | 4 +- .../note_previews/index.test.tsx | 53 +++++++----- .../open_timeline/note_previews/index.tsx | 59 +++++++++++-- .../note_previews/translations.ts | 7 ++ .../timelines_table/common_columns.test.tsx | 9 ++ .../timelines_table/common_columns.tsx | 12 ++- .../timelines/components/timeline/index.tsx | 5 +- .../timeline/notes_tab_content/index.tsx | 29 ++++++- .../timeline/tabs_content/index.tsx | 11 ++- .../timeline/tabs_content/selectors.ts | 3 + .../timelines/store/timeline/actions.ts | 3 +- .../public/timelines/store/timeline/model.ts | 6 ++ .../timelines/store/timeline/reducer.ts | 7 +- 27 files changed, 354 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_is_overflow.tsx diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 6a3d812b1bf5b..cdd9b35a7fa30 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -460,6 +460,17 @@ export enum TimelineTabs { eql = 'eql', } +/** + * Used for scrolling top inside a tab. Especially when swiching tabs. + */ +export interface ScrollToTopEvent { + /** + * Timestamp of the moment when the event happened. + * The timestamp might be necessary for the scenario where the event could happen multiple times. + */ + timestamp: number; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type EmptyObject = Record; diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index 4203b9125d155..096ac0595d76c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -16,6 +16,7 @@ import { TIMELINE_FLYOUT_WRAPPER, TIMELINE_PANEL, TIMELINE_TAB_CONTENT_EQL, + TIMELINE_TAB_CONTENT_GRAPHS_NOTES, } from '../../screens/timeline'; import { createTimelineTemplate } from '../../tasks/api_calls/timelines'; @@ -90,7 +91,9 @@ describe('Timelines', (): void => { it('can be added notes', () => { addNotesToTimeline(getTimeline().notes); - cy.get(NOTES_TEXT).should('have.text', getTimeline().notes); + cy.get(TIMELINE_TAB_CONTENT_GRAPHS_NOTES) + .find(NOTES_TEXT) + .should('have.text', getTimeline().notes); }); it('should update timeline after adding eql', () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index d487cf6d00ed3..4a61a94e4acea 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -133,15 +133,16 @@ export const goToQueryTab = () => { export const addNotesToTimeline = (notes: string) => { goToNotesTab().then(() => { - cy.get(NOTES_TEXT_AREA).type(notes); - cy.root() - .pipe(($el) => { - $el.find(ADD_NOTE_BUTTON).trigger('click'); - return $el.find(NOTES_TAB_BUTTON).find('.euiBadge'); - }) - .should('have.text', '1'); + cy.get(NOTES_TAB_BUTTON) + .find('.euiBadge__text') + .then(($el) => { + const notesCount = parseInt($el.text(), 10); + + cy.get(NOTES_TEXT_AREA).type(notes); + cy.get(ADD_NOTE_BUTTON).trigger('click'); + cy.get(`${NOTES_TAB_BUTTON} .euiBadge`).should('have.text', `${notesCount + 1}`); + }); }); - goToQueryTab(); goToNotesTab(); }; diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx index 372e7fd466b07..17e0262a2cffa 100644 --- a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx @@ -6,8 +6,9 @@ */ import { EuiButtonEmpty } from '@elastic/eui'; -import React, { useRef, useState, useEffect, useCallback, ReactNode } from 'react'; +import React, { useState, useCallback, ReactNode } from 'react'; import styled from 'styled-components'; +import { useIsOverflow } from '../../hooks/use_is_overflow'; import * as i18n from './translations'; const LINE_CLAMP = 3; @@ -39,29 +40,13 @@ const LineClampComponent: React.FC<{ children: ReactNode; lineClampHeight?: number; }> = ({ children, lineClampHeight = LINE_CLAMP_HEIGHT }) => { - const [isOverflow, setIsOverflow] = useState(null); const [isExpanded, setIsExpanded] = useState(null); - const descriptionRef = useRef(null); + const [isOverflow, descriptionRef] = useIsOverflow(children); + const toggleReadMore = useCallback(() => { setIsExpanded((prevState) => !prevState); }, []); - useEffect(() => { - if (descriptionRef?.current?.clientHeight != null) { - if ( - (descriptionRef?.current?.scrollHeight ?? 0) > (descriptionRef?.current?.clientHeight ?? 0) - ) { - setIsOverflow(true); - } - - if ( - (descriptionRef?.current?.scrollHeight ?? 0) <= (descriptionRef?.current?.clientHeight ?? 0) - ) { - setIsOverflow(false); - } - } - }, []); - if (isExpanded) { return ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx index 12084a17e888a..f1fa6dc0fa1ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx @@ -17,6 +17,7 @@ interface MarkdownEditorProps { editorId?: string; dataTestSubj?: string; height?: number; + autoFocusDisabled?: boolean; } const MarkdownEditorComponent: React.FC = ({ @@ -26,16 +27,18 @@ const MarkdownEditorComponent: React.FC = ({ editorId, dataTestSubj, height, + autoFocusDisabled = false, }) => { const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); const onParse = useCallback((err, { messages }) => { setMarkdownErrorMessages(err ? [err] : messages); }, []); - useEffect( - () => document.querySelector('textarea.euiMarkdownEditorTextArea')?.focus(), - [] - ); + useEffect(() => { + if (!autoFocusDisabled) { + document.querySelector('textarea.euiMarkdownEditorTextArea')?.focus(); + } + }, [autoFocusDisabled]); return ( { Object.defineProperty(globalNode.window, 'scroll', { value: null }); Object.defineProperty(globalNode.window, 'scrollTo', { value: spyScrollTo }); mount( useScrollToTop()} />); + expect(spyScrollTo).toHaveBeenCalled(); }); + + test('should not scroll when `shouldScroll` is false', () => { + Object.defineProperty(globalNode.window, 'scroll', { value: spyScroll }); + mount( useScrollToTop(undefined, false)} />); + + expect(spyScrollTo).not.toHaveBeenCalled(); + }); + + test('should scroll the element matching the given selector', () => { + const fakeElement = { scroll: spyScroll }; + Object.defineProperty(globalNode.document, 'querySelector', { + value: () => fakeElement, + }); + mount( useScrollToTop('fake selector')} />); + + expect(spyScroll).toHaveBeenCalledWith(0, 0); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.tsx b/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.tsx index d9f80b7e1c3d2..79e5273b9735e 100644 --- a/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.tsx @@ -7,14 +7,22 @@ import { useEffect } from 'react'; -export const useScrollToTop = () => { +/** + * containerSelector: The element with scrolling. It defaults to the window. + * shouldScroll: It should be used for conditional scrolling. + */ +export const useScrollToTop = (containerSelector?: string, shouldScroll = true) => { useEffect(() => { + const container = containerSelector ? document.querySelector(containerSelector) : window; + + if (!shouldScroll || !container) return; + // trying to use new API - https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo - if (window.scroll) { - window.scroll(0, 0); + if (container.scroll) { + container.scroll(0, 0); } else { // just a fallback for older browsers - window.scrollTo(0, 0); + container.scrollTo(0, 0); } }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_is_overflow.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_is_overflow.tsx new file mode 100644 index 0000000000000..c191b945cc31e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_is_overflow.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useRef, useState } from 'react'; + +/** + * It checks if the element that receives the returned Ref has oveflow the max height. + */ +export const useIsOverflow: ( + dependency: unknown +) => [isOveflow: boolean | null, ref: React.RefObject] = (dependency) => { + const [isOverflow, setIsOverflow] = useState(null); + const ref = useRef(null); + + useEffect(() => { + if (ref.current?.clientHeight != null) { + if ((ref?.current?.scrollHeight ?? 0) > (ref?.current?.clientHeight ?? 0)) { + setIsOverflow(true); + } + + if ((ref.current?.scrollHeight ?? 0) <= (ref?.current?.clientHeight ?? 0)) { + setIsOverflow(false); + } + } + }, [ref, dependency]); + + return [isOverflow, ref]; +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts index 0d9e2f4f367ec..b1851fd055b33 100644 --- a/x-pack/plugins/security_solution/public/common/mock/utils.ts +++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts @@ -24,6 +24,8 @@ import { defaultHeaders } from '../../timelines/components/timeline/body/column_ interface Global extends NodeJS.Global { // eslint-disable-next-line @typescript-eslint/no-explicit-any window?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + document?: any; } export const globalNode: Global = global; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx index 64832bf7f039d..4eb91ca8ee272 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -41,6 +41,12 @@ const StyledEuiButtonEmpty = styled(EuiButtonEmpty)` } `; +const TitleConatiner = styled(EuiFlexItem)` + overflow: hidden; + display: inline-block; + text-overflow: ellipsis; +`; + const ActiveTimelinesComponent: React.FC = ({ timelineId, timelineStatus, @@ -100,7 +106,7 @@ const ActiveTimelinesComponent: React.FC = ({ /> - {title} + {title} {!isOpen && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index ee994e2a16f46..e3a1152428d62 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -12,9 +12,10 @@ import { EuiToolTip, EuiButtonIcon, EuiText, + EuiButtonEmpty, EuiTextColor, } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { MouseEventHandler, MouseEvent, useCallback, useMemo } from 'react'; import { isEmpty, get, pick } from 'lodash/fp'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; @@ -52,7 +53,9 @@ import * as i18n from './translations'; import * as commonI18n from '../../timeline/properties/translations'; import { getTimelineStatusByIdSelector } from './selectors'; import { TimelineKPIs } from './kpis'; -import { LineClamp } from '../../../../common/components/line_clamp'; + +import { setActiveTabTimeline } from '../../../store/timeline/actions'; +import { useIsOverflow } from '../../../../common/hooks/use_is_overflow'; // to hide side borders const StyledPanel = styled(EuiPanel)` @@ -67,6 +70,10 @@ interface FlyoutHeaderPanelProps { timelineId: string; } +const ActiveTimelinesContainer = styled(EuiFlexItem)` + overflow: hidden; +`; + const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); const { indexPattern, browserFields } = useSourcererScope(SourcererScopeName.timeline); @@ -145,7 +152,7 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline > - + = ({ timeline isOpen={show} updated={updated} /> - + {show && ( @@ -190,6 +197,34 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline export const FlyoutHeaderPanel = React.memo(FlyoutHeaderPanelComponent); +const StyledDiv = styled.div` + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; +`; + +const ReadMoreButton = ({ + description, + onclick, +}: { + description: string; + onclick: MouseEventHandler; +}) => { + const [isOverflow, ref] = useIsOverflow(description); + return ( + <> + {description} + {isOverflow && ( + + {i18n.READ_MORE} + + )} + + ); +}; + const StyledTimelineHeader = styled(EuiFlexGroup)` ${({ theme }) => `margin: ${theme.eui.euiSizeXS} ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS};`} flex: 0; @@ -197,6 +232,7 @@ const StyledTimelineHeader = styled(EuiFlexGroup)` const TimelineStatusInfoContainer = styled.span` ${({ theme }) => `margin-left: ${theme.eui.euiSizeS};`} + white-space: nowrap; `; const KpisContainer = styled.div` @@ -208,6 +244,14 @@ const RowFlexItem = styled(EuiFlexItem)` align-items: center; `; +const TimelineTitleContainer = styled.h3` + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + word-break: break-word; +`; + const TimelineNameComponent: React.FC = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { title, timelineType } = useDeepEqualSelector((state) => @@ -224,9 +268,11 @@ const TimelineNameComponent: React.FC = ({ timelineId }) => { const content = useMemo(() => title || placeholder, [title, placeholder]); return ( - -

{content}

-
+ + + {content} + + ); }; @@ -237,15 +283,24 @@ const TimelineDescriptionComponent: React.FC = ({ timelineId const description = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).description ); + const dispatch = useDispatch(); + + const onReadMore: MouseEventHandler = useCallback( + (event: MouseEvent) => { + dispatch( + setActiveTabTimeline({ + id: timelineId, + activeTab: TimelineTabs.notes, + scrollToTop: true, + }) + ); + }, + [dispatch, timelineId] + ); + return ( - {description ? ( - - {description} - - ) : ( - commonI18n.DESCRIPTION - )} + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts index 7483d0cae71c5..2f0717dea32aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts @@ -61,6 +61,10 @@ export const USER_KPI_TITLE = i18n.translate('xpack.securitySolution.timeline.kp defaultMessage: 'Users', }); +export const READ_MORE = i18n.translate('xpack.securitySolution.timeline.properties.readMore', { + defaultMessage: 'Read More', +}); + export const TIMELINE_TOGGLE_BUTTON_ARIA_LABEL = ({ isOpen, title, diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap index 69e06bc7e0d1b..32e17a19045b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap @@ -6,6 +6,7 @@ exports[`NewNote renders correctly 1`] = ` > void; updateNewNote: UpdateInternalNewNote; -}>(({ associateNote, newNote, onCancelAddNote, updateNewNote }) => { + autoFocusDisabled?: boolean; +}>(({ associateNote, newNote, onCancelAddNote, updateNewNote, autoFocusDisabled = false }) => { const dispatch = useDispatch(); const updateNote = useCallback((note: Note) => dispatch(appActions.updateNote({ note })), [ @@ -87,7 +88,12 @@ export const AddNote = React.memo<{

{i18n.YOU_ARE_EDITING_A_NOTE}

- + {onCancelAddNote != null ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx index 761df470e6f4d..bf1a2227f6f99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx @@ -24,7 +24,8 @@ export const NewNote = React.memo<{ noteInputHeight: number; note: string; updateNewNote: UpdateInternalNewNote; -}>(({ note, noteInputHeight, updateNewNote }) => { + autoFocusDisabled?: boolean; +}>(({ note, noteInputHeight, updateNewNote, autoFocusDisabled = false }) => { return ( ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx index 0c611ca5106e8..1cca5a3999b81 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -10,11 +10,21 @@ import moment from 'moment'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import '../../../../common/mock/formatted_relative'; - +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult, TimelineResultNote } from '../types'; import { NotePreviews } from '.'; +jest.mock('../../../../common/hooks/use_selector'); + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => jest.fn(), + }; +}); + describe('NotePreviews', () => { let mockResults: OpenTimelineResult[]; let note1updated: number; @@ -26,6 +36,7 @@ describe('NotePreviews', () => { note1updated = moment('2019-03-24T04:12:33.000Z').valueOf(); note2updated = moment(note1updated).add(1, 'minute').valueOf(); note3updated = moment(note2updated).add(1, 'minute').valueOf(); + (useDeepEqualSelector as jest.Mock).mockReset(); }); test('it renders a note preview for each note when isModal is false', () => { @@ -48,24 +59,6 @@ describe('NotePreviews', () => { }); }); - test('it does NOT render the preview container if notes is undefined', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - - test('it does NOT render the preview container if notes is null', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - - test('it does NOT render the preview container if notes is empty', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - test('it filters-out non-unique savedObjectIds', () => { const nonUniqueNotes: TimelineResultNote[] = [ { @@ -145,4 +138,26 @@ describe('NotePreviews', () => { expect(wrapper.find(`.euiCommentEvent__headerUsername`).at(2).text()).toEqual('bob'); }); + + test('it renders timeline description as a note when showTimelineDescription is true and timelineId is defined', () => { + const timeline = mockTimelineResults[0]; + (useDeepEqualSelector as jest.Mock).mockReturnValue(timeline); + + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="note-preview-description"]').first().text()).toContain( + timeline.description + ); + }); + + test('it does`t render timeline description as a note when it is undefined', () => { + const timeline = mockTimelineResults[0]; + (useDeepEqualSelector as jest.Mock).mockReturnValue({ ...timeline, description: undefined }); + + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="note-preview-description"]').exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 5581ea4e5c165..aff12b74fbfbf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -6,7 +6,13 @@ */ import { uniqBy } from 'lodash/fp'; -import { EuiAvatar, EuiButtonIcon, EuiCommentList, EuiScreenReaderOnly } from '@elastic/eui'; +import { + EuiAvatar, + EuiButtonIcon, + EuiCommentList, + EuiScreenReaderOnly, + EuiText, +} from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -15,12 +21,13 @@ import { useDispatch } from 'react-redux'; import { TimelineResultNote } from '../types'; import { getEmptyValue, defaultToEmptyTag } from '../../../../common/components/empty_value'; import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; -import { timelineActions } from '../../../store/timeline'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { NOTE_CONTENT_CLASS_NAME } from '../../timeline/body/helpers'; import * as i18n from './translations'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { sourcererSelectors } from '../../../../common/store'; +import { SaveTimelineButton } from '../../timeline/header/save_timeline_button'; export const NotePreviewsContainer = styled.section` padding-top: ${({ theme }) => `${theme.eui.euiSizeS}`}; @@ -78,10 +85,45 @@ interface NotePreviewsProps { eventIdToNoteIds?: Record; notes?: TimelineResultNote[] | null; timelineId?: string; + showTimelineDescription?: boolean; } export const NotePreviews = React.memo( - ({ eventIdToNoteIds, notes, timelineId }) => { + ({ eventIdToNoteIds, notes, timelineId, showTimelineDescription }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const timeline = useDeepEqualSelector((state) => + timelineId ? getTimeline(state, timelineId) : null + ); + + const descriptionList = useMemo( + () => + showTimelineDescription && timelineId && timeline?.description + ? [ + { + username: defaultToEmptyTag(timeline.updatedBy), + event: i18n.ADDED_A_DESCRIPTION, + 'data-test-subj': 'note-preview-description', + id: 'note-preview-description', + timestamp: timeline.updated ? ( + + ) : ( + getEmptyValue() + ), + children: {timeline.description}, + timelineIcon: ( + + ), + actions: , + }, + ] + : [], + [timeline, timelineId, showTimelineDescription] + ); + const notesList = useMemo( () => uniqBy('savedObjectId', notes).map((note) => { @@ -125,11 +167,12 @@ export const NotePreviews = React.memo( [eventIdToNoteIds, notes, timelineId] ); - if (notes == null || notes.length === 0) { - return null; - } - - return ; + return ( + + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts index 0945050a34a4d..c2d01704c2d9e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts @@ -18,6 +18,13 @@ export const ADDED_A_NOTE = i18n.translate('xpack.securitySolution.timeline.adde defaultMessage: 'added a note', }); +export const ADDED_A_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.timeline.addedADescriptionLabel', + { + defaultMessage: 'added description', + } +); + export const AN_UNKNOWN_USER = i18n.translate( 'xpack.securitySolution.timeline.anUnknownUserLabel', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx index 1826413110f1e..bdb55aaf20969 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx @@ -27,6 +27,15 @@ const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); jest.mock('../../../../common/lib/kibana'); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => jest.fn(), + useSelector: () => jest.fn(), + }; +}); + describe('#getCommonColumns', () => { let mockResults: OpenTimelineResult[]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx index 65963c9609320..21262d66fdbfe 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx @@ -20,7 +20,7 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; import { TimelineType } from '../../../../../common/types/timeline'; -const DescriptionCell = styled.span` +const LineClampTextContainer = styled.span` text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 5; @@ -79,7 +79,11 @@ export const getCommonColumns = ({ }) } > - {isUntitled(timelineResult) ? i18n.UNTITLED_TIMELINE : title} + {isUntitled(timelineResult) ? ( + i18n.UNTITLED_TIMELINE + ) : ( + {title} + )} ) : (
@@ -93,9 +97,9 @@ export const getCommonColumns = ({ field: 'description', name: i18n.DESCRIPTION, render: (description: string) => ( - + {description != null && description.trim().length > 0 ? description : getEmptyTagValue()} - + ), sortable: false, }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index e95efdf754418..e8846d88ef919 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -62,9 +62,9 @@ const StatefulTimelineComponent: React.FC = ({ const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { selectedPatterns } = useSourcererScope(SourcererScopeName.timeline); - const { graphEventId, savedObjectId, timelineType } = useDeepEqualSelector((state) => + const { graphEventId, savedObjectId, timelineType, description } = useDeepEqualSelector((state) => pick( - ['graphEventId', 'savedObjectId', 'timelineType'], + ['graphEventId', 'savedObjectId', 'timelineType', 'description'], getTimeline(state, timelineId) ?? timelineDefaults ) ); @@ -146,6 +146,7 @@ const StatefulTimelineComponent: React.FC = ({ setTimelineFullScreen={setTimelineFullScreen} timelineId={timelineId} timelineType={timelineType} + timelineDescription={description} timelineFullScreen={timelineFullScreen} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx index 2853a5afccdd2..7605bc8607bb0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx @@ -25,7 +25,10 @@ import styled from 'styled-components'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineActions } from '../../../store/timeline'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { + useDeepEqualSelector, + useShallowEqualSelector, +} from '../../../../common/hooks/use_selector'; import { TimelineStatus, TimelineTabs } from '../../../../../common/types/timeline'; import { appSelectors } from '../../../../common/store/app'; import { AddNote } from '../../notes/add_note'; @@ -35,6 +38,8 @@ import { NotePreviews } from '../../open_timeline/note_previews'; import { TimelineResultNote } from '../../open_timeline/types'; import { getTimelineNoteSelector } from './selectors'; import { DetailsPanel } from '../../side_panel'; +import { getScrollToTopSelector } from '../tabs_content/selectors'; +import { useScrollToTop } from '../../../../common/components/scroll_to_top'; const FullWidthFlexGroup = styled(EuiFlexGroup)` width: 100%; @@ -126,6 +131,12 @@ interface NotesTabContentProps { const NotesTabContentComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); + + const getScrollToTop = useMemo(() => getScrollToTopSelector(), []); + const scrollToTop = useShallowEqualSelector((state) => getScrollToTop(state, timelineId)); + + useScrollToTop('#scrollableNotes', !!scrollToTop); + const getTimelineNotes = useMemo(() => getTimelineNoteSelector(), []); const { createdBy, @@ -207,16 +218,26 @@ const NotesTabContentComponent: React.FC = ({ timelineId } return ( - +

{NOTES}

- + {!isImmutable && ( - + )}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 8cdd7722d7fbd..cfe2af0ab7c31 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -6,6 +6,7 @@ */ import { EuiBadge, EuiLoadingContent, EuiTabs, EuiTab } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; @@ -59,6 +60,7 @@ interface BasicTimelineTab { timelineId: TimelineId; timelineType: TimelineType; graphEventId?: string; + timelineDescription: string; } const QueryTab: React.FC<{ @@ -222,6 +224,7 @@ const TabsContentComponent: React.FC = ({ timelineFullScreen, timelineType, graphEventId, + timelineDescription, }) => { const dispatch = useDispatch(); const getActiveTab = useMemo(() => getActiveTabSelector(), []); @@ -233,6 +236,7 @@ const TabsContentComponent: React.FC = ({ const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId)); const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId)); + const numberOfPinnedEvents = useShallowEqualSelector((state) => getNumberOfPinnedEvents(state, timelineId) ); @@ -253,8 +257,10 @@ const TabsContentComponent: React.FC = ({ }, [globalTimelineNoteIds, eventIdToNoteIds]); const numberOfNotes = useMemo( - () => appNotes.filter((appNote) => allTimelineNoteIds.includes(appNote.id)).length, - [appNotes, allTimelineNoteIds] + () => + appNotes.filter((appNote) => allTimelineNoteIds.includes(appNote.id)).length + + (isEmpty(timelineDescription) ? 0 : 1), + [appNotes, allTimelineNoteIds, timelineDescription] ); const setQueryAsActiveTab = useCallback(() => { @@ -362,6 +368,7 @@ const TabsContentComponent: React.FC = ({ rowRenderers={rowRenderers} timelineId={timelineId} timelineType={timelineType} + timelineDescription={timelineDescription} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts index ccb07135747f5..04045e94aee25 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts @@ -27,3 +27,6 @@ export const getEventIdToNoteIdsSelector = () => export const getNotesSelector = () => createSelector(selectNotesById, (notesById) => Object.values(notesById)); + +export const getScrollToTopSelector = () => + createSelector(selectTimeline, (timeline) => timeline?.scrollToTop); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index d5f692cc9dc17..d0d5fdacad312 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -17,7 +17,7 @@ import { import { KqlMode, TimelineModel } from './model'; import { InsertTimeline } from './types'; import { FieldsEqlOptions } from '../../../../common/search_strategy/timeline'; -import { +import type { TimelineEventsType, RowRendererId, TimelineTabs, @@ -204,6 +204,7 @@ export const updateIndexNames = actionCreator<{ export const setActiveTabTimeline = actionCreator<{ id: string; activeTab: TimelineTabs; + scrollToTop?: boolean; }>('SET_ACTIVE_TAB_TIMELINE'); export const toggleModalSaveTimeline = actionCreator<{ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index ef47b474350c7..3c2449a2e787d 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -11,6 +11,7 @@ import type { TimelineType, TimelineStatus, TimelineTabs, + ScrollToTopEvent, } from '../../../../common/types/timeline'; import { PinnedEvent } from '../../../../common/types/timeline/pinned_event'; import type { TGridModelForTimeline } from '../../../../../timelines/public'; @@ -23,6 +24,9 @@ export type TimelineModel = TGridModelForTimeline & { /** The selected tab to displayed in the timeline */ activeTab: TimelineTabs; prevActiveTab: TimelineTabs; + + /** Used for scrolling to top when swiching tabs. It includes the timestamp of when the event happened */ + scrollToTop?: ScrollToTopEvent; /** Timeline saved object owner */ createdBy?: string; /** A summary of the events and notes in this timeline */ @@ -63,6 +67,8 @@ export type TimelineModel = TGridModelForTimeline & { status: TimelineStatus; /** updated saved object timestamp */ updated?: number; + /** updated saved object user */ + updatedBy?: string | null; /** timeline is saving */ isSaving: boolean; version: string | null; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index a302f43e61b13..97fa72667a3c6 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -331,7 +331,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) }, }, })) - .case(setActiveTabTimeline, (state, { id, activeTab }) => ({ + .case(setActiveTabTimeline, (state, { id, activeTab, scrollToTop }) => ({ ...state, timelineById: { ...state.timelineById, @@ -339,6 +339,11 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state.timelineById[id], activeTab, prevActiveTab: state.timelineById[id].activeTab, + scrollToTop: scrollToTop + ? { + timestamp: Math.floor(Date.now() / 1000), // convert to seconds to avoid unnecessary rerenders for multiple clicks + } + : undefined, }, }, })) From 5b30b3d31684b54adb3e77ed941fed8afbb5e0e4 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 17 Aug 2021 19:21:17 +0300 Subject: [PATCH 11/11] [TSVB] Markdown variables should be clickable (#108844) * [TSVB] Markdown variables should be clickable * fix test --- .../application/components/markdown_editor.js | 16 +++++++++------- test/functional/apps/visualize/_tsvb_markdown.ts | 12 +++++++++++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js index b9aa70f5207af..27622e29c2061 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js @@ -26,15 +26,17 @@ export class MarkdownEditor extends Component { this.props.onChange({ markdown: value }); }; - handleOnLoad = (ace) => { - this.ace = ace; + handleOnLoad = (editor) => { + this.editor = editor; }; - handleVarClick(snippet) { - return () => { - if (this.ace) this.ace.insert(snippet); - }; - } + handleVarClick = (snippet) => () => { + if (this.editor) { + const range = this.editor.getSelection(); + + this.editor.executeEdits('', [{ range, text: snippet }]); + } + }; render() { const { visData, model, getConfig } = this.props; diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index 89db60bc7645c..b8b74d5cd7bf3 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -11,10 +11,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualBuilder, timePicker, visualize } = getPageObjects([ + const { visualBuilder, timePicker, visualize, visChart } = getPageObjects([ 'visualBuilder', 'timePicker', 'visualize', + 'visChart', ]); const retry = getService('retry'); @@ -76,6 +77,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(markdownText).to.be(html); }); + it('markdown variables should be clickable', async () => { + await visualBuilder.clearMarkdown(); + const [firstVariable] = await visualBuilder.getMarkdownTableVariables(); + await firstVariable.selector.click(); + await visChart.waitForVisualizationRenderingStabilized(); + const markdownText = await visualBuilder.getMarkdownText(); + expect(markdownText).to.be('46'); + }); + it('should render mustache list', async () => { const list = '{{#each _all}}\n{{ data.formatted.[0] }} {{ data.raw.[0] }}\n{{/each}}'; const expectedRenderer = 'Sep 22, 2015 @ 06:00:00.000,6 1442901600000,6';