From 96c4350289adace86b877e6836be0029a062f662 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Mon, 28 Jun 2021 08:13:41 -0400 Subject: [PATCH 01/41] Remove post-installation redirect for integrations (#103179) When installation integrations via the browse -> add integration flow in the integrations UI, we will no longer redirect the user back to the integration details page. Closes #100978 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../sections/epm/screens/detail/index.tsx | 67 ++----------------- 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index cf6007026afeb..e840da142cfbf 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -6,7 +6,7 @@ */ import type { ReactEventHandler } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Redirect, Route, Switch, useLocation, useParams, useHistory } from 'react-router-dom'; +import { Redirect, Route, Switch, useLocation, useParams } from 'react-router-dom'; import styled from 'styled-components'; import { EuiBetaBadge, @@ -31,12 +31,7 @@ import { useBreadcrumbs, useStartServices, } from '../../../../hooks'; -import { - PLUGIN_ID, - INTEGRATIONS_PLUGIN_ID, - INTEGRATIONS_ROUTING_PATHS, - pagePathGetters, -} from '../../../../constants'; +import { PLUGIN_ID, INTEGRATIONS_ROUTING_PATHS, pagePathGetters } from '../../../../constants'; import { useCapabilities, useGetPackageInfoByKey, @@ -44,11 +39,7 @@ import { useAgentPolicyContext, } from '../../../../hooks'; import { pkgKeyFromPackageInfo } from '../../../../services'; -import type { - CreatePackagePolicyRouteState, - DetailViewPanelName, - PackageInfo, -} from '../../../../types'; +import type { DetailViewPanelName, PackageInfo } from '../../../../types'; import { InstallStatus } from '../../../../types'; import { Error, Loading } from '../../../../components'; import type { WithHeaderLayoutProps } from '../../../../layouts'; @@ -89,8 +80,7 @@ export function Detail() { const { pkgkey, panel } = useParams(); const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; - const history = useHistory(); - const { pathname, search, hash } = useLocation(); + const { search } = useLocation(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); const integration = useMemo(() => queryParams.get('integration'), [queryParams]); const services = useStartServices(); @@ -212,66 +202,19 @@ export function Detail() { (ev) => { ev.preventDefault(); - // The object below, given to `createHref` is explicitly accessing keys of `location` in order - // to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable) - const currentPath = history.createHref({ - pathname, - search, - hash, - }); - const path = pagePathGetters.add_integration_to_policy({ pkgkey, ...(integration ? { integration } : {}), ...(agentPolicyIdFromContext ? { agentPolicyId: agentPolicyIdFromContext } : {}), })[1]; - let redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] & - CreatePackagePolicyRouteState['onCancelNavigateTo']; - - if (agentPolicyIdFromContext) { - redirectToPath = [ - PLUGIN_ID, - { - path: `#${ - pagePathGetters.policy_details({ - policyId: agentPolicyIdFromContext, - })[1] - }`, - }, - ]; - } else { - redirectToPath = [ - INTEGRATIONS_PLUGIN_ID, - { - path: currentPath, - }, - ]; - } - - const redirectBackRouteState: CreatePackagePolicyRouteState = { - onSaveNavigateTo: redirectToPath, - onCancelNavigateTo: redirectToPath, - onCancelUrl: currentPath, - }; - services.application.navigateToApp(PLUGIN_ID, { // Necessary because of Fleet's HashRouter. Can be changed when // https://github.com/elastic/kibana/issues/96134 is resolved path: `#${path}`, - state: redirectBackRouteState, }); }, - [ - history, - hash, - pathname, - search, - pkgkey, - integration, - services.application, - agentPolicyIdFromContext, - ] + [pkgkey, integration, services.application, agentPolicyIdFromContext] ); const headerRightContent = useMemo( From f89dc9cc31aa139f47d83133bdc10145ab7c0468 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 28 Jun 2021 14:16:08 +0200 Subject: [PATCH 02/41] sanitize drilldown (#103299) --- .../application/components/vis_types/table/vis.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index faf6fef0aa549..8f19644132d3f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -8,6 +8,7 @@ import _, { isArray, last, get } from 'lodash'; import React, { Component } from 'react'; +import { parse as parseUrl } from 'url'; import PropTypes from 'prop-types'; import { RedirectAppLinks } from '../../../../../../kibana_react/public'; import { createTickFormatter } from '../../lib/tick_formatter'; @@ -33,6 +34,14 @@ function getColor(rules, colorKey, value) { return color; } +function sanitizeUrl(url) { + // eslint-disable-next-line no-script-url + if (parseUrl(url).protocol === 'javascript:') { + return ''; + } + return url; +} + class TableVis extends Component { constructor(props) { super(props); @@ -52,7 +61,7 @@ class TableVis extends Component { let rowDisplay = model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key; if (model.drilldown_url) { const url = replaceVars(model.drilldown_url, {}, { key: row.key }); - rowDisplay = {rowDisplay}; + rowDisplay = {rowDisplay}; } const columns = row.series .filter((item) => item) From 2ff2a6fa5029671cb5cc8d68a53bdf0a63d75ad1 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 28 Jun 2021 09:47:32 -0400 Subject: [PATCH 03/41] Adding tooltip to rules that are disabled due to license (#103295) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/alerts_list.test.tsx | 120 ++++++++++++++++++ .../alerts_list/components/alerts_list.tsx | 37 ++++-- 2 files changed, 148 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 01e63f2c60814..311166f09e466 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -69,6 +69,7 @@ const alertTypeFromApi = { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, minimumLicenseRequired: 'basic', + enabledInLicense: true, authorizedConsumers: { [ALERTS_FEATURE_ID]: { read: true, all: true }, }, @@ -520,3 +521,122 @@ describe('alerts_list with show only capability', () => { // TODO: check delete button }); }); + +describe('alerts_list with disabled itmes', () => { + let wrapper: ReactWrapper; + + async function setup() { + loadAlerts.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 2, + data: [ + { + id: '1', + name: 'test alert', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + }, + { + id: '2', + name: 'test alert 2', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type_disabled_by_license', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + }, + ], + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + + loadAlertTypes.mockResolvedValue([ + alertTypeFromApi, + { + id: 'test_alert_type_disabled_by_license', + name: 'some alert type that is not allowed', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + minimumLicenseRequired: 'platinum', + enabledInLicense: false, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + }, + }, + ]); + loadAllActions.mockResolvedValue([]); + + alertTypeRegistry.has.mockReturnValue(false); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.alertTypeRegistry = alertTypeRegistry; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + + it('renders rules list with disabled indicator if disabled due to license', async () => { + await setup(); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual(''); + expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( + 'actAlertsList__tableRowDisabled' + ); + expect(wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').length).toBe( + 1 + ); + expect( + wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().type + ).toEqual('questionInCircle'); + expect( + wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().content + ).toEqual('This rule type requires a Platinum license.'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 1fb688c4dd6bf..1c1633ff4a72f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -18,6 +18,7 @@ import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, + EuiIconTip, EuiSpacer, EuiLink, EuiEmptyPrompt, @@ -63,6 +64,7 @@ import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants'; import './alerts_list.scss'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import { ManageLicenseModal } from './manage_license_modal'; +import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled'; const ENTER_KEY = 13; @@ -318,15 +320,32 @@ export const AlertsList: React.FunctionComponent = () => { width: '35%', 'data-test-subj': 'alertsTableCell-name', render: (name: string, alert: AlertTableItem) => { - return ( - { - history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); - }} - > - {name} - + const ruleType = alertTypesState.data.get(alert.alertTypeId); + const checkEnabledResult = checkAlertTypeEnabled(ruleType); + const link = ( + <> + { + history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); + }} + > + {name} + + + ); + return checkEnabledResult.isEnabled ? ( + link + ) : ( + <> + {link} + + ); }, }, From 4f45535c90bdf2f273e9def0b9d0cf2efcf8fa39 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 28 Jun 2021 15:55:53 +0200 Subject: [PATCH 04/41] [Exploratory view] Update types names (#103214) --- .../apm/service_latency_config.ts | 53 ------ .../configurations/constants/constants.ts | 2 + .../configurations/constants/url_constants.ts | 1 + .../configurations/lens_attributes.test.ts | 57 +++++-- .../configurations/lens_attributes.ts | 154 +++++++++--------- .../metrics/cpu_usage_config.ts | 38 ----- .../metrics/memory_usage_config.ts | 38 ----- .../metrics/network_activity_config.ts | 37 ----- .../mobile/device_distribution_config.ts | 17 +- .../mobile/distribution_config.ts | 55 +++---- .../mobile/kpi_over_time_config.ts | 77 ++++----- .../rum/core_web_vitals_config.ts | 127 +++++++-------- .../rum/data_distribution_config.ts | 48 ++---- .../rum/kpi_over_time_config.ts | 58 +++---- .../synthetics/data_distribution_config.ts | 34 ++-- .../synthetics/kpi_over_time_config.ts | 53 +++--- .../test_data/sample_attribute_kpi.ts | 71 ++++++++ .../exploratory_view/configurations/utils.ts | 2 + .../hooks/use_lens_attributes.ts | 26 +-- .../hooks/use_series_storage.tsx | 4 +- .../columns/report_breakdowns.test.tsx | 6 +- .../columns/report_breakdowns.tsx | 10 +- .../columns/report_definition_col.test.tsx | 8 +- .../columns/report_definition_col.tsx | 56 ++++--- .../columns/report_definition_field.tsx | 16 +- .../columns/report_filters.test.tsx | 2 +- .../series_builder/columns/report_filters.tsx | 14 +- .../columns/report_types_col.test.tsx | 2 +- .../columns/report_types_col.tsx | 12 +- ...rt_field.tsx => report_metric_options.tsx} | 18 +- .../series_builder/series_builder.tsx | 17 +- .../series_editor/chart_edit_options.tsx | 12 +- .../series_editor/columns/breakdowns.test.tsx | 8 +- .../series_editor/columns/breakdowns.tsx | 10 +- .../series_editor/columns/chart_options.tsx | 13 +- .../series_editor/columns/filter_expanded.tsx | 4 +- .../series_editor/columns/series_filter.tsx | 20 +-- .../series_editor/selected_filters.test.tsx | 4 +- .../series_editor/selected_filters.tsx | 10 +- .../series_editor/series_editor.tsx | 16 +- .../shared/exploratory_view/types.ts | 33 ++-- 41 files changed, 543 insertions(+), 700 deletions(-) delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts rename x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/{custom_report_field.tsx => report_metric_options.tsx} (66%) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts deleted file mode 100644 index 7c3abba3e5b05..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels } from '../constants'; -import { buildPhraseFilter } from '../utils'; -import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames'; - -export function getServiceLatencyLensConfig({ indexPattern }: ConfigProps): DataSeries { - return { - reportType: 'kpi-over-time', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'transaction.duration.us', - label: 'Latency', - }, - ], - hasOperationType: true, - defaultFilters: [ - 'user_agent.name', - 'user_agent.os.name', - 'client.geo.country_name', - 'user_agent.device.name', - ], - breakdowns: [ - 'user_agent.name', - 'user_agent.os.name', - 'client.geo.country_name', - 'user_agent.device.name', - ], - filters: buildPhraseFilter('transaction.type', 'request', indexPattern), - labels: { ...FieldLabels, [TRANSACTION_DURATION]: 'Latency' }, - reportDefinitions: [ - { - field: 'service.name', - required: true, - }, - { - field: 'service.environment', - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index 01e8d023ae96b..52faa2dccaeac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -96,3 +96,5 @@ export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; export const OPERATION_COLUMN = 'operation'; + +export const REPORT_METRIC_FIELD = 'REPORT_METRIC_FIELD'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index b5a5169216b7b..6f990015fbc62 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -13,4 +13,5 @@ export enum URL_KEYS { BREAK_DOWN = 'bd', FILTERS = 'ft', REPORT_DEFINITIONS = 'rdf', + SELECTED_METRIC = 'mt', } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 5189a529bda8f..72b4bd7919c3e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -9,8 +9,14 @@ import { LayerConfig, LensAttributes } from './lens_attributes'; import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; import { sampleAttribute } from './test_data/sample_attribute'; -import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { + LCP_FIELD, + TRANSACTION_DURATION, + USER_AGENT_NAME, +} from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; +import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; +import { REPORT_METRIC_FIELD } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -21,12 +27,12 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, }); - reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); + reportViewConfig.baseFilters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); let lnsAttr: LensAttributes; const layerConfig: LayerConfig = { - reportConfig: reportViewConfig, + seriesConfig: reportViewConfig, seriesType: 'line', operationType: 'count', indexPattern: mockIndexPattern, @@ -42,6 +48,27 @@ describe('Lens Attribute', () => { expect(lnsAttr.getJSON()).toEqual(sampleAttribute); }); + it('should return expected json for kpi report type', function () { + const seriesConfigKpi = getDefaultConfigs({ + reportType: 'kpi-over-time', + dataType: 'ux', + indexPattern: mockIndexPattern, + }); + + const lnsAttrKpi = new LensAttributes([ + { + seriesConfig: seriesConfigKpi, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'service.name': ['elastic-co'] }, + time: { from: 'now-15m', to: 'now' }, + }, + ]); + + expect(lnsAttrKpi.getJSON()).toEqual(sampleAttributeKpi); + }); + it('should return main y axis', function () { expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({ dataType: 'number', @@ -72,7 +99,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with default value', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta(REPORT_METRIC_FIELD, layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -92,7 +119,7 @@ describe('Lens Attribute', () => { it('should return expected field type for custom field with passed value', function () { const layerConfig1: LayerConfig = { - reportConfig: reportViewConfig, + seriesConfig: reportViewConfig, seriesType: 'line', operationType: 'count', indexPattern: mockIndexPattern, @@ -102,20 +129,20 @@ describe('Lens Attribute', () => { lnsAttr = new LensAttributes([layerConfig1]); - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta(REPORT_METRIC_FIELD, layerConfig1))).toEqual( JSON.stringify({ fieldMeta: { count: 0, - name: LCP_FIELD, + name: TRANSACTION_DURATION, type: 'number', - esTypes: ['scaled_float'], + esTypes: ['long'], scripted: false, searchable: true, aggregatable: true, readFromDocValues: true, }, - fieldName: LCP_FIELD, - columnLabel: 'Largest contentful paint', + fieldName: TRANSACTION_DURATION, + columnLabel: 'Page load time', }) ); }); @@ -269,7 +296,7 @@ describe('Lens Attribute', () => { describe('Layer breakdowns', function () { it('should return breakdown column', function () { const layerConfig1: LayerConfig = { - reportConfig: reportViewConfig, + seriesConfig: reportViewConfig, seriesType: 'line', operationType: 'count', indexPattern: mockIndexPattern, @@ -322,7 +349,7 @@ describe('Lens Attribute', () => { 'x-axis-column-layer0': { dataType: 'number', isBucketed: true, - label: 'Largest contentful paint', + label: 'Page load time', operationType: 'range', params: { maxBars: 'auto', @@ -330,7 +357,7 @@ describe('Lens Attribute', () => { type: 'histogram', }, scale: 'interval', - sourceField: 'transaction.marks.agent.largestContentfulPaint', + sourceField: 'transaction.duration.us', }, 'y-axis-column-layer0': { dataType: 'number', @@ -353,12 +380,12 @@ describe('Lens Attribute', () => { describe('Layer Filters', function () { it('should return expected filters', function () { - reportViewConfig.filters?.push( + reportViewConfig.baseFilters?.push( ...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern) ); const layerConfig1: LayerConfig = { - reportConfig: reportViewConfig, + seriesConfig: reportViewConfig, seriesType: 'line', operationType: 'count', indexPattern: mockIndexPattern, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 208e8d8ba43c2..eaf9c1c884a9d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -29,8 +29,14 @@ import { } from '../../../../../../lens/public'; import { urlFiltersToKueryString } from '../utils/stringify_kueries'; import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants'; -import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types'; +import { + FieldLabels, + FILTER_RECORDS, + USE_BREAK_DOWN_COLUMN, + TERMS_COLUMN, + REPORT_METRIC_FIELD, +} from './constants'; +import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; @@ -47,54 +53,47 @@ function buildNumberColumn(sourceField: string) { }; } -export const parseCustomFieldName = ( - sourceField: string, - reportViewConfig: DataSeries, - selectedDefinitions: URLReportDefinition -) => { - let fieldName = sourceField; +export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricField?: string) => { let columnType; let columnFilters; let timeScale; let columnLabel; - const rdf = reportViewConfig.reportDefinitions ?? []; - - const customField = rdf.find(({ field }) => field === fieldName); - - if (customField) { - if (selectedDefinitions[fieldName]) { - fieldName = selectedDefinitions[fieldName][0]; - if (customField?.options) { - const currField = customField?.options?.find( - ({ field, id }) => field === fieldName || id === fieldName - ); - columnType = currField?.columnType; - columnFilters = currField?.columnFilters; - timeScale = currField?.timeScale; - columnLabel = currField?.label; - } - } else if (customField.options?.[0].field || customField.options?.[0].id) { - fieldName = customField.options?.[0].field || customField.options?.[0].id; - columnType = customField.options?.[0].columnType; - columnFilters = customField.options?.[0].columnFilters; - timeScale = customField.options?.[0].timeScale; - columnLabel = customField.options?.[0].label; + const metricOptions = seriesConfig.metricOptions ?? []; + + if (selectedMetricField) { + if (metricOptions) { + const currField = metricOptions.find( + ({ field, id }) => field === selectedMetricField || id === selectedMetricField + ); + columnType = currField?.columnType; + columnFilters = currField?.columnFilters; + timeScale = currField?.timeScale; + columnLabel = currField?.label; } + } else if (metricOptions?.[0].field || metricOptions?.[0].id) { + const firstMetricOption = metricOptions?.[0]; + + selectedMetricField = firstMetricOption.field || firstMetricOption.id; + columnType = firstMetricOption.columnType; + columnFilters = firstMetricOption.columnFilters; + timeScale = firstMetricOption.timeScale; + columnLabel = firstMetricOption.label; } - return { fieldName, columnType, columnFilters, timeScale, columnLabel }; + return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; }; export interface LayerConfig { filters?: UrlFilter[]; - reportConfig: DataSeries; + seriesConfig: SeriesConfig; breakdown?: string; seriesType?: SeriesType; operationType?: OperationType; reportDefinitions: URLReportDefinition; time: { to: string; from: string }; indexPattern: IndexPattern; + selectedMetricField?: string; } export class LensAttributes { @@ -105,9 +104,9 @@ export class LensAttributes { constructor(layerConfigs: LayerConfig[]) { this.layers = {}; - layerConfigs.forEach(({ reportConfig, operationType }) => { + layerConfigs.forEach(({ seriesConfig, operationType }) => { if (operationType) { - reportConfig.yAxisColumns.forEach((yAxisColumn) => { + seriesConfig.yAxisColumns.forEach((yAxisColumn) => { if (typeof yAxisColumn.operationType !== undefined) { yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; } @@ -150,12 +149,12 @@ export class LensAttributes { getNumberRangeColumn( sourceField: string, - reportViewConfig: DataSeries, + seriesConfig: SeriesConfig, label?: string ): RangeIndexPatternColumn { return { sourceField, - label: reportViewConfig.labels[sourceField] ?? label, + label: seriesConfig.labels[sourceField] ?? label, dataType: 'number', operationType: 'range', isBucketed: true, @@ -171,22 +170,22 @@ export class LensAttributes { getCardinalityColumn({ sourceField, label, - reportViewConfig, + seriesConfig, }: { sourceField: string; label?: string; - reportViewConfig: DataSeries; + seriesConfig: SeriesConfig; }) { return this.getNumberOperationColumn({ sourceField, operationType: 'unique_count', label, - reportViewConfig, + seriesConfig, }); } getNumberColumn({ - reportViewConfig, + seriesConfig, label, sourceField, columnType, @@ -196,7 +195,7 @@ export class LensAttributes { columnType?: string; operationType?: string; label?: string; - reportViewConfig: DataSeries; + seriesConfig: SeriesConfig; }) { if (columnType === 'operation' || operationType) { if ( @@ -209,26 +208,26 @@ export class LensAttributes { sourceField, operationType, label, - reportViewConfig, + seriesConfig, }); } if (operationType?.includes('th')) { - return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!); + return this.getPercentileNumberColumn(sourceField, operationType, seriesConfig!); } } - return this.getNumberRangeColumn(sourceField, reportViewConfig!, label); + return this.getNumberRangeColumn(sourceField, seriesConfig!, label); } getNumberOperationColumn({ sourceField, label, - reportViewConfig, + seriesConfig, operationType, }: { sourceField: string; operationType: 'average' | 'median' | 'sum' | 'unique_count'; label?: string; - reportViewConfig: DataSeries; + seriesConfig: SeriesConfig; }): | AvgIndexPatternColumn | MedianIndexPatternColumn @@ -239,7 +238,7 @@ export class LensAttributes { label: i18n.translate('xpack.observability.expView.columns.operation.label', { defaultMessage: '{operationType} of {sourceField}', values: { - sourceField: label || reportViewConfig.labels[sourceField], + sourceField: label || seriesConfig.labels[sourceField], operationType: capitalize(operationType), }, }), @@ -250,13 +249,13 @@ export class LensAttributes { getPercentileNumberColumn( sourceField: string, percentileValue: string, - reportViewConfig: DataSeries + seriesConfig: SeriesConfig ): PercentileIndexPatternColumn { return { ...buildNumberColumn(sourceField), label: i18n.translate('xpack.observability.expView.columns.label', { defaultMessage: '{percentileValue} percentile of {sourceField}', - values: { sourceField: reportViewConfig.labels[sourceField], percentileValue }, + values: { sourceField: seriesConfig.labels[sourceField], percentileValue }, }), operationType: 'percentile', params: { percentile: Number(percentileValue.split('th')[0]) }, @@ -295,13 +294,13 @@ export class LensAttributes { } getXAxis(layerConfig: LayerConfig, layerId: string) { - const { xAxisColumn } = layerConfig.reportConfig; + const { xAxisColumn } = layerConfig.seriesConfig; if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { return this.getBreakdownColumn({ layerId, indexPattern: layerConfig.indexPattern, - sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0], + sourceField: layerConfig.breakdown || layerConfig.seriesConfig.breakdownFields[0], }); } @@ -333,6 +332,7 @@ export class LensAttributes { timeScale, columnFilters, } = this.getFieldMeta(sourceField, layerConfig); + const { type: fieldType } = fieldMeta ?? {}; if (columnType === TERMS_COLUMN) { @@ -356,14 +356,14 @@ export class LensAttributes { columnType, operationType, label: columnLabel || label, - reportViewConfig: layerConfig.reportConfig, + seriesConfig: layerConfig.seriesConfig, }); } if (operationType === 'unique_count') { return this.getCardinalityColumn({ sourceField: fieldName, label: columnLabel || label, - reportViewConfig: layerConfig.reportConfig, + seriesConfig: layerConfig.seriesConfig, }); } @@ -378,32 +378,26 @@ export class LensAttributes { sourceField: string; layerConfig: LayerConfig; }) { - return parseCustomFieldName( - sourceField, - layerConfig.reportConfig, - layerConfig.reportDefinitions - ); + return parseCustomFieldName(layerConfig.seriesConfig, sourceField); } getFieldMeta(sourceField: string, layerConfig: LayerConfig) { - const { - fieldName, - columnType, - columnLabel, - columnFilters, - timeScale, - } = this.getCustomFieldName({ - sourceField, - layerConfig, - }); - - const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName); + if (sourceField === REPORT_METRIC_FIELD) { + const { fieldName, columnType, columnLabel, columnFilters, timeScale } = parseCustomFieldName( + layerConfig.seriesConfig, + layerConfig.selectedMetricField + ); + const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName!); + return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; + } else { + const fieldMeta = layerConfig.indexPattern.getFieldByName(sourceField); - return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; + return { fieldMeta, fieldName: sourceField }; + } } getMainYAxis(layerConfig: LayerConfig) { - const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0]; + const { sourceField, operationType, label } = layerConfig.seriesConfig.yAxisColumns[0]; if (sourceField === 'Records' || !sourceField) { return this.getRecordsColumn(label); @@ -420,7 +414,7 @@ export class LensAttributes { getChildYAxises(layerConfig: LayerConfig) { const lensColumns: Record = {}; - const yAxisColumns = layerConfig.reportConfig.yAxisColumns; + const yAxisColumns = layerConfig.seriesConfig.yAxisColumns; // 1 means there is only main y axis if (yAxisColumns.length === 1) { return lensColumns; @@ -460,7 +454,7 @@ export class LensAttributes { const { filters, time: { from, to }, - reportConfig: { filters: layerFilters, reportType }, + seriesConfig: { baseFilters: layerFilters, reportType }, } = layerConfig; let baseFilters = ''; if (reportType !== 'kpi-over-time' && totalLayers > 1) { @@ -522,7 +516,7 @@ export class LensAttributes { } getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { - if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') { + if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') { return null; } @@ -603,16 +597,16 @@ export class LensAttributes { ...Object.keys(this.getChildYAxises(layerConfig)), ], layerId: `layer${index}`, - seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType, - palette: layerConfig.reportConfig.palette, - yConfig: layerConfig.reportConfig.yConfig || [ + seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, + palette: layerConfig.seriesConfig.palette, + yConfig: layerConfig.seriesConfig.yConfig || [ { forAccessor: `y-axis-column-layer${index}` }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}), })), - ...(this.layerConfigs[0].reportConfig.yTitle - ? { yTitle: this.layerConfigs[0].reportConfig.yTitle } + ...(this.layerConfigs[0].seriesConfig.yTitle + ? { yTitle: this.layerConfigs[0].seriesConfig.yTitle } : {}), }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts deleted file mode 100644 index 2d44e122af82d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataSeries, ConfigProps } from '../../types'; -import { FieldLabels } from '../constants'; - -export function getCPUUsageLensConfig({}: ConfigProps): DataSeries { - return { - reportType: 'kpi-over-time', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'system.cpu.user.pct', - label: 'CPU Usage %', - }, - ], - hasOperationType: true, - defaultFilters: [], - breakdowns: ['host.hostname'], - filters: [], - labels: { ...FieldLabels, 'host.hostname': 'Host name' }, - reportDefinitions: [ - { - field: 'agent.hostname', - required: true, - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts deleted file mode 100644 index deaa551dce657..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataSeries, ConfigProps } from '../../types'; -import { FieldLabels } from '../constants'; - -export function getMemoryUsageLensConfig({}: ConfigProps): DataSeries { - return { - reportType: 'kpi-over-time', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'system.memory.used.pct', - label: 'Memory Usage %', - }, - ], - hasOperationType: true, - defaultFilters: [], - breakdowns: ['host.hostname'], - filters: [], - labels: { ...FieldLabels, 'host.hostname': 'Host name' }, - reportDefinitions: [ - { - field: 'host.hostname', - required: true, - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts deleted file mode 100644 index d27cdba207d63..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataSeries, ConfigProps } from '../../types'; -import { FieldLabels } from '../constants'; - -export function getNetworkActivityLensConfig({}: ConfigProps): DataSeries { - return { - reportType: 'kpi-over-time', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'system.memory.used.pct', - }, - ], - hasOperationType: true, - defaultFilters: [], - breakdowns: ['host.hostname'], - filters: [], - labels: { ...FieldLabels, 'host.hostname': 'Host name' }, - reportDefinitions: [ - { - field: 'host.hostname', - required: true, - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index e1cb5a0370fb2..98979b9922a86 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; +import { ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; import { MobileFields } from './mobile_fields'; -export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'device-data-distribution', defaultSeriesType: 'bar', @@ -28,9 +28,9 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) }, ], hasOperationType: false, - defaultFilters: Object.keys(MobileFields), - breakdowns: Object.keys(MobileFields), - filters: [ + filterFields: Object.keys(MobileFields), + breakdownFields: Object.keys(MobileFields), + baseFilters: [ ...buildPhraseFilter('agent.name', 'iOS/swift', indexPattern), ...buildPhraseFilter('processor.event', 'transaction', indexPattern), ], @@ -39,11 +39,6 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, - reportDefinitions: [ - { - field: SERVICE_NAME, - required: true, - }, - ], + definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index 62dd38e55a32a..b9894347d96c0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -19,13 +19,13 @@ import { import { CPU_USAGE, MEMORY_USAGE, MOBILE_APP, RESPONSE_LATENCY } from '../constants/labels'; import { MobileFields } from './mobile_fields'; -export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'data-distribution', defaultSeriesType: 'bar', seriesTypes: ['line', 'bar'], xAxisColumn: { - sourceField: 'performance.metric', + sourceField: REPORT_METRIC_FIELD, }, yAxisColumns: [ { @@ -33,9 +33,9 @@ export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): D }, ], hasOperationType: false, - defaultFilters: Object.keys(MobileFields), - breakdowns: Object.keys(MobileFields), - filters: [ + filterFields: Object.keys(MobileFields), + breakdownFields: Object.keys(MobileFields), + baseFilters: [ ...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern), ], labels: { @@ -43,38 +43,25 @@ export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): D ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, - reportDefinitions: [ + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ { - field: SERVICE_NAME, - required: true, + label: RESPONSE_LATENCY, + field: TRANSACTION_DURATION, + id: TRANSACTION_DURATION, + columnType: OPERATION_COLUMN, }, { - field: SERVICE_ENVIRONMENT, - required: true, + label: MEMORY_USAGE, + field: METRIC_SYSTEM_MEMORY_USAGE, + id: METRIC_SYSTEM_MEMORY_USAGE, + columnType: OPERATION_COLUMN, }, { - field: 'performance.metric', - custom: true, - options: [ - { - label: RESPONSE_LATENCY, - field: TRANSACTION_DURATION, - id: TRANSACTION_DURATION, - columnType: OPERATION_COLUMN, - }, - { - label: MEMORY_USAGE, - field: METRIC_SYSTEM_MEMORY_USAGE, - id: METRIC_SYSTEM_MEMORY_USAGE, - columnType: OPERATION_COLUMN, - }, - { - label: CPU_USAGE, - field: METRIC_SYSTEM_CPU_USAGE, - id: METRIC_SYSTEM_CPU_USAGE, - columnType: OPERATION_COLUMN, - }, - ], + label: CPU_USAGE, + field: METRIC_SYSTEM_CPU_USAGE, + id: METRIC_SYSTEM_CPU_USAGE, + columnType: OPERATION_COLUMN, }, ], }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 9a2e86a8f7969..945a631078a33 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -24,7 +24,7 @@ import { } from '../constants/labels'; import { MobileFields } from './mobile_fields'; -export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { +export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'kpi-over-time', defaultSeriesType: 'line', @@ -34,14 +34,14 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { }, yAxisColumns: [ { - sourceField: 'business.kpi', + sourceField: REPORT_METRIC_FIELD, operationType: 'median', }, ], hasOperationType: true, - defaultFilters: Object.keys(MobileFields), - breakdowns: Object.keys(MobileFields), - filters: [ + filterFields: Object.keys(MobileFields), + breakdownFields: Object.keys(MobileFields), + baseFilters: [ ...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern), ], labels: { @@ -52,50 +52,37 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { [METRIC_SYSTEM_MEMORY_USAGE]: MEMORY_USAGE, [METRIC_SYSTEM_CPU_USAGE]: CPU_USAGE, }, - reportDefinitions: [ + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ { - field: SERVICE_NAME, - required: true, + label: RESPONSE_LATENCY, + field: TRANSACTION_DURATION, + id: TRANSACTION_DURATION, + columnType: OPERATION_COLUMN, }, { - field: SERVICE_ENVIRONMENT, - required: true, - }, - { - field: 'business.kpi', - custom: true, - options: [ - { - label: RESPONSE_LATENCY, - field: TRANSACTION_DURATION, - id: TRANSACTION_DURATION, - columnType: OPERATION_COLUMN, - }, - { - field: RECORDS_FIELD, - id: RECORDS_FIELD, - label: TRANSACTIONS_PER_MINUTE, - columnFilters: [ - { - language: 'kuery', - query: `processor.event: transaction`, - }, - ], - timeScale: 'm', - }, + field: RECORDS_FIELD, + id: RECORDS_FIELD, + label: TRANSACTIONS_PER_MINUTE, + columnFilters: [ { - label: MEMORY_USAGE, - field: METRIC_SYSTEM_MEMORY_USAGE, - id: METRIC_SYSTEM_MEMORY_USAGE, - columnType: OPERATION_COLUMN, - }, - { - label: CPU_USAGE, - field: METRIC_SYSTEM_CPU_USAGE, - id: METRIC_SYSTEM_CPU_USAGE, - columnType: OPERATION_COLUMN, + language: 'kuery', + query: `processor.event: transaction`, }, ], + timeScale: 'm', + }, + { + label: MEMORY_USAGE, + field: METRIC_SYSTEM_MEMORY_USAGE, + id: METRIC_SYSTEM_MEMORY_USAGE, + columnType: OPERATION_COLUMN, + }, + { + label: CPU_USAGE, + field: METRIC_SYSTEM_CPU_USAGE, + id: METRIC_SYSTEM_CPU_USAGE, + columnType: OPERATION_COLUMN, }, ], }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index e34d8b0dcfdd0..1d04a9b389503 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -6,8 +6,13 @@ */ import { euiPaletteForStatus } from '@elastic/eui'; -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { + FieldLabels, + FILTER_RECORDS, + REPORT_METRIC_FIELD, + USE_BREAK_DOWN_COLUMN, +} from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -27,7 +32,7 @@ import { SERVICE_ENVIRONMENT, } from '../constants/elasticsearch_fieldnames'; -export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSeries { +export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesConfig { const statusPallete = euiPaletteForStatus(3); return { @@ -39,20 +44,20 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSerie }, yAxisColumns: [ { - sourceField: 'core.web.vitals', + sourceField: REPORT_METRIC_FIELD, label: 'Good', }, { - sourceField: 'core.web.vitals', + sourceField: REPORT_METRIC_FIELD, label: 'Average', }, { - sourceField: 'core.web.vitals', + sourceField: REPORT_METRIC_FIELD, label: 'Poor', }, ], hasOperationType: false, - defaultFilters: [ + filterFields: [ { field: TRANSACTION_URL, isNegated: false, @@ -69,7 +74,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSerie nested: USER_AGENT_VERSION, }, ], - breakdowns: [ + breakdownFields: [ SERVICE_NAME, USER_AGENT_NAME, USER_AGENT_OS, @@ -77,79 +82,67 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSerie USER_AGENT_DEVICE, URL_FULL, ], - filters: [ + baseFilters: [ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), ], labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application' }, - reportDefinitions: [ + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ { - field: SERVICE_NAME, - required: true, + id: LCP_FIELD, + label: 'Largest contentful paint', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: `${LCP_FIELD} < 2500`, + }, + { + language: 'kuery', + query: `${LCP_FIELD} > 2500 and ${LCP_FIELD} < 4000`, + }, + { + language: 'kuery', + query: `${LCP_FIELD} > 4000`, + }, + ], }, { - field: SERVICE_ENVIRONMENT, + label: 'First input delay', + id: FID_FIELD, + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: `${FID_FIELD} < 100`, + }, + { + language: 'kuery', + query: `${FID_FIELD} > 100 and ${FID_FIELD} < 300`, + }, + { + language: 'kuery', + query: `${FID_FIELD} > 300`, + }, + ], }, { - field: 'core.web.vitals', - custom: true, - options: [ + label: 'Cumulative layout shift', + id: CLS_FIELD, + columnType: FILTER_RECORDS, + columnFilters: [ { - id: LCP_FIELD, - label: 'Largest contentful paint', - columnType: FILTER_RECORDS, - columnFilters: [ - { - language: 'kuery', - query: `${LCP_FIELD} < 2500`, - }, - { - language: 'kuery', - query: `${LCP_FIELD} > 2500 and ${LCP_FIELD} < 4000`, - }, - { - language: 'kuery', - query: `${LCP_FIELD} > 4000`, - }, - ], + language: 'kuery', + query: `${CLS_FIELD} < 0.1`, }, { - label: 'First input delay', - id: FID_FIELD, - columnType: FILTER_RECORDS, - columnFilters: [ - { - language: 'kuery', - query: `${FID_FIELD} < 100`, - }, - { - language: 'kuery', - query: `${FID_FIELD} > 100 and ${FID_FIELD} < 300`, - }, - { - language: 'kuery', - query: `${FID_FIELD} > 300`, - }, - ], + language: 'kuery', + query: `${CLS_FIELD} > 0.1 and ${CLS_FIELD} < 0.25`, }, { - label: 'Cumulative layout shift', - id: CLS_FIELD, - columnType: FILTER_RECORDS, - columnFilters: [ - { - language: 'kuery', - query: `${CLS_FIELD} < 0.1`, - }, - { - language: 'kuery', - query: `${CLS_FIELD} > 0.1 and ${CLS_FIELD} < 0.25`, - }, - { - language: 'kuery', - query: `${CLS_FIELD} > 0.25`, - }, - ], + language: 'kuery', + query: `${CLS_FIELD} > 0.25`, }, ], }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index 812f1b2e4cf33..b171edf2901d5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -39,13 +39,13 @@ import { WEB_APPLICATION_LABEL, } from '../constants/labels'; -export function getRumDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'data-distribution', defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { - sourceField: 'performance.metric', + sourceField: REPORT_METRIC_FIELD, }, yAxisColumns: [ { @@ -54,7 +54,7 @@ export function getRumDistributionConfig({ indexPattern }: ConfigProps): DataSer }, ], hasOperationType: false, - defaultFilters: [ + filterFields: [ { field: TRANSACTION_URL, isNegated: false, @@ -67,34 +67,22 @@ export function getRumDistributionConfig({ indexPattern }: ConfigProps): DataSer nested: USER_AGENT_VERSION, }, ], - breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], - reportDefinitions: [ + breakdownFields: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ + { label: PAGE_LOAD_TIME_LABEL, id: TRANSACTION_DURATION, field: TRANSACTION_DURATION }, { - field: SERVICE_NAME, - required: true, - }, - { - field: SERVICE_ENVIRONMENT, - }, - { - field: 'performance.metric', - custom: true, - options: [ - { label: PAGE_LOAD_TIME_LABEL, id: TRANSACTION_DURATION, field: TRANSACTION_DURATION }, - { - label: BACKEND_TIME_LABEL, - id: TRANSACTION_TIME_TO_FIRST_BYTE, - field: TRANSACTION_TIME_TO_FIRST_BYTE, - }, - { label: FCP_LABEL, id: FCP_FIELD, field: FCP_FIELD }, - { label: TBT_LABEL, id: TBT_FIELD, field: TBT_FIELD }, - { label: LCP_LABEL, id: LCP_FIELD, field: LCP_FIELD }, - { label: FID_LABEL, id: FID_FIELD, field: FID_FIELD }, - { label: CLS_LABEL, id: CLS_FIELD, field: CLS_FIELD }, - ], + label: BACKEND_TIME_LABEL, + id: TRANSACTION_TIME_TO_FIRST_BYTE, + field: TRANSACTION_TIME_TO_FIRST_BYTE, }, + { label: FCP_LABEL, id: FCP_FIELD, field: FCP_FIELD }, + { label: TBT_LABEL, id: TBT_FIELD, field: TBT_FIELD }, + { label: LCP_LABEL, id: LCP_FIELD, field: LCP_FIELD }, + { label: FID_LABEL, id: FID_FIELD, field: FID_FIELD }, + { label: CLS_LABEL, id: CLS_FIELD, field: CLS_FIELD }, ], - filters: [ + baseFilters: [ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), ], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index 12d66c55c7d00..5899b16d12b4f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -39,7 +39,7 @@ import { WEB_APPLICATION_LABEL, } from '../constants/labels'; -export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): DataSeries { +export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesConfig { return { defaultSeriesType: 'bar_stacked', seriesTypes: [], @@ -49,12 +49,12 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): DataSerie }, yAxisColumns: [ { - sourceField: 'business.kpi', + sourceField: REPORT_METRIC_FIELD, operationType: 'median', }, ], hasOperationType: false, - defaultFilters: [ + filterFields: [ { field: TRANSACTION_URL, isNegated: false, @@ -67,44 +67,32 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): DataSerie nested: USER_AGENT_VERSION, }, ], - breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], - filters: [ + breakdownFields: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + baseFilters: [ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), ], labels: { ...FieldLabels, [SERVICE_NAME]: WEB_APPLICATION_LABEL }, - reportDefinitions: [ + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ + { field: RECORDS_FIELD, id: RECORDS_FIELD, label: PAGE_VIEWS_LABEL }, { - field: SERVICE_NAME, - required: true, + label: PAGE_LOAD_TIME_LABEL, + field: TRANSACTION_DURATION, + id: TRANSACTION_DURATION, + columnType: OPERATION_COLUMN, }, { - field: SERVICE_ENVIRONMENT, - }, - { - field: 'business.kpi', - custom: true, - options: [ - { field: RECORDS_FIELD, id: RECORDS_FIELD, label: PAGE_VIEWS_LABEL }, - { - label: PAGE_LOAD_TIME_LABEL, - field: TRANSACTION_DURATION, - id: TRANSACTION_DURATION, - columnType: OPERATION_COLUMN, - }, - { - label: BACKEND_TIME_LABEL, - field: TRANSACTION_TIME_TO_FIRST_BYTE, - id: TRANSACTION_TIME_TO_FIRST_BYTE, - columnType: OPERATION_COLUMN, - }, - { label: FCP_LABEL, field: FCP_FIELD, id: FCP_FIELD, columnType: OPERATION_COLUMN }, - { label: TBT_LABEL, field: TBT_FIELD, id: TBT_FIELD, columnType: OPERATION_COLUMN }, - { label: LCP_LABEL, field: LCP_FIELD, id: LCP_FIELD, columnType: OPERATION_COLUMN }, - { label: FID_LABEL, field: FID_FIELD, id: FID_FIELD, columnType: OPERATION_COLUMN }, - { label: CLS_LABEL, field: CLS_FIELD, id: CLS_FIELD, columnType: OPERATION_COLUMN }, - ], + label: BACKEND_TIME_LABEL, + field: TRANSACTION_TIME_TO_FIRST_BYTE, + id: TRANSACTION_TIME_TO_FIRST_BYTE, + columnType: OPERATION_COLUMN, }, + { label: FCP_LABEL, field: FCP_FIELD, id: FCP_FIELD, columnType: OPERATION_COLUMN }, + { label: TBT_LABEL, field: TBT_FIELD, id: TBT_FIELD, columnType: OPERATION_COLUMN }, + { label: LCP_LABEL, field: LCP_FIELD, id: LCP_FIELD, columnType: OPERATION_COLUMN }, + { label: FID_LABEL, field: FID_FIELD, id: FID_FIELD, columnType: OPERATION_COLUMN }, + { label: CLS_LABEL, field: CLS_FIELD, id: CLS_FIELD, columnType: OPERATION_COLUMN }, ], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index b958c0dd71528..9783f63f5b901 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -5,18 +5,21 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels'; -export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsDistributionConfig({ + series, + indexPattern, +}: ConfigProps): SeriesConfig { return { reportType: 'data-distribution', defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { - sourceField: 'performance.metric', + sourceField: REPORT_METRIC_FIELD, }, yAxisColumns: [ { @@ -25,8 +28,8 @@ export function getSyntheticsDistributionConfig({ series, indexPattern }: Config }, ], hasOperationType: false, - defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'], - breakdowns: [ + filterFields: ['monitor.type', 'observer.geo.name', 'tags'], + breakdownFields: [ 'observer.geo.name', 'monitor.name', 'monitor.id', @@ -34,21 +37,10 @@ export function getSyntheticsDistributionConfig({ series, indexPattern }: Config 'tags', 'url.port', ], - filters: [...buildExistsFilter('summary.up', indexPattern)], - reportDefinitions: [ - { - field: 'monitor.name', - }, - { - field: 'url.full', - }, - { - field: 'performance.metric', - custom: true, - options: [ - { label: 'Monitor duration', id: 'monitor.duration.us', field: 'monitor.duration.us' }, - ], - }, + baseFilters: [...buildExistsFilter('summary.up', indexPattern)], + definitionFields: ['monitor.name', 'url.full'], + metricOptions: [ + { label: 'Monitor duration', id: 'monitor.duration.us', field: 'monitor.duration.us' }, ], labels: { ...FieldLabels, 'monitor.duration.us': MONITORS_DURATION_LABEL }, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 3e92845436363..6bf280e93eb11 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, OPERATION_COLUMN } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; import { DOWN_LABEL, MONITORS_DURATION_LABEL, UP_LABEL } from '../constants/labels'; import { MONITOR_DURATION_US } from '../constants/field_names/synthetics'; const SUMMARY_UP = 'summary.up'; const SUMMARY_DOWN = 'summary.down'; -export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'kpi-over-time', defaultSeriesType: 'bar_stacked', @@ -23,45 +23,34 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): DataSerie }, yAxisColumns: [ { - sourceField: 'business.kpi', + sourceField: REPORT_METRIC_FIELD, operationType: 'median', }, ], hasOperationType: false, - defaultFilters: ['observer.geo.name', 'monitor.type', 'tags'], - breakdowns: ['observer.geo.name', 'monitor.type'], - filters: [...buildExistsFilter('summary.up', indexPattern)], + filterFields: ['observer.geo.name', 'monitor.type', 'tags'], + breakdownFields: ['observer.geo.name', 'monitor.type'], + baseFilters: [...buildExistsFilter('summary.up', indexPattern)], palette: { type: 'palette', name: 'status' }, - reportDefinitions: [ + definitionFields: ['monitor.name', 'url.full'], + metricOptions: [ { - field: 'monitor.name', + label: MONITORS_DURATION_LABEL, + field: MONITOR_DURATION_US, + id: MONITOR_DURATION_US, + columnType: OPERATION_COLUMN, }, { - field: 'url.full', + field: SUMMARY_UP, + id: SUMMARY_UP, + label: UP_LABEL, + columnType: OPERATION_COLUMN, }, { - field: 'business.kpi', - custom: true, - options: [ - { - label: MONITORS_DURATION_LABEL, - field: MONITOR_DURATION_US, - id: MONITOR_DURATION_US, - columnType: OPERATION_COLUMN, - }, - { - field: SUMMARY_UP, - id: SUMMARY_UP, - label: UP_LABEL, - columnType: OPERATION_COLUMN, - }, - { - field: SUMMARY_DOWN, - id: SUMMARY_DOWN, - label: DOWN_LABEL, - columnType: OPERATION_COLUMN, - }, - ], + field: SUMMARY_DOWN, + id: SUMMARY_DOWN, + label: DOWN_LABEL, + columnType: OPERATION_COLUMN, }, ], labels: { ...FieldLabels }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts new file mode 100644 index 0000000000000..7f066caf66bf1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const sampleAttributeKpi = { + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', + references: [ + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], + columns: { + 'x-axis-column-layer0': { + sourceField: '@timestamp', + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + }, + 'y-axis-column-layer0': { + dataType: 'number', + isBucketed: false, + label: 'Page views', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + filter: { + query: 'transaction.type: page-load and processor.event: transaction', + language: 'kuery', + }, + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + fittingFunction: 'Linear', + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', + layers: [ + { + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', + seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + xAccessor: 'x-axis-column-layer0', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 9b1e7ec141ca2..f7df2939d9909 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -21,6 +21,7 @@ export function convertToShortUrl(series: SeriesUrl) { filters, reportDefinitions, dataType, + selectedMetricField, ...restSeries } = series; @@ -32,6 +33,7 @@ export function convertToShortUrl(series: SeriesUrl) { [URL_KEYS.FILTERS]: filters, [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, [URL_KEYS.DATA_TYPE]: dataType, + [URL_KEYS.SELECTED_METRIC]: selectedMetricField, ...restSeries, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 11487afe28e96..d14a26d13d928 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -12,25 +12,16 @@ import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { DataSeries, SeriesUrl, UrlFilter } from '../types'; +import { SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; -export const getFiltersFromDefs = ( - reportDefinitions: SeriesUrl['reportDefinitions'], - dataViewConfig: DataSeries -) => { - const rdfFilters = Object.entries(reportDefinitions ?? {}).map(([field, value]) => { +export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { + return Object.entries(reportDefinitions ?? {}).map(([field, value]) => { return { field, values: value, }; }) as UrlFilter[]; - - // let's filter out custom fields - return rdfFilters.filter(({ field }) => { - const rdf = dataViewConfig.reportDefinitions.find(({ field: fd }) => field === fd); - return !rdf?.custom; - }); }; export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { @@ -49,25 +40,26 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const seriesT = allSeries[seriesIdT]; const indexPattern = indexPatterns?.[seriesT?.dataType]; if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { - const reportViewConfig = getDefaultConfigs({ + const seriesConfig = getDefaultConfigs({ reportType: seriesT.reportType, dataType: seriesT.dataType, indexPattern, }); const filters: UrlFilter[] = (seriesT.filters ?? []).concat( - getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig) + getFiltersFromDefs(seriesT.reportDefinitions) ); layerConfigs.push({ filters, indexPattern, - reportConfig: reportViewConfig, + seriesConfig, + time: seriesT.time, breakdown: seriesT.breakdown, - operationType: seriesT.operationType, seriesType: seriesT.seriesType, + operationType: seriesT.operationType, reportDefinitions: seriesT.reportDefinitions ?? {}, - time: seriesT.time, + selectedMetricField: seriesT.selectedMetricField, }); } }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index e9ae43950d47d..7e9b69a276d0b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -110,7 +110,7 @@ export function useSeriesStorage() { } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { dt, op, st, rt, bd, ft, time, rdf, ...restSeries } = newValue; + const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue; return { operationType: op, reportType: rt!, @@ -120,6 +120,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { time: time!, reportDefinitions: rdf, dataType: dt!, + selectedMetricField: mt, ...restSeries, }; } @@ -132,6 +133,7 @@ interface ShortUrlSeries { [URL_KEYS.BREAK_DOWN]?: string; [URL_KEYS.FILTERS]?: UrlFilter[]; [URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition; + [URL_KEYS.SELECTED_METRIC]?: string; time?: { to: string; from: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index 203382afc1624..a5e5ad3900ded 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -21,7 +21,7 @@ describe('Series Builder ReportBreakdowns', function () { }); it('should render properly', function () { - render(); + render(); screen.getByText('Select an option: , is selected'); screen.getAllByText('Browser family'); @@ -29,7 +29,7 @@ describe('Series Builder ReportBreakdowns', function () { it('should set new series breakdown on change', function () { const { setSeries } = render( - + ); const btn = screen.getByRole('button', { @@ -51,7 +51,7 @@ describe('Series Builder ReportBreakdowns', function () { }); it('should set undefined on new series on no select breakdown', function () { const { setSeries } = render( - + ); const btn = screen.getByRole('button', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx index e95cd894df5f2..fa2d01691ce1d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -7,19 +7,19 @@ import React from 'react'; import { Breakdowns } from '../../series_editor/columns/breakdowns'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; export function ReportBreakdowns({ seriesId, - dataViewSeries, + seriesConfig, }: { - dataViewSeries: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; }) { return ( ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 2e5c674b9fad8..cac1eccada311 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -21,7 +21,7 @@ describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); const seriesId = 'test-series-id'; - const dataViewSeries = getDefaultConfigs({ + const seriesConfig = getDefaultConfigs({ reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', @@ -41,7 +41,7 @@ describe('Series Builder ReportDefinitionCol', function () { mockUseValuesList([{ label: 'elastic-co', count: 10 }]); it('should render properly', async function () { - render(, { + render(, { initSeries, }); @@ -52,7 +52,7 @@ describe('Series Builder ReportDefinitionCol', function () { }); it('should render selected report definitions', async function () { - render(, { + render(, { initSeries, }); @@ -63,7 +63,7 @@ describe('Series Builder ReportDefinitionCol', function () { it('should be able to remove selected definition', async function () { const { setSeries } = render( - , + , { initSeries } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index 47962af0d4bc4..0c620abf56e8a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -9,39 +9,40 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import styled from 'styled-components'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { CustomReportField } from '../custom_report_field'; -import { DataSeries, URLReportDefinition } from '../../types'; +import { ReportMetricOptions } from '../report_metric_options'; +import { SeriesConfig } from '../../types'; import { SeriesChartTypesSelect } from './chart_types'; import { OperationTypeSelect } from './operation_type_select'; import { DatePickerCol } from './date_picker_col'; import { parseCustomFieldName } from '../../configurations/lens_attributes'; import { ReportDefinitionField } from './report_definition_field'; -function getColumnType(dataView: DataSeries, selectedDefinition: URLReportDefinition) { - const { reportDefinitions } = dataView; - const customColumn = reportDefinitions.find((item) => item.custom); - if (customColumn?.field && selectedDefinition[customColumn?.field]) { - const { columnType } = parseCustomFieldName(customColumn.field, dataView, selectedDefinition); +function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { + const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); - return columnType; - } - return null; + return columnType; } export function ReportDefinitionCol({ - dataViewSeries, + seriesConfig, seriesId, }: { - dataViewSeries: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; }) { const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); - const { reportDefinitions: selectedReportDefinitions = {} } = series ?? {}; + const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {}; - const { reportDefinitions, defaultSeriesType, hasOperationType, yAxisColumns } = dataViewSeries; + const { + definitionFields, + defaultSeriesType, + hasOperationType, + yAxisColumns, + metricOptions, + } = seriesConfig; const onChange = (field: string, value?: string[]) => { if (!value?.[0]) { @@ -58,7 +59,7 @@ export function ReportDefinitionCol({ } }; - const columnType = getColumnType(dataViewSeries, selectedReportDefinitions); + const columnType = getColumnType(seriesConfig, selectedMetricField); return ( @@ -66,20 +67,21 @@ export function ReportDefinitionCol({ - {reportDefinitions.map(({ field, custom, options }) => ( + {definitionFields.map((field) => ( - {!custom ? ( - - ) : ( - - )} + ))} + {metricOptions && ( + + + + )} {(hasOperationType || columnType === 'operation') && ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index 51f4edaae93da..61f6f85dbeaf2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -15,16 +15,16 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc import { PersistableFilter } from '../../../../../../../lens/common'; import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; import { buildPhrasesFilter } from '../../configurations/utils'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; interface Props { seriesId: string; field: string; - dataSeries: DataSeries; + seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; } -export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: Props) { +export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) { const { getSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -33,11 +33,11 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: const { reportDefinitions: selectedReportDefinitions = {} } = series; - const { labels, filters, reportDefinitions } = dataSeries; + const { labels, baseFilters, definitionFields } = seriesConfig; const queryFilters = useMemo(() => { const filtersN: ESFilter[] = []; - (filters ?? []).forEach((qFilter: PersistableFilter | ExistsFilter) => { + (baseFilters ?? []).forEach((qFilter: PersistableFilter | ExistsFilter) => { if (qFilter.query) { filtersN.push(qFilter.query); } @@ -48,8 +48,8 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: }); if (!isEmpty(selectedReportDefinitions)) { - reportDefinitions.forEach(({ field: fieldT, custom }) => { - if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { + definitionFields.forEach((fieldT) => { + if (indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0]; filtersN.push(valueFilter.query); @@ -59,7 +59,7 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: return filtersN; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(filters)]); + }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]); return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx index f35639388aac5..0b183b5f20c03 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -21,7 +21,7 @@ describe('Series Builder ReportFilters', function () { }); it('should render properly', function () { - render(); + render(); screen.getByText('Add filter'); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx index 4571ecfe252e9..d5938c5387e8f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -7,23 +7,23 @@ import React from 'react'; import { SeriesFilter } from '../../series_editor/columns/series_filter'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; export function ReportFilters({ - dataViewSeries, + seriesConfig, seriesId, }: { - dataViewSeries: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; }) { return ( ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index f7cfe06c0d928..07048d47b2bc3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -38,7 +38,7 @@ describe('ReportTypesCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: 'user_agent.name', dataType: 'ux', - reportDefinitions: {}, + selectedMetricField: undefined, reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 64c7b48c668b8..396f8c4f1deb3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -15,7 +15,7 @@ import { ReportViewType, SeriesUrl } from '../../types'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { DEFAULT_TIME } from '../../configurations/constants'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { ReportTypeItem, SELECT_DATA_TYPE } from '../series_builder'; +import { ReportTypeItem } from '../series_builder'; interface Props { seriesId: string; @@ -30,7 +30,12 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); if (!restSeries.dataType) { - return {SELECT_DATA_TYPE}; + return ( + + ); } if (!loading && !hasData) { @@ -72,8 +77,7 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { setSeries(seriesId, { ...restSeries, reportType, - operationType: undefined, - reportDefinitions: {}, + selectedMetricField: undefined, time: restSeries?.time ?? DEFAULT_TIME, }); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx similarity index 66% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx index 201df9628e135..a2a3e34c21834 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx @@ -8,28 +8,26 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { ReportDefinition } from '../types'; +import { SeriesConfig } from '../types'; interface Props { - field: string; seriesId: string; defaultValue?: string; - options: ReportDefinition['options']; + options: SeriesConfig['metricOptions']; } -export function CustomReportField({ field, seriesId, options: opts }: Props) { +export function ReportMetricOptions({ seriesId, options: opts }: Props) { const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); - const { reportDefinitions: rtd = {} } = series; - const onChange = (value: string) => { - setSeries(seriesId, { ...series, reportDefinitions: { ...rtd, [field]: [value] } }); + setSeries(seriesId, { + ...series, + selectedMetricField: value, + }); }; - const { reportDefinitions } = series; - const options = opts ?? []; return ( @@ -41,7 +39,7 @@ export function CustomReportField({ field, seriesId, options: opts }: Props) { value: fd || id, inputDisplay: label, }))} - valueOfSelected={reportDefinitions?.[field]?.[0] || options?.[0].field || options?.[0].id} + valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id} onChange={(value) => onChange(value)} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx index e596eb6be354a..684cf3a210a51 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -17,7 +17,7 @@ import { EuiSwitch, } from '@elastic/eui'; import { rgba } from 'polished'; -import { AppDataType, DataSeries, ReportViewType, SeriesUrl } from '../types'; +import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; import { DataTypesCol } from './columns/data_types_col'; import { ReportTypesCol } from './columns/report_types_col'; import { ReportDefinitionCol } from './columns/report_definition_col'; @@ -66,7 +66,7 @@ export const ReportTypes: Record = { interface BuilderItem { id: string; series: SeriesUrl; - seriesConfig?: DataSeries; + seriesConfig?: SeriesConfig; } export function SeriesBuilder({ @@ -142,7 +142,7 @@ export function SeriesBuilder({ return loading ? ( LOADING_VIEW ) : reportType ? ( - + ) : ( SELECT_REPORT_TYPE ); @@ -159,7 +159,7 @@ export function SeriesBuilder({ field: 'id', render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => reportType && seriesConfig ? ( - + ) : null, }, { @@ -170,7 +170,7 @@ export function SeriesBuilder({ field: 'id', render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => reportType && seriesConfig ? ( - + ) : null, }, ...(multiSeries @@ -301,10 +301,3 @@ export const SELECT_REPORT_TYPE = i18n.translate( defaultMessage: 'No report type selected', } ); - -export const SELECT_DATA_TYPE = i18n.translate( - 'xpack.observability.expView.seriesBuilder.selectDataType', - { - defaultMessage: 'No data type selected', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx index a0d2fd86482a5..207a53e13f1ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx @@ -8,22 +8,22 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Breakdowns } from './columns/breakdowns'; -import { DataSeries } from '../types'; +import { SeriesConfig } from '../types'; import { ChartOptions } from './columns/chart_options'; interface Props { - series: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; - breakdowns: string[]; + breakdownFields: string[]; } -export function ChartEditOptions({ series, seriesId, breakdowns }: Props) { +export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) { return ( - + - + ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index d180bf4529c20..84568e1c5068a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -23,8 +23,8 @@ describe('Breakdowns', function () { render( ); @@ -37,8 +37,8 @@ describe('Breakdowns', function () { const { setSeries } = render( , { initSeries } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index cf24cb31951b1..2237935d466ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -10,15 +10,15 @@ import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; interface Props { seriesId: string; breakdowns: string[]; - reportViewConfig: DataSeries; + seriesConfig: SeriesConfig; } -export function Breakdowns({ reportViewConfig, seriesId, breakdowns = [] }: Props) { +export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { const { setSeries, getSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -40,11 +40,11 @@ export function Breakdowns({ reportViewConfig, seriesId, breakdowns = [] }: Prop } }; - const hasUseBreakdownColumn = reportViewConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; + const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; const items = breakdowns.map((breakdown) => ({ id: breakdown, - label: reportViewConfig.labels[breakdown], + label: seriesConfig.labels[breakdown], })); if (!hasUseBreakdownColumn) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx index 08664ac75eb8d..f2a6377fd9b71 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx @@ -7,22 +7,25 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; interface Props { - series: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; } -export function ChartOptions({ series, seriesId }: Props) { +export function ChartOptions({ seriesConfig, seriesId }: Props) { return ( - + - {series.hasOperationType && ( + {seriesConfig.hasOperationType && ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 0f0cec0fbfcff..6f9d8efdc0681 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -14,7 +14,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DataSeries, UrlFilter } from '../../types'; +import { SeriesConfig, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; @@ -29,7 +29,7 @@ interface Props { isNegated?: boolean; goBack: () => void; nestedField?: string; - filters: DataSeries['filters']; + filters: SeriesConfig['baseFilters']; } export function FilterExpanded({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx index b7e20b341b572..02144c6929b38 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -16,16 +16,16 @@ import { EuiFlexGroup, } from '@elastic/eui'; import { FilterExpanded } from './filter_expanded'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; import { FieldLabels } from '../../configurations/constants/constants'; import { SelectedFilters } from '../selected_filters'; import { useSeriesStorage } from '../../hooks/use_series_storage'; interface Props { seriesId: string; - defaultFilters: DataSeries['defaultFilters']; - filters: DataSeries['filters']; - series: DataSeries; + filterFields: SeriesConfig['filterFields']; + baseFilters: SeriesConfig['baseFilters']; + seriesConfig: SeriesConfig; isNew?: boolean; labels?: Record; } @@ -38,18 +38,18 @@ export interface Field { } export function SeriesFilter({ - series, + seriesConfig, isNew, seriesId, - defaultFilters = [], - filters, + filterFields = [], + baseFilters, labels, }: Props) { const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [selectedField, setSelectedField] = useState(); - const options: Field[] = defaultFilters.map((field) => { + const options: Field[] = filterFields.map((field) => { if (typeof field === 'string') { return { label: labels?.[field] ?? FieldLabels[field], field }; } @@ -111,7 +111,7 @@ export function SeriesFilter({ goBack={() => { setSelectedField(undefined); }} - filters={filters} + filters={baseFilters} /> ) : null; @@ -122,7 +122,7 @@ export function SeriesFilter({ return ( - + , { initSeries }); + render(, { + initSeries, + }); await waitFor(() => { screen.getByText('Chrome'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx index 33496e617a3a6..5d2ce6ba84951 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -9,28 +9,28 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { FilterLabel } from '../components/filter_label'; -import { DataSeries, UrlFilter } from '../types'; +import { SeriesConfig, UrlFilter } from '../types'; import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; interface Props { seriesId: string; - series: DataSeries; + seriesConfig: SeriesConfig; isNew?: boolean; } -export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) { +export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) { const { getSeries } = useSeriesStorage(); const series = getSeries(seriesId); const { reportDefinitions = {} } = series; - const { labels } = dataSeries; + const { labels } = seriesConfig; const filters: UrlFilter[] = series.filters ?? []; - let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions, dataSeries); + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); // we don't want to display report definition filters in new series view if (isNew) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index bcceeb204a31e..c3cc8484d1751 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { SeriesFilter } from './columns/series_filter'; -import { DataSeries } from '../types'; +import { SeriesConfig } from '../types'; import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DatePickerCol } from './columns/date_picker_col'; @@ -19,7 +19,7 @@ import { SeriesActions } from './columns/series_actions'; import { ChartEditOptions } from './chart_edit_options'; interface EditItem { - seriesConfig: DataSeries; + seriesConfig: SeriesConfig; id: string; } @@ -48,10 +48,10 @@ export function SeriesEditor() { width: '15%', render: (seriesId: string, { seriesConfig, id }: EditItem) => ( ), }, @@ -64,8 +64,8 @@ export function SeriesEditor() { render: (seriesId: string, { seriesConfig, id }: EditItem) => ( ), }, @@ -123,7 +123,7 @@ export function SeriesEditor() { rowHeader="firstName" columns={columns} noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.seriesNotFound', { - defaultMessage: 'No series found, please add a series.', + defaultMessage: 'No series found. Please add a series.', })} cellProps={{ style: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index e8fccc5baab34..ad7c654c9a168 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -37,31 +37,27 @@ export interface ColumnFilter { query: string; } -export interface ReportDefinition { - field: string; - required?: boolean; - custom?: boolean; - options?: Array<{ - id: string; - field?: string; - label: string; - description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; - columnFilters?: ColumnFilter[]; - timeScale?: string; - }>; +export interface MetricOption { + id: string; + field?: string; + label: string; + description?: string; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; + columnFilters?: ColumnFilter[]; + timeScale?: string; } -export interface DataSeries { +export interface SeriesConfig { reportType: ReportViewType; xAxisColumn: Partial | Partial; yAxisColumns: Array>; - breakdowns: string[]; + breakdownFields: string[]; defaultSeriesType: SeriesType; - defaultFilters: Array; + filterFields: Array; seriesTypes: SeriesType[]; - filters?: PersistableFilter[] | ExistsFilter[]; - reportDefinitions: ReportDefinition[]; + baseFilters?: PersistableFilter[] | ExistsFilter[]; + definitionFields: string[]; + metricOptions?: MetricOption[]; labels: Record; hasOperationType: boolean; palette?: PaletteOutput; @@ -83,6 +79,7 @@ export interface SeriesUrl { operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; + selectedMetricField?: string; isNew?: boolean; } From 6ba26db8d32c28750695632b8b2e275e566cfbeb Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Mon, 28 Jun 2021 16:24:13 +0200 Subject: [PATCH 05/41] Add warning toast when server.publicBaseUrl not configured correctly (#85344) --- ...-plugin-core-public.doclinksstart.links.md | 1 + ...kibana-plugin-core-public.doclinksstart.md | 2 +- src/core/public/core_app/core_app.ts | 14 ++- src/core/public/core_app/errors/index.ts | 1 + .../core_app/errors/public_base_url.test.tsx | 114 ++++++++++++++++++ .../core_app/errors/public_base_url.tsx | 88 ++++++++++++++ src/core/public/core_system.ts | 2 +- .../public/doc_links/doc_links_service.ts | 2 + src/core/public/http/http_service.mock.ts | 7 +- src/core/public/public.api.md | 1 + 10 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 src/core/public/core_app/errors/public_base_url.test.tsx create mode 100644 src/core/public/core_app/errors/public_base_url.tsx diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index b10ad949c4944..63d791db452d0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -8,6 +8,7 @@ ```typescript readonly links: { + readonly settings: string; readonly canvas: { readonly guide: string; }; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index c020f57faa882..947eece498130 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
} | | diff --git a/src/core/public/core_app/core_app.ts b/src/core/public/core_app/core_app.ts index aa0223dbe08a7..00532b9150aef 100644 --- a/src/core/public/core_app/core_app.ts +++ b/src/core/public/core_app/core_app.ts @@ -18,8 +18,13 @@ import type { CoreContext } from '../core_system'; import type { NotificationsSetup, NotificationsStart } from '../notifications'; import type { IUiSettingsClient } from '../ui_settings'; import type { InjectedMetadataSetup } from '../injected_metadata'; -import { renderApp as renderErrorApp, setupUrlOverflowDetection } from './errors'; +import { + renderApp as renderErrorApp, + setupPublicBaseUrlConfigWarning, + setupUrlOverflowDetection, +} from './errors'; import { renderApp as renderStatusApp } from './status'; +import { DocLinksStart } from '../doc_links'; interface SetupDeps { application: InternalApplicationSetup; @@ -30,6 +35,7 @@ interface SetupDeps { interface StartDeps { application: InternalApplicationStart; + docLinks: DocLinksStart; http: HttpStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; @@ -40,7 +46,7 @@ export class CoreApp { constructor(private readonly coreContext: CoreContext) {} - public setup({ http, application, injectedMetadata, notifications }: SetupDeps) { + public setup({ application, http, injectedMetadata, notifications }: SetupDeps) { application.register(this.coreContext.coreId, { id: 'error', title: 'App Error', @@ -68,7 +74,7 @@ export class CoreApp { }); } - public start({ application, http, notifications, uiSettings }: StartDeps) { + public start({ application, docLinks, http, notifications, uiSettings }: StartDeps) { if (!application.history) { return; } @@ -79,6 +85,8 @@ export class CoreApp { toasts: notifications.toasts, uiSettings, }); + + setupPublicBaseUrlConfigWarning({ docLinks, http, notifications }); } public stop() { diff --git a/src/core/public/core_app/errors/index.ts b/src/core/public/core_app/errors/index.ts index 02666103de349..e991fa455ab31 100644 --- a/src/core/public/core_app/errors/index.ts +++ b/src/core/public/core_app/errors/index.ts @@ -8,3 +8,4 @@ export { renderApp } from './error_application'; export { setupUrlOverflowDetection, URL_MAX_LENGTH } from './url_overflow'; +export { setupPublicBaseUrlConfigWarning } from './public_base_url'; diff --git a/src/core/public/core_app/errors/public_base_url.test.tsx b/src/core/public/core_app/errors/public_base_url.test.tsx new file mode 100644 index 0000000000000..d1fb5a5093f15 --- /dev/null +++ b/src/core/public/core_app/errors/public_base_url.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { docLinksServiceMock } from '../../doc_links/doc_links_service.mock'; +import { httpServiceMock } from '../../http/http_service.mock'; +import { notificationServiceMock } from '../../notifications/notifications_service.mock'; + +import { setupPublicBaseUrlConfigWarning } from './public_base_url'; + +describe('publicBaseUrl warning', () => { + const docLinks = docLinksServiceMock.createStartContract(); + const notifications = notificationServiceMock.createStartContract(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('does not show any toast on localhost', () => { + const http = httpServiceMock.createStartContract(); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: 'localhost', + } as Location, + }); + + expect(notifications.toasts.addWarning).not.toHaveBeenCalled(); + }); + + it('does not show any toast on 127.0.0.1', () => { + const http = httpServiceMock.createStartContract(); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: '127.0.0.1', + } as Location, + }); + + expect(notifications.toasts.addWarning).not.toHaveBeenCalled(); + }); + + it('does not show toast if configured correctly', () => { + const http = httpServiceMock.createStartContract({ publicBaseUrl: 'http://myhost.com' }); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: 'myhost.com', + toString() { + return 'http://myhost.com/'; + }, + } as Location, + }); + + expect(notifications.toasts.addWarning).not.toHaveBeenCalled(); + }); + + describe('config missing toast', () => { + it('adds toast if publicBaseUrl is missing', () => { + const http = httpServiceMock.createStartContract({ publicBaseUrl: undefined }); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: 'myhost.com', + toString() { + return 'http://myhost.com/'; + }, + } as Location, + }); + + expect(notifications.toasts.addWarning).toHaveBeenCalledWith({ + title: 'Configuration missing', + text: expect.any(Function), + }); + }); + + it('does not add toast if storage key set', () => { + const http = httpServiceMock.createStartContract({ publicBaseUrl: undefined }); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: 'myhost.com', + toString() { + return 'http://myhost.com/'; + }, + } as Location, + storage: { + getItem: (id: string) => 'true', + } as Storage, + }); + + expect(notifications.toasts.addWarning).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/core/public/core_app/errors/public_base_url.tsx b/src/core/public/core_app/errors/public_base_url.tsx new file mode 100644 index 0000000000000..263367a4cb09a --- /dev/null +++ b/src/core/public/core_app/errors/public_base_url.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { HttpStart, NotificationsStart } from '../..'; +import type { DocLinksStart } from '../../doc_links'; +import { mountReactNode } from '../../utils'; + +/** Only exported for tests */ +export const MISSING_CONFIG_STORAGE_KEY = `core.warnings.publicBaseUrlMissingDismissed`; + +interface Deps { + docLinks: DocLinksStart; + http: HttpStart; + notifications: NotificationsStart; + // Exposed for easier testing + storage?: Storage; + location?: Location; +} + +export const setupPublicBaseUrlConfigWarning = ({ + docLinks, + http, + notifications, + storage = window.localStorage, + location = window.location, +}: Deps) => { + if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { + return; + } + + const missingWarningSeen = storage.getItem(MISSING_CONFIG_STORAGE_KEY) === 'true'; + if (missingWarningSeen || http.basePath.publicBaseUrl) { + return; + } + + const toast = notifications.toasts.addWarning({ + title: i18n.translate('core.ui.publicBaseUrlWarning.configMissingTitle', { + defaultMessage: 'Configuration missing', + }), + text: mountReactNode( + <> +

+ server.publicBaseUrl, + }} + />{' '} + + + +

+ + + + { + notifications.toasts.remove(toast); + storage.setItem(MISSING_CONFIG_STORAGE_KEY, 'true'); + }} + id="mute" + > + + + + + + ), + }); +}; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 9a28bf45df927..e5dcd8f817a0a 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -202,7 +202,7 @@ export class CoreSystem { }); const deprecations = this.deprecations.start({ http }); - this.coreApp.start({ application, http, notifications, uiSettings }); + this.coreApp.start({ application, docLinks, http, notifications, uiSettings }); const core: InternalCoreStart = { application, diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 502b22a6f8e89..43c21b37ee298 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -29,6 +29,7 @@ export class DocLinksService { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL, links: { + settings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/settings.html`, canvas: { guide: `${KIBANA_DOCS}canvas.html`, }, @@ -426,6 +427,7 @@ export interface DocLinksStart { readonly DOC_LINK_VERSION: string; readonly ELASTIC_WEBSITE_URL: string; readonly links: { + readonly settings: string; readonly canvas: { readonly guide: string; }; diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 61f501c844f30..fff99d84a76a6 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -18,7 +18,10 @@ export type HttpSetupMock = jest.Mocked & { anonymousPaths: jest.Mocked; }; -const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ +const createServiceMock = ({ + basePath = '', + publicBaseUrl, +}: { basePath?: string; publicBaseUrl?: string } = {}): HttpSetupMock => ({ fetch: jest.fn(), get: jest.fn(), head: jest.fn(), @@ -27,7 +30,7 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ patch: jest.fn(), delete: jest.fn(), options: jest.fn(), - basePath: new BasePath(basePath), + basePath: new BasePath(basePath, undefined, publicBaseUrl), anonymousPaths: { register: jest.fn(), isAnonymous: jest.fn(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index ca95b253f9cdb..32897f10425d6 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -487,6 +487,7 @@ export interface DocLinksStart { readonly ELASTIC_WEBSITE_URL: string; // (undocumented) readonly links: { + readonly settings: string; readonly canvas: { readonly guide: string; }; From 648841429c716a78a7ca2e300fef2b11d7112648 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 28 Jun 2021 07:35:03 -0700 Subject: [PATCH 06/41] Update health.asciidoc --- docs/api/task-manager/health.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api/task-manager/health.asciidoc b/docs/api/task-manager/health.asciidoc index 22006725da00c..7418d44bbfd33 100644 --- a/docs/api/task-manager/health.asciidoc +++ b/docs/api/task-manager/health.asciidoc @@ -6,17 +6,20 @@ Retrieve the health status of the {kib} Task Manager. +[float] [[task-manager-api-health-request]] ==== Request `GET :/api/task_manager/_health` +[float] [[task-manager-api-health-codes]] ==== Response code `200`:: Indicates a successful call. +[float] [[task-manager-api-health-example]] ==== Example From 91fc3cc2b9a797e2ada23a89217214d18921b9f6 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 28 Jun 2021 10:39:23 -0400 Subject: [PATCH 07/41] [Security Solution][Endpoint] Refresh action and comments on the Case Details view when Isolation actions are created (#103160) * Expose new Prop from `CaseComponent` that exposes data refresh callbacks * Refresh case actions and comments if isolation was created successfully --- x-pack/plugins/cases/common/ui/types.ts | 17 +++++ .../public/components/case_view/index.tsx | 72 +++++++++++++++++-- .../public/containers/use_get_case.test.tsx | 13 ++++ .../cases/public/containers/use_get_case.tsx | 61 +++++++++------- .../containers/use_get_case_user_actions.tsx | 6 +- .../cases/components/case_view/index.tsx | 13 ++-- .../endpoint_host_isolation_cases_context.tsx | 36 ++++++++++ .../components/host_isolation/index.tsx | 4 ++ .../components/host_isolation/isolate.tsx | 8 ++- .../components/host_isolation/unisolate.tsx | 12 +++- .../alerts/use_host_isolation.tsx | 1 + .../side_panel/event_details/index.tsx | 11 +++ .../endpoint/routes/actions/isolation.ts | 32 +++++---- 13 files changed, 234 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context.tsx diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 1dbb633e32adf..3edbd3443ffc1 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -23,6 +23,23 @@ export type StatusAllType = typeof StatusAll; export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType; +/** + * The type for the `refreshRef` prop (a `React.Ref`) defined by the `CaseViewComponentProps`. + * + * @example + * const refreshRef = useRef(null); + * return + */ +export type CaseViewRefreshPropInterface = null | { + /** + * Refreshes the all of the user actions/comments in the view's timeline + * (note: this also triggers a silent `refreshCase()`) + */ + refreshUserActionsAndComments: () => Promise; + /** Refreshes the Case information only */ + refreshCase: () => Promise; +}; + export type Comment = CommentRequest & { associationType: AssociationType; id: string; diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 9c6e9442c8f56..d5b535b8ddad1 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef, MutableRefObject } from 'react'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; import { @@ -16,11 +16,19 @@ import { EuiHorizontalRule, } from '@elastic/eui'; -import { CaseStatuses, CaseAttributes, CaseType, Case, CaseConnector, Ecs } from '../../../common'; +import { + CaseStatuses, + CaseAttributes, + CaseType, + Case, + CaseConnector, + Ecs, + CaseViewRefreshPropInterface, +} from '../../../common'; import { HeaderPage } from '../header_page'; import { EditableTitle } from '../header_page/editable_title'; import { TagList } from '../tag_list'; -import { useGetCase } from '../../containers/use_get_case'; +import { UseGetCase, useGetCase } from '../../containers/use_get_case'; import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../containers/use_update_case'; @@ -42,6 +50,7 @@ import { getConnectorById } from '../utils'; import { DoesNotExist } from './does_not_exist'; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file + export interface CaseViewComponentProps { allCasesNavigation: CasesNavigation; caseDetailsNavigation: CasesNavigation; @@ -54,12 +63,18 @@ export interface CaseViewComponentProps { subCaseId?: string; useFetchAlertData: (alertIds: string[]) => [boolean, Record]; userCanCrud: boolean; + /** + * A React `Ref` that Exposes data refresh callbacks. + * **NOTE**: Do not hold on to the `.current` object, as it could become stale + */ + refreshRef?: MutableRefObject; } export interface CaseViewProps extends CaseViewComponentProps { onCaseDataSuccess?: (data: Case) => void; timelineIntegration?: CasesTimelineIntegration; } + export interface OnUpdateFields { key: keyof Case; value: Case[keyof Case]; @@ -78,13 +93,14 @@ const MyEuiFlexGroup = styled(EuiFlexGroup)` const MyEuiHorizontalRule = styled(EuiHorizontalRule)` margin-left: 48px; + &.euiHorizontalRule--full { width: calc(100% - 48px); } `; export interface CaseComponentProps extends CaseViewComponentProps { - fetchCase: () => void; + fetchCase: UseGetCase['fetchCase']; caseData: Case; updateCase: (newCase: Case) => void; } @@ -105,6 +121,7 @@ export const CaseComponent = React.memo( updateCase, useFetchAlertData, userCanCrud, + refreshRef, }) => { const [initLoadingData, setInitLoadingData] = useState(true); const init = useRef(true); @@ -124,6 +141,51 @@ export const CaseComponent = React.memo( subCaseId, }); + // Set `refreshRef` if needed + useEffect(() => { + let isStale = false; + + if (refreshRef) { + refreshRef.current = { + refreshCase: async () => { + // Do nothing if component (or instance of this render cycle) is stale + if (isStale) { + return; + } + + await fetchCase(); + }, + refreshUserActionsAndComments: async () => { + // Do nothing if component (or instance of this render cycle) is stale + // -- OR -- + // it is already loading + if (isStale || isLoadingUserActions) { + return; + } + + await Promise.all([ + fetchCase(true), + fetchCaseUserActions(caseId, caseData.connector.id, subCaseId), + ]); + }, + }; + + return () => { + isStale = true; + refreshRef.current = null; + }; + } + }, [ + caseData.connector.id, + caseId, + fetchCase, + fetchCaseUserActions, + isLoadingUserActions, + refreshRef, + subCaseId, + updateCase, + ]); + // Update Fields const onUpdateField = useCallback( ({ key, value, onSuccess, onError }: OnUpdateFields) => { @@ -491,6 +553,7 @@ export const CaseView = React.memo( timelineIntegration, useFetchAlertData, userCanCrud, + refreshRef, }: CaseViewProps) => { const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId); if (isError) { @@ -528,6 +591,7 @@ export const CaseView = React.memo( updateCase={updateCase} useFetchAlertData={useFetchAlertData} userCanCrud={userCanCrud} + refreshRef={refreshRef} /> diff --git a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx index 75d9ac74a8ccf..c88f530709c8a 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx @@ -89,6 +89,19 @@ describe('useGetCase', () => { }); }); + it('set isLoading to false when refetching case "silent"ly', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchCase(true); + + expect(result.current.isLoading).toBe(false); + }); + }); + it('unhappy path', async () => { const spyOnGetCase = jest.spyOn(api, 'getCase'); spyOnGetCase.mockImplementation(() => { diff --git a/x-pack/plugins/cases/public/containers/use_get_case.tsx b/x-pack/plugins/cases/public/containers/use_get_case.tsx index 7b59f8e06b7af..b9326ad057c9e 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.tsx @@ -19,7 +19,7 @@ interface CaseState { } type Action = - | { type: 'FETCH_INIT' } + | { type: 'FETCH_INIT'; payload: { silent: boolean } } | { type: 'FETCH_SUCCESS'; payload: Case } | { type: 'FETCH_FAILURE' } | { type: 'UPDATE_CASE'; payload: Case }; @@ -29,7 +29,10 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { case 'FETCH_INIT': return { ...state, - isLoading: true, + // If doing a silent fetch, then don't set `isLoading`. This helps + // with preventing screen flashing when wanting to refresh the actions + // and comments + isLoading: !action.payload?.silent, isError: false, }; case 'FETCH_SUCCESS': @@ -56,7 +59,11 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { }; export interface UseGetCase extends CaseState { - fetchCase: () => void; + /** + * @param [silent] When set to `true`, the `isLoading` property will not be set to `true` + * while doing the API call + */ + fetchCase: (silent?: boolean) => Promise; updateCase: (newCase: Case) => void; } @@ -74,33 +81,35 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { dispatch({ type: 'UPDATE_CASE', payload: newCase }); }, []); - const callFetch = useCallback(async () => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - dispatch({ type: 'FETCH_INIT' }); + const callFetch = useCallback( + async (silent: boolean = false) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + dispatch({ type: 'FETCH_INIT', payload: { silent } }); - const response = await (subCaseId - ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) - : getCase(caseId, true, abortCtrlRef.current.signal)); + const response = await (subCaseId + ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) + : getCase(caseId, true, abortCtrlRef.current.signal)); - if (!isCancelledRef.current) { - dispatch({ type: 'FETCH_SUCCESS', payload: response }); - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_TITLE } - ); + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: response }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + toasts.addError( + error.body && error.body.message ? new Error(error.body.message) : error, + { title: i18n.ERROR_TITLE } + ); + } + dispatch({ type: 'FETCH_FAILURE' }); } - dispatch({ type: 'FETCH_FAILURE' }); } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [caseId, subCaseId]); + }, + [caseId, subCaseId, toasts] + ); useEffect(() => { callFetch(); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx index 66aa93154b318..edafa1b9a10a9 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx @@ -51,7 +51,11 @@ export const initialData: CaseUserActionsState = { }; export interface UseGetCaseUserActions extends CaseUserActionsState { - fetchCaseUserActions: (caseId: string, caseConnectorId: string, subCaseId?: string) => void; + fetchCaseUserActions: ( + caseId: string, + caseConnectorId: string, + subCaseId?: string + ) => Promise; } const getExternalService = (value: string): CaseExternalService | null => diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 71bc241f65e10..dec2d409b020d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { SearchResponse } from 'elasticsearch'; import { isEmpty } from 'lodash'; @@ -19,7 +19,7 @@ import { useFormatUrl, } from '../../../common/components/link_to'; import { Ecs } from '../../../../common/ecs'; -import { Case } from '../../../../../cases/common'; +import { Case, CaseViewRefreshPropInterface } from '../../../../../cases/common'; import { TimelineId } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { KibanaServices, useKibana } from '../../../common/lib/kibana'; @@ -38,6 +38,7 @@ import { SEND_ALERT_TO_TIMELINE } from './translations'; import { useInsertTimeline } from '../use_insert_timeline'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; +import { CaseDetailsRefreshContext } from '../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context'; interface Props { caseId: string; @@ -176,9 +177,13 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = }) ); }, [dispatch]); + + const refreshRef = useRef(null); + return ( - <> + {casesUi.getCaseView({ + refreshRef, allCasesNavigation: { href: formattedAllCasesLink, onClick: async (e) => { @@ -247,7 +252,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = userCanCrud, })} - + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context.tsx new file mode 100644 index 0000000000000..8ae9d98a31429 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { MutableRefObject, useContext } from 'react'; +import { CaseViewRefreshPropInterface } from '../../../../../../cases/common'; + +/** + * React Context that can hold the `Ref` that is created an passed to `CaseViewProps['refreshRef`]`, enabling + * child components to trigger a refresh of a case. + */ +export const CaseDetailsRefreshContext = React.createContext | null>( + null +); + +/** + * Returns the closes CaseDetails Refresh interface if any. Used in conjuction with `CaseDetailsRefreshContext` component + * + * @example + * // Higher-order component + * const refreshRef = useRef(null); + * return .... + * + * // Now, use the hook from a hild component that was rendered inside of `` + * const caseDetailsRefresh = useWithCaseDetailsRefresh(); + * ... + * if (caseDetailsRefresh) { + * caseDetailsRefresh.refreshUserActionsAndComments(); + * } + */ +export const useWithCaseDetailsRefresh = (): Readonly | undefined => { + return useContext(CaseDetailsRefreshContext)?.current; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx index ef311a7ca43b1..36443cc91f4e8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx @@ -20,10 +20,12 @@ export const HostIsolationPanel = React.memo( ({ details, cancelCallback, + successCallback, isolateAction, }: { details: Maybe; cancelCallback: () => void; + successCallback?: () => void; isolateAction: string; }) => { const endpointId = useMemo(() => { @@ -92,6 +94,7 @@ export const HostIsolationPanel = React.memo( cases={associatedCases} casesInfo={casesInfo} cancelCallback={cancelCallback} + successCallback={successCallback} /> ) : ( ); } diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx index b209c2f9c6e24..75dd850c30f43 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx @@ -24,12 +24,14 @@ export const IsolateHost = React.memo( cases, casesInfo, cancelCallback, + successCallback, }: { endpointId: string; hostName: string; cases: ReactNode; casesInfo: CasesFromAlertsResponse; cancelCallback: () => void; + successCallback?: () => void; }) => { const [comment, setComment] = useState(''); const [isIsolated, setIsIsolated] = useState(false); @@ -43,7 +45,11 @@ export const IsolateHost = React.memo( const confirmHostIsolation = useCallback(async () => { const hostIsolated = await isolateHost(); setIsIsolated(hostIsolated); - }, [isolateHost]); + + if (hostIsolated && successCallback) { + successCallback(); + } + }, [isolateHost, successCallback]); const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx index ad8e8eaddb39e..2b810dc16eec8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx @@ -24,12 +24,14 @@ export const UnisolateHost = React.memo( cases, casesInfo, cancelCallback, + successCallback, }: { endpointId: string; hostName: string; cases: ReactNode; casesInfo: CasesFromAlertsResponse; cancelCallback: () => void; + successCallback?: () => void; }) => { const [comment, setComment] = useState(''); const [isUnIsolated, setIsUnIsolated] = useState(false); @@ -41,9 +43,13 @@ export const UnisolateHost = React.memo( const { loading, unIsolateHost } = useHostUnisolation({ endpointId, comment, caseIds }); const confirmHostUnIsolation = useCallback(async () => { - const hostIsolated = await unIsolateHost(); - setIsUnIsolated(hostIsolated); - }, [unIsolateHost]); + const hostUnIsolated = await unIsolateHost(); + setIsUnIsolated(hostUnIsolated); + + if (hostUnIsolated && successCallback) { + successCallback(); + } + }, [successCallback, unIsolateHost]); const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx index 12426e05ba528..70d1d5ab5e194 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx @@ -12,6 +12,7 @@ import { createHostIsolation } from './api'; interface HostIsolationStatus { loading: boolean; + /** Boolean return will indicate if isolation action was created successful */ isolateHost: () => Promise; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 395538610f567..c4b19863ce7fc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -33,6 +33,7 @@ import { import { ALERT_DETAILS } from './translations'; import { useIsolationPrivileges } from '../../../../common/hooks/endpoint/use_isolate_privileges'; import { endpointAlertCheck } from '../../../../common/utils/endpoint_alert_check'; +import { useWithCaseDetailsRefresh } from '../../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflow { @@ -121,6 +122,15 @@ const EventDetailsPanelComponent: React.FC = ({ ); }, [showAlertDetails, isolateAction]); + const caseDetailsRefresh = useWithCaseDetailsRefresh(); + + const handleIsolationActionSuccess = useCallback(() => { + // If a case details refresh ref is defined, then refresh actions and comments + if (caseDetailsRefresh) { + caseDetailsRefresh.refreshUserActionsAndComments(); + } + }, [caseDetailsRefresh]); + if (!expandedEvent?.eventId) { return null; } @@ -139,6 +149,7 @@ const EventDetailsPanelComponent: React.FC = ({ ) : ( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 50fe2ffe2cea9..785434aa17ec6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -61,6 +61,7 @@ export const isolationRequestHandler = function ( TypeOf, SecuritySolutionRequestHandlerContext > { + // eslint-disable-next-line complexity return async (context, req, res) => { if ( (!req.body.agent_ids || req.body.agent_ids.length === 0) && @@ -100,14 +101,14 @@ export const isolationRequestHandler = function ( } agentIDs = [...new Set(agentIDs)]; // dedupe + const casesClient = await endpointContext.service.getCasesClient(req); + // convert any alert IDs into cases let caseIDs: string[] = req.body.case_ids?.slice() || []; if (req.body.alert_ids && req.body.alert_ids.length > 0) { const newIDs: string[][] = await Promise.all( req.body.alert_ids.map(async (a: string) => { - const cases: CasesByAlertId = await ( - await endpointContext.service.getCasesClient(req) - ).cases.getCasesByAlertID({ + const cases: CasesByAlertId = await casesClient.cases.getCasesByAlertID({ alertID: a, options: { owner: APP_ID }, }); @@ -167,16 +168,21 @@ export const isolationRequestHandler = function ( commentLines.push(`\n\nWith Comment:\n> ${req.body.comment}`); } - caseIDs.forEach(async (caseId) => { - (await endpointContext.service.getCasesClient(req)).attachments.add({ - caseId, - comment: { - comment: commentLines.join('\n'), - type: CommentType.user, - owner: APP_ID, - }, - }); - }); + // Update all cases with a comment + if (caseIDs.length > 0) { + await Promise.all( + caseIDs.map((caseId) => + casesClient.attachments.add({ + caseId, + comment: { + comment: commentLines.join('\n'), + type: CommentType.user, + owner: APP_ID, + }, + }) + ) + ); + } return res.ok({ body: { From 22d5c90855a9e4c23fec00b7d29ac13322eb353d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 28 Jun 2021 15:58:41 +0100 Subject: [PATCH 08/41] chore(NA): moving @kbn/spec-to-console into bazel (#103470) * chore(NA): moving @kbn/spec-to-console into bazel * chore(NA): fix licenses --- .../monorepo-packages.asciidoc | 1 + package.json | 1 + packages/BUILD.bazel | 1 + packages/kbn-spec-to-console/BUILD.bazel | 55 +++++++++++++++++++ .../cluster_health_autocomplete.json | 0 .../__fixtures__}/cluster_health_spec.json | 0 .../kbn-spec-to-console/lib/convert.test.js | 4 +- packages/kbn-spec-to-console/package.json | 5 +- scripts/spec_to_console.js | 2 +- yarn.lock | 4 ++ 10 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 packages/kbn-spec-to-console/BUILD.bazel rename packages/kbn-spec-to-console/{test/fixtures => lib/__fixtures__}/cluster_health_autocomplete.json (100%) rename packages/kbn-spec-to-console/{test/fixtures => lib/__fixtures__}/cluster_health_spec.json (100%) diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 217645b903818..0ee4c09192896 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -103,6 +103,7 @@ yarn kbn watch-bazel - @kbn/securitysolution-utils - @kbn/server-http-tools - @kbn/server-route-repository +- @kbn/spec-to-console - @kbn/std - @kbn/storybook - @kbn/telemetry-utils diff --git a/package.json b/package.json index ceb178d068519..b589153d2af90 100644 --- a/package.json +++ b/package.json @@ -470,6 +470,7 @@ "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator", "@kbn/plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", + "@kbn/spec-to-console": "link:bazel-bin/packages/kbn-spec-to-console", "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook", "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 1094a2def3e70..225a41a5fd8b6 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -48,6 +48,7 @@ filegroup( "//packages/kbn-securitysolution-hook-utils:build", "//packages/kbn-server-http-tools:build", "//packages/kbn-server-route-repository:build", + "//packages/kbn-spec-to-console:build", "//packages/kbn-std:build", "//packages/kbn-storybook:build", "//packages/kbn-telemetry-tools:build", diff --git a/packages/kbn-spec-to-console/BUILD.bazel b/packages/kbn-spec-to-console/BUILD.bazel new file mode 100644 index 0000000000000..8a083be9fce91 --- /dev/null +++ b/packages/kbn-spec-to-console/BUILD.bazel @@ -0,0 +1,55 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-spec-to-console" +PKG_REQUIRE_NAME = "@kbn/spec-to-console" + +SOURCE_FILES = glob( + [ + "bin/**/*", + "lib/**/*", + "index.js" + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +DEPS = [] + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES + [ + ":srcs", + ], + deps = DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-spec-to-console/test/fixtures/cluster_health_autocomplete.json b/packages/kbn-spec-to-console/lib/__fixtures__/cluster_health_autocomplete.json similarity index 100% rename from packages/kbn-spec-to-console/test/fixtures/cluster_health_autocomplete.json rename to packages/kbn-spec-to-console/lib/__fixtures__/cluster_health_autocomplete.json diff --git a/packages/kbn-spec-to-console/test/fixtures/cluster_health_spec.json b/packages/kbn-spec-to-console/lib/__fixtures__/cluster_health_spec.json similarity index 100% rename from packages/kbn-spec-to-console/test/fixtures/cluster_health_spec.json rename to packages/kbn-spec-to-console/lib/__fixtures__/cluster_health_spec.json diff --git a/packages/kbn-spec-to-console/lib/convert.test.js b/packages/kbn-spec-to-console/lib/convert.test.js index 6d6b6ba364d38..14cb2dd7b6c04 100644 --- a/packages/kbn-spec-to-console/lib/convert.test.js +++ b/packages/kbn-spec-to-console/lib/convert.test.js @@ -8,8 +8,8 @@ const convert = require('./convert'); -const clusterHealthSpec = require('../test/fixtures/cluster_health_spec'); -const clusterHealthAutocomplete = require('../test/fixtures/cluster_health_autocomplete'); +const clusterHealthSpec = require('./__fixtures__/cluster_health_spec'); +const clusterHealthAutocomplete = require('./__fixtures__/cluster_health_autocomplete'); test('convert', () => { expect(convert(clusterHealthSpec)).toEqual(clusterHealthAutocomplete); diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index c6cf8cf9ec46d..b4e488db7f4d9 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -1,11 +1,12 @@ { - "name": "spec-to-console", - "version": "0.0.0", + "name": "@kbn/spec-to-console", + "version": "1.0.0", "description": "ES REST spec -> Console autocomplete", "main": "index.js", "directories": { "lib": "lib" }, + "private": true, "scripts": { "format": "../../node_modules/.bin/prettier **/*.js --write" }, diff --git a/scripts/spec_to_console.js b/scripts/spec_to_console.js index cbb152f55f8fb..37e246323a11f 100644 --- a/scripts/spec_to_console.js +++ b/scripts/spec_to_console.js @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -require('../packages/kbn-spec-to-console/bin/spec_to_console'); +require('@kbn/spec-to-console/bin/spec_to_console'); diff --git a/yarn.lock b/yarn.lock index 9d7569b6ab4f2..2ea799810e3a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2768,6 +2768,10 @@ version "0.0.0" uid "" +"@kbn/spec-to-console@link:bazel-bin/packages/kbn-spec-to-console": + version "0.0.0" + uid "" + "@kbn/std@link:bazel-bin/packages/kbn-std": version "0.0.0" uid "" From 8fa4421cac0e52302121fa57880bc2e6a2a4f791 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 28 Jun 2021 18:13:31 +0300 Subject: [PATCH 09/41] Update TSVB docs with guidelines on how to group by multiple fields (#103224) * Update TSVB FAQ section with guidelines on how to group by multiple fields * Apply text suggestion --- .../images/tsvb_group_by_multiple_fields.png | Bin 0 -> 146657 bytes docs/user/dashboard/tsvb.asciidoc | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 docs/user/dashboard/images/tsvb_group_by_multiple_fields.png diff --git a/docs/user/dashboard/images/tsvb_group_by_multiple_fields.png b/docs/user/dashboard/images/tsvb_group_by_multiple_fields.png new file mode 100644 index 0000000000000000000000000000000000000000..3f23189f7725424129a356848c170daec82d2957 GIT binary patch literal 146657 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 z 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 Q7Xd%V%`8lF4_^%bKZRKwg#Z8m literal 0 HcmV?d00001 diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index b69df7c7d26d6..93ee3627bd8a0 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -166,3 +166,15 @@ For other types of month over month calculations, use <> o Calculating the duration between the start and end of an event is unsupported in *TSVB* because *TSVB* requires correlation between different time periods. *TSVB* requires that the duration is pre-calculated. + +[float] +===== How do I group on multiple fields? + +To group with multiple fields, create runtime fields in the index pattern you are visualizing. + +. Create a runtime field. Refer to <> for more information. ++ +[role="screenshot"] +image::images/tsvb_group_by_multiple_fields.png[Group by multiple fields] + +. Create a new TSVB visualization and group by this field. \ No newline at end of file From 43a897e640477b69c42b1fa551d855c4d2e6d5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Mon, 28 Jun 2021 17:24:26 +0200 Subject: [PATCH 10/41] [Security Solution][Endpoint] The refresh button triggers always a refresh action (#103118) * Triggers search even if the query hasn't changed * Add await on async function call * refactor, use forceSearch store flag instead of new actin type to force search * fix error when refreshing trusted apps * Fix ts-error by adding ts-ignore '{ type: "LoadingResourceState"; previousState: ImmutableObject | ImmutableObject> | ImmutableObject<...> | ImmutableObject<...>; }' is not assignable to type 'ImmutableObject<{ forceRefresh: boolean; }>'. Object literal may only specify known properties, and 'type' does not exist in type 'ImmutableObject<{ forceRefresh: boolean; }>' --- .../pages/event_filters/store/action.ts | 9 +++++- .../pages/event_filters/store/middleware.ts | 2 +- .../pages/event_filters/store/reducer.test.ts | 31 +++++++++++++++++++ .../pages/event_filters/store/reducer.ts | 13 ++++++++ .../pages/event_filters/store/selector.ts | 3 +- .../event_filters/store/selectors.test.ts | 5 --- .../view/event_filters_list_page.tsx | 10 ++++-- .../state/trusted_apps_list_page_state.ts | 1 + .../pages/trusted_apps/store/action.ts | 9 +++++- .../pages/trusted_apps/store/builders.ts | 1 + .../pages/trusted_apps/store/middleware.ts | 3 +- .../pages/trusted_apps/store/reducer.test.ts | 25 +++++++++++++++ .../pages/trusted_apps/store/reducer.ts | 11 +++++++ .../pages/trusted_apps/store/selectors.ts | 17 +++++----- .../trusted_apps/view/trusted_apps_page.tsx | 15 +++++++-- 15 files changed, 131 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts index 016170686c7dd..588eb9275ad21 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts @@ -66,6 +66,12 @@ export type EventFiltersFormStateChanged = Action<'eventFiltersFormStateChanged' payload: AsyncResourceState; }; +export type EventFiltersForceRefresh = Action<'eventFiltersForceRefresh'> & { + payload: { + forceRefresh: boolean; + }; +}; + export type EventFiltersPageAction = | EventFiltersListPageDataChanged | EventFiltersListPageDataExistsChanged @@ -81,4 +87,5 @@ export type EventFiltersPageAction = | EventFilterForDeletion | EventFilterDeletionReset | EventFilterDeleteSubmit - | EventFilterDeleteStatusChanged; + | EventFilterDeleteStatusChanged + | EventFiltersForceRefresh; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts index c1ade4e2cadec..ad9e3d32a3f4c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts @@ -232,9 +232,9 @@ const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFilt dispatch({ type: 'eventFiltersListPageDataChanged', payload: { - type: 'LoadingResourceState', // Ignore will be fixed with when AsyncResourceState is refactored (#830) // @ts-ignore + type: 'LoadingResourceState', previousState: getCurrentListPageDataState(state), }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts index 5366b6dcf155a..2bfc6b4378839 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts @@ -168,4 +168,35 @@ describe('event filters reducer', () => { }); }); }); + + describe('ForceRefresh', () => { + it('sets the force refresh state to true', () => { + const result = eventFiltersPageReducer( + { + ...initialState, + listPage: { ...initialState.listPage, forceRefresh: false }, + }, + { type: 'eventFiltersForceRefresh', payload: { forceRefresh: true } } + ); + + expect(result).toStrictEqual({ + ...initialState, + listPage: { ...initialState.listPage, forceRefresh: true }, + }); + }); + it('sets the force refresh state to false', () => { + const result = eventFiltersPageReducer( + { + ...initialState, + listPage: { ...initialState.listPage, forceRefresh: true }, + }, + { type: 'eventFiltersForceRefresh', payload: { forceRefresh: false } } + ); + + expect(result).toStrictEqual({ + ...initialState, + listPage: { ...initialState.listPage, forceRefresh: false }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts index 28292bdb1ed1c..b6e853ca4bf0f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts @@ -30,6 +30,7 @@ import { EventFilterForDeletion, EventFilterDeletionReset, EventFilterDeleteStatusChanged, + EventFiltersForceRefresh, } from './action'; import { initialEventFiltersPageState } from './builders'; @@ -220,6 +221,16 @@ const handleEventFilterDeleteStatusChanges: CaseReducer = (state, action) => { + return { + ...state, + listPage: { + ...state.listPage, + forceRefresh: action.payload.forceRefresh, + }, + }; +}; + export const eventFiltersPageReducer: StateReducer = ( state = initialEventFiltersPageState(), action @@ -237,6 +248,8 @@ export const eventFiltersPageReducer: StateReducer = ( return eventFiltersUpdateSuccess(state, action); case 'userChangedUrl': return userChangedUrl(state, action); + case 'eventFiltersForceRefresh': + return handleEventFilterForceRefresh(state, action); } // actions only handled if we're on the List Page diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts index fef6ccb99a17a..2fa196a053f78 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts @@ -184,8 +184,7 @@ export const listDataNeedsRefresh: EventFiltersSelector = createSelecto return ( forceRefresh || location.page_index + 1 !== currentQuery.page || - location.page_size !== currentQuery.perPage || - location.filter !== currentQuery.filter + location.page_size !== currentQuery.perPage ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts index 9d2d3c394c416..be3de3017d1f3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts @@ -250,11 +250,6 @@ describe('event filters selectors', () => { initialState.location.page_index = 10; expect(listDataNeedsRefresh(initialState)).toBe(true); }); - - it('should should return true if filter param differ from last api call', () => { - initialState.location.filter = 'query'; - expect(listDataNeedsRefresh(initialState)).toBe(true); - }); }); describe('getFormEntry()', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 0975104f02297..1f3b721fd51e3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -177,9 +177,13 @@ export const EventFiltersListPage = memo(() => { [navigateCallback] ); - const handleOnSearch = useCallback((query: string) => navigateCallback({ filter: query }), [ - navigateCallback, - ]); + const handleOnSearch = useCallback( + (query: string) => { + dispatch({ type: 'eventFiltersForceRefresh', payload: { forceRefresh: true } }); + navigateCallback({ filter: query }); + }, + [navigateCallback, dispatch] + ); return ( ; location: TrustedAppsListPageLocation; active: boolean; + forceRefresh: boolean; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index 34f48142c7032..a3f804ed6cd77 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -68,6 +68,12 @@ export type TrustedAppsPoliciesStateChanged = Action<'trustedAppsPoliciesStateCh payload: AsyncResourceState; }; +export type TrustedAppForceRefresh = Action<'trustedAppForceRefresh'> & { + payload: { + forceRefresh: boolean; + }; +}; + export type TrustedAppsPageAction = | TrustedAppsListDataOutdated | TrustedAppsListResourceStateChanged @@ -82,4 +88,5 @@ export type TrustedAppsPageAction = | TrustedAppCreationDialogConfirmed | TrustedAppsExistResponse | TrustedAppsPoliciesStateChanged - | TrustedAppCreationDialogClosed; + | TrustedAppCreationDialogClosed + | TrustedAppForceRefresh; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts index 5a22badec9afb..988d3d6e828cc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -59,4 +59,5 @@ export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ filter: '', }, active: false, + forceRefresh: false, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 878938aa20e1b..da6394a9ab896 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -80,6 +80,7 @@ const refreshListIfNeeded = async ( trustedAppsService: TrustedAppsService ) => { if (needsRefreshOfListData(store.getState())) { + store.dispatch({ type: 'trustedAppForceRefresh', payload: { forceRefresh: false } }); store.dispatch( createTrustedAppsListResourceStateChangedAction({ type: 'LoadingResourceState', @@ -395,11 +396,11 @@ const fetchEditTrustedAppIfNeeded = async ( dispatch({ type: 'trustedAppCreationEditItemStateChanged', payload: { - type: 'LoadingResourceState', // No easy way to get around this that I can see. `previousState` does not // seem to allow everything that `editItem` state can hold, so not even sure if using // type guards would work here // @ts-ignore + type: 'LoadingResourceState', previousState: editItemState(currentState)!, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index 58193eea3de52..42659e5cc3498 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -191,4 +191,29 @@ describe('reducer', () => { expect(result).toStrictEqual(initialState); }); }); + + describe('TrustedAppsForceRefresh', () => { + it('sets the force refresh state to true', () => { + const result = trustedAppsPageReducer( + { + ...initialState, + forceRefresh: false, + }, + { type: 'trustedAppForceRefresh', payload: { forceRefresh: true } } + ); + + expect(result).toStrictEqual({ ...initialState, forceRefresh: true }); + }); + it('sets the force refresh state to false', () => { + const result = trustedAppsPageReducer( + { + ...initialState, + forceRefresh: true, + }, + { type: 'trustedAppForceRefresh', payload: { forceRefresh: false } } + ); + + expect(result).toStrictEqual({ ...initialState, forceRefresh: false }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index ea7bbb44c9bf2..d0de5dc80ee79 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -31,6 +31,7 @@ import { TrustedAppsExistResponse, TrustedAppsPoliciesStateChanged, TrustedAppCreationEditItemStateChanged, + TrustedAppForceRefresh, } from './action'; import { TrustedAppsListPageState } from '../state'; @@ -177,6 +178,13 @@ const updatePolicies: CaseReducer = (state, { p return state; }; +const forceRefresh: CaseReducer = (state, { payload }) => { + return { + ...state, + forceRefresh: payload.forceRefresh, + }; +}; + export const trustedAppsPageReducer: StateReducer = ( state = initialTrustedAppsPageState(), action @@ -226,6 +234,9 @@ export const trustedAppsPageReducer: StateReducer = ( case 'trustedAppsPoliciesStateChanged': return updatePolicies(state, action); + + case 'trustedAppForceRefresh': + return forceRefresh(state, action); } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index 43506f98193a0..338f30b447a8a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -30,16 +30,17 @@ export const needsRefreshOfListData = (state: Immutable { - return ( - data.pageIndex === location.page_index && - data.pageSize === location.page_size && - data.timestamp >= freshDataTimestamp && - data.filter === location.filter - ); - }) + (forceRefresh || + isOutdatedResourceState(currentPage, (data) => { + return ( + data.pageIndex === location.page_index && + data.pageSize === location.page_size && + data.timestamp >= freshDataTimestamp + ); + })) ); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 4cd6ad62f3a35..ec80b4c5ae21b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; import { useLocation } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -33,6 +35,7 @@ import { TrustedAppsGrid } from './components/trusted_apps_grid'; import { TrustedAppsList } from './components/trusted_apps_list'; import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; import { TrustedAppsNotifications } from './trusted_apps_notifications'; +import { AppAction } from '../../../../common/store/actions'; import { ABOUT_TRUSTED_APPS, SEARCH_TRUSTED_APP_PLACEHOLDER } from './translations'; import { EmptyState } from './components/empty_state'; import { SearchBar } from '../../../components/search_bar'; @@ -40,11 +43,13 @@ import { BackToExternalAppButton } from '../../../components/back_to_external_ap import { ListPageRouteState } from '../../../../../common/endpoint/types'; export const TrustedAppsPage = memo(() => { + const dispatch = useDispatch>(); const { state: routeState } = useLocation(); const location = useTrustedAppsSelector(getCurrentLocation); const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount); const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist); const doEntriesExist = useTrustedAppsSelector(entriesExist) === true; + const navigationCallback = useTrustedAppsNavigateCallback((query: string) => ({ filter: query })); const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({ show: 'create', id: undefined, @@ -56,7 +61,13 @@ export const TrustedAppsPage = memo(() => { const handleViewTypeChange = useTrustedAppsNavigateCallback((viewType: ViewType) => ({ view_type: viewType, })); - const handleOnSearch = useTrustedAppsNavigateCallback((query: string) => ({ filter: query })); + const handleOnSearch = useCallback( + (query: string) => { + dispatch({ type: 'trustedAppForceRefresh', payload: { forceRefresh: true } }); + navigationCallback(query); + }, + [dispatch, navigationCallback] + ); const showCreateFlyout = !!location.show; From 84e1b01ceb2954a74c4ea67f8695fe41b736d85a Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 28 Jun 2021 08:39:27 -0700 Subject: [PATCH 11/41] Result settings: Fix restore defaults copy (#103413) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/result_settings/result_settings_logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts index 13530c2c29ef0..216c43e1d3072 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -92,7 +92,7 @@ const RESET_CONFIRMATION_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.confirmResetMessage', { defaultMessage: - 'This will revert your settings back to the default: all fields set to raw. The default will take over immediately and impact your search results.', + 'Are you sure you want to restore result settings defaults? This will set all fields back to raw with no limits.', } ); From 96fe9c23f878354df5b9122d48093e696f014c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Mon, 28 Jun 2021 17:48:54 +0200 Subject: [PATCH 12/41] [Security solution][Endpoint] Get os name from host.os.name when agent type endpoint (#103450) * When type endpoint gets os type from os name instead of os family * Allow users add event filters only for endpoint events * Fixes error with wrong map function Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/event_filters/store/utils.ts | 16 +++++++++------- .../pages/event_filters/test_utils/index.ts | 1 + .../components/timeline/body/actions/index.tsx | 8 ++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts index 6adc490b40e78..e0f9a6bcc965c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts @@ -10,6 +10,14 @@ import type { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts- import { Ecs } from '../../../../../common/ecs'; import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../constants'; +const osTypeBasedOnAgentType = (data?: Ecs) => { + if (data?.agent?.type?.includes('endpoint')) { + return (data?.host?.os?.name || ['windows']).map((name) => name.toLowerCase()); + } else { + return data?.host?.os?.family ?? ['windows']; + } +}; + export const getInitialExceptionFromEvent = (data?: Ecs): CreateExceptionListItemSchema => ({ comments: [], description: '', @@ -46,11 +54,5 @@ export const getInitialExceptionFromEvent = (data?: Ecs): CreateExceptionListIte namespace_type: 'agnostic', tags: ['policy:all'], type: 'simple', - // TODO: Try to fix this type casting - os_types: [ - (data && data.host ? data.host.os?.family ?? ['windows'] : ['windows'])[0] as - | 'windows' - | 'linux' - | 'macos', - ], + os_types: osTypeBasedOnAgentType(data) as Array<'windows' | 'linux' | 'macos'>, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts index dc235cf511157..c45d0f88927be 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts @@ -50,6 +50,7 @@ export const ecsEventMock = (): Ecs => ({ name: ['Host-tvs68wo3qc'], os: { family: ['windows'], + name: ['Windows'], }, id: ['a563b365-2bee-40df-adcd-ae84d889f523'], ip: ['10.242.233.187'], diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 0a3a1cd88accc..29e00d169b4e4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -87,9 +87,9 @@ const ActionsComponent: React.FC = ({ ); const eventType = getEventType(ecsData); - const isEventContextMenuEnabled = useMemo( - () => !!ecsData.event?.kind && ecsData.event?.kind[0] === 'event', - [ecsData.event?.kind] + const isEventContextMenuEnabledForEndpoint = useMemo( + () => ecsData.event?.kind?.includes('event') && ecsData.agent?.type?.includes('endpoint'), + [ecsData.event?.kind, ecsData.agent?.type] ); return ( @@ -174,7 +174,7 @@ const ActionsComponent: React.FC = ({ key="alert-context-menu" ecsRowData={ecsData} timelineId={timelineId} - disabled={eventType !== 'signal' && !isEventContextMenuEnabled} + disabled={eventType !== 'signal' && !isEventContextMenuEnabledForEndpoint} refetch={refetch ?? noop} onRuleChange={onRuleChange} /> From 15b0dbff7bc27cee3bfbd938f698ac5053dc854e Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 28 Jun 2021 19:18:30 +0300 Subject: [PATCH 13/41] [TSVB] Fix references to the index pattern are not embedded when exporting a saved object (#103255) * [TSVB] Importing a dashboard with only TSVB viz on another space, breaks the dashboard Closes: #103059 * move index-pattern to constant Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...na-plugin-plugins-data-public.esfilters.md | 8 ++-- ...bana-plugin-plugins-data-public.eskuery.md | 2 +- ...bana-plugin-plugins-data-public.esquery.md | 2 +- ...lugin-plugins-data-public.iindexpattern.md | 2 +- ...-public.index_pattern_saved_object_type.md | 13 +++++++ .../kibana-plugin-plugins-data-public.md | 1 + ...na-plugin-plugins-data-server.esfilters.md | 8 ++-- ...bana-plugin-plugins-data-server.eskuery.md | 2 +- ...bana-plugin-plugins-data-server.esquery.md | 2 +- ...-server.index_pattern_saved_object_type.md | 13 +++++++ .../kibana-plugin-plugins-data-server.md | 1 + .../saved_objects/dashboard_migrations.ts | 29 ++++++++------ .../replace_index_pattern_reference.test.ts | 39 +++++++++++++++++++ .../replace_index_pattern_reference.ts | 22 +++++++++++ src/plugins/data/common/constants.ts | 3 ++ .../index_patterns/index_patterns.ts | 23 ++++++----- .../common/index_patterns/lib/get_title.ts | 6 ++- .../data/common/index_patterns/utils.ts | 4 +- .../search_source/extract_references.ts | 6 ++- src/plugins/data/public/index.ts | 1 + src/plugins/data/public/public.api.md | 31 ++++++++------- src/plugins/data/server/index.ts | 7 +++- .../data/server/index_patterns/utils.ts | 9 ++++- .../server/saved_objects/index_patterns.ts | 5 ++- src/plugins/data/server/server.api.md | 31 ++++++++------- .../controls_references.ts | 3 +- .../timeseries_references.ts | 6 +-- ...ualization_saved_object_migrations.test.ts | 30 ++++++++++++++ .../visualization_saved_object_migrations.ts | 29 +++++++++++--- 29 files changed, 257 insertions(+), 81 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.index_pattern_saved_object_type.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.index_pattern_saved_object_type.md create mode 100644 src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.test.ts create mode 100644 src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index 2ca4847d6dc39..80c321ce6b320 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -13,11 +13,11 @@ esFilters: { FILTERS: typeof FILTERS; FilterStateStore: typeof FilterStateStore; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhrasesFilter; - buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").MinimalIndexPattern) => import("../common").ExistsFilter; - buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhraseFilter; + buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter; + buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter; + buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter; buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; - buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").MinimalIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter; + buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter; isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter; isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter; isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md index 881a1fa803ca6..332114e637586 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md @@ -10,6 +10,6 @@ esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md index 70805aaaaee8c..0bc9c0c12fc3a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md @@ -10,7 +10,7 @@ esQuery: { buildEsQuery: typeof buildEsQuery; getEsQueryConfig: typeof getEsQueryConfig; - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").MinimalIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { must: never[]; filter: import("../common").Filter[]; should: never[]; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 88d8520a373c6..ec29ef81a6e69 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -12,7 +12,7 @@ Signature: ```typescript -export interface IIndexPattern extends MinimalIndexPattern +export interface IIndexPattern extends IndexPatternBase ``` ## Properties diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.index_pattern_saved_object_type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.index_pattern_saved_object_type.md new file mode 100644 index 0000000000000..552d131984517 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.index_pattern_saved_object_type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [INDEX\_PATTERN\_SAVED\_OBJECT\_TYPE](./kibana-plugin-plugins-data-public.index_pattern_saved_object_type.md) + +## INDEX\_PATTERN\_SAVED\_OBJECT\_TYPE variable + +\* + +Signature: + +```typescript +INDEX_PATTERN_SAVED_OBJECT_TYPE = "index-pattern" +``` 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 7c023e756ebd5..65c4601d5faec 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 @@ -118,6 +118,7 @@ | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | | [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | +| [INDEX\_PATTERN\_SAVED\_OBJECT\_TYPE](./kibana-plugin-plugins-data-public.index_pattern_saved_object_type.md) | \* | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | | | [isCompleteResponse](./kibana-plugin-plugins-data-public.iscompleteresponse.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md index d951cb2426943..d009cad9ec601 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md @@ -11,11 +11,11 @@ esFilters: { buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; buildCustomFilter: typeof buildCustomFilter; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").MinimalIndexPattern) => import("../common").ExistsFilter; + buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter; buildFilter: typeof buildFilter; - buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhraseFilter; - buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhrasesFilter; - buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").MinimalIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter; + buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter; + buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter; + buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter; isFilterDisabled: (filter: import("../common").Filter) => boolean; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md index 6274eb5f4f4a5..fce25a899de8e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md @@ -10,6 +10,6 @@ esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md index 0d1baecb014f5..68507f3fb9b81 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md @@ -8,7 +8,7 @@ ```typescript esQuery: { - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").MinimalIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { must: never[]; filter: import("../common").Filter[]; should: never[]; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.index_pattern_saved_object_type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.index_pattern_saved_object_type.md new file mode 100644 index 0000000000000..34f76d4ab13b1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.index_pattern_saved_object_type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [INDEX\_PATTERN\_SAVED\_OBJECT\_TYPE](./kibana-plugin-plugins-data-server.index_pattern_saved_object_type.md) + +## INDEX\_PATTERN\_SAVED\_OBJECT\_TYPE variable + +\* + +Signature: + +```typescript +INDEX_PATTERN_SAVED_OBJECT_TYPE = "index-pattern" +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 9816b884c4614..ab14abdd74e87 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -83,6 +83,7 @@ | [esQuery](./kibana-plugin-plugins-data-server.esquery.md) | | | [exporters](./kibana-plugin-plugins-data-server.exporters.md) | | | [fieldFormats](./kibana-plugin-plugins-data-server.fieldformats.md) | | +| [INDEX\_PATTERN\_SAVED\_OBJECT\_TYPE](./kibana-plugin-plugins-data-server.index_pattern_saved_object_type.md) | \* | | [indexPatterns](./kibana-plugin-plugins-data-server.indexpatterns.md) | | | [mergeCapabilitiesWithFields](./kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md) | | | [search](./kibana-plugin-plugins-data-server.search.md) | | diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index 4ebca5ba8965e..0bd100b3d5803 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -7,7 +7,7 @@ */ import semver from 'semver'; -import { get, flow } from 'lodash'; +import { get, flow, identity } from 'lodash'; import { SavedObjectAttributes, SavedObjectMigrationFn, @@ -25,7 +25,9 @@ import { convertSavedDashboardPanelToPanelState, } from '../../common/embeddable/embeddable_saved_object_converters'; import { SavedObjectEmbeddableInput } from '../../../embeddable/common'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; import { SerializableValue } from '../../../kibana_utils/common'; +import { replaceIndexPatternReference } from './replace_index_pattern_reference'; function migrateIndexPattern(doc: DashboardDoc700To720) { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); @@ -43,7 +45,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; doc.references.push({ name: searchSource.indexRefName, - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: searchSource.index, }); delete searchSource.index; @@ -56,7 +58,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; doc.references.push({ name: filterRow.meta.indexRefName, - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: filterRow.meta.index, }); delete filterRow.meta.index; @@ -214,12 +216,14 @@ export interface DashboardSavedObjectTypeMigrationsDeps { export const createDashboardSavedObjectTypeMigrations = ( deps: DashboardSavedObjectTypeMigrationsDeps ): SavedObjectMigrationMap => { - const embeddableMigrations = deps.embeddable - .getMigrationVersions() - .filter((version) => semver.gt(version, '7.12.0')) - .map((version): [string, SavedObjectMigrationFn] => { - return [version, migrateByValuePanels(deps, version)]; - }); + const embeddableMigrations = Object.fromEntries( + deps.embeddable + .getMigrationVersions() + .filter((version) => semver.gt(version, '7.12.0')) + .map((version): [string, SavedObjectMigrationFn] => { + return [version, migrateByValuePanels(deps, version)]; + }) + ); return { /** @@ -237,12 +241,15 @@ export const createDashboardSavedObjectTypeMigrations = ( '7.3.0': flow(migrations730), '7.9.3': flow(migrateMatchAllQuery), '7.11.0': flow(createExtractPanelReferencesMigration(deps)), - ...Object.fromEntries(embeddableMigrations), + + ...embeddableMigrations, /** * Any dashboard saved object migrations that come after this point will have to be wary of * potentially overwriting embeddable migrations. An example of how to mitigate this follows: */ - // '7.x': flow(yourNewMigrationFunction, embeddableMigrations['7.x']) + // '7.x': flow(yourNewMigrationFunction, embeddableMigrations['7.x'] ?? identity), + + '7.14.0': flow(replaceIndexPatternReference, embeddableMigrations['7.14.0'] ?? identity), }; }; diff --git a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.test.ts b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.test.ts new file mode 100644 index 0000000000000..01207fb4e3404 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { SavedObjectMigrationContext, SavedObjectMigrationFn } from 'kibana/server'; + +import { replaceIndexPatternReference } from './replace_index_pattern_reference'; + +describe('replaceIndexPatternReference', () => { + const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + + test('should replace index_pattern to index-pattern', () => { + const migratedDoc = replaceIndexPatternReference( + { + references: [ + { + name: 'name', + type: 'index_pattern', + }, + ], + } as Parameters[0], + savedObjectMigrationContext + ); + + expect(migratedDoc).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "name": "name", + "type": "index-pattern", + }, + ], + } + `); + }); +}); diff --git a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts new file mode 100644 index 0000000000000..ddd1c45841b9c --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectMigrationFn } from 'kibana/server'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; + +export const replaceIndexPatternReference: SavedObjectMigrationFn = (doc) => ({ + ...doc, + references: Array.isArray(doc.references) + ? doc.references.map((reference) => { + if (reference.type === 'index_pattern') { + reference.type = INDEX_PATTERN_SAVED_OBJECT_TYPE; + } + return reference; + }) + : doc.references, +}); diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 79a9e0ac5451b..c6bfbfc75c290 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -9,6 +9,9 @@ export const DEFAULT_QUERY_LANGUAGE = 'kuery'; export const KIBANA_USER_QUERY_LANGUAGE_KEY = 'kibana.userQueryLanguage'; +/** @public **/ +export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; + export const UI_SETTINGS = { META_FIELDS: 'metaFields', DOC_HIGHLIGHT: 'doc_table:highlight', 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 e67e72f295b8e..cecf3b8c07d1a 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 @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { PublicMethodsOf } from '@kbn/utility-types'; -import { SavedObjectsClientCommon } from '../..'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE, SavedObjectsClientCommon } from '../..'; import { createIndexPatternCache } from '.'; import type { RuntimeField } from '../types'; @@ -38,7 +38,6 @@ import { DuplicateIndexPatternError } from '../errors'; import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; -const savedObjectType = 'index-pattern'; export interface IndexPatternSavedObjectAttrs { title: string; @@ -94,7 +93,7 @@ export class IndexPatternsService { */ private async refreshSavedObjectsCache() { const so = await this.savedObjectsClient.find({ - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, fields: ['title'], perPage: 10000, }); @@ -137,7 +136,7 @@ export class IndexPatternsService { */ find = async (search: string, size: number = 10): Promise => { const savedObjects = await this.savedObjectsClient.find({ - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, fields: ['title'], search, searchFields: ['title'], @@ -395,12 +394,16 @@ export class IndexPatternsService { private getSavedObjectAndInit = async (id: string): Promise => { const savedObject = await this.savedObjectsClient.get( - savedObjectType, + INDEX_PATTERN_SAVED_OBJECT_TYPE, id ); if (!savedObject.version) { - throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns'); + throw new SavedObjectNotFound( + INDEX_PATTERN_SAVED_OBJECT_TYPE, + id, + 'management/kibana/indexPatterns' + ); } return this.initFromSavedObject(savedObject); @@ -546,7 +549,7 @@ export class IndexPatternsService { const body = indexPattern.getAsSavedObjectBody(); const response: SavedObject = (await this.savedObjectsClient.create( - savedObjectType, + INDEX_PATTERN_SAVED_OBJECT_TYPE, body, { id: indexPattern.id, @@ -587,7 +590,9 @@ export class IndexPatternsService { }); return this.savedObjectsClient - .update(savedObjectType, indexPattern.id, body, { version: indexPattern.version }) + .update(INDEX_PATTERN_SAVED_OBJECT_TYPE, indexPattern.id, body, { + version: indexPattern.version, + }) .then((resp) => { indexPattern.id = resp.id; indexPattern.version = resp.version; @@ -655,7 +660,7 @@ export class IndexPatternsService { */ async delete(indexPatternId: string) { this.indexPatternCache.clear(indexPatternId); - return this.savedObjectsClient.delete('index-pattern', indexPatternId); + return this.savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, indexPatternId); } } diff --git a/src/plugins/data/common/index_patterns/lib/get_title.ts b/src/plugins/data/common/index_patterns/lib/get_title.ts index 2dd122092f688..69afad486a745 100644 --- a/src/plugins/data/common/index_patterns/lib/get_title.ts +++ b/src/plugins/data/common/index_patterns/lib/get_title.ts @@ -7,12 +7,16 @@ */ import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../constants'; export async function getTitle( client: SavedObjectsClientContract, indexPatternId: string ): Promise> { - const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; + const savedObject = (await client.get( + INDEX_PATTERN_SAVED_OBJECT_TYPE, + indexPatternId + )) as SimpleSavedObject; if (savedObject.error) { throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); diff --git a/src/plugins/data/common/index_patterns/utils.ts b/src/plugins/data/common/index_patterns/utils.ts index 941ad3c47066b..925f646b83bb7 100644 --- a/src/plugins/data/common/index_patterns/utils.ts +++ b/src/plugins/data/common/index_patterns/utils.ts @@ -9,6 +9,8 @@ import type { IndexPatternSavedObjectAttrs } from './index_patterns'; import type { SavedObjectsClientCommon } from '../types'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../constants'; + /** * Returns an object matching a given title * @@ -19,7 +21,7 @@ import type { SavedObjectsClientCommon } from '../types'; export async function findByTitle(client: SavedObjectsClientCommon, title: string) { if (title) { const savedObjects = await client.find({ - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, perPage: 10, search: `"${title}"`, searchFields: ['title'], diff --git a/src/plugins/data/common/search/search_source/extract_references.ts b/src/plugins/data/common/search/search_source/extract_references.ts index 1b4d1732a5e37..b63b8ed1cfee2 100644 --- a/src/plugins/data/common/search/search_source/extract_references.ts +++ b/src/plugins/data/common/search/search_source/extract_references.ts @@ -10,6 +10,8 @@ import { SavedObjectReference } from 'src/core/types'; import { Filter } from '../../es_query/filters'; import { SearchSourceFields } from './types'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../constants'; + export const extractReferences = ( state: SearchSourceFields ): [SearchSourceFields & { indexRefName?: string }, SavedObjectReference[]] => { @@ -20,7 +22,7 @@ export const extractReferences = ( const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; references.push({ name: refName, - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: indexId, }); searchSourceFields = { @@ -40,7 +42,7 @@ export const extractReferences = ( const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; references.push({ name: refName, - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: filterRow.meta.index, }); return { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index d7667f20d517e..e9e50ebfaf138 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -268,6 +268,7 @@ export { IndexPatternSpec, IndexPatternLoadExpressionFunctionDefinition, fieldList, + INDEX_PATTERN_SAVED_OBJECT_TYPE, } from '../common'; export { DuplicateIndexPatternError } from '../common/index_patterns/errors'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 2849b93b14483..6a49fab0e33ff 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1361,6 +1361,9 @@ export interface IKibanaSearchResponse { // @public (undocumented) export type IMetricAggType = MetricAggType; +// @public (undocumented) +export const INDEX_PATTERN_SAVED_OBJECT_TYPE = "index-pattern"; + // Warning: (ae-missing-release-tag) "IndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2772,20 +2775,20 @@ export interface WaitUntilNextSessionCompletesOptions { // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:435:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index dd60951e6d228..143400a2c09d3 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -117,7 +117,12 @@ export const fieldFormats = { HistogramFormat, }; -export { IFieldFormatsRegistry, FieldFormatsGetConfigFn, FieldFormatConfig } from '../common'; +export { + IFieldFormatsRegistry, + FieldFormatsGetConfigFn, + FieldFormatConfig, + INDEX_PATTERN_SAVED_OBJECT_TYPE, +} from '../common'; /* * Index patterns: diff --git a/src/plugins/data/server/index_patterns/utils.ts b/src/plugins/data/server/index_patterns/utils.ts index bb16be23edc7d..7f1a953c482d0 100644 --- a/src/plugins/data/server/index_patterns/utils.ts +++ b/src/plugins/data/server/index_patterns/utils.ts @@ -7,7 +7,12 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { IFieldType, IndexPatternAttributes, SavedObject } from '../../common'; +import { + IFieldType, + INDEX_PATTERN_SAVED_OBJECT_TYPE, + IndexPatternAttributes, + SavedObject, +} from '../../common'; export const getFieldByName = ( fieldName: string, @@ -24,7 +29,7 @@ export const findIndexPatternById = async ( index: string ): Promise | undefined> => { const savedObjectsResponse = await savedObjectsClient.find({ - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, fields: ['fields'], search: `"${index}"`, searchFields: ['title'], diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts index f570e239c3c64..a809f2ce73e1b 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ -import { SavedObjectsType } from 'kibana/server'; +import type { SavedObjectsType } from 'kibana/server'; import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migrations'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../common'; export const indexPatternSavedObjectType: SavedObjectsType = { - name: 'index-pattern', + name: INDEX_PATTERN_SAVED_OBJECT_TYPE, hidden: false, namespaceType: 'single', management: { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 5ca19f9e1e509..86aaf64dea852 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -745,6 +745,9 @@ export interface IFieldType { // @public (undocumented) export type IMetricAggType = MetricAggType; +// @public (undocumented) +export const INDEX_PATTERN_SAVED_OBJECT_TYPE = "index-pattern"; + // Warning: (ae-missing-release-tag) "IndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1543,20 +1546,20 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:133:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:133:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:269:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:276:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:277:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts index d116fd2e2e9a7..7a0bb4584e83a 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts @@ -8,6 +8,7 @@ import { SavedObjectReference } from '../../../../../core/types'; import { VisParams } from '../../../common'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../data/public'; const isControlsVis = (visType: string) => visType === 'input_control_vis'; @@ -25,7 +26,7 @@ export const extractControlsReferences = ( control.indexPatternRefName = `${prefix}_${i}_index_pattern`; references.push({ name: control.indexPatternRefName, - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: control.indexPattern, }); delete control.indexPattern; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts index 57706ee824e8d..98970a0127c71 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts @@ -8,13 +8,11 @@ import { SavedObjectReference } from '../../../../../core/types'; import { VisParams } from '../../../common'; +import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../data/public'; /** @internal **/ const REF_NAME_POSTFIX = '_ref_name'; -/** @internal **/ -const INDEX_PATTERN_REF_TYPE = 'index_pattern'; - /** @internal **/ type Action = (object: Record, key: string) => void; @@ -51,7 +49,7 @@ export const extractTimeSeriesReferences = ( object[key + REF_NAME_POSTFIX] = name; references.push({ name, - type: INDEX_PATTERN_REF_TYPE, + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: object[key].id, }); delete object[key]; diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index 7debc9412925e..869a9add89066 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -2163,6 +2163,36 @@ describe('migration visualization', () => { }); }); + describe('7.14.0 replaceIndexPatternReference', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.14.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + test('should replace index_pattern to index-pattern', () => { + expect( + migrate({ + references: [ + { + name: 'name', + type: 'index_pattern', + }, + ], + } as Parameters[0]) + ).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "name": "name", + "type": "index-pattern", + }, + ], + } + `); + }); + }); + describe('7.14.0 update tagcloud defaults', () => { const migrate = (doc: any) => visualizationSavedObjectTypeMigrations['7.14.0']( diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index 7fb54b0425935..1f50e26ea9ec1 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -8,9 +8,9 @@ import { cloneDeep, get, omit, has, flow, forOwn } from 'lodash'; -import { SavedObjectMigrationFn } from 'kibana/server'; +import type { SavedObjectMigrationFn } from 'kibana/server'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; +import { DEFAULT_QUERY_LANGUAGE, INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; import { commonAddSupportOfDualIndexSelectionModeInTSVB, commonHideTSVBLastValueIndicator, @@ -37,7 +37,7 @@ const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; doc.references.push({ name: searchSource.indexRefName, - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: searchSource.index, }); delete searchSource.index; @@ -50,7 +50,7 @@ const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; doc.references.push({ name: filterRow.meta.indexRefName, - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: filterRow.meta.index, }); delete filterRow.meta.index; @@ -648,7 +648,7 @@ const migrateControls: SavedObjectMigrationFn = (doc) => { control.indexPatternRefName = `control_${i}_index_pattern`; doc.references.push({ name: control.indexPatternRefName, - type: 'index-pattern', + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, id: control.indexPattern, }); delete control.indexPattern; @@ -1038,6 +1038,18 @@ const migrateTagCloud: SavedObjectMigrationFn = (doc) => { return doc; }; +export const replaceIndexPatternReference: SavedObjectMigrationFn = (doc) => ({ + ...doc, + references: Array.isArray(doc.references) + ? doc.references.map((reference) => { + if (reference.type === 'index_pattern') { + reference.type = INDEX_PATTERN_SAVED_OBJECT_TYPE; + } + return reference; + }) + : doc.references, +}); + export const visualizationSavedObjectTypeMigrations = { /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version @@ -1084,5 +1096,10 @@ export const visualizationSavedObjectTypeMigrations = { hideTSVBLastValueIndicator, removeDefaultIndexPatternAndTimeFieldFromTSVBModel ), - '7.14.0': flow(addEmptyValueColorRule, migrateVislibPie, migrateTagCloud), + '7.14.0': flow( + addEmptyValueColorRule, + migrateVislibPie, + migrateTagCloud, + replaceIndexPatternReference + ), }; From dfeecb902fd26d6e41bab81fc4a31c45663f8894 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 28 Jun 2021 12:31:10 -0400 Subject: [PATCH 14/41] [ML] Data Frame Analytics creation wizard: ensure included fields table updates correctly (#103191) * fix includes table rerender loop * remove unnecessary comment --- .../analysis_fields_table.tsx | 2 - .../configuration_step_form.tsx | 41 ++++++++----------- .../configuration_step/job_type.tsx | 1 - 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx index 0b6843d49e95c..9dd4c5c42cca7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx @@ -84,7 +84,6 @@ const checkboxDisabledCheck = (item: FieldSelectionItem) => export const AnalysisFieldsTable: FC<{ dependentVariable?: string; includes: string[]; - loadingItems: boolean; setFormState: React.Dispatch>; minimumFieldsRequiredMessage?: string; setMinimumFieldsRequiredMessage: React.Dispatch>; @@ -94,7 +93,6 @@ export const AnalysisFieldsTable: FC<{ }> = ({ dependentVariable, includes, - loadingItems, setFormState, minimumFieldsRequiredMessage, setMinimumFieldsRequiredMessage, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 930c32ce7e4da..9b68b03853990 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -105,7 +105,6 @@ export const ConfigurationStepForm: FC = ({ const { currentSavedSearch, currentIndexPattern } = mlContext; const { savedSearchQuery, savedSearchQueryStr } = useSavedSearch(); - const [loadingFieldOptions, setLoadingFieldOptions] = useState(false); const [fieldOptionsFetchFail, setFieldOptionsFetchFail] = useState(false); const [loadingDepVarOptions, setLoadingDepVarOptions] = useState(false); const [dependentVariableFetchFail, setDependentVariableFetchFail] = useState(false); @@ -247,21 +246,17 @@ export const ConfigurationStepForm: FC = ({ if (firstUpdate.current) { firstUpdate.current = false; } - // Reset if jobType changes (jobType requires dependent_variable to be set - - // which won't be the case if switching from outlier detection) - if (jobTypeChanged) { - setLoadingFieldOptions(true); - } + + const depVarNotIncluded = + isJobTypeWithDepVar && includes.length > 0 && includes.includes(dependentVariable) === false; // Ensure runtime field is in 'includes' table if it is set as dependent variable const depVarIsRuntimeField = - isJobTypeWithDepVar && + depVarNotIncluded && runtimeMappings && - Object.keys(runtimeMappings).includes(dependentVariable) && - includes.length > 0 && - includes.includes(dependentVariable) === false; + Object.keys(runtimeMappings).includes(dependentVariable); let formToUse = form; - if (depVarIsRuntimeField) { + if (depVarIsRuntimeField || depVarNotIncluded) { formToUse = cloneDeep(form); formToUse.includes = [...includes, dependentVariable]; } @@ -279,24 +274,22 @@ export const ConfigurationStepForm: FC = ({ (field) => field.is_included === true && field.is_required === false ); + const formStateUpdated = { + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), + ...(depVarIsRuntimeField || jobTypeChanged || depVarNotIncluded + ? { includes: formToUse.includes } + : {}), + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + }; + if (jobTypeChanged) { - setLoadingFieldOptions(false); setFieldOptionsFetchFail(false); setMaxDistinctValuesError(undefined); setUnsupportedFieldsError(undefined); - setFormState({ - ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), - requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, - includes: formToUse.includes, - }); setIncludesTableItems(fieldSelection ? fieldSelection : []); - } else { - setFormState({ - ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), - requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, - includes: formToUse.includes, - }); } + + setFormState(formStateUpdated); setFetchingExplainData(false); } else { const { @@ -319,7 +312,6 @@ export const ConfigurationStepForm: FC = ({ : DEFAULT_MODEL_MEMORY_LIMIT.outlier_detection; setEstimatedModelMemoryLimit(fallbackModelMemoryLimit); - setLoadingFieldOptions(false); setFieldOptionsFetchFail(true); setMaxDistinctValuesError(maxDistinctValuesErrorMessage); setUnsupportedFieldsError(unsupportedFieldsErrorMessage); @@ -650,7 +642,6 @@ export const ConfigurationStepForm: FC = ({ tableItems={includesTableItems} unsupportedFieldsError={unsupportedFieldsError} setUnsupportedFieldsError={setUnsupportedFieldsError} - loadingItems={loadingFieldOptions} setFormState={setFormState} /> {showScatterplotMatrix && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx index 443e2cfacbb5e..5f54ba3c2bb7c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -82,7 +82,6 @@ export const JobType: FC = ({ type, setFormState }) => { setFormState({ previousJobType: type, jobType, - includes: [], requiredFieldsError: undefined, }); setSelectedCard({ [jobType]: !selectedCard[jobType] }); From bd32299c13036ac28e1a7dd1120dbe4f241a6c15 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 28 Jun 2021 11:31:23 -0500 Subject: [PATCH 15/41] Upgrade EUI to v34.5.1 (#103297) * eui to 34.5.0 * snapshot updates * Fix some page layouts * eui to 34.5.1 Co-authored-by: cchaos --- package.json | 2 +- .../page_template/solution_nav/solution_nav.tsx | 2 +- .../public/components/home/home.component.tsx | 1 - .../settings/__snapshots__/settings.test.tsx.snap | 14 +++++++------- .../home_integration/tutorial_directory_notice.tsx | 2 +- .../entity_by_expression.test.tsx.snap | 2 +- yarn.lock | 11 ++++++----- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index b589153d2af90..b071b587a3620 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.14.0", - "@elastic/eui": "34.3.0", + "@elastic/eui": "34.5.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx index bd9ee8eb4d0e8..4aa456f716dbd 100644 --- a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx @@ -17,7 +17,7 @@ import { KibanaPageTemplateSolutionNavAvatarProps, } from './solution_nav_avatar'; -export type KibanaPageTemplateSolutionNavProps = Partial> & { +export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & { /** * Name of the solution, i.e. "Observability" */ diff --git a/x-pack/plugins/canvas/public/components/home/home.component.tsx b/x-pack/plugins/canvas/public/components/home/home.component.tsx index 96a773186da2b..6e98439a0c530 100644 --- a/x-pack/plugins/canvas/public/components/home/home.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.component.tsx @@ -31,7 +31,6 @@ export const Home = ({ activeTab = 'workpads' }: Props) => { pageHeader={{ pageTitle: 'Canvas', rightSideItems: [], - bottomBorder: true, tabs: [ { label: strings.getMyWorkpadsTabLabel(), diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap index 27f0d3610fb9f..075c0cd386759 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap @@ -31,7 +31,7 @@ exports[` can navigate Autoplay Settings 1`] = ` >
can navigate Autoplay Settings 2`] = ` >
can navigate Autoplay Settings 2`] = `
`; -exports[` can navigate Toolbar Settings, closes when activated 3`] = `"

You are in a dialog. To close this dialog, hit escape.

Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"

You are in a dialog. To close this dialog, hit escape.

Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx index 5e4357d95235b..23754571c5bc1 100644 --- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx +++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx @@ -66,7 +66,6 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => { return hasIngestManager && !hasSeenNotice ? ( <> - { + ) : null; }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap index d9dd6ec4a0be5..8f59fdd7df000 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap @@ -141,7 +141,7 @@ exports[`should render entity by expression with aggregatable field options for value="FlightNum" >