import { i18n } from '@kbn/i18n' ; import uuid from 'uuid/v4' ; import { Filter } from '@kbn/es-query' ; import { IndexPatternField , IndexPattern , ISearchSource } from 'src/plugins/data/public' ; import type { Query } from 'src/plugins/data/common' ; import { AbstractVectorSource , BoundsRequestMeta } from '../vector_source' ; import { getAutocompleteService , getIndexPatternService , getTimeFilter , getSearchService , } from '../../../kibana_services' ; import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters' ; import { createExtentFilter } from '../../../../common/elasticsearch_util' ; import { copyPersistentState } from '../../../reducers/copy_persistent_state' ; import { DataRequestAbortError } from '../../util/data_request' ; import { expandToTileBoundaries } from '../../util/geo_tile_utils' ; import { IVectorSource } from '../vector_source' ; import { TimeRange } from '../../../../../../../src/plugins/data/common' ; import { AbstractESSourceDescriptor , AbstractSourceDescriptor , DynamicStylePropertyOptions , MapExtent , VectorJoinSourceRequestMeta , VectorSourceRequestMeta , } from '../../../../common/descriptor_types' ; import { IVectorStyle } from '../../styles/vector/vector_style' ; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property' ; import { IField } from '../../fields/field' ; import { FieldFormatter } from '../../../../common/constants' ; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters' ; import { isValidStringConfig } from '../../util/valid_string_config' ; export function isSearchSourceAbortError ( error ) { return error . name === 'AbortError' ; } export interface IESSource extends IVectorSource { isESSource ( ) : true ; getId ( ) : string ; getIndexPattern ( ) : Promise < IndexPattern > ; getIndexPatternId ( ) : string ; getGeoFieldName ( ) : string ; loadStylePropsMeta ( { layerName , style , dynamicStyleProps , registerCancelCallback , sourceQuery , timeFilters , searchSessionId , } ) : Promise < object > ; } export class AbstractESSource extends AbstractVectorSource implements IESSource { indexPattern ? : IndexPattern ; readonly _descriptor : AbstractESSourceDescriptor ; static createDescriptor ( descriptor ) { if ( ! isValidStringConfig ( descriptor . indexPatternId ) ) { throw new Error ( 'Cannot create AbstractESSourceDescriptor when indexPatternId is not provided' ) ; } return { ... descriptor , id : isValidStringConfig ( descriptor . id ) ? descriptor . id ! : uuid ( ) , type : isValidStringConfig ( descriptor . type ) ? descriptor . type ! : '' , indexPatternId : descriptor . indexPatternId ! , applyGlobalQuery : typeof descriptor . applyGlobalQuery !== 'undefined' ? descriptor . applyGlobalQuery : true , applyGlobalTime : typeof descriptor . applyGlobalTime !== 'undefined' ? descriptor . applyGlobalTime : true , applyForceRefresh : typeof descriptor . applyForceRefresh !== 'undefined' ? descriptor . applyForceRefresh : true , } ; } constructor ( descriptor , inspectorAdapters ? ) { super ( AbstractESSource . createDescriptor ( descriptor ) , inspectorAdapters ) ; this . _descriptor = descriptor ; } getId ( ) { return this . _descriptor . id ; } getApplyGlobalQuery ( ) { return this . _descriptor . applyGlobalQuery ; } getApplyGlobalTime ( ) { return this . _descriptor . applyGlobalTime ; } getApplyForceRefresh ( ) { return this . _descriptor . applyForceRefresh ; } isFieldAware ( ) { return true ; } isQueryAware ( ) { return true ; } getIndexPatternIds ( ) { return [ this . getIndexPatternId ( ) ] ; } getQueryableIndexPatternIds ( ) { if ( this . getApplyGlobalQuery ( ) ) { return [ this . getIndexPatternId ( ) ] ; } return [ ] ; } isESSource ( ) { return true ; } destroy ( ) { const inspectorAdapters = this . getInspectorAdapters ( ) ; if ( inspectorAdapters ?. requests ) { inspectorAdapters . requests . resetRequest ( this . getId ( ) ) ; } } cloneDescriptor ( ) { const clonedDescriptor = copyPersistentState ( this . _descriptor ) ; clonedDescriptor . id = uuid ( ) ; return clonedDescriptor ; } async _runEsQuery ( { registerCancelCallback , requestDescription , requestId , requestName , searchSessionId , searchSource , } ) { const abortController = new AbortController ( ) ; registerCancelCallback ( ( ) => abortController . abort ( ) ) ; try { const { rawResponse : resp } = await searchSource . fetch$ ( { abortSignal : abortController . signal , sessionId : searchSessionId , legacyHitsTotal : false , inspector : { adapter : this . getInspectorAdapters ( ) ?. requests , id : requestId , title : requestName , description : requestDescription , } , } ) . toPromise ( ) ; return resp ; } catch ( error ) { if ( isSearchSourceAbortError ( error ) ) { throw new DataRequestAbortError ( ) ; } throw new Error ( i18n . translate ( 'xpack.maps.source.esSource.requestFailedErrorMessage' , { defaultMessage : `Elasticsearch search request failed, error: {message}` , values : { message : error . message } , } ) ) ; } } async makeSearchSource ( searchFilters , limit , initialSearchContext ? ) { const indexPattern = await this . getIndexPattern ( ) ; const globalFilters = searchFilters . applyGlobalQuery ? searchFilters . filters : [ ] ; const allFilters = [ ... globalFilters ] ; if ( this . isFilterByMapBounds ( ) && 'buffer' in searchFilters && searchFilters . buffer ) { const geoField = await this . _getGeoField ( ) ; const buffer = this . isGeoGridPrecisionAware ( ) && 'geogridPrecision' in searchFilters && typeof searchFilters . geogridPrecision === 'number' ? expandToTileBoundaries ( searchFilters . buffer , searchFilters . geogridPrecision ) : searchFilters . buffer ; const extentFilter = createExtentFilter ( buffer , [ geoField . name ] ) ; allFilters . push ( extentFilter ) ; } if ( searchFilters . applyGlobalTime && ( await this . isTimeAware ( ) ) ) { const timeRange = searchFilters . timeslice ? { from : new Date ( searchFilters . timeslice . from ) . toISOString ( ) , to : new Date ( searchFilters . timeslice . to ) . toISOString ( ) , mode : 'absolute' as 'absolute' , } : searchFilters . timeFilters ; const filter = getTimeFilter ( ) . createFilter ( indexPattern , timeRange ) ; if ( filter ) { allFilters . push ( filter ) ; } } const searchService = getSearchService ( ) ; const searchSource = await searchService . searchSource . create ( initialSearchContext ) ; searchSource . setField ( 'index' , indexPattern ) ; searchSource . setField ( 'size' , limit ) ; searchSource . setField ( 'filter' , allFilters ) ; if ( searchFilters . applyGlobalQuery ) { searchSource . setField ( 'query' , searchFilters . query ) ; } if ( searchFilters . sourceQuery ) { const layerSearchSource = searchService . searchSource . createEmpty ( ) ; layerSearchSource . setField ( 'index' , indexPattern ) ; layerSearchSource . setField ( 'query' , searchFilters . sourceQuery ) ; searchSource . setParent ( layerSearchSource ) ; } return searchSource ; } async getBoundsForFilters ( boundsFilters , registerCancelCallback ) { const searchSource = await this . makeSearchSource ( boundsFilters , 0 ) ; searchSource . setField ( 'trackTotalHits' , false ) ; searchSource . setField ( 'aggs' , { fitToBounds : { geo_bounds : { field : this . getGeoFieldName ( ) , } , } , } ) ; let esBounds ; try { const abortController = new AbortController ( ) ; registerCancelCallback ( ( ) => abortController . abort ( ) ) ; const esResp = await searchSource . fetch ( { abortSignal : abortController . signal , legacyHitsTotal : false , } ) ; if ( ! esResp . aggregations ) { return null ; } const fitToBounds = esResp . aggregations . fitToBounds as { bounds ? : { top_left : { lat : number ; lon : number ; } ; bottom_right : { lat : number ; lon : number ; } ; } ; } ; if ( ! fitToBounds . bounds ) { return null ; } esBounds = fitToBounds . bounds ; } catch ( error ) { if ( error . name === 'AbortError' ) { throw new DataRequestAbortError ( ) ; } return null ; } const minLon = esBounds . top_left . lon ; const maxLon = esBounds . bottom_right . lon ; return { minLon : minLon > maxLon ? minLon - 360 : minLon , maxLon , minLat : esBounds . bottom_right . lat , maxLat : esBounds . top_left . lat , } ; } async isTimeAware ( ) { try { const indexPattern = await this . getIndexPattern ( ) ; const timeField = indexPattern . timeFieldName ; return ! ! timeField ; } catch ( error ) { return false ; } } getIndexPatternId ( ) { return this . _descriptor . indexPatternId ; } getGeoFieldName ( ) { if ( ! this . _descriptor . geoField ) { throw new Error ( 'Should not call' ) ; } return this . _descriptor . geoField ; } async getIndexPattern ( ) { if ( this . indexPattern ) { return this . indexPattern ; } try { this . indexPattern = await getIndexPatternService ( ) . get ( this . getIndexPatternId ( ) ) ; return this . indexPattern ; } catch ( error ) { throw new Error ( getDataViewNotFoundMessage ( this . getIndexPatternId ( ) ) ) ; } } async supportsFitToBounds ( ) { try { const geoField = await this . _getGeoField ( ) ; return ! ! geoField . aggregatable ; } catch ( error ) { return false ; } } async _getGeoField ( ) { const indexPattern = await this . getIndexPattern ( ) ; const geoField = indexPattern . fields . getByName ( this . getGeoFieldName ( ) ) ; if ( ! geoField ) { throw new Error ( i18n . translate ( 'xpack.maps.source.esSource.noGeoFieldErrorMessage' , { defaultMessage : `Data view {indexPatternTitle} no longer contains the geo field {geoField}` , values : { indexPatternTitle : indexPattern . title , geoField : this . getGeoFieldName ( ) } , } ) ) ; } return geoField ; } async getDisplayName ( ) { try { const indexPattern = await this . getIndexPattern ( ) ; return indexPattern . title ; } catch ( error ) { return this . getIndexPatternId ( ) ; } } isBoundsAware ( ) { return true ; } async createFieldFormatter ( field ) { let indexPattern ; try { indexPattern = await this . getIndexPattern ( ) ; } catch ( error ) { return null ; } const fieldFromIndexPattern = indexPattern . fields . getByName ( field . getRootName ( ) ) ; if ( ! fieldFromIndexPattern ) { return null ; } return indexPattern . getFormatterForField ( fieldFromIndexPattern ) . getConverterFor ( 'text' ) ; } async loadStylePropsMeta ( { layerName , style , dynamicStyleProps , registerCancelCallback , sourceQuery , timeFilters , searchSessionId , } ) { const promises = dynamicStyleProps . map ( ( dynamicStyleProp ) => { return dynamicStyleProp . getFieldMetaRequest ( ) ; } ) ; const fieldAggRequests = await Promise . all ( promises ) ; const allAggs = fieldAggRequests . reduce ( ( aggs , fieldAggRequest ) => { return fieldAggRequest ? { ... aggs , ... ( fieldAggRequest as Record < string , any > ) } : aggs ; } , { } ) ; const indexPattern = await this . getIndexPattern ( ) ; const searchService = getSearchService ( ) ; const searchSource = searchService . searchSource . createEmpty ( ) ; searchSource . setField ( 'index' , indexPattern ) ; searchSource . setField ( 'size' , 0 ) ; searchSource . setField ( 'aggs' , allAggs ) ; if ( sourceQuery ) { searchSource . setField ( 'query' , sourceQuery ) ; } if ( style . isTimeAware ( ) && ( await this . isTimeAware ( ) ) ) { const timeFilter = getTimeFilter ( ) . createFilter ( indexPattern , timeFilters ) ; if ( timeFilter ) { searchSource . setField ( 'filter' , [ timeFilter ] ) ; } } const resp = await this . _runEsQuery ( { requestId : `${ this . getId ( ) }_styleMeta` , requestName : i18n . translate ( 'xpack.maps.source.esSource.stylePropsMetaRequestName' , { defaultMessage : '{layerName} - metadata' , values : { layerName } , } ) , searchSource , registerCancelCallback , requestDescription : i18n . translate ( 'xpack.maps.source.esSource.stylePropsMetaRequestDescription' , { defaultMessage : 'Elasticsearch request retrieving field metadata used for calculating symbolization bands.' , } ) , searchSessionId , } ) ; return resp . aggregations ; } getValueSuggestions = async ( field , query ) : Promise < string [ ] > => { try { const indexPattern = await this . getIndexPattern ( ) ; const indexPatternField = indexPattern . fields . getByName ( field . getRootName ( ) ) ! ; return await getAutocompleteService ( ) . getValueSuggestions ( { indexPattern , field : indexPatternField , query , } ) ; } catch ( error ) { console . warn ( `Unable to fetch suggestions for field: ${ field . getRootName ( ) }, query: ${ query }, error: ${ error . message }` ) ; return [ ] ; } } ; }