From f37c576c74563233ed2343f09db055caaa44693d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 22 Jan 2024 14:43:32 -0700 Subject: [PATCH] [Maps] ESQL geo_shape support (#175156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/elastic/elasticsearch/pull/104269 adds geo_shape support to ESQL This PR updates maps ESQL source to support geo_shape column type Screenshot 2024-01-18 at 1 15 31 PM --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../esql_source/create_source_editor.test.tsx | 93 +++++++++++++++++++ .../esql_source/create_source_editor.tsx | 25 +++-- .../sources/esql_source/esql_source.test.ts | 51 ++++++++++ .../sources/esql_source/esql_source.tsx | 20 +++- .../sources/esql_source/esql_utils.test.ts | 22 +++++ .../classes/sources/esql_source/esql_utils.ts | 5 +- 6 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx create mode 100644 x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts create mode 100644 x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.test.ts diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx new file mode 100644 index 000000000000000..397305db2a928fa --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx @@ -0,0 +1,93 @@ +/* + * 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 { render, waitFor } from '@testing-library/react'; +import { CreateSourceEditor } from './create_source_editor'; + +jest.mock('../../../kibana_services', () => { + const mockDefaultDataView = { + fields: [ + { + name: 'location', + type: 'geo_point', + }, + { + name: '@timestamp', + type: 'date', + }, + ], + timeFieldName: '@timestamp', + getIndexPattern: () => { + return 'logs'; + }, + }; + const mockDataView = { + fields: [ + { + name: 'geometry', + type: 'geo_shape', + }, + ], + getIndexPattern: () => { + return 'world_countries'; + }, + }; + return { + getIndexPatternService() { + return { + get: async () => { + return mockDataView; + }, + getDefaultDataView: async () => { + return mockDefaultDataView; + }, + }; + }, + }; +}); + +describe('CreateSourceEditor', () => { + test('should preview default data view on load', async () => { + const onSourceConfigChange = jest.fn(); + render(); + await waitFor(() => + expect(onSourceConfigChange).toBeCalledWith({ + columns: [ + { + name: 'location', + type: 'geo_point', + }, + ], + dateField: '@timestamp', + esql: 'from logs | keep location | limit 10000', + }) + ); + }); + + test('should preview requested data view on load when mostCommonDataViewId prop provided', async () => { + const onSourceConfigChange = jest.fn(); + render( + + ); + await waitFor(() => + expect(onSourceConfigChange).toBeCalledWith({ + columns: [ + { + name: 'geometry', + type: 'geo_shape', + }, + ], + dateField: undefined, + esql: 'from world_countries | keep geometry | limit 10000', + }) + ); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx index 20670e0121c72c2..3ac0b8a1ea4e36a 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx @@ -7,11 +7,12 @@ import React, { useEffect, useState } from 'react'; import { EuiSkeletonText } from '@elastic/eui'; +import { DataViewField } from '@kbn/data-views-plugin/public'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import type { ESQLSourceDescriptor } from '../../../../common/descriptor_types'; import { getIndexPatternService } from '../../../kibana_services'; import { ESQLEditor } from './esql_editor'; -import { ESQL_GEO_POINT_TYPE } from './esql_utils'; +import { ESQL_GEO_POINT_TYPE, ESQL_GEO_SHAPE_TYPE } from './esql_utils'; interface Props { mostCommonDataViewId?: string; @@ -39,12 +40,17 @@ export function CreateSourceEditor(props: Props) { } if (dataView) { - let geoField: string | undefined; + let geoField: DataViewField | undefined; const initialDateFields: string[] = []; for (let i = 0; i < dataView.fields.length; i++) { const field = dataView.fields[i]; - if (!geoField && ES_GEO_FIELD_TYPE.GEO_POINT === field.type) { - geoField = field.name; + if ( + !geoField && + [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE].includes( + field.type as ES_GEO_FIELD_TYPE + ) + ) { + geoField = field; } else if ('date' === field.type) { initialDateFields.push(field.name); } @@ -57,14 +63,19 @@ export function CreateSourceEditor(props: Props) { } else if (initialDateFields.length) { initialDateField = initialDateFields[0]; } - const initialEsql = `from ${dataView.getIndexPattern()} | keep ${geoField} | limit 10000`; + const initialEsql = `from ${dataView.getIndexPattern()} | keep ${ + geoField.name + } | limit 10000`; setDateField(initialDateField); setEsql(initialEsql); props.onSourceConfigChange({ columns: [ { - name: geoField, - type: ESQL_GEO_POINT_TYPE, + name: geoField.name, + type: + geoField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE + ? ESQL_GEO_SHAPE_TYPE + : ESQL_GEO_POINT_TYPE, }, ], dateField: initialDateField, diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts new file mode 100644 index 000000000000000..42446ad8cb7e460 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts @@ -0,0 +1,51 @@ +/* + * 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 { ESQLSource } from './esql_source'; +import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; + +describe('getSupportedShapeTypes', () => { + test('should return point for geo_point column', async () => { + const descriptor = ESQLSource.createDescriptor({ + esql: 'from kibana_sample_data_logs | keep geo.coordinates | limit 10000', + columns: [ + { + name: 'geo.coordinates', + type: 'geo_point', + }, + ], + }); + const esqlSource = new ESQLSource(descriptor); + expect(await esqlSource.getSupportedShapeTypes()).toEqual([VECTOR_SHAPE_TYPE.POINT]); + }); + + test('should return all geometry types for geo_shape column', async () => { + const descriptor = ESQLSource.createDescriptor({ + esql: 'from world_countries | keep geometry | limit 10000', + columns: [ + { + name: 'geometry', + type: 'geo_shape', + }, + ], + }); + const esqlSource = new ESQLSource(descriptor); + expect(await esqlSource.getSupportedShapeTypes()).toEqual([ + VECTOR_SHAPE_TYPE.POINT, + VECTOR_SHAPE_TYPE.LINE, + VECTOR_SHAPE_TYPE.POLYGON, + ]); + }); + + test('should fallback to point when geometry column can not be found', async () => { + const descriptor = ESQLSource.createDescriptor({ + esql: 'from world_countries | keep geometry | limit 10000', + }); + const esqlSource = new ESQLSource(descriptor); + expect(await esqlSource.getSupportedShapeTypes()).toEqual([VECTOR_SHAPE_TYPE.POINT]); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx index b92ccd1fb82f959..c424aa5733b6824 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx @@ -31,7 +31,12 @@ import type { IField } from '../../fields/field'; import { InlineField } from '../../fields/inline_field'; import { getData, getUiSettings } from '../../../kibana_services'; import { convertToGeoJson } from './convert_to_geojson'; -import { getFieldType, getGeometryColumnIndex } from './esql_utils'; +import { + getFieldType, + getGeometryColumnIndex, + ESQL_GEO_POINT_TYPE, + ESQL_GEO_SHAPE_TYPE, +} from './esql_utils'; import { UpdateSourceEditor } from './update_source_editor'; type ESQLSourceSyncMeta = Pick< @@ -114,7 +119,18 @@ export class ESQLSource extends AbstractVectorSource implements IVectorSource { } async getSupportedShapeTypes() { - return [VECTOR_SHAPE_TYPE.POINT]; + let geomtryColumnType = ESQL_GEO_POINT_TYPE; + try { + const index = getGeometryColumnIndex(this._descriptor.columns); + if (index > -1) { + geomtryColumnType = this._descriptor.columns[index].type; + } + } catch (error) { + // errors for missing geometry columns surfaced in UI by data loading + } + return geomtryColumnType === ESQL_GEO_SHAPE_TYPE + ? [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON] + : [VECTOR_SHAPE_TYPE.POINT]; } supportsJoins() { diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.test.ts b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.test.ts new file mode 100644 index 000000000000000..b2441770ec8e75c --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isGeometryColumn } from './esql_utils'; + +describe('isGeometryColumn', () => { + test('should return true for geo_point columns', () => { + expect(isGeometryColumn({ name: 'myColumn', type: 'geo_point' })).toBe(true); + }); + + test('should return true for geo_shape columns', () => { + expect(isGeometryColumn({ name: 'myColumn', type: 'geo_shape' })).toBe(true); + }); + + test('should return false for non-geometry columns', () => { + expect(isGeometryColumn({ name: 'myColumn', type: 'string' })).toBe(false); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts index 79cd2aaf70b5006..865db6dc14faf10 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts @@ -12,6 +12,7 @@ import type { ESQLColumn } from '@kbn/es-types'; import { getData, getIndexPatternService } from '../../../kibana_services'; export const ESQL_GEO_POINT_TYPE = 'geo_point'; +export const ESQL_GEO_SHAPE_TYPE = 'geo_shape'; const NO_GEOMETRY_COLUMN_ERROR_MSG = i18n.translate( 'xpack.maps.source.esql.noGeometryColumnErrorMsg', @@ -20,8 +21,8 @@ const NO_GEOMETRY_COLUMN_ERROR_MSG = i18n.translate( } ); -function isGeometryColumn(column: ESQLColumn) { - return column.type === ESQL_GEO_POINT_TYPE; +export function isGeometryColumn(column: ESQLColumn) { + return [ESQL_GEO_POINT_TYPE, ESQL_GEO_SHAPE_TYPE].includes(column.type); } export function verifyGeometryColumn(columns: ESQLColumn[]) {