From 54b8d288b36de44a97663a7388b1cecbf1e5f156 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 2 Jun 2021 08:33:23 -0400 Subject: [PATCH 1/5] [Exploratory view] Core web vitals (#100320) (#101144) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad --- x-pack/plugins/lens/public/index.ts | 1 + .../configurations/constants/constants.ts | 7 + .../constants/elasticsearch_fieldnames.ts | 1 + .../configurations/constants/labels.ts | 8 + .../configurations/default_configs.ts | 3 + .../configurations/lens_attributes.test.ts | 2 +- .../configurations/lens_attributes.ts | 80 ++++++--- .../rum/core_web_vitals_config.ts | 164 ++++++++++++++++++ .../configurations/rum/kpi_trends_config.ts | 25 +-- .../rum/performance_dist_config.ts | 19 +- .../hooks/use_lens_attributes.ts | 3 +- .../series_builder/columns/chart_types.tsx | 14 +- .../columns/report_breakdowns.tsx | 8 +- .../columns/report_definition_col.tsx | 18 +- .../series_builder/custom_report_field.tsx | 8 +- .../series_builder/series_builder.tsx | 1 + .../series_editor/chart_edit_options.tsx | 2 +- .../series_editor/columns/breakdowns.test.tsx | 16 +- .../series_editor/columns/breakdowns.tsx | 33 ++-- .../shared/exploratory_view/types.ts | 15 +- 20 files changed, 354 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index fcfed9b9f1fc5..0fdd3bf426232 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -20,6 +20,7 @@ export type { ValueLabelConfig, YAxisMode, XYCurveType, + YConfig, } from './xy_visualization/types'; export type { DataType, OperationMetadata } from './types'; export type { 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 220ae197a15bb..e1142a071aab5 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 @@ -12,6 +12,7 @@ import { BROWSER_FAMILY_LABEL, BROWSER_VERSION_LABEL, CLS_LABEL, + CORE_WEB_VITALS_LABEL, CPU_USAGE_LABEL, DEVICE_LABEL, ENVIRONMENT_LABEL, @@ -92,6 +93,7 @@ export const DataViewLabels: Record = { logs: LOGS_FREQUENCY_LABEL, mem: MEMORY_USAGE_LABEL, nwk: NETWORK_ACTIVITY_LABEL, + cwv: CORE_WEB_VITALS_LABEL, }; export const ReportToDataTypeMap: Record = { @@ -105,4 +107,9 @@ export const ReportToDataTypeMap: Record = { mem: 'infra_metrics', logs: 'infra_logs', cpu: 'infra_metrics', + cwv: 'ux', }; + +export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; +export const FILTER_RECORDS = 'FILTER_RECORDS'; +export const OPERATION_COLUMN = 'operation'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts index 3faf54fff3140..5ecc5b758de84 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts @@ -119,6 +119,7 @@ export const TRANSACTION_URL = 'url.full'; export const CLIENT_GEO = 'client.geo'; export const USER_AGENT_DEVICE = 'user_agent.device.name'; export const USER_AGENT_OS = 'user_agent.os.name'; +export const USER_AGENT_OS_VERSION = 'user_agent.os.version'; export const TRANSACTION_TIME_TO_FIRST_BYTE = 'transaction.marks.agent.timeToFirstByte'; export const TRANSACTION_DOM_INTERACTIVE = 'transaction.marks.agent.domInteractive'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index ba820a25f868a..92150a76319f8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -200,6 +200,14 @@ export const NETWORK_ACTIVITY_LABEL = i18n.translate( defaultMessage: 'Network activity', } ); + +export const CORE_WEB_VITALS_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.coreWebVitals', + { + defaultMessage: 'Core web vitals', + } +); + export const MEMORY_USAGE_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.memoryUsage', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index c6089b2316784..797ee0c2e0977 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -17,6 +17,7 @@ import { getMemoryUsageLensConfig } from './metrics/memory_usage_config'; import { getNetworkActivityLensConfig } from './metrics/network_activity_config'; import { getLogsFrequencyLensConfig } from './logs/logs_frequency_config'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; +import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; interface Props { reportType: keyof typeof ReportViewTypes; @@ -30,6 +31,8 @@ export const getDefaultConfigs = ({ reportType, seriesId, indexPattern }: Props) return getPerformanceDistLensConfig({ seriesId, indexPattern }); case 'kpi-trends': return getKPITrendsLensConfig({ seriesId, indexPattern }); + case 'core-web-vitals': + return getCoreWebVitalsConfig({ seriesId, indexPattern }); case 'uptime-duration': return getMonitorDurationConfig({ seriesId, indexPattern }); case 'uptime-pings': 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 a5fdd4971a86f..6976b55921b09 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 @@ -342,7 +342,7 @@ describe('Lens Attribute', () => { orderBy: { columnId: 'y-axis-column', type: 'column' }, orderDirection: 'desc', otherBucket: true, - size: 3, + size: 10, }, scale: 'ordinal', sourceField: 'user_agent.name', 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 3e22c4da6115a..89fc9ca5fcc58 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 @@ -24,14 +24,15 @@ import { OperationMetadata, FieldBasedIndexPatternColumn, SumIndexPatternColumn, + TermsIndexPatternColumn, } from '../../../../../../lens/public'; import { buildPhraseFilter, buildPhrasesFilter, IndexPattern, } from '../../../../../../../../src/plugins/data/common'; -import { FieldLabels } from './constants'; -import { DataSeries, UrlFilter, URLReportDefinition } from '../types'; +import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN } from './constants'; +import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types'; function getLayerReferenceName(layerId: string) { return `indexpattern-datasource-layer-${layerId}`; @@ -53,6 +54,7 @@ export const parseCustomFieldName = ( ) => { let fieldName = sourceField; let columnType; + let columnFilters; const rdf = reportViewConfig.reportDefinitions ?? []; @@ -61,17 +63,21 @@ export const parseCustomFieldName = ( if (customField) { if (selectedDefinitions[fieldName]) { fieldName = selectedDefinitions[fieldName][0]; - if (customField?.options) - columnType = customField?.options?.find(({ field }) => field === fieldName)?.columnType; - } else if (customField.defaultValue) { - fieldName = customField.defaultValue; - } else if (customField.options?.[0].field) { - fieldName = customField.options?.[0].field; + if (customField?.options) { + const currField = customField?.options?.find( + ({ field, id }) => field === fieldName || id === fieldName + ); + columnType = currField?.columnType; + columnFilters = currField?.columnFilters; + } + } 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; } } - return { fieldName, columnType }; + return { fieldName, columnType, columnFilters }; }; export class LensAttributes { @@ -82,6 +88,7 @@ export class LensAttributes { seriesType: SeriesType; reportViewConfig: DataSeries; reportDefinitions: URLReportDefinition; + breakdownSource?: string; constructor( indexPattern: IndexPattern, @@ -89,12 +96,14 @@ export class LensAttributes { seriesType?: SeriesType, filters?: UrlFilter[], operationType?: OperationType, - reportDefinitions?: URLReportDefinition + reportDefinitions?: URLReportDefinition, + breakdownSource?: string ) { this.indexPattern = indexPattern; this.layers = {}; this.filters = filters ?? []; this.reportDefinitions = reportDefinitions ?? {}; + this.breakdownSource = breakdownSource; if (operationType) { reportViewConfig.yAxisColumns.forEach((yAxisColumn) => { @@ -109,10 +118,10 @@ export class LensAttributes { this.visualization = this.getXyState(); } - addBreakdown(sourceField: string) { + getBreakdownColumn(sourceField: string): TermsIndexPatternColumn { const fieldMeta = this.indexPattern.getFieldByName(sourceField); - this.layers.layer1.columns['break-down-column'] = { + return { sourceField, label: `Top values of ${FieldLabels[sourceField]}`, dataType: fieldMeta?.type as DataType, @@ -120,13 +129,22 @@ export class LensAttributes { scale: 'ordinal', isBucketed: true, params: { - size: 3, + size: 10, orderBy: { type: 'column', columnId: 'y-axis-column' }, orderDirection: 'desc', otherBucket: true, missingBucket: false, }, }; + } + + addBreakdown(sourceField: string) { + const { xAxisColumn } = this.reportViewConfig; + if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { + // do nothing since this will be used a x axis source + return; + } + this.layers.layer1.columns['break-down-column'] = this.getBreakdownColumn(sourceField); this.layers.layer1.columnOrder = [ 'x-axis-column', @@ -229,15 +247,27 @@ export class LensAttributes { getXAxis() { const { xAxisColumn } = this.reportViewConfig; + if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { + return this.getBreakdownColumn(this.breakdownSource || this.reportViewConfig.breakdowns[0]); + } + return this.getColumnBasedOnType(xAxisColumn.sourceField!, undefined, xAxisColumn.label); } - getColumnBasedOnType(sourceField: string, operationType?: OperationType, label?: string) { - const { fieldMeta, columnType, fieldName } = this.getFieldMeta(sourceField); + getColumnBasedOnType( + sourceField: string, + operationType?: OperationType, + label?: string, + colIndex?: number + ) { + const { fieldMeta, columnType, fieldName, columnFilters } = this.getFieldMeta(sourceField); const { type: fieldType } = fieldMeta ?? {}; - if (fieldName === 'Records') { - return this.getRecordsColumn(); + if (fieldName === 'Records' || columnType === FILTER_RECORDS) { + return this.getRecordsColumn( + label, + colIndex !== undefined ? columnFilters?.[colIndex] : undefined + ); } if (fieldType === 'date') { @@ -256,11 +286,11 @@ export class LensAttributes { } getFieldMeta(sourceField: string) { - const { fieldName, columnType } = this.getCustomFieldName(sourceField); + const { fieldName, columnType, columnFilters } = this.getCustomFieldName(sourceField); const fieldMeta = this.indexPattern.getFieldByName(fieldName); - return { fieldMeta, fieldName, columnType }; + return { fieldMeta, fieldName, columnType, columnFilters }; } getMainYAxis() { @@ -270,7 +300,7 @@ export class LensAttributes { return this.getRecordsColumn(label); } - return this.getColumnBasedOnType(sourceField!, operationType, label); + return this.getColumnBasedOnType(sourceField!, operationType, label, 0); } getChildYAxises() { @@ -286,13 +316,14 @@ export class LensAttributes { lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType( sourceField!, operationType, - label + label, + i ); } return lensColumns; } - getRecordsColumn(label?: string): CountIndexPatternColumn { + getRecordsColumn(label?: string, columnFilter?: ColumnFilter): CountIndexPatternColumn { return { dataType: 'number', isBucketed: false, @@ -300,6 +331,7 @@ export class LensAttributes { operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: columnFilter, } as CountIndexPatternColumn; } @@ -331,7 +363,9 @@ export class LensAttributes { layerId: 'layer1', seriesType: this.seriesType ?? 'line', palette: this.reportViewConfig.palette, - yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], + yConfig: this.reportViewConfig.yConfig || [ + { forAccessor: 'y-axis-column', color: 'green' }, + ], xAccessor: 'x-axis-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 new file mode 100644 index 0000000000000..de9ea12be20cf --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiPaletteForStatus } from '@elastic/eui'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { buildPhraseFilter } from '../utils'; +import { + CLIENT_GEO_COUNTRY_NAME, + CLS_FIELD, + FID_FIELD, + LCP_FIELD, + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_TYPE, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, + USER_AGENT_VERSION, + TRANSACTION_URL, + USER_AGENT_OS_VERSION, + URL_FULL, + SERVICE_ENVIRONMENT, +} from '../constants/elasticsearch_fieldnames'; + +export function getCoreWebVitalsConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { + const statusPallete = euiPaletteForStatus(3); + + return { + id: seriesId, + defaultSeriesType: 'bar_horizontal_percentage_stacked', + reportType: 'kpi-trends', + seriesTypes: ['bar_horizontal_percentage_stacked'], + xAxisColumn: { + sourceField: USE_BREAK_DOWN_COLUMN, + }, + yAxisColumns: [ + { + sourceField: 'core.web.vitals', + label: 'Good', + }, + { + sourceField: 'core.web.vitals', + label: 'Average', + }, + { + sourceField: 'core.web.vitals', + label: 'Poor', + }, + ], + hasOperationType: false, + defaultFilters: [ + { + field: TRANSACTION_URL, + isNegated: false, + }, + SERVICE_NAME, + { + field: USER_AGENT_OS, + nested: USER_AGENT_OS_VERSION, + }, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + { + field: USER_AGENT_NAME, + nested: USER_AGENT_VERSION, + }, + ], + breakdowns: [ + SERVICE_NAME, + USER_AGENT_NAME, + USER_AGENT_OS, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + URL_FULL, + ], + filters: [ + ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), + ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), + ], + labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application' }, + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + { + field: SERVICE_ENVIRONMENT, + }, + { + field: 'core.web.vitals', + custom: true, + options: [ + { + 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`, + }, + ], + }, + { + 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`, + }, + ], + }, + { + 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`, + }, + ], + }, + ], + }, + ], + yConfig: [ + { color: statusPallete[0], forAccessor: 'y-axis-column' }, + { color: statusPallete[1], forAccessor: 'y-axis-column-1' }, + { color: statusPallete[2], forAccessor: 'y-axis-column-2' }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts index 029fe5534965e..5e2d3440526b5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, RECORDS_FIELD } from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -85,20 +85,25 @@ export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): { field: 'business.kpi', custom: true, - defaultValue: RECORDS_FIELD, options: [ - { field: RECORDS_FIELD, label: PAGE_VIEWS_LABEL }, - { label: PAGE_LOAD_TIME_LABEL, field: TRANSACTION_DURATION, columnType: 'operation' }, + { 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, - columnType: 'operation', + id: TRANSACTION_TIME_TO_FIRST_BYTE, + columnType: OPERATION_COLUMN, }, - { label: FCP_LABEL, field: FCP_FIELD, columnType: 'operation' }, - { label: TBT_LABEL, field: TBT_FIELD, columnType: 'operation' }, - { label: LCP_LABEL, field: LCP_FIELD, columnType: 'operation' }, - { label: FID_LABEL, field: FID_FIELD, columnType: 'operation' }, - { label: CLS_LABEL, field: CLS_FIELD, columnType: 'operation' }, + { 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/rum/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts index af8bd00a69553..0dc582c5683dd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts @@ -80,15 +80,18 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP { field: 'performance.metric', custom: true, - defaultValue: TRANSACTION_DURATION, options: [ - { label: PAGE_LOAD_TIME_LABEL, field: TRANSACTION_DURATION }, - { label: BACKEND_TIME_LABEL, field: TRANSACTION_TIME_TO_FIRST_BYTE }, - { label: FCP_LABEL, field: FCP_FIELD }, - { label: TBT_LABEL, field: TBT_FIELD }, - { label: LCP_LABEL, field: LCP_FIELD }, - { label: FID_LABEL, field: FID_FIELD }, - { label: CLS_LABEL, field: CLS_FIELD }, + { 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 }, ], }, ], 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 dc6b4bd0ec879..ea6f435460401 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 @@ -67,7 +67,8 @@ export const useLensAttributes = ({ seriesType, filters, operationType, - reportDefinitions + reportDefinitions, + breakdown ); if (breakdown) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx index 3943ae3710209..a296d2520db34 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -20,9 +20,11 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes. export function SeriesChartTypesSelect({ seriesId, + seriesTypes, defaultChartType, }: { seriesId: string; + seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { const { series, setSeries, allSeries } = useUrlStorage(seriesId); @@ -42,8 +44,18 @@ export function SeriesChartTypesSelect({ onChange={onChange} value={seriesType} excludeChartTypes={['bar_percentage_stacked']} + includeChartTypes={ + seriesTypes || [ + 'bar', + 'bar_horizontal', + 'line', + 'area', + 'bar_stacked', + 'area_stacked', + 'bar_horizontal_percentage_stacked', + ] + } label={CHART_TYPE_LABEL} - includeChartTypes={['bar', 'bar_horizontal', 'line', 'area', 'bar_stacked', 'area_stacked']} /> ); } 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 162892071dbff..e95cd894df5f2 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 @@ -16,5 +16,11 @@ export function ReportBreakdowns({ dataViewSeries: DataSeries; seriesId: string; }) { - return ; + return ( + + ); } 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 717309e064ba3..ff8b0f7aa578b 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 @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import styled from 'styled-components'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useUrlStorage } from '../../hooks/use_url_storage'; @@ -66,8 +66,9 @@ export function ReportDefinitionCol({ + {indexPattern && - reportDefinitions.map(({ field, custom, options, defaultValue }) => ( + reportDefinitions.map(({ field, custom, options }) => ( {!custom ? ( ) : ( - + )} ))} @@ -95,7 +91,11 @@ export function ReportDefinitionCol({ )} - + ); 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/custom_report_field.tsx index 6b74ad45b2c07..b41f3a603e5da 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/custom_report_field.tsx @@ -17,7 +17,7 @@ interface Props { options: ReportDefinition['options']; } -export function CustomReportField({ field, seriesId, options: opts, defaultValue }: Props) { +export function CustomReportField({ field, seriesId, options: opts }: Props) { const { series, setSeries } = useUrlStorage(seriesId); const { reportDefinitions: rtd = {} } = series; @@ -35,11 +35,11 @@ export function CustomReportField({ field, seriesId, options: opts, defaultValue fullWidth compressed prepend={'Metric'} - options={options.map(({ label, field: fd }) => ({ - value: fd, + options={options.map(({ label, field: fd, id }) => ({ + value: fd || id, inputDisplay: label, }))} - valueOfSelected={reportDefinitions?.[field]?.[0] || defaultValue || options?.[0].field} + valueOfSelected={reportDefinitions?.[field]?.[0] || 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 5e270524b1880..1944bb281598b 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 @@ -27,6 +27,7 @@ export const ReportTypes: Record - + 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 eff62a60509e3..9d26ec79c31ad 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,7 +23,13 @@ describe('Breakdowns', function () { it('should render properly', async function () { mockUrlStorage({}); - render(); + render( + + ); screen.getAllByText('Browser family'); }); @@ -31,7 +37,13 @@ describe('Breakdowns', function () { it('should call set series on change', function () { const { setSeries } = mockUrlStorage({ breakdown: USER_AGENT_OS }); - render(); + render( + + ); screen.getAllByText('Operating system'); 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 5561779daa8c4..5cf6ac47aa8c7 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 @@ -8,15 +8,17 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldLabels } from '../../configurations/constants'; +import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; import { useUrlStorage } from '../../hooks/use_url_storage'; +import { DataSeries } from '../../types'; interface Props { seriesId: string; breakdowns: string[]; + reportViewConfig: DataSeries; } -export function Breakdowns({ seriesId, breakdowns = [] }: Props) { +export function Breakdowns({ reportViewConfig, seriesId, breakdowns = [] }: Props) { const { setSeries, series } = useUrlStorage(seriesId); const selectedBreakdown = series.breakdown; @@ -36,13 +38,21 @@ export function Breakdowns({ seriesId, breakdowns = [] }: Props) { } }; - const items = breakdowns.map((breakdown) => ({ id: breakdown, label: FieldLabels[breakdown] })); - items.push({ - id: NO_BREAKDOWN, - label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { - defaultMessage: 'No breakdown', - }), - }); + const hasUseBreakdownColumn = reportViewConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; + + const items = breakdowns.map((breakdown) => ({ + id: breakdown, + label: reportViewConfig.labels[breakdown], + })); + + if (!hasUseBreakdownColumn) { + items.push({ + id: NO_BREAKDOWN, + label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { + defaultMessage: 'No breakdown', + }), + }); + } const options = items.map(({ id, label }) => ({ inputDisplay: id === NO_BREAKDOWN ? label : {label}, @@ -50,13 +60,16 @@ export function Breakdowns({ seriesId, breakdowns = [] }: Props) { dropdownDisplay: label, })); + const valueOfSelected = + selectedBreakdown || (hasUseBreakdownColumn ? options[0].value : NO_BREAKDOWN); + return (
onOptionChange(value)} data-test-subj={'seriesBreakdown'} /> 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 7b9278352925e..c3dd824e441a8 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 @@ -12,6 +12,7 @@ import { FieldBasedIndexPatternColumn, SeriesType, OperationType, + YConfig, } from '../../../../../lens/public'; import { PersistableFilter } from '../../../../../lens/common'; @@ -21,6 +22,7 @@ import { ExistsFilter } from '../../../../../../../src/plugins/data/common/es_qu export const ReportViewTypes = { pld: 'page-load-dist', kpi: 'kpi-trends', + cwv: 'core-web-vitals', upd: 'uptime-duration', upp: 'uptime-pings', svl: 'service-latency', @@ -37,16 +39,22 @@ export type ReportViewTypeId = keyof typeof ReportViewTypes; export type ReportViewType = ValueOf; +export interface ColumnFilter { + language: 'kuery'; + query: string; +} + export interface ReportDefinition { field: string; required?: boolean; custom?: boolean; - defaultValue?: string; options?: Array<{ - field: string; + id: string; + field?: string; label: string; description?: string; - columnType?: 'range' | 'operation'; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS'; + columnFilters?: ColumnFilter[]; }>; } @@ -66,6 +74,7 @@ export interface DataSeries { hasOperationType: boolean; palette?: PaletteOutput; yTitle?: string; + yConfig?: YConfig[]; } export type URLReportDefinition = Record; From 137a720b979d9c7aea239ab77b7fd61af74a3a2c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 2 Jun 2021 10:19:52 -0400 Subject: [PATCH 2/5] [Maps] spatially filter by all geo fields (#100735) (#101153) * [Maps] spatial filter by all geo fields * replace geoFields with geoFieldNames * update mapSpatialFilter to be able to reconize multi field filters * add check for geoFieldNames * i18n fixes and fix GeometryFilterForm jest test * tslint * tslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nathan Reese --- .../lib/mappers/map_spatial_filter.test.ts | 55 ++ .../lib/mappers/map_spatial_filter.ts | 13 + .../common/descriptor_types/map_descriptor.ts | 5 +- .../elasticsearch_geo_utils.test.js | 218 ------- .../elasticsearch_geo_utils.ts | 218 +------ .../maps/common/elasticsearch_util/index.ts | 2 + .../spatial_filter_utils.test.ts | 534 ++++++++++++++++++ .../spatial_filter_utils.ts | 221 ++++++++ .../maps/common/elasticsearch_util/types.ts | 54 ++ .../geometry_filter_form.test.js.snap | 226 +------- .../components/distance_filter_form.tsx | 34 +- .../public/components/geo_field_with_index.ts | 19 - .../public/components/geometry_filter_form.js | 36 +- .../components/geometry_filter_form.test.js | 68 +-- .../multi_index_geo_field_select.tsx | 79 --- .../map_container/map_container.tsx | 48 +- .../draw_filter_control.tsx | 14 +- .../draw_control/draw_filter_control/index.ts | 7 +- .../connected_components/mb_map/mb_map.tsx | 3 - .../feature_geometry_filter_form.tsx | 11 +- .../mb_map/tooltip_control/index.ts | 2 + .../tooltip_control/tooltip_control.test.tsx | 2 +- .../tooltip_control/tooltip_control.tsx | 37 +- .../toolbar_overlay.test.tsx.snap | 13 +- .../toolbar_overlay/index.ts | 14 +- .../toolbar_overlay/toolbar_overlay.test.tsx | 14 +- .../toolbar_overlay/toolbar_overlay.tsx | 6 +- .../__snapshots__/tools_control.test.tsx.snap | 60 -- .../tools_control/tools_control.tsx | 15 +- .../maps/public/embeddable/map_embeddable.tsx | 1 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 32 files changed, 942 insertions(+), 1091 deletions(-) create mode 100644 x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts create mode 100644 x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts create mode 100644 x-pack/plugins/maps/common/elasticsearch_util/types.ts delete mode 100644 x-pack/plugins/maps/public/components/geo_field_with_index.ts delete mode 100644 x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts index 479cbb140fc70..0ae1513ae5d1b 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts @@ -54,6 +54,61 @@ describe('mapSpatialFilter()', () => { expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER); }); + test('should return the key for matching multi field filter', async () => { + const filter = { + meta: { + alias: 'my spatial filter', + isMultiIndex: true, + type: FILTERS.SPATIAL_FILTER, + } as FilterMeta, + query: { + bool: { + should: [ + { + bool: { + must: [ + { + exists: { + field: 'geo.coordinates', + }, + }, + { + geo_distance: { + distance: '1000km', + 'geo.coordinates': [120, 30], + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + exists: { + field: 'location', + }, + }, + { + geo_distance: { + distance: '1000km', + location: [120, 30], + }, + }, + ], + }, + }, + ], + }, + }, + } as Filter; + const result = mapSpatialFilter(filter); + + expect(result).toHaveProperty('key', 'query'); + expect(result).toHaveProperty('value', ''); + expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER); + }); + test('should return undefined for none matching', async (done) => { const filter = { meta: { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts index 0703bc055a39b..229257c1a7d81 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts @@ -22,5 +22,18 @@ export const mapSpatialFilter = (filter: Filter) => { value: '', }; } + + if ( + filter.meta && + filter.meta.type === FILTERS.SPATIAL_FILTER && + filter.meta.isMultiIndex && + filter.query?.bool?.should + ) { + return { + key: 'query', + type: filter.meta.type, + value: '', + }; + } throw filter; }; diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index 102434ffda161..4092ba49888bc 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -11,7 +11,7 @@ import { ReactNode } from 'react'; import { GeoJsonProperties } from 'geojson'; import { Geometry } from 'geojson'; import { Query } from '../../../../../src/plugins/data/common'; -import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; +import { DRAW_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; export type MapExtent = { minLon: number; @@ -70,9 +70,6 @@ export type DrawState = { actionId: string; drawType: DRAW_TYPE; filterLabel?: string; // point radius filter alias - geoFieldName?: string; - geoFieldType?: ES_GEO_FIELD_TYPE; geometryLabel?: string; - indexPatternId?: string; relation?: ES_SPATIAL_RELATIONS; }; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index c2ca952c3e8c9..e6b27e78fd36b 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -9,9 +9,7 @@ import { hitsToGeoJson, geoPointToGeometry, geoShapeToGeometry, - createExtentFilter, roundCoordinates, - extractFeaturesFromFilters, makeESBbox, scaleBounds, } from './elasticsearch_geo_utils'; @@ -388,94 +386,6 @@ describe('geoShapeToGeometry', () => { }); }); -describe('createExtentFilter', () => { - it('should return elasticsearch geo_bounding_box filter', () => { - const mapExtent = { - maxLat: 39, - maxLon: -83, - minLat: 35, - minLon: -89, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [-89, 39], - bottom_right: [-83, 35], - }, - }); - }); - - it('should clamp longitudes to -180 to 180 and latitudes to -90 to 90', () => { - const mapExtent = { - maxLat: 120, - maxLon: 200, - minLat: -100, - minLon: -190, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [-180, 89], - bottom_right: [180, -89], - }, - }); - }); - - it('should make left longitude greater than right longitude when area crosses 180 meridian east to west', () => { - const mapExtent = { - maxLat: 39, - maxLon: 200, - minLat: 35, - minLon: 100, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - const leftLon = filter.geo_bounding_box.location.top_left[0]; - const rightLon = filter.geo_bounding_box.location.bottom_right[0]; - expect(leftLon).toBeGreaterThan(rightLon); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [100, 39], - bottom_right: [-160, 35], - }, - }); - }); - - it('should make left longitude greater than right longitude when area crosses 180 meridian west to east', () => { - const mapExtent = { - maxLat: 39, - maxLon: -100, - minLat: 35, - minLon: -200, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - const leftLon = filter.geo_bounding_box.location.top_left[0]; - const rightLon = filter.geo_bounding_box.location.bottom_right[0]; - expect(leftLon).toBeGreaterThan(rightLon); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [160, 39], - bottom_right: [-100, 35], - }, - }); - }); - - it('should clamp longitudes to -180 to 180 when longitude wraps globe', () => { - const mapExtent = { - maxLat: 39, - maxLon: 209, - minLat: 35, - minLon: -191, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [-180, 39], - bottom_right: [180, 35], - }, - }); - }); -}); - describe('roundCoordinates', () => { it('should set coordinates precision', () => { const coordinates = [ @@ -492,134 +402,6 @@ describe('roundCoordinates', () => { }); }); -describe('extractFeaturesFromFilters', () => { - it('should ignore non-spatial filers', () => { - const phraseFilter = { - meta: { - alias: null, - disabled: false, - index: '90943e30-9a47-11e8-b64d-95841ca0b247', - key: 'machine.os', - negate: false, - params: { - query: 'ios', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'machine.os': 'ios', - }, - }, - }; - expect(extractFeaturesFromFilters([phraseFilter])).toEqual([]); - }); - - it('should convert geo_distance filter to feature', () => { - const spatialFilter = { - geo_distance: { - distance: '1096km', - 'geo.coordinates': [-89.87125, 53.49454], - }, - meta: { - alias: 'geo.coordinates within 1096km of -89.87125,53.49454', - disabled: false, - index: '90943e30-9a47-11e8-b64d-95841ca0b247', - key: 'geo.coordinates', - negate: false, - type: 'spatial_filter', - value: '', - }, - }; - - const features = extractFeaturesFromFilters([spatialFilter]); - expect(features[0].geometry.coordinates[0][0]).toEqual([-89.87125, 63.35109118642093]); - expect(features[0].properties).toEqual({ - filter: 'geo.coordinates within 1096km of -89.87125,53.49454', - }); - }); - - it('should convert geo_shape filter to feature', () => { - const spatialFilter = { - geo_shape: { - 'geo.coordinates': { - relation: 'INTERSECTS', - shape: { - coordinates: [ - [ - [-101.21639, 48.1413], - [-101.21639, 41.84905], - [-90.95149, 41.84905], - [-90.95149, 48.1413], - [-101.21639, 48.1413], - ], - ], - type: 'Polygon', - }, - }, - ignore_unmapped: true, - }, - meta: { - alias: 'geo.coordinates in bounds', - disabled: false, - index: '90943e30-9a47-11e8-b64d-95841ca0b247', - key: 'geo.coordinates', - negate: false, - type: 'spatial_filter', - value: '', - }, - }; - - expect(extractFeaturesFromFilters([spatialFilter])).toEqual([ - { - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [ - [ - [-101.21639, 48.1413], - [-101.21639, 41.84905], - [-90.95149, 41.84905], - [-90.95149, 48.1413], - [-101.21639, 48.1413], - ], - ], - }, - properties: { - filter: 'geo.coordinates in bounds', - }, - }, - ]); - }); - - it('should ignore geo_shape filter with pre-index shape', () => { - const spatialFilter = { - geo_shape: { - 'geo.coordinates': { - indexed_shape: { - id: 's5gldXEBkTB2HMwpC8y0', - index: 'world_countries_v1', - path: 'coordinates', - }, - relation: 'INTERSECTS', - }, - ignore_unmapped: true, - }, - meta: { - alias: 'geo.coordinates in multipolygon', - disabled: false, - index: '90943e30-9a47-11e8-b64d-95841ca0b247', - key: 'geo.coordinates', - negate: false, - type: 'spatial_filter', - value: '', - }, - }; - - expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]); - }); -}); - describe('makeESBbox', () => { it('Should invert Y-axis', () => { const bbox = makeESBbox({ diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts index e47afce77f779..8033f8d187fd5 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts @@ -16,61 +16,13 @@ import { BBox } from '@turf/helpers'; import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, - ES_SPATIAL_RELATIONS, GEO_JSON_TYPE, POLYGON_COORDINATES_EXTERIOR_INDEX, LON_INDEX, LAT_INDEX, } from '../constants'; -import { getEsSpatialRelationLabel } from '../i18n_getters'; -import { Filter, FilterMeta, FILTERS } from '../../../../../src/plugins/data/common'; import { MapExtent } from '../descriptor_types'; - -const SPATIAL_FILTER_TYPE = FILTERS.SPATIAL_FILTER; - -type Coordinates = Position | Position[] | Position[][] | Position[][][]; - -// Elasticsearch stores more then just GeoJSON. -// 1) geometry.type as lower case string -// 2) circle and envelope types -interface ESGeometry { - type: string; - coordinates: Coordinates; -} - -export interface ESBBox { - top_left: number[]; - bottom_right: number[]; -} - -interface GeoShapeQueryBody { - shape?: Polygon; - relation?: ES_SPATIAL_RELATIONS; - indexed_shape?: PreIndexedShape; -} - -// Index signature explicitly states that anything stored in an object using a string conforms to the structure -// problem is that Elasticsearch signature also allows for other string keys to conform to other structures, like 'ignore_unmapped' -// Use intersection type to exclude certain properties from the index signature -// https://basarat.gitbook.io/typescript/type-system/index-signatures#excluding-certain-properties-from-the-index-signature -type GeoShapeQuery = { ignore_unmapped: boolean } & { [geoFieldName: string]: GeoShapeQueryBody }; - -export type GeoFilter = Filter & { - geo_bounding_box?: { - [geoFieldName: string]: ESBBox; - }; - geo_distance?: { - distance: string; - [geoFieldName: string]: Position | { lat: number; lon: number } | string; - }; - geo_shape?: GeoShapeQuery; -}; - -export interface PreIndexedShape { - index: string; - id: string | number; - path: string; -} +import { Coordinates, ESBBox, ESGeometry } from './types'; function ensureGeoField(type: string) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; @@ -349,136 +301,6 @@ export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBo return esBbox; } -export function createExtentFilter(mapExtent: MapExtent, geoFieldNames: string[]): GeoFilter { - const esBbox = makeESBbox(mapExtent); - return geoFieldNames.length === 1 - ? { - geo_bounding_box: { - [geoFieldNames[0]]: esBbox, - }, - meta: { - alias: null, - disabled: false, - negate: false, - key: geoFieldNames[0], - }, - } - : { - query: { - bool: { - should: geoFieldNames.map((geoFieldName) => { - return { - bool: { - must: [ - { - exists: { - field: geoFieldName, - }, - }, - { - geo_bounding_box: { - [geoFieldName]: esBbox, - }, - }, - ], - }, - }; - }), - }, - }, - meta: { - alias: null, - disabled: false, - negate: false, - }, - }; -} - -export function createSpatialFilterWithGeometry({ - preIndexedShape, - geometry, - geometryLabel, - indexPatternId, - geoFieldName, - relation = ES_SPATIAL_RELATIONS.INTERSECTS, -}: { - preIndexedShape?: PreIndexedShape | null; - geometry: Polygon; - geometryLabel: string; - indexPatternId: string; - geoFieldName: string; - relation: ES_SPATIAL_RELATIONS; -}): GeoFilter { - const meta: FilterMeta = { - type: SPATIAL_FILTER_TYPE, - negate: false, - index: indexPatternId, - key: geoFieldName, - alias: `${geoFieldName} ${getEsSpatialRelationLabel(relation)} ${geometryLabel}`, - disabled: false, - }; - - const shapeQuery: GeoShapeQueryBody = { - relation, - }; - if (preIndexedShape) { - shapeQuery.indexed_shape = preIndexedShape; - } else { - shapeQuery.shape = geometry; - } - - return { - meta, - // Currently no way to create an object with exclude property from index signature - // typescript error for "ignore_unmapped is not assignable to type 'GeoShapeQueryBody'" expected" - // @ts-expect-error - geo_shape: { - ignore_unmapped: true, - [geoFieldName]: shapeQuery, - }, - }; -} - -export function createDistanceFilterWithMeta({ - alias, - distanceKm, - geoFieldName, - indexPatternId, - point, -}: { - alias: string; - distanceKm: number; - geoFieldName: string; - indexPatternId: string; - point: Position; -}): GeoFilter { - const meta: FilterMeta = { - type: SPATIAL_FILTER_TYPE, - negate: false, - index: indexPatternId, - key: geoFieldName, - alias: alias - ? alias - : i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', { - defaultMessage: '{geoFieldName} within {distanceKm}km of {pointLabel}', - values: { - distanceKm, - geoFieldName, - pointLabel: point.join(', '), - }, - }), - disabled: false, - }; - - return { - geo_distance: { - distance: `${distanceKm}km`, - [geoFieldName]: point, - }, - meta, - }; -} - export function roundCoordinates(coordinates: Coordinates): void { for (let i = 0; i < coordinates.length; i++) { const value = coordinates[i]; @@ -549,44 +371,6 @@ export function clamp(val: number, min: number, max: number): number { } } -export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] { - const features: Feature[] = []; - filters - .filter((filter) => { - return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE; - }) - .forEach((filter) => { - const geoFieldName = filter.meta.key!; - let geometry; - if (filter.geo_distance && filter.geo_distance[geoFieldName]) { - const distanceSplit = filter.geo_distance.distance.split('km'); - const distance = parseFloat(distanceSplit[0]); - const circleFeature = turfCircle(filter.geo_distance[geoFieldName], distance); - geometry = circleFeature.geometry; - } else if ( - filter.geo_shape && - filter.geo_shape[geoFieldName] && - filter.geo_shape[geoFieldName].shape - ) { - geometry = filter.geo_shape[geoFieldName].shape; - } else { - // do not know how to convert spatial filter to geometry - // this includes pre-indexed shapes - return; - } - - features.push({ - type: 'Feature', - geometry, - properties: { - filter: filter.meta.alias, - }, - }); - }); - - return features; -} - export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent { const width = bounds.maxLon - bounds.minLon; const height = bounds.maxLat - bounds.minLat; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/index.ts b/x-pack/plugins/maps/common/elasticsearch_util/index.ts index 24dd56b217401..7073a4201f7a5 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/index.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/index.ts @@ -8,4 +8,6 @@ export * from './es_agg_utils'; export * from './convert_to_geojson'; export * from './elasticsearch_geo_utils'; +export * from './spatial_filter_utils'; +export * from './types'; export { isTotalHitsGreaterThan, TotalHits } from './total_hits'; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts new file mode 100644 index 0000000000000..d828aca4a1a00 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts @@ -0,0 +1,534 @@ +/* + * 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 { Polygon } from 'geojson'; +import { + createDistanceFilterWithMeta, + createExtentFilter, + createSpatialFilterWithGeometry, + extractFeaturesFromFilters, +} from './spatial_filter_utils'; + +const geoFieldName = 'location'; + +describe('createExtentFilter', () => { + it('should return elasticsearch geo_bounding_box filter', () => { + const mapExtent = { + maxLat: 39, + maxLon: -83, + minLat: 35, + minLon: -89, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-89, 39], + bottom_right: [-83, 35], + }, + }); + }); + + it('should clamp longitudes to -180 to 180 and latitudes to -90 to 90', () => { + const mapExtent = { + maxLat: 120, + maxLon: 200, + minLat: -100, + minLon: -190, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-180, 89], + bottom_right: [180, -89], + }, + }); + }); + + it('should make left longitude greater than right longitude when area crosses 180 meridian east to west', () => { + const mapExtent = { + maxLat: 39, + maxLon: 200, + minLat: 35, + minLon: 100, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [100, 39], + bottom_right: [-160, 35], + }, + }); + }); + + it('should make left longitude greater than right longitude when area crosses 180 meridian west to east', () => { + const mapExtent = { + maxLat: 39, + maxLon: -100, + minLat: 35, + minLon: -200, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [160, 39], + bottom_right: [-100, 35], + }, + }); + }); + + it('should clamp longitudes to -180 to 180 when longitude wraps globe', () => { + const mapExtent = { + maxLat: 39, + maxLon: 209, + minLat: 35, + minLon: -191, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-180, 39], + bottom_right: [180, 35], + }, + }); + }); + + it('should support multiple geo fields', () => { + const mapExtent = { + maxLat: 39, + maxLon: -83, + minLat: 35, + minLon: -89, + }; + expect(createExtentFilter(mapExtent, [geoFieldName, 'myOtherLocation'])).toEqual({ + meta: { + alias: null, + disabled: false, + isMultiIndex: true, + negate: false, + }, + query: { + bool: { + should: [ + { + bool: { + must: [ + { + exists: { + field: 'location', + }, + }, + { + geo_bounding_box: { + location: { + top_left: [-89, 39], + bottom_right: [-83, 35], + }, + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + exists: { + field: 'myOtherLocation', + }, + }, + { + geo_bounding_box: { + myOtherLocation: { + top_left: [-89, 39], + bottom_right: [-83, 35], + }, + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); +}); + +describe('createSpatialFilterWithGeometry', () => { + it('should build filter for single field', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + geometry: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates'], + }); + expect(spatialFilter).toEqual({ + meta: { + alias: 'intersects myShape', + disabled: false, + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + }, + geo_shape: { + 'geo.coordinates': { + relation: 'INTERSECTS', + shape: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + }, + ignore_unmapped: true, + }, + }); + }); + + it('should build filter for multiple field', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + geometry: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates', 'location'], + }); + expect(spatialFilter).toEqual({ + meta: { + alias: 'intersects myShape', + disabled: false, + isMultiIndex: true, + key: undefined, + negate: false, + type: 'spatial_filter', + }, + query: { + bool: { + should: [ + { + bool: { + must: [ + { + exists: { + field: 'geo.coordinates', + }, + }, + { + geo_shape: { + 'geo.coordinates': { + relation: 'INTERSECTS', + shape: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + }, + ignore_unmapped: true, + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + exists: { + field: 'location', + }, + }, + { + geo_shape: { + location: { + relation: 'INTERSECTS', + shape: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + }, + ignore_unmapped: true, + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); +}); + +describe('createDistanceFilterWithMeta', () => { + it('should build filter for single field', () => { + const spatialFilter = createDistanceFilterWithMeta({ + point: [120, 30], + distanceKm: 1000, + geoFieldNames: ['geo.coordinates'], + }); + expect(spatialFilter).toEqual({ + meta: { + alias: 'within 1000km of 120, 30', + disabled: false, + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + }, + geo_distance: { + distance: '1000km', + 'geo.coordinates': [120, 30], + }, + }); + }); + + it('should build filter for multiple field', () => { + const spatialFilter = createDistanceFilterWithMeta({ + point: [120, 30], + distanceKm: 1000, + geoFieldNames: ['geo.coordinates', 'location'], + }); + expect(spatialFilter).toEqual({ + meta: { + alias: 'within 1000km of 120, 30', + disabled: false, + isMultiIndex: true, + key: undefined, + negate: false, + type: 'spatial_filter', + }, + query: { + bool: { + should: [ + { + bool: { + must: [ + { + exists: { + field: 'geo.coordinates', + }, + }, + { + geo_distance: { + distance: '1000km', + 'geo.coordinates': [120, 30], + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + exists: { + field: 'location', + }, + }, + { + geo_distance: { + distance: '1000km', + location: [120, 30], + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); +}); + +describe('extractFeaturesFromFilters', () => { + it('should ignore non-spatial filers', () => { + const phraseFilter = { + meta: { + alias: null, + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'machine.os', + negate: false, + params: { + query: 'ios', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'machine.os': 'ios', + }, + }, + }; + expect(extractFeaturesFromFilters([phraseFilter])).toEqual([]); + }); + + it('should convert single field geo_distance filter to feature', () => { + const spatialFilter = createDistanceFilterWithMeta({ + point: [-89.87125, 53.49454], + distanceKm: 1096, + geoFieldNames: ['geo.coordinates', 'location'], + }); + + const features = extractFeaturesFromFilters([spatialFilter]); + expect((features[0].geometry as Polygon).coordinates[0][0]).toEqual([ + -89.87125, + 63.35109118642093, + ]); + expect(features[0].properties).toEqual({ + filter: 'within 1096km of -89.87125, 53.49454', + }); + }); + + it('should convert multi field geo_distance filter to feature', () => { + const spatialFilter = createDistanceFilterWithMeta({ + point: [-89.87125, 53.49454], + distanceKm: 1096, + geoFieldNames: ['geo.coordinates', 'location'], + }); + + const features = extractFeaturesFromFilters([spatialFilter]); + expect((features[0].geometry as Polygon).coordinates[0][0]).toEqual([ + -89.87125, + 63.35109118642093, + ]); + expect(features[0].properties).toEqual({ + filter: 'within 1096km of -89.87125, 53.49454', + }); + }); + + it('should convert single field geo_shape filter to feature', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + geometry: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates'], + }); + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + } as Polygon, + properties: { + filter: 'intersects myShape', + }, + }, + ]); + }); + + it('should convert multi field geo_shape filter to feature', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + geometry: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates', 'location'], + }); + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + } as Polygon, + properties: { + filter: 'intersects myShape', + }, + }, + ]); + }); + + it('should ignore geo_shape filter with pre-index shape', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + preIndexedShape: { + index: 'world_countries_v1', + id: 's5gldXEBkTB2HMwpC8y0', + path: 'coordinates', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates'], + }); + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]); + }); +}); diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts new file mode 100644 index 0000000000000..70df9e9646f50 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts @@ -0,0 +1,221 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Feature, Geometry, Polygon, Position } from 'geojson'; +// @ts-expect-error +import turfCircle from '@turf/circle'; +import { FilterMeta, FILTERS } from '../../../../../src/plugins/data/common'; +import { MapExtent } from '../descriptor_types'; +import { ES_SPATIAL_RELATIONS } from '../constants'; +import { getEsSpatialRelationLabel } from '../i18n_getters'; +import { GeoFilter, GeoShapeQueryBody, PreIndexedShape } from './types'; +import { makeESBbox } from './elasticsearch_geo_utils'; + +const SPATIAL_FILTER_TYPE = FILTERS.SPATIAL_FILTER; + +// wrapper around boiler plate code for creating bool.should clause with nested bool.must clauses +// ensuring geoField exists prior to running geoField query +// This allows for writing a single geo filter that spans multiple indices with different geo fields. +function createMultiGeoFieldFilter( + geoFieldNames: string[], + meta: FilterMeta, + createGeoFilter: (geoFieldName: string) => Omit +): GeoFilter { + if (geoFieldNames.length === 0) { + throw new Error('Unable to create filter, geo fields not provided'); + } + + if (geoFieldNames.length === 1) { + const geoFilter = createGeoFilter(geoFieldNames[0]); + return { + meta: { + ...meta, + key: geoFieldNames[0], + }, + ...geoFilter, + }; + } + + return { + meta: { + ...meta, + key: undefined, + isMultiIndex: true, + }, + query: { + bool: { + should: geoFieldNames.map((geoFieldName) => { + return { + bool: { + must: [ + { + exists: { + field: geoFieldName, + }, + }, + createGeoFilter(geoFieldName), + ], + }, + }; + }), + }, + }, + }; +} + +export function createExtentFilter(mapExtent: MapExtent, geoFieldNames: string[]): GeoFilter { + const esBbox = makeESBbox(mapExtent); + function createGeoFilter(geoFieldName: string) { + return { + geo_bounding_box: { + [geoFieldName]: esBbox, + }, + }; + } + + const meta: FilterMeta = { + alias: null, + disabled: false, + negate: false, + }; + + return createMultiGeoFieldFilter(geoFieldNames, meta, createGeoFilter); +} + +export function createSpatialFilterWithGeometry({ + preIndexedShape, + geometry, + geometryLabel, + geoFieldNames, + relation = ES_SPATIAL_RELATIONS.INTERSECTS, +}: { + preIndexedShape?: PreIndexedShape | null; + geometry?: Polygon; + geometryLabel: string; + geoFieldNames: string[]; + relation?: ES_SPATIAL_RELATIONS; +}): GeoFilter { + const meta: FilterMeta = { + type: SPATIAL_FILTER_TYPE, + negate: false, + key: geoFieldNames.length === 1 ? geoFieldNames[0] : undefined, + alias: `${getEsSpatialRelationLabel(relation)} ${geometryLabel}`, + disabled: false, + }; + + function createGeoFilter(geoFieldName: string) { + const shapeQuery: GeoShapeQueryBody = { + relation, + }; + if (preIndexedShape) { + shapeQuery.indexed_shape = preIndexedShape; + } else if (geometry) { + shapeQuery.shape = geometry; + } else { + throw new Error('Must supply either preIndexedShape or geometry, you did not supply either'); + } + + return { + geo_shape: { + ignore_unmapped: true, + [geoFieldName]: shapeQuery, + }, + }; + } + + // Currently no way to create an object with exclude property from index signature + // typescript error for "ignore_unmapped is not assignable to type 'GeoShapeQueryBody'" expected" + // @ts-expect-error + return createMultiGeoFieldFilter(geoFieldNames, meta, createGeoFilter); +} + +export function createDistanceFilterWithMeta({ + alias, + distanceKm, + geoFieldNames, + point, +}: { + alias?: string; + distanceKm: number; + geoFieldNames: string[]; + point: Position; +}): GeoFilter { + const meta: FilterMeta = { + type: SPATIAL_FILTER_TYPE, + negate: false, + alias: alias + ? alias + : i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', { + defaultMessage: 'within {distanceKm}km of {pointLabel}', + values: { + distanceKm, + pointLabel: point.join(', '), + }, + }), + disabled: false, + }; + + function createGeoFilter(geoFieldName: string) { + return { + geo_distance: { + distance: `${distanceKm}km`, + [geoFieldName]: point, + }, + }; + } + + return createMultiGeoFieldFilter(geoFieldNames, meta, createGeoFilter); +} + +function extractGeometryFromFilter(geoFieldName: string, filter: GeoFilter): Geometry | undefined { + if (filter.geo_distance && filter.geo_distance[geoFieldName]) { + const distanceSplit = filter.geo_distance.distance.split('km'); + const distance = parseFloat(distanceSplit[0]); + const circleFeature = turfCircle(filter.geo_distance[geoFieldName], distance); + return circleFeature.geometry; + } + + if (filter.geo_shape && filter.geo_shape[geoFieldName] && filter.geo_shape[geoFieldName].shape) { + return filter.geo_shape[geoFieldName].shape; + } +} + +export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] { + const features: Feature[] = []; + filters + .filter((filter) => { + return filter.meta.type === SPATIAL_FILTER_TYPE; + }) + .forEach((filter) => { + let geometry: Geometry | undefined; + if (filter.meta.isMultiIndex) { + const geoFieldName = filter?.query?.bool?.should?.[0]?.bool?.must?.[0]?.exists?.field; + const spatialClause = filter?.query?.bool?.should?.[0]?.bool?.must?.[1]; + if (geoFieldName && spatialClause) { + geometry = extractGeometryFromFilter(geoFieldName, spatialClause); + } + } else { + const geoFieldName = filter.meta.key; + if (geoFieldName) { + geometry = extractGeometryFromFilter(geoFieldName, filter); + } + } + + if (geometry) { + features.push({ + type: 'Feature', + geometry, + properties: { + filter: filter.meta.alias, + }, + }); + } + }); + + return features; +} diff --git a/x-pack/plugins/maps/common/elasticsearch_util/types.ts b/x-pack/plugins/maps/common/elasticsearch_util/types.ts new file mode 100644 index 0000000000000..bbb508ce69275 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/types.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Polygon, Position } from 'geojson'; +import { Filter } from '../../../../../src/plugins/data/common'; +import { ES_SPATIAL_RELATIONS } from '../constants'; + +export type Coordinates = Position | Position[] | Position[][] | Position[][][]; + +// Elasticsearch stores more then just GeoJSON. +// 1) geometry.type as lower case string +// 2) circle and envelope types +export interface ESGeometry { + type: string; + coordinates: Coordinates; +} + +export interface ESBBox { + top_left: number[]; + bottom_right: number[]; +} + +export interface GeoShapeQueryBody { + shape?: Polygon; + relation?: ES_SPATIAL_RELATIONS; + indexed_shape?: PreIndexedShape; +} + +// Index signature explicitly states that anything stored in an object using a string conforms to the structure +// problem is that Elasticsearch signature also allows for other string keys to conform to other structures, like 'ignore_unmapped' +// Use intersection type to exclude certain properties from the index signature +// https://basarat.gitbook.io/typescript/type-system/index-signatures#excluding-certain-properties-from-the-index-signature +type GeoShapeQuery = { ignore_unmapped: boolean } & { [geoFieldName: string]: GeoShapeQueryBody }; + +export type GeoFilter = Filter & { + geo_bounding_box?: { + [geoFieldName: string]: ESBBox; + }; + geo_distance?: { + distance: string; + [geoFieldName: string]: Position | { lat: number; lon: number } | string; + }; + geo_shape?: GeoShapeQuery; +}; + +export interface PreIndexedShape { + index: string; + id: string | number; + path: string; +} diff --git a/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index ccbe4667b78ea..80238bf3a4d17 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should not show "within" relation when filter geometry is not closed 1`] = ` +exports[`render 1`] = ` - - - - - - - Create filter - - - -`; - -exports[`should render error message 1`] = ` - - - - - - - - - Simulated error - @@ -177,7 +70,7 @@ exports[`should render error message 1`] = ` `; -exports[`should render relation select when geo field is geo_shape 1`] = ` +exports[`should render error message 1`] = ` - - - - Create filter - - - -`; - -exports[`should render relation select without "within"-relation when geo field is geo_point 1`] = ` - - - - - - - - - - + + Simulated error + diff --git a/x-pack/plugins/maps/public/components/distance_filter_form.tsx b/x-pack/plugins/maps/public/components/distance_filter_form.tsx index 14ae6b11b85c8..b5fdcbc46b932 100644 --- a/x-pack/plugins/maps/public/components/distance_filter_form.tsx +++ b/x-pack/plugins/maps/public/components/distance_filter_form.tsx @@ -16,47 +16,28 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; -import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; -import { GeoFieldWithIndex } from './geo_field_with_index'; import { ActionSelect } from './action_select'; import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; interface Props { className?: string; buttonLabel: string; - geoFields: GeoFieldWithIndex[]; getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; - onSubmit: ({ - actionId, - filterLabel, - indexPatternId, - geoFieldName, - }: { - actionId: string; - filterLabel: string; - indexPatternId: string; - geoFieldName: string; - }) => void; + onSubmit: ({ actionId, filterLabel }: { actionId: string; filterLabel: string }) => void; } interface State { actionId: string; - selectedField: GeoFieldWithIndex | undefined; filterLabel: string; } export class DistanceFilterForm extends Component { state: State = { actionId: ACTION_GLOBAL_APPLY_FILTER, - selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, filterLabel: '', }; - _onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => { - this.setState({ selectedField }); - }; - _onFilterLabelChange = (e: ChangeEvent) => { this.setState({ filterLabel: e.target.value, @@ -68,14 +49,9 @@ export class DistanceFilterForm extends Component { }; _onSubmit = () => { - if (!this.state.selectedField) { - return; - } this.props.onSubmit({ actionId: this.state.actionId, filterLabel: this.state.filterLabel, - indexPatternId: this.state.selectedField.indexPatternId, - geoFieldName: this.state.selectedField.geoFieldName, }); }; @@ -95,12 +71,6 @@ export class DistanceFilterForm extends Component { /> - - { - + {this.props.buttonLabel} diff --git a/x-pack/plugins/maps/public/components/geo_field_with_index.ts b/x-pack/plugins/maps/public/components/geo_field_with_index.ts deleted file mode 100644 index 5273bff44f8d7..0000000000000 --- a/x-pack/plugins/maps/public/components/geo_field_with_index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -// Maps can contain geo fields from multiple index patterns. GeoFieldWithIndex is used to: -// 1) Combine the geo field along with associated index pattern state. -// 2) Package asynchronously looked up state via getIndexPatternService() to avoid -// PITA of looking up async state in downstream react consumers. -export type GeoFieldWithIndex = { - geoFieldName: string; - geoFieldType: string; - indexPatternTitle: string; - indexPatternId: string; -}; diff --git a/x-pack/plugins/maps/public/components/geometry_filter_form.js b/x-pack/plugins/maps/public/components/geometry_filter_form.js index 624d3b60fe14b..2e13f63b79883 100644 --- a/x-pack/plugins/maps/public/components/geometry_filter_form.js +++ b/x-pack/plugins/maps/public/components/geometry_filter_form.js @@ -18,16 +18,14 @@ import { EuiFormErrorText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants'; +import { ES_SPATIAL_RELATIONS } from '../../common/constants'; import { getEsSpatialRelationLabel } from '../../common/i18n_getters'; -import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; import { ActionSelect } from './action_select'; import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; export class GeometryFilterForm extends Component { static propTypes = { buttonLabel: PropTypes.string.isRequired, - geoFields: PropTypes.array.isRequired, getFilterActions: PropTypes.func, getActionContext: PropTypes.func, intitialGeometryLabel: PropTypes.string.isRequired, @@ -42,15 +40,10 @@ export class GeometryFilterForm extends Component { state = { actionId: ACTION_GLOBAL_APPLY_FILTER, - selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, geometryLabel: this.props.intitialGeometryLabel, relation: ES_SPATIAL_RELATIONS.INTERSECTS, }; - _onGeoFieldChange = (selectedField) => { - this.setState({ selectedField }); - }; - _onGeometryLabelChange = (e) => { this.setState({ geometryLabel: e.target.value, @@ -71,29 +64,12 @@ export class GeometryFilterForm extends Component { this.props.onSubmit({ actionId: this.state.actionId, geometryLabel: this.state.geometryLabel, - indexPatternId: this.state.selectedField.indexPatternId, - geoFieldName: this.state.selectedField.geoFieldName, relation: this.state.relation, }); }; _renderRelationInput() { - // relationship only used when filtering geo_shape fields - if (!this.state.selectedField) { - return null; - } - - const spatialRelations = - this.props.isFilterGeometryClosed && - this.state.selectedField.geoFieldType !== ES_GEO_FIELD_TYPE.GEO_POINT - ? Object.values(ES_SPATIAL_RELATIONS) - : Object.values(ES_SPATIAL_RELATIONS).filter((relation) => { - // - cannot filter by "within"-relation when filtering geometry is not closed - // - do not distinguish between intersects/within for filtering for points since they are equivalent - return relation !== ES_SPATIAL_RELATIONS.WITHIN; - }); - - const options = spatialRelations.map((relation) => { + const options = Object.values(ES_SPATIAL_RELATIONS).map((relation) => { return { value: relation, text: getEsSpatialRelationLabel(relation), @@ -137,12 +113,6 @@ export class GeometryFilterForm extends Component { /> - - {this._renderRelationInput()} {this.props.buttonLabel} diff --git a/x-pack/plugins/maps/public/components/geometry_filter_form.test.js b/x-pack/plugins/maps/public/components/geometry_filter_form.test.js index d981caf944ab9..3ce79782788b0 100644 --- a/x-pack/plugins/maps/public/components/geometry_filter_form.test.js +++ b/x-pack/plugins/maps/public/components/geometry_filter_form.test.js @@ -16,76 +16,14 @@ const defaultProps = { onSubmit: () => {}, }; -test('should render relation select without "within"-relation when geo field is geo_point', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); -}); - -test('should render relation select when geo field is geo_shape', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); -}); - -test('should not show "within" relation when filter geometry is not closed', async () => { - const component = shallow( - - ); +test('render', async () => { + const component = shallow(); expect(component).toMatchSnapshot(); }); test('should render error message', async () => { - const component = shallow( - - ); + const component = shallow(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx b/x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx deleted file mode 100644 index 564b84ae84300..0000000000000 --- a/x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFormRow, EuiSuperSelect, EuiTextColor, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { GeoFieldWithIndex } from './geo_field_with_index'; - -const OPTION_ID_DELIMITER = '/'; - -function createOptionId(geoField: GeoFieldWithIndex): string { - // Namespace field with indexPatterId to avoid collisions between field names - return `${geoField.indexPatternId}${OPTION_ID_DELIMITER}${geoField.geoFieldName}`; -} - -function splitOptionId(optionId: string) { - const split = optionId.split(OPTION_ID_DELIMITER); - return { - indexPatternId: split[0], - geoFieldName: split[1], - }; -} - -interface Props { - fields: GeoFieldWithIndex[]; - onChange: (newSelectedField: GeoFieldWithIndex | undefined) => void; - selectedField: GeoFieldWithIndex | undefined; -} - -export function MultiIndexGeoFieldSelect({ fields, onChange, selectedField }: Props) { - function onFieldSelect(selectedOptionId: string) { - const { indexPatternId, geoFieldName } = splitOptionId(selectedOptionId); - - const newSelectedField = fields.find((field) => { - return field.indexPatternId === indexPatternId && field.geoFieldName === geoFieldName; - }); - onChange(newSelectedField); - } - - const options = fields.map((geoField: GeoFieldWithIndex) => { - return { - inputDisplay: ( - - - {geoField.indexPatternTitle} - -
- {geoField.geoFieldName} -
- ), - value: createOptionId(geoField), - }; - }); - - return ( - - - - ); -} diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 02374932a4c70..26746d9ad2416 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -20,15 +20,12 @@ import { ToolbarOverlay } from '../toolbar_overlay'; import { EditLayerPanel } from '../edit_layer_panel'; import { AddLayerPanel } from '../add_layer_panel'; import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; -import { getIndexPatternsFromIds } from '../../index_pattern_util'; -import { ES_GEO_FIELD_TYPE, RawValue } from '../../../common/constants'; -import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public'; +import { RawValue } from '../../../common/constants'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapSettings } from '../../reducers/map'; import { MapSettingsPanel } from '../map_settings_panel'; import { registerLayerWizards } from '../../classes/layers/load_layer_wizards'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; -import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { MapRefreshConfig } from '../../../common/descriptor_types'; import { ILayer } from '../../classes/layers/layer'; @@ -58,7 +55,6 @@ export interface Props { interface State { isInitialLoadRenderTimeoutComplete: boolean; domId: string; - geoFields: GeoFieldWithIndex[]; showFitToBoundsButton: boolean; showTimesliderButton: boolean; } @@ -66,7 +62,6 @@ interface State { export class MapContainer extends Component { private _isMounted: boolean = false; private _isInitalLoadRenderTimerStarted: boolean = false; - private _prevIndexPatternIds: string[] = []; private _refreshTimerId: number | null = null; private _prevIsPaused: boolean | null = null; private _prevInterval: number | null = null; @@ -74,7 +69,6 @@ export class MapContainer extends Component { state: State = { isInitialLoadRenderTimeoutComplete: false, domId: uuid(), - geoFields: [], showFitToBoundsButton: false, showTimesliderButton: false, }; @@ -95,10 +89,6 @@ export class MapContainer extends Component { this._isInitalLoadRenderTimerStarted = true; this._startInitialLoadRenderTimer(); } - - if (!!this.props.addFilters) { - this._loadGeoFields(this.props.indexPatternIds); - } } componentWillUnmount() { @@ -151,40 +141,6 @@ export class MapContainer extends Component { } } - async _loadGeoFields(nextIndexPatternIds: string[]) { - if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { - // all ready loaded index pattern ids - return; - } - - this._prevIndexPatternIds = nextIndexPatternIds; - - const geoFields: GeoFieldWithIndex[] = []; - const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); - indexPatterns.forEach((indexPattern) => { - indexPattern.fields.forEach((field) => { - if ( - indexPattern.id && - !indexPatternsUtils.isNestedField(field) && - (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) - ) { - geoFields.push({ - geoFieldName: field.name, - geoFieldType: field.type, - indexPatternTitle: indexPattern.title, - indexPatternId: indexPattern.id, - }); - } - }); - }); - - if (!this._isMounted) { - return; - } - - this.setState({ geoFields }); - } - _setRefreshTimer = () => { const { isPaused, interval } = this.props.refreshConfig; @@ -289,13 +245,11 @@ export class MapContainer extends Component { getFilterActions={getFilterActions} getActionContext={getActionContext} onSingleValueTrigger={onSingleValueTrigger} - geoFields={this.state.geoFields} renderTooltipContent={renderTooltipContent} /> {!this.props.settings.hideToolbarOverlay && ( { _onDraw = async (e: { features: Feature[] }) => { - if ( - !e.features.length || - !this.props.drawState || - !this.props.drawState.geoFieldName || - !this.props.drawState.indexPatternId - ) { + if (!e.features.length || !this.props.drawState || !this.props.geoFieldNames.length) { return; } @@ -61,8 +57,7 @@ export class DrawFilterControl extends Component { filter = createDistanceFilterWithMeta({ alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '', distanceKm, - geoFieldName: this.props.drawState.geoFieldName, - indexPatternId: this.props.drawState.indexPatternId, + geoFieldNames: this.props.geoFieldNames, point: [ _.round(circle.properties.center[0], precision), _.round(circle.properties.center[1], precision), @@ -78,8 +73,7 @@ export class DrawFilterControl extends Component { this.props.drawState.drawType === DRAW_TYPE.BOUNDS ? getBoundingBoxGeometry(geometry) : geometry, - indexPatternId: this.props.drawState.indexPatternId, - geoFieldName: this.props.drawState.geoFieldName, + geoFieldNames: this.props.geoFieldNames, geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '', relation: this.props.drawState.relation ? this.props.drawState.relation diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts index 17f4d919fb7e0..58ad530093557 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts @@ -10,13 +10,18 @@ import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { DrawFilterControl } from './draw_filter_control'; import { updateDrawState } from '../../../../actions'; -import { getDrawState, isDrawingFilter } from '../../../../selectors/map_selectors'; +import { + getDrawState, + isDrawingFilter, + getGeoFieldNames, +} from '../../../../selectors/map_selectors'; import { MapStoreState } from '../../../../reducers/store'; function mapStateToProps(state: MapStoreState) { return { isDrawingFilter: isDrawingFilter(state), drawState: getDrawState(state), + geoFieldNames: getGeoFieldNames(state), }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 877de10e11383..9d8bce083c292 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -43,7 +43,6 @@ import { // @ts-expect-error } from './utils'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; -import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { MapExtentState } from '../../actions'; import { TileStatusTracker } from './tile_status_tracker'; @@ -68,7 +67,6 @@ export interface Props { getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; - geoFields: GeoFieldWithIndex[]; renderTooltipContent?: RenderToolTipContent; setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; } @@ -432,7 +430,6 @@ export class MBMap extends Component { getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} onSingleValueTrigger={this.props.onSingleValueTrigger} - geoFields={this.props.geoFields} renderTooltipContent={this.props.renderTooltipContent} /> ) : null; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_geometry_filter_form.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_geometry_filter_form.tsx index 04e564943fa39..5f26715c86004 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_geometry_filter_form.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_geometry_filter_form.tsx @@ -21,7 +21,6 @@ import { import { ES_SPATIAL_RELATIONS, GEO_JSON_TYPE } from '../../../../../common/constants'; // @ts-expect-error import { GeometryFilterForm } from '../../../../components/geometry_filter_form'; -import { GeoFieldWithIndex } from '../../../../components/geo_field_with_index'; // over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped. const META_OVERHEAD = 100; @@ -29,11 +28,11 @@ const META_OVERHEAD = 100; interface Props { onClose: () => void; geometry: Geometry; - geoFields: GeoFieldWithIndex[]; addFilters: (filters: Filter[], actionId: string) => Promise; getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; loadPreIndexedShape: () => Promise; + geoFieldNames: string[]; } interface State { @@ -77,13 +76,9 @@ export class FeatureGeometryFilterForm extends Component { _createFilter = async ({ geometryLabel, - indexPatternId, - geoFieldName, relation, }: { geometryLabel: string; - indexPatternId: string; - geoFieldName: string; relation: ES_SPATIAL_RELATIONS; }) => { this.setState({ errorMsg: undefined }); @@ -97,8 +92,7 @@ export class FeatureGeometryFilterForm extends Component { preIndexedShape, geometry: this.props.geometry as Polygon, geometryLabel, - indexPatternId, - geoFieldName, + geoFieldNames: this.props.geoFieldNames, relation, }); @@ -130,7 +124,6 @@ export class FeatureGeometryFilterForm extends Component { defaultMessage: 'Create filter', } )} - geoFields={this.props.geoFields} getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} intitialGeometryLabel={this.props.geometry.type.toLowerCase()} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts index 28510815d1a2e..2861c80306526 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts @@ -20,6 +20,7 @@ import { getLayerList, getOpenTooltips, getHasLockedTooltips, + getGeoFieldNames, isDrawingFilter, } from '../../../selectors/map_selectors'; import { MapStoreState } from '../../../reducers/store'; @@ -30,6 +31,7 @@ function mapStateToProps(state: MapStoreState) { hasLockedTooltips: getHasLockedTooltips(state), isDrawingFilter: isDrawingFilter(state), openTooltips: getOpenTooltips(state), + geoFieldNames: getGeoFieldNames(state), }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx index ac6e3cfcccf4e..a11000a48866f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx @@ -77,7 +77,7 @@ const defaultProps = { layerList: [mockLayer], isDrawingFilter: false, addFilters: async () => {}, - geoFields: [], + geoFieldNames: [], openTooltips: [], hasLockedTooltips: false, }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx index 09dd9ee4f51d9..e9af9dcc89e07 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx @@ -20,7 +20,6 @@ import { Geometry } from 'geojson'; import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { - ES_GEO_FIELD_TYPE, FEATURE_ID_PROPERTY_NAME, GEO_JSON_TYPE, LON_INDEX, @@ -37,7 +36,6 @@ import { FeatureGeometryFilterForm } from './features_tooltip'; import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions'; import { ILayer } from '../../../classes/layers/layer'; import { IVectorLayer } from '../../../classes/layers/vector_layer'; -import { GeoFieldWithIndex } from '../../../components/geo_field_with_index'; import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property'; function justifyAnchorLocation( @@ -70,7 +68,7 @@ export interface Props { closeOnHoverTooltip: () => void; getActionContext?: () => ActionExecutionContext; getFilterActions?: () => Promise; - geoFields: GeoFieldWithIndex[]; + geoFieldNames: string[]; hasLockedTooltips: boolean; isDrawingFilter: boolean; layerList: ILayer[]; @@ -163,8 +161,10 @@ export class TooltipControl extends Component { const actions = []; const geometry = this._getFeatureGeometry({ layerId, featureId }); - const geoFieldsForFeature = this._filterGeoFieldsByFeatureGeometry(geometry); - if (geometry && geoFieldsForFeature.length && this.props.addFilters) { + const isPolygon = + geometry && + (geometry.type === GEO_JSON_TYPE.POLYGON || geometry.type === GEO_JSON_TYPE.MULTI_POLYGON); + if (isPolygon && this.props.geoFieldNames.length && this.props.addFilters) { actions.push({ label: i18n.translate('xpack.maps.tooltip.action.filterByGeometryLabel', { defaultMessage: 'Filter by geometry', @@ -175,8 +175,8 @@ export class TooltipControl extends Component { onClose={() => { this.props.closeOnClickTooltip(tooltipId); }} - geometry={geometry} - geoFields={geoFieldsForFeature} + geometry={geometry!} + geoFieldNames={this.props.geoFieldNames} addFilters={this.props.addFilters} getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} @@ -191,29 +191,6 @@ export class TooltipControl extends Component { return actions; } - _filterGeoFieldsByFeatureGeometry(geometry: Geometry | null) { - if (!geometry) { - return []; - } - - // line geometry can only create filters for geo_shape fields. - if ( - geometry.type === GEO_JSON_TYPE.LINE_STRING || - geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING - ) { - return this.props.geoFields.filter(({ geoFieldType }) => { - return geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE; - }); - } - - // TODO support geo distance filters for points - if (geometry.type === GEO_JSON_TYPE.POINT || geometry.type === GEO_JSON_TYPE.MULTI_POINT) { - return []; - } - - return this.props.geoFields; - } - _getTooltipFeatures( mbFeatures: MapboxGeoJSONFeature[], isLocked: boolean, diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap index 168a070b07744..245cf6e5b2a48 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap @@ -29,18 +29,7 @@ exports[`Should show all controls 1`] = ` - + diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts index d1008edfd572d..7d176c13da049 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts @@ -5,4 +5,16 @@ * 2.0. */ -export { ToolbarOverlay } from './toolbar_overlay'; +import { connect } from 'react-redux'; +import { MapStoreState } from '../../reducers/store'; +import { getGeoFieldNames } from '../../selectors/map_selectors'; +import { ToolbarOverlay } from './toolbar_overlay'; + +function mapStateToProps(state: MapStoreState) { + return { + showToolsControl: getGeoFieldNames(state).length !== 0, + }; +} + +const connected = connect(mapStateToProps)(ToolbarOverlay); +export { connected as ToolbarOverlay }; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx index 28b5ab9c78f40..9efbd82fe390a 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx @@ -21,22 +21,20 @@ import { ToolbarOverlay } from './toolbar_overlay'; test('Should only show set view control', async () => { const component = shallow( - + ); expect(component).toMatchSnapshot(); }); test('Should show all controls', async () => { - const geoFieldWithIndex = { - geoFieldName: 'myGeoFieldName', - geoFieldType: 'geo_point', - indexPatternTitle: 'myIndex', - indexPatternId: '1', - }; const component = shallow( {}} - geoFields={[geoFieldWithIndex]} + showToolsControl={true} showFitToBoundsButton={true} showTimesliderButton={true} /> diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx index 41c6c1f7c4a7c..2b35793218969 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx @@ -13,11 +13,10 @@ import { SetViewControl } from './set_view_control'; import { ToolsControl } from './tools_control'; import { FitToData } from './fit_to_data'; import { TimesliderToggleButton } from './timeslider_toggle_button'; -import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; export interface Props { addFilters?: ((filters: Filter[], actionId: string) => Promise) | null; - geoFields: GeoFieldWithIndex[]; + showToolsControl: boolean; getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; showFitToBoundsButton: boolean; @@ -26,10 +25,9 @@ export interface Props { export function ToolbarOverlay(props: Props) { const toolsButton = - props.addFilters && props.geoFields.length ? ( + props.addFilters && props.showToolsControl ? ( diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap index b6d217d690764..aa5c6aa42c77d 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap @@ -56,16 +56,6 @@ exports[`Should render cancel button when drawing 1`] = ` "content": , "id": 3, @@ -187,16 +157,6 @@ exports[`renders 1`] = ` "content": , "id": 3, diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx index 6779fe945137e..9ea20914c6422 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx @@ -22,7 +22,6 @@ import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../ // @ts-expect-error import { GeometryFilterForm } from '../../../components/geometry_filter_form'; import { DistanceFilterForm } from '../../../components/distance_filter_form'; -import { GeoFieldWithIndex } from '../../../components/geo_field_with_index'; import { DrawState } from '../../../../common/descriptor_types'; const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { @@ -54,7 +53,6 @@ const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( export interface Props { cancelDraw: () => void; - geoFields: GeoFieldWithIndex[]; initiateDraw: (drawState: DrawState) => void; isDrawingFilter: boolean; getFilterActions?: () => Promise; @@ -98,9 +96,6 @@ export class ToolsControl extends Component { _initiateBoundsDraw = (options: { actionId: string; geometryLabel: string; - indexPatternId: string; - geoFieldName: string; - geoFieldType: ES_GEO_FIELD_TYPE; relation: ES_SPATIAL_RELATIONS; }) => { this.props.initiateDraw({ @@ -110,12 +105,7 @@ export class ToolsControl extends Component { this._closePopover(); }; - _initiateDistanceDraw = (options: { - actionId: string; - filterLabel: string; - indexPatternId: string; - geoFieldName: string; - }) => { + _initiateDistanceDraw = (options: { actionId: string; filterLabel: string }) => { this.props.initiateDraw({ drawType: DRAW_TYPE.DISTANCE, ...options, @@ -154,7 +144,6 @@ export class ToolsControl extends Component { { { Date: Wed, 2 Jun 2021 09:31:32 -0500 Subject: [PATCH 3/5] [DOCS] Removes 7.13.1 coming tags (#101094) --- docs/CHANGELOG.asciidoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 99660ff865526..784b952cccfe4 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -57,8 +57,6 @@ Review important information about the {kib} 7.x releases. [[release-notes-7.13.1]] == {kib} 7.13.1 -coming::[7.13.1] - The 7.13.1 release includes the following bug fixes. [float] From 5a296a3a28d5504411112e19f4f370385d28051d Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 2 Jun 2021 08:16:25 -0700 Subject: [PATCH 4/5] skip flaky suite (#101126) (cherry picked from commit ef9d2bfb013325f016a88b1f85db6aa27e13ae4d) --- .../public/batching/create_streaming_batched_function.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts index 01cebcb15963b..458b691573e56 100644 --- a/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts @@ -48,7 +48,8 @@ const setup = () => { }; }; -describe('createStreamingBatchedFunction()', () => { +// FLAKY: https://github.com/elastic/kibana/issues/101126 +describe.skip('createStreamingBatchedFunction()', () => { test('returns a function', () => { const { fetchStreaming } = setup(); const fn = createStreamingBatchedFunction({ From 3e755ccf1fb4ee6f93d78ce0c141233a8f39d1fb Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 2 Jun 2021 18:28:22 +0300 Subject: [PATCH 5/5] [7.x] [Deprecations service] make `correctiveActions.manualSteps` required (#100997) (#101156) * merge conflicts * update deprecation message --- ...r.deprecationsdetails.correctiveactions.md | 2 +- ...-plugin-core-server.deprecationsdetails.md | 2 +- .../kbn-config/src/config_service.test.ts | 38 +++++- packages/kbn-config/src/deprecation/types.ts | 4 +- .../deprecations/deprecations_client.test.ts | 11 +- .../deprecation/core_deprecations.test.ts | 6 +- .../config/deprecation/core_deprecations.ts | 121 +++++++++++++++++- .../deprecations/deprecations_service.ts | 4 +- src/core/server/deprecations/types.ts | 8 +- .../elasticsearch/elasticsearch_config.ts | 22 ++++ src/core/server/kibana_config.ts | 6 + .../saved_objects/saved_objects_config.ts | 3 + src/core/server/server.api.md | 2 +- .../core_plugin_deprecations/server/config.ts | 5 + .../core_plugin_deprecations/server/plugin.ts | 4 +- .../test_suites/core/deprecations.ts | 13 +- x-pack/plugins/actions/server/index.ts | 37 +++++- x-pack/plugins/banners/server/config.ts | 6 + .../plugins/monitoring/server/deprecations.ts | 21 +++ .../reporting/server/config/index.test.ts | 2 +- .../plugins/reporting/server/config/index.ts | 15 ++- .../server/config_deprecations.test.ts | 4 +- .../security/server/config_deprecations.ts | 24 +++- x-pack/plugins/spaces/server/config.ts | 3 + x-pack/plugins/task_manager/server/index.ts | 12 ++ .../client_integration/overview.test.ts | 2 +- 26 files changed, 340 insertions(+), 37 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md index e362bc4e0329c..447823a5c3491 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md @@ -15,6 +15,6 @@ correctiveActions: { [key: string]: any; }; }; - manualSteps?: string[]; + manualSteps: string[]; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md index 6e46ce0b8611f..7592b8486d950 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md @@ -14,7 +14,7 @@ export interface DeprecationsDetails | Property | Type | Description | | --- | --- | --- | -| [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
};
manualSteps?: string[];
} | | +| [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
};
manualSteps: string[];
} | | | [deprecationType](./kibana-plugin-core-server.deprecationsdetails.deprecationtype.md) | 'config' | 'feature' | (optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab.Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. | | [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) | string | | | [level](./kibana-plugin-core-server.deprecationsdetails.level.md) | 'warning' | 'critical' | 'fetch_error' | levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. | diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index c2d4f15b6d915..b1b622381abb1 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -418,8 +418,14 @@ test('logs deprecation warning during validation', async () => { const configService = new ConfigService(rawConfig, defaultEnv, logger); mockApplyDeprecations.mockImplementationOnce((config, deprecations, createAddDeprecation) => { const addDeprecation = createAddDeprecation!(''); - addDeprecation({ message: 'some deprecation message' }); - addDeprecation({ message: 'another deprecation message' }); + addDeprecation({ + message: 'some deprecation message', + correctiveActions: { manualSteps: ['do X'] }, + }); + addDeprecation({ + message: 'another deprecation message', + correctiveActions: { manualSteps: ['do Y'] }, + }); return { config, changedPaths: mockedChangedPaths }; }); @@ -444,13 +450,24 @@ test('does not log warnings for silent deprecations during validation', async () mockApplyDeprecations .mockImplementationOnce((config, deprecations, createAddDeprecation) => { const addDeprecation = createAddDeprecation!(''); - addDeprecation({ message: 'some deprecation message', silent: true }); - addDeprecation({ message: 'another deprecation message' }); + addDeprecation({ + message: 'some deprecation message', + correctiveActions: { manualSteps: ['do X'] }, + silent: true, + }); + addDeprecation({ + message: 'another deprecation message', + correctiveActions: { manualSteps: ['do Y'] }, + }); return { config, changedPaths: mockedChangedPaths }; }) .mockImplementationOnce((config, deprecations, createAddDeprecation) => { const addDeprecation = createAddDeprecation!(''); - addDeprecation({ message: 'I am silent', silent: true }); + addDeprecation({ + message: 'I am silent', + silent: true, + correctiveActions: { manualSteps: ['do Z'] }, + }); return { config, changedPaths: mockedChangedPaths }; }); @@ -519,7 +536,11 @@ describe('getHandledDeprecatedConfigs', () => { mockApplyDeprecations.mockImplementationOnce((config, deprecations, createAddDeprecation) => { deprecations.forEach((deprecation) => { const addDeprecation = createAddDeprecation!(deprecation.path); - addDeprecation({ message: `some deprecation message`, documentationUrl: 'some-url' }); + addDeprecation({ + message: `some deprecation message`, + documentationUrl: 'some-url', + correctiveActions: { manualSteps: ['do X'] }, + }); }); return { config, changedPaths: mockedChangedPaths }; }); @@ -532,6 +553,11 @@ describe('getHandledDeprecatedConfigs', () => { "base", Array [ Object { + "correctiveActions": Object { + "manualSteps": Array [ + "do X", + ], + }, "documentationUrl": "some-url", "message": "some deprecation message", }, diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 0522365ad76c1..1791dac060e2b 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -25,8 +25,8 @@ export interface DeprecatedConfigDetails { silent?: boolean; /* (optional) link to the documentation for more details on the deprecation. */ documentationUrl?: string; - /* (optional) corrective action needed to fix this deprecation. */ - correctiveActions?: { + /* corrective action needed to fix this deprecation. */ + correctiveActions: { /** * Specify a list of manual steps our users need to follow * to fix the deprecation before upgrade. diff --git a/src/core/public/deprecations/deprecations_client.test.ts b/src/core/public/deprecations/deprecations_client.test.ts index 2f52f7b4af195..a998a03772cca 100644 --- a/src/core/public/deprecations/deprecations_client.test.ts +++ b/src/core/public/deprecations/deprecations_client.test.ts @@ -90,6 +90,7 @@ describe('DeprecationsClient', () => { path: 'some-path', method: 'POST', }, + manualSteps: ['manual-step'], }, }; @@ -104,7 +105,9 @@ describe('DeprecationsClient', () => { domainId: 'testPluginId-1', message: 'some-message', level: 'warning', - correctiveActions: {}, + correctiveActions: { + manualSteps: ['manual-step'], + }, }; const isResolvable = deprecationsClient.isDeprecationResolvable(mockDeprecationDetails); @@ -120,7 +123,9 @@ describe('DeprecationsClient', () => { domainId: 'testPluginId-1', message: 'some-message', level: 'warning', - correctiveActions: {}, + correctiveActions: { + manualSteps: ['manual-step'], + }, }; const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); @@ -144,6 +149,7 @@ describe('DeprecationsClient', () => { extra_param: 123, }, }, + manualSteps: ['manual-step'], }, }; const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); @@ -176,6 +182,7 @@ describe('DeprecationsClient', () => { extra_param: 123, }, }, + manualSteps: ['manual-step'], }, }; http.fetch.mockRejectedValue({ body: { message: mockResponse } }); diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 26fb96154223c..5abe7da721ed2 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -24,7 +24,7 @@ describe('core deprecations', () => { const { messages } = applyCoreDeprecations(); expect(messages).toMatchInlineSnapshot(` Array [ - "Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder", + "Environment variable \\"CONFIG_PATH\\" is deprecated. It has been replaced with \\"KBN_PATH_CONF\\" pointing to a config folder", ] `); }); @@ -425,7 +425,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.events.log\\" has been deprecated and will be removed in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ", + "\\"logging.events.log\\" has been deprecated and will be removed in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration.", ] `); }); @@ -438,7 +438,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.events.error\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level: error\\" in your logging configuration. ", + "\\"logging.events.error\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level: error\\" in your logging configuration.", ] `); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index e270a5c67aa1c..04889f20956b5 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -11,7 +11,10 @@ import { ConfigDeprecationProvider, ConfigDeprecation } from '@kbn/config'; const configPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (process.env?.CONFIG_PATH) { addDeprecation({ - message: `Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder`, + message: `Environment variable "CONFIG_PATH" is deprecated. It has been replaced with "KBN_PATH_CONF" pointing to a config folder`, + correctiveActions: { + manualSteps: ['Use "KBN_PATH_CONF" instead of "CONFIG_PATH" to point to a config folder.'], + }, }); } }; @@ -20,6 +23,11 @@ const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati if (process.env?.DATA_PATH) { addDeprecation({ message: `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`, + correctiveActions: { + manualSteps: [ + `Set 'path.data' in the config file or CLI flag with the value of the environment variable "DATA_PATH".`, + ], + }, }); } }; @@ -32,6 +40,12 @@ const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDe 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + 'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' + 'current behavior and silence this warning.', + correctiveActions: { + manualSteps: [ + `Set 'server.rewriteBasePath' in the config file, CLI flag, or environment variable (in Docker only).`, + `Set to false to preserve the current behavior and silence this warning.`, + ], + }, }); } }; @@ -41,6 +55,11 @@ const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, addDeprecati if (typeof corsSettings === 'boolean') { addDeprecation({ message: '"server.cors" is deprecated and has been replaced by "server.cors.enabled"', + correctiveActions: { + manualSteps: [ + `Replace "server.cors: ${corsSettings}" with "server.cors.enabled: ${corsSettings}"`, + ], + }, }); return { @@ -72,6 +91,9 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati if (sourceList.find((source) => source.includes(NONCE_STRING))) { addDeprecation({ message: `csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`, + correctiveActions: { + manualSteps: [`Replace {nonce} syntax with 'self' in ${policy}`], + }, }); sourceList = sourceList.filter((source) => !source.includes(NONCE_STRING)); @@ -87,6 +109,9 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati ) { addDeprecation({ message: `csp.rules must contain the 'self' source. Automatically adding to ${policy}.`, + correctiveActions: { + manualSteps: [`Add 'self' source to ${policy}.`], + }, }); sourceList.push(SELF_STRING); } @@ -111,6 +136,12 @@ const mapManifestServiceUrlDeprecation: ConfigDeprecation = ( 'of the Elastic Maps Service settings. These settings have moved to the "map.emsTileApiUrl" and ' + '"map.emsFileApiUrl" settings instead. These settings are for development use only and should not be ' + 'modified for use in production environments.', + correctiveActions: { + manualSteps: [ + `Use "map.emsTileApiUrl" and "map.emsFileApiUrl" config instead of "map.manifestServiceUrl".`, + `These settings are for development use only and should not be modified for use in production environments.`, + ], + }, }); } }; @@ -121,6 +152,11 @@ const serverHostZeroDeprecation: ConfigDeprecation = (settings, fromPath, addDep message: 'Support for setting server.host to "0" in kibana.yml is deprecated and will be removed in Kibana version 8.0.0. ' + 'Instead use "0.0.0.0" to bind to all interfaces.', + correctiveActions: { + manualSteps: [ + `Replace "server.host: 0" to "server.host: 0.0.0.0" in your kibana configurations.`, + ], + }, }); } return settings; @@ -136,12 +172,28 @@ const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDe 'in 8.0. To access ops data moving forward, please enable debug logs for the ' + '"metrics.ops" context in your logging configuration. For more details, see ' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + correctiveActions: { + manualSteps: [ + `Remove "logging.events.ops" from your kibana settings.`, + `Enable debug logs for the "metrics.ops" context in your logging configuration`, + ], + }, }); } }; const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (settings.logging?.events?.request || settings.logging?.events?.response) { + const removeConfigsSteps = []; + + if (settings.logging?.events?.request) { + removeConfigsSteps.push(`Remove "logging.events.request" from your kibana configs.`); + } + + if (settings.logging?.events?.response) { + removeConfigsSteps.push(`Remove "logging.events.response" from your kibana configs.`); + } + addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', @@ -150,6 +202,12 @@ const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, a 'in 8.0. To access request and/or response data moving forward, please enable debug logs for the ' + '"http.server.response" context in your logging configuration. For more details, see ' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + correctiveActions: { + manualSteps: [ + ...removeConfigsSteps, + `enable debug logs for the "http.server.response" context in your logging configuration.`, + ], + }, }); } }; @@ -164,6 +222,12 @@ const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDe 'in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern ' + 'in your logging configuration. For more details, see ' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + correctiveActions: { + manualSteps: [ + `Remove "logging.timezone" from your kibana configs.`, + `To set the timezone add a timezone date modifier to the log pattern in your logging configuration.`, + ], + }, }); } }; @@ -178,6 +242,12 @@ const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprec 'in 8.0. To set the destination moving forward, you can use the "console" appender ' + 'in your logging configuration or define a custom one. For more details, see ' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + correctiveActions: { + manualSteps: [ + `Remove "logging.dest" from your kibana configs.`, + `To set the destination use the "console" appender in your logging configuration or define a custom one.`, + ], + }, }); } }; @@ -190,6 +260,12 @@ const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDepre message: '"logging.quiet" has been deprecated and will be removed ' + 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ', + correctiveActions: { + manualSteps: [ + `Remove "logging.quiet" from your kibana configs.`, + `Use "logging.root.level:error" in your logging configuration.`, + ], + }, }); } }; @@ -202,6 +278,12 @@ const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDepr message: '"logging.silent" has been deprecated and will be removed ' + 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ', + correctiveActions: { + manualSteps: [ + `Remove "logging.silent" from your kibana configs.`, + `Use "logging.root.level:off" in your logging configuration.`, + ], + }, }); } }; @@ -214,6 +296,12 @@ const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDep message: '"logging.verbose" has been deprecated and will be removed ' + 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ', + correctiveActions: { + manualSteps: [ + `Remove "logging.verbose" from your kibana configs.`, + `Use "logging.root.level:all" in your logging configuration.`, + ], + }, }); } }; @@ -234,6 +322,12 @@ const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprec 'There is currently no default layout for custom appenders and each one must be declared explicitly. ' + 'For more details, see ' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + correctiveActions: { + manualSteps: [ + `Remove "logging.json" from your kibana configs.`, + `Configure the "appender.layout" property for every custom appender in your logging configuration.`, + ], + }, }); } }; @@ -248,6 +342,12 @@ const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecat 'Moving forward, you can enable log rotation using the "rolling-file" appender for a logger ' + 'in your logging configuration. For more details, see ' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', + correctiveActions: { + manualSteps: [ + `Remove "logging.rotate" from your kibana configs.`, + `Enable log rotation using the "rolling-file" appender for a logger in your logging configuration.`, + ], + }, }); } }; @@ -259,7 +359,13 @@ const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, addDepre 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', message: '"logging.events.log" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ', + 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration.', + correctiveActions: { + manualSteps: [ + `Remove "logging.events.log" from your kibana configs.`, + `Customize log levels can be per-logger using the new logging configuration.`, + ], + }, }); } }; @@ -271,7 +377,13 @@ const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, addDep 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', message: '"logging.events.error" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration. ', + 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration.', + correctiveActions: { + manualSteps: [ + `Remove "logging.events.error" from your kibana configs.`, + `Use "logging.root.level: error" in your logging configuration.`, + ], + }, }); } }; @@ -282,6 +394,9 @@ const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecat documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingfilter', message: '"logging.filter" has been deprecated and will be removed in 8.0.', + correctiveActions: { + manualSteps: [`Remove "logging.filter" from your kibana configs.`], + }, }); } }; diff --git a/src/core/server/deprecations/deprecations_service.ts b/src/core/server/deprecations/deprecations_service.ts index 205dd964468c1..ede7f859ffd0d 100644 --- a/src/core/server/deprecations/deprecations_service.ts +++ b/src/core/server/deprecations/deprecations_service.ts @@ -116,7 +116,7 @@ export interface DeprecationsSetupDeps { export class DeprecationsService implements CoreService { private readonly logger: Logger; - constructor(private readonly coreContext: CoreContext) { + constructor(private readonly coreContext: Pick) { this.logger = coreContext.logger.get('deprecations-service'); } @@ -154,7 +154,7 @@ export class DeprecationsService implements CoreService [ if (es.username === 'elastic') { addDeprecation({ message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, + correctiveActions: { + manualSteps: [`Replace [${fromPath}.username] from "elastic" to "kibana_system".`], + }, }); } else if (es.username === 'kibana') { addDeprecation({ message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, + correctiveActions: { + manualSteps: [`Replace [${fromPath}.username] from "kibana" to "kibana_system".`], + }, }); } if (es.ssl?.key !== undefined && es.ssl?.certificate === undefined) { addDeprecation({ message: `Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + correctiveActions: { + manualSteps: [ + `Set [${fromPath}.ssl.certificate] in your kibana configs to enable TLS client authentication to Elasticsearch.`, + ], + }, }); } else if (es.ssl?.certificate !== undefined && es.ssl?.key === undefined) { addDeprecation({ message: `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + correctiveActions: { + manualSteps: [ + `Set [${fromPath}.ssl.key] in your kibana configs to enable TLS client authentication to Elasticsearch.`, + ], + }, }); } else if (es.logQueries === true) { addDeprecation({ message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".`, + correctiveActions: { + manualSteps: [ + `Remove Setting [${fromPath}.logQueries] from your kibana configs`, + `Set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers".`, + ], + }, }); } return; diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts index 623ffa86c0e8d..77ee3197b988d 100644 --- a/src/core/server/kibana_config.ts +++ b/src/core/server/kibana_config.ts @@ -18,6 +18,12 @@ const deprecations: ConfigDeprecationProvider = () => [ addDeprecation({ message: `"kibana.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', + correctiveActions: { + manualSteps: [ + `If you rely on this setting to achieve multitenancy you should use Spaces, cross-cluster replication, or cross-cluster search instead.`, + `To migrate to Spaces, we encourage using saved object management to export your saved objects from a tenant into the default tenant in a space.`, + ], + }, }); } return settings; diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 7182df74c597f..c62d322f0bf8d 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -29,6 +29,9 @@ const migrationDeprecations: ConfigDeprecationProvider = () => [ message: '"migrations.enableV2" is deprecated and will be removed in an upcoming release without any further notice.', documentationUrl: 'https://ela.st/kbn-so-migration-v2', + correctiveActions: { + manualSteps: [`Remove "migrations.enableV2" from your kibana configs.`], + }, }); } return settings; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 0c35177f51f99..379e4147ae024 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -872,7 +872,7 @@ export interface DeprecationsDetails { [key: string]: any; }; }; - manualSteps?: string[]; + manualSteps: string[]; }; deprecationType?: 'config' | 'feature'; // (undocumented) diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts index e051c39f68150..650567f761aa3 100644 --- a/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts @@ -26,6 +26,11 @@ const configSecretDeprecation: ConfigDeprecation = (settings, fromPath, addDepre message: 'Kibana plugin functional tests will no longer allow corePluginDeprecations.secret ' + 'config to be set to anything except 42.', + correctiveActions: { + manualSteps: [ + `This is an intentional deprecation for testing with no intention for having it fixed!`, + ], + }, }); } return settings; diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts index 65a2ce02aa0a4..9922e56f44bd9 100644 --- a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts @@ -29,7 +29,9 @@ async function getDeprecations({ message: `SavedObject test-deprecations-plugin is still being used.`, documentationUrl: 'another-test-url', level: 'critical', - correctiveActions: {}, + correctiveActions: { + manualSteps: ['Step a', 'Step b'], + }, }); } diff --git a/test/plugin_functional/test_suites/core/deprecations.ts b/test/plugin_functional/test_suites/core/deprecations.ts index 99b1a79fb51e3..38a8b835b118c 100644 --- a/test/plugin_functional/test_suites/core/deprecations.ts +++ b/test/plugin_functional/test_suites/core/deprecations.ts @@ -45,7 +45,11 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide level: 'critical', message: 'Kibana plugin functional tests will no longer allow corePluginDeprecations.secret config to be set to anything except 42.', - correctiveActions: {}, + correctiveActions: { + manualSteps: [ + 'This is an intentional deprecation for testing with no intention for having it fixed!', + ], + }, documentationUrl: 'config-secret-doc-url', deprecationType: 'config', domainId: 'corePluginDeprecations', @@ -64,7 +68,9 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide message: 'SavedObject test-deprecations-plugin is still being used.', documentationUrl: 'another-test-url', level: 'critical', - correctiveActions: {}, + correctiveActions: { + manualSteps: ['Step a', 'Step b'], + }, domainId: 'corePluginDeprecations', }, ]; @@ -151,6 +157,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide mockFail: true, }, }, + manualSteps: ['Step a', 'Step b'], }, domainId: 'corePluginDeprecations', }) @@ -178,6 +185,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide mockFail: true, }, }, + manualSteps: ['Step a', 'Step b'], }, domainId: 'corePluginDeprecations', }) @@ -213,6 +221,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide path: '/api/core_deprecations_resolve/', body: { keyId }, }, + manualSteps: ['Step a', 'Step b'], }, domainId: 'corePluginDeprecations', }) diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 6a0f06b34d670..692ff6fa0a508 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -69,7 +69,18 @@ export const config: PluginConfigDescriptor = { ) { addDeprecation({ message: - '`xpack.actions.customHostSettings[].tls.rejectUnauthorized` is deprecated. Use `xpack.actions.customHostSettings[].tls.verificationMode` instead, with the setting `verificationMode:full` eql to `rejectUnauthorized:true`, and `verificationMode:none` eql to `rejectUnauthorized:false`.', + `"xpack.actions.customHostSettings[].tls.rejectUnauthorized" is deprecated.` + + `Use "xpack.actions.customHostSettings[].tls.verificationMode" instead, ` + + `with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` + + `and "verificationMode:none" eql to "rejectUnauthorized:false".`, + correctiveActions: { + manualSteps: [ + `Remove "xpack.actions.customHostSettings[].tls.rejectUnauthorized" from your kibana configs.`, + `Use "xpack.actions.customHostSettings[].tls.verificationMode" ` + + `with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` + + `and "verificationMode:none" eql to "rejectUnauthorized:false".`, + ], + }, }); } }, @@ -77,7 +88,17 @@ export const config: PluginConfigDescriptor = { if (!!settings?.xpack?.actions?.rejectUnauthorized) { addDeprecation({ message: - '`xpack.actions.rejectUnauthorized` is deprecated. Use `xpack.actions.verificationMode` instead, with the setting `verificationMode:full` eql to `rejectUnauthorized:true`, and `verificationMode:none` eql to `rejectUnauthorized:false`.', + `"xpack.actions.rejectUnauthorized" is deprecated. Use "xpack.actions.verificationMode" instead, ` + + `with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` + + `and "verificationMode:none" eql to "rejectUnauthorized:false".`, + correctiveActions: { + manualSteps: [ + `Remove "xpack.actions.rejectUnauthorized" from your kibana configs.`, + `Use "xpack.actions.verificationMode" ` + + `with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` + + `and "verificationMode:none" eql to "rejectUnauthorized:false".`, + ], + }, }); } }, @@ -85,7 +106,17 @@ export const config: PluginConfigDescriptor = { if (!!settings?.xpack?.actions?.proxyRejectUnauthorizedCertificates) { addDeprecation({ message: - '`xpack.actions.proxyRejectUnauthorizedCertificates` is deprecated. Use `xpack.actions.proxyVerificationMode` instead, with the setting `proxyVerificationMode:full` eql to `rejectUnauthorized:true`, and `proxyVerificationMode:none` eql to `rejectUnauthorized:false`.', + `"xpack.actions.proxyRejectUnauthorizedCertificates" is deprecated. Use "xpack.actions.proxyVerificationMode" instead, ` + + `with the setting "proxyVerificationMode:full" eql to "rejectUnauthorized:true",` + + `and "proxyVerificationMode:none" eql to "rejectUnauthorized:false".`, + correctiveActions: { + manualSteps: [ + `Remove "xpack.actions.proxyRejectUnauthorizedCertificates" from your kibana configs.`, + `Use "xpack.actions.proxyVerificationMode" ` + + `with the setting "proxyVerificationMode:full" eql to "rejectUnauthorized:true",` + + `and "proxyVerificationMode:none" eql to "rejectUnauthorized:false".`, + ], + }, }); } }, diff --git a/x-pack/plugins/banners/server/config.ts b/x-pack/plugins/banners/server/config.ts index 5ee01ec2b0b19..37b4c57fc2ce1 100644 --- a/x-pack/plugins/banners/server/config.ts +++ b/x-pack/plugins/banners/server/config.ts @@ -45,6 +45,12 @@ export const config: PluginConfigDescriptor = { if (pluginConfig?.placement === 'header') { addDeprecation({ message: 'The `header` value for xpack.banners.placement has been replaced by `top`', + correctiveActions: { + manualSteps: [ + `Remove "xpack.banners.placement: header" from your kibana configs.`, + `Add "xpack.banners.placement: to" to your kibana configs instead.`, + ], + }, }); return { set: [{ path: `${fromPath}.placement`, value: 'top' }], diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index a38f81d72b78f..ac1c2a0d7ac10 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -54,6 +54,11 @@ export const deprecations = ({ if (emailNotificationsEnabled && !updatedKey && !legacyKey) { addDeprecation({ message: `Config key [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] will be required for email notifications to work in 8.0."`, + correctiveActions: { + manualSteps: [ + `Add [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] to your kibana configs."`, + ], + }, }); } return config; @@ -64,10 +69,16 @@ export const deprecations = ({ if (es.username === 'elastic') { addDeprecation({ message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, + correctiveActions: { + manualSteps: [`Replace [${fromPath}.username] from "elastic" to "kibana_system".`], + }, }); } else if (es.username === 'kibana') { addDeprecation({ message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, + correctiveActions: { + manualSteps: [`Replace [${fromPath}.username] from "kibana" to "kibana_system".`], + }, }); } } @@ -79,10 +90,20 @@ export const deprecations = ({ if (ssl.key !== undefined && ssl.certificate === undefined) { addDeprecation({ message: `Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + correctiveActions: { + manualSteps: [ + `Set [${fromPath}.ssl.certificate] in your kibana configs to enable TLS client authentication to Elasticsearch.`, + ], + }, }); } else if (ssl.certificate !== undefined && ssl.key === undefined) { addDeprecation({ message: `Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + correctiveActions: { + manualSteps: [ + `Set [${fromPath}.ssl.key] in your kibana configs to enable TLS client authentication to Elasticsearch.`, + ], + }, }); } } diff --git a/x-pack/plugins/reporting/server/config/index.test.ts b/x-pack/plugins/reporting/server/config/index.test.ts index 327b03d679cae..b9665759f9f52 100644 --- a/x-pack/plugins/reporting/server/config/index.test.ts +++ b/x-pack/plugins/reporting/server/config/index.test.ts @@ -45,7 +45,7 @@ describe('deprecations', () => { const { messages } = applyReportingDeprecations({ roles: { enabled: true } }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.reporting.roles\\" is deprecated. Granting reporting privilege through a \\"reporting_user\\" role will not be supported starting in 8.0. Please set 'xpack.reporting.roles.enabled' to 'false' and grant reporting privileges to users using Kibana application privileges **Management > Security > Roles**.", + "\\"xpack.reporting.roles\\" is deprecated. Granting reporting privilege through a \\"reporting_user\\" role will not be supported starting in 8.0. Please set \\"xpack.reporting.roles.enabled\\" to \\"false\\" and grant reporting privileges to users using Kibana application privileges **Management > Security > Roles**.", ] `); }); diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index 8927bd8ee94d5..d0c743f859b3c 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -28,6 +28,12 @@ export const config: PluginConfigDescriptor = { if (reporting?.index) { addDeprecation({ message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, + correctiveActions: { + manualSteps: [ + `If you rely on this setting to achieve multitenancy you should use Spaces, cross-cluster replication, or cross-cluster search instead.`, + `To migrate to Spaces, we encourage using saved object management to export your saved objects from a tenant into the default tenant in a space.`, + ], + }, }); } @@ -35,8 +41,15 @@ export const config: PluginConfigDescriptor = { addDeprecation({ message: `"${fromPath}.roles" is deprecated. Granting reporting privilege through a "reporting_user" role will not be supported ` + - `starting in 8.0. Please set 'xpack.reporting.roles.enabled' to 'false' and grant reporting privileges to users ` + + `starting in 8.0. Please set "xpack.reporting.roles.enabled" to "false" and grant reporting privileges to users ` + `using Kibana application privileges **Management > Security > Roles**.`, + correctiveActions: { + manualSteps: [ + `Set 'xpack.reporting.roles.enabled' to 'false' in your kibana configs.`, + `Grant reporting privileges to users using Kibana application privileges` + + `under **Management > Security > Roles**.`, + ], + }, }); } }, diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index a233d760359e5..d2c75fd2331b9 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -243,7 +243,7 @@ describe('Config Deprecations', () => { expect(migrated).toEqual(config); expect(messages).toMatchInlineSnapshot(` Array [ - "Defining \`xpack.security.authc.providers\` as an array of provider types is deprecated. Use extended \`object\` format instead.", + "Defining \\"xpack.security.authc.providers\\" as an array of provider types is deprecated. Use extended \\"object\\" format instead.", ] `); }); @@ -262,7 +262,7 @@ describe('Config Deprecations', () => { expect(migrated).toEqual(config); expect(messages).toMatchInlineSnapshot(` Array [ - "Defining \`xpack.security.authc.providers\` as an array of provider types is deprecated. Use extended \`object\` format instead.", + "Defining \\"xpack.security.authc.providers\\" as an array of provider types is deprecated. Use extended \\"object\\" format instead.", "Enabling both \`basic\` and \`token\` authentication providers in \`xpack.security.authc.providers\` is deprecated. Login page will only use \`token\` provider.", ] `); diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index f99f3450417f2..a12f2f738f14c 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -27,7 +27,13 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ if (Array.isArray(settings?.xpack?.security?.authc?.providers)) { addDeprecation({ message: - 'Defining `xpack.security.authc.providers` as an array of provider types is deprecated. Use extended `object` format instead.', + `Defining "xpack.security.authc.providers" as an array of provider types is deprecated. ` + + `Use extended "object" format instead.`, + correctiveActions: { + manualSteps: [ + `Use the extended object format for "xpack.security.authc.providers" in your Kibana configuration.`, + ], + }, }); } }, @@ -47,6 +53,11 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ addDeprecation({ message: 'Enabling both `basic` and `token` authentication providers in `xpack.security.authc.providers` is deprecated. Login page will only use `token` provider.', + correctiveActions: { + manualSteps: [ + 'Remove either the `basic` or `token` auth provider in "xpack.security.authc.providers" from your Kibana configuration.', + ], + }, }); } }, @@ -59,6 +70,11 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ addDeprecation({ message: '`xpack.security.authc.providers.saml..maxRedirectURLSize` is deprecated and is no longer used', + correctiveActions: { + manualSteps: [ + `Remove "xpack.security.authc.providers.saml..maxRedirectURLSize" from your Kibana configuration.`, + ], + }, }); } }, @@ -68,6 +84,12 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ message: 'Disabling the security plugin (`xpack.security.enabled`) will not be supported in the next major version (8.0). ' + 'To turn off security features, disable them in Elasticsearch instead.', + correctiveActions: { + manualSteps: [ + `Remove "xpack.security.enabled" from your Kibana configuration.`, + `To turn off security features, disable them in Elasticsearch instead.`, + ], + }, }); } }, diff --git a/x-pack/plugins/spaces/server/config.ts b/x-pack/plugins/spaces/server/config.ts index 2ad8ed654ebb6..2a2f363d769f7 100644 --- a/x-pack/plugins/spaces/server/config.ts +++ b/x-pack/plugins/spaces/server/config.ts @@ -28,6 +28,9 @@ const disabledDeprecation: ConfigDeprecation = (config, fromPath, addDeprecation if (config.xpack?.spaces?.enabled === false) { addDeprecation({ message: `Disabling the Spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)`, + correctiveActions: { + manualSteps: [`Remove "xpack.spaces.enabled: false" from your Kibana configuration`], + }, }); } }; diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 155b8246905d1..80f0e298a8ac3 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -38,11 +38,23 @@ export const config: PluginConfigDescriptor = { addDeprecation({ documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, + correctiveActions: { + manualSteps: [ + `If you rely on this setting to achieve multitenancy you should use Spaces, cross-cluster replication, or cross-cluster search instead.`, + `To migrate to Spaces, we encourage using saved object management to export your saved objects from a tenant into the default tenant in a space.`, + ], + }, }); } if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { addDeprecation({ message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`, + correctiveActions: { + manualSteps: [ + `Maximum allowed value of "${fromPath}.max_workers" is ${MAX_WORKERS_LIMIT}.` + + `Replace "${fromPath}.max_workers: ${taskManager?.max_workers}" with (${MAX_WORKERS_LIMIT}).`, + ], + }, }); } }, diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview.test.ts index f3f76c3a6688e..85efaf38f32a7 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview.test.ts @@ -45,7 +45,7 @@ describe('Overview page', () => { const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [ { - correctiveActions: {}, + correctiveActions: { manualSteps: ['test-step'] }, domainId: 'xpack.spaces', level: 'critical', message: