[Maps] Convert ES-sources to typescript (#81951)

This commit is contained in:
Thomas Neirynck 2020-11-05 08:56:11 -05:00 committed by GitHub
parent f4386fc5b0
commit 051ed13858
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1066 additions and 879 deletions

View file

@ -19,28 +19,29 @@
import { schema, TypeOf } from '@kbn/config-schema';
export const configSchema = schema.object({
includeElasticMapsService: schema.boolean({ defaultValue: true }),
layers: schema.arrayOf(
const layerConfigSchema = schema.object({
url: schema.string(),
format: schema.object({
type: schema.string({ defaultValue: 'geojson' }),
}),
meta: schema.object({
feature_collection_path: schema.string({ defaultValue: 'data' }),
}),
attribution: schema.string(),
name: schema.string(),
fields: schema.arrayOf(
schema.object({
url: schema.string(),
format: schema.object({
type: schema.string({ defaultValue: 'geojson' }),
}),
meta: schema.object({
feature_collection_path: schema.string({ defaultValue: 'data' }),
}),
attribution: schema.string(),
name: schema.string(),
fields: schema.arrayOf(
schema.object({
name: schema.string(),
description: schema.string(),
})
),
}),
{ defaultValue: [] }
description: schema.string(),
})
),
});
export const configSchema = schema.object({
includeElasticMapsService: schema.boolean({ defaultValue: true }),
layers: schema.arrayOf(layerConfigSchema, { defaultValue: [] }),
});
export type LayerConfig = TypeOf<typeof layerConfigSchema>;
export type ConfigSchema = TypeOf<typeof configSchema>;

View file

@ -60,11 +60,6 @@ export enum LAYER_TYPE {
TILED_VECTOR = 'TILED_VECTOR', // similar to a regular vector-layer, but it consumes the data as .mvt tilea iso GeoJson. It supports similar ad-hoc configurations like a regular vector layer (E.g. using IVectorStyle), although there is some loss of functionality e.g. does not support term joining
}
export enum SORT_ORDER {
ASC = 'asc',
DESC = 'desc',
}
export enum SOURCE_TYPES {
EMS_TMS = 'EMS_TMS',
EMS_FILE = 'EMS_FILE',
@ -237,6 +232,11 @@ export enum SCALING_TYPES {
MVT = 'MVT',
}
export enum FORMAT_TYPE {
GEOJSON = 'geojson',
TOPOJSON = 'topojson',
}
export enum MVT_FIELD_TYPE {
STRING = 'String',
NUMBER = 'Number',

View file

@ -5,7 +5,9 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants';
import { Query } from 'src/plugins/data/public';
import { SortDirection } from 'src/plugins/data/common/search';
import { RENDER_AS, SCALING_TYPES } from '../constants';
import { MapExtent, MapQuery } from './map_descriptor';
import { Filter, TimeRange } from '../../../../../src/plugins/data/common';
@ -22,7 +24,7 @@ export type MapFilters = {
type ESSearchSourceSyncMeta = {
sortField: string;
sortOrder: SORT_ORDER;
sortOrder: SortDirection;
scalingType: SCALING_TYPES;
topHitsSplitField: string;
topHitsSize: number;
@ -45,7 +47,7 @@ export type VectorSourceRequestMeta = MapFilters & {
export type VectorJoinSourceRequestMeta = MapFilters & {
applyGlobalQuery: boolean;
fieldNames: string[];
sourceQuery: MapQuery;
sourceQuery?: Query;
};
export type VectorStyleRequestMeta = MapFilters & {

View file

@ -7,14 +7,8 @@
import { FeatureCollection } from 'geojson';
import { Query } from 'src/plugins/data/public';
import {
AGG_TYPE,
GRID_RESOLUTION,
RENDER_AS,
SORT_ORDER,
SCALING_TYPES,
MVT_FIELD_TYPE,
} from '../constants';
import { SortDirection } from 'src/plugins/data/common/search';
import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SCALING_TYPES, MVT_FIELD_TYPE } from '../constants';
export type AttributionDescriptor = {
attributionText?: string;
@ -40,6 +34,7 @@ export type EMSFileSourceDescriptor = AbstractSourceDescriptor & {
export type AbstractESSourceDescriptor = AbstractSourceDescriptor & {
// id: UUID
id: string;
indexPatternId: string;
geoField?: string;
};
@ -55,18 +50,20 @@ export type AbstractESAggSourceDescriptor = AbstractESSourceDescriptor & {
};
export type ESGeoGridSourceDescriptor = AbstractESAggSourceDescriptor & {
requestType?: RENDER_AS;
resolution?: GRID_RESOLUTION;
geoField: string;
requestType: RENDER_AS;
resolution: GRID_RESOLUTION;
};
export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & {
geoField: string;
filterByMapBounds?: boolean;
tooltipProperties?: string[];
sortField?: string;
sortOrder?: SORT_ORDER;
sortField: string;
sortOrder: SortDirection;
scalingType: SCALING_TYPES;
topHitsSplitField?: string;
topHitsSize?: number;
topHitsSplitField: string;
topHitsSize: number;
};
export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & {
@ -76,7 +73,7 @@ export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & {
export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & {
indexPatternTitle?: string;
term?: string; // term field name
term: string; // term field name
whereQuery?: Query;
};

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { FeatureCollection, GeoJsonProperties } from 'geojson';
import { FeatureCollection, GeoJsonProperties, Polygon } from 'geojson';
import { MapExtent } from '../descriptor_types';
import { ES_GEO_FIELD_TYPE } from '../constants';
import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent;
@ -23,3 +23,31 @@ export function hitsToGeoJson(
geoFieldType: ES_GEO_FIELD_TYPE,
epochMillisFields: string[]
): FeatureCollection;
export interface ESBBox {
top_left: number[];
bottom_right: number[];
}
export interface ESGeoBoundingBoxFilter {
geo_bounding_box: {
[geoFieldName: string]: ESBBox;
};
}
export interface ESPolygonFilter {
geo_shape: {
[geoFieldName: string]: {
shape: Polygon;
relation: ES_SPATIAL_RELATIONS.INTERSECTS;
};
};
}
export function createExtentFilter(
mapExtent: MapExtent,
geoFieldName: string,
geoFieldType: ES_GEO_FIELD_TYPE
): ESPolygonFilter | ESGeoBoundingBoxFilter;
export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBox;

View file

@ -8,7 +8,10 @@ import _ from 'lodash';
import { IndexPattern, IFieldType } from '../../../../../src/plugins/data/common';
import { TOP_TERM_PERCENTAGE_SUFFIX } from '../constants';
export function getField(indexPattern: IndexPattern, fieldName: string) {
export type BucketProperties = Record<string | number, unknown>;
export type PropertiesMap = Map<string, BucketProperties>;
export function getField(indexPattern: IndexPattern, fieldName: string): IFieldType {
const field = indexPattern.fields.getByName(fieldName);
if (!field) {
throw new Error(
@ -33,9 +36,10 @@ export function addFieldToDSL(dsl: object, field: IFieldType) {
};
}
export type BucketProperties = Record<string | number, unknown>;
export function extractPropertiesFromBucket(bucket: any, ignoreKeys: string[] = []) {
export function extractPropertiesFromBucket(
bucket: any,
ignoreKeys: string[] = []
): BucketProperties {
const properties: BucketProperties = {};
for (const key in bucket) {
if (ignoreKeys.includes(key) || !bucket.hasOwnProperty(key)) {

View file

@ -5,7 +5,8 @@
*/
import _ from 'lodash';
import { SOURCE_TYPES, SORT_ORDER } from '../constants';
import { SOURCE_TYPES } from '../constants';
import { SortDirection } from '../../../../../src/plugins/data/common/search';
function isEsDocumentSource(layerDescriptor) {
const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type');
@ -23,7 +24,7 @@ export function topHitsTimeToSort({ attributes }) {
if (_.has(layerDescriptor, 'sourceDescriptor.topHitsTimeField')) {
layerDescriptor.sourceDescriptor.sortField =
layerDescriptor.sourceDescriptor.topHitsTimeField;
layerDescriptor.sourceDescriptor.sortOrder = SORT_ORDER.DESC;
layerDescriptor.sourceDescriptor.sortOrder = SortDirection.desc;
delete layerDescriptor.sourceDescriptor.topHitsTimeField;
}
}

View file

@ -5,12 +5,12 @@
*/
import { IField, AbstractField } from './field';
import { IKibanaRegionSource } from '../sources/kibana_regionmap_source/kibana_regionmap_source';
import { KibanaRegionmapSource } from '../sources/kibana_regionmap_source/kibana_regionmap_source';
import { FIELD_ORIGIN } from '../../../common/constants';
import { IVectorSource } from '../sources/vector_source';
export class KibanaRegionField extends AbstractField implements IField {
private readonly _source: IKibanaRegionSource;
private readonly _source: KibanaRegionmapSource;
constructor({
fieldName,
@ -18,7 +18,7 @@ export class KibanaRegionField extends AbstractField implements IField {
origin,
}: {
fieldName: string;
source: IKibanaRegionSource;
source: KibanaRegionmapSource;
origin: FIELD_ORIGIN;
}) {
super({ fieldName, origin });

View file

@ -5,19 +5,20 @@
*/
import { Feature, GeoJsonProperties } from 'geojson';
import { IESTermSource } from '../sources/es_term_source';
import { IJoin, PropertiesMap } from './join';
import { ESTermSource } from '../sources/es_term_source';
import { IJoin } from './join';
import { JoinDescriptor } from '../../../common/descriptor_types';
import { ISource } from '../sources/source';
import { ITooltipProperty } from '../tooltips/tooltip_property';
import { IField } from '../fields/field';
import { PropertiesMap } from '../../../common/elasticsearch_util';
export class InnerJoin implements IJoin {
constructor(joinDescriptor: JoinDescriptor, leftSource: ISource);
destroy: () => void;
getRightJoinSource(): IESTermSource;
getRightJoinSource(): ESTermSource;
toDescriptor(): JoinDescriptor;

View file

@ -5,18 +5,16 @@
*/
import { Feature, GeoJsonProperties } from 'geojson';
import { IESTermSource } from '../sources/es_term_source';
import { ESTermSource } from '../sources/es_term_source';
import { JoinDescriptor } from '../../../common/descriptor_types';
import { ITooltipProperty } from '../tooltips/tooltip_property';
import { IField } from '../fields/field';
import { BucketProperties } from '../../../common/elasticsearch_util';
export type PropertiesMap = Map<string, BucketProperties>;
import { PropertiesMap } from '../../../common/elasticsearch_util';
export interface IJoin {
destroy: () => void;
getRightJoinSource: () => IESTermSource;
getRightJoinSource: () => ESTermSource;
toDescriptor: () => JoinDescriptor;

View file

@ -25,7 +25,6 @@ import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_so
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
import { IVectorLayer } from '../vector_layer/vector_layer';
import { IESSource } from '../../sources/es_source';
import { IESAggSource } from '../../sources/es_agg_source';
import { ISource } from '../../sources/source';
import { DataRequestContext } from '../../../actions';
import { DataRequestAbortError } from '../../util/data_request';
@ -36,9 +35,11 @@ import {
StylePropertyOptions,
LayerDescriptor,
VectorLayerDescriptor,
VectorSourceRequestMeta,
} from '../../../../common/descriptor_types';
import { IVectorSource } from '../../sources/vector_source';
import { LICENSED_FEATURES } from '../../../licensed_features';
import { ESSearchSource } from '../../sources/es_search_source/es_search_source';
const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID';
@ -50,7 +51,7 @@ function getAggType(dynamicProperty: IDynamicStyleProperty<DynamicStylePropertyO
return dynamicProperty.isOrdinal() ? AGG_TYPE.AVG : AGG_TYPE.TERMS;
}
function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle): IESAggSource {
function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle): ESGeoGridSource {
const clusterSourceDescriptor = ESGeoGridSource.createDescriptor({
indexPatternId: documentSource.getIndexPatternId(),
geoField: documentSource.getGeoFieldName(),
@ -75,7 +76,7 @@ function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle
function getClusterStyleDescriptor(
documentStyle: IVectorStyle,
clusterSource: IESAggSource
clusterSource: ESGeoGridSource
): VectorStyleDescriptor {
const defaultDynamicProperties = getDefaultDynamicProperties();
const clusterStyleDescriptor: VectorStyleDescriptor = {
@ -177,9 +178,9 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
}
private readonly _isClustered: boolean;
private readonly _clusterSource: IESAggSource;
private readonly _clusterSource: ESGeoGridSource;
private readonly _clusterStyle: IVectorStyle;
private readonly _documentSource: IESSource;
private readonly _documentSource: ESSearchSource;
private readonly _documentStyle: IVectorStyle;
constructor(options: BlendedVectorLayerArguments) {
@ -188,7 +189,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
joins: [],
});
this._documentSource = this._source as IESSource; // VectorLayer constructor sets _source as document source
this._documentSource = this._source as ESSearchSource; // VectorLayer constructor sets _source as document source
this._documentStyle = this._style as IVectorStyle; // VectorLayer constructor sets _style as document source
this._clusterSource = getClusterSource(this._documentSource, this._documentStyle);
@ -279,7 +280,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
async syncData(syncContext: DataRequestContext) {
const dataRequestId = ACTIVE_COUNT_DATA_ID;
const requestToken = Symbol(`layer-active-count:${this.getId()}`);
const searchFilters = this._getSearchFilters(
const searchFilters: VectorSourceRequestMeta = this._getSearchFilters(
syncContext.dataFilters,
this.getSource(),
this.getCurrentStyle()

View file

@ -415,7 +415,7 @@ export class AbstractLayer implements ILayer {
return this._descriptor.query ? this._descriptor.query : null;
}
async getImmutableSourceProperties() {
async getImmutableSourceProperties(): Promise<ImmutableSourceProperty[]> {
const source = this.getSource();
return await source.getImmutableProperties();
}

View file

@ -175,6 +175,7 @@ describe('createLayerDescriptor', () => {
query: 'processor.event:"transaction"',
},
sourceDescriptor: {
applyGlobalQuery: true,
geoField: 'client.geo.location',
id: '12345',
indexPatternId: 'apm_static_index_pattern_id',
@ -216,6 +217,7 @@ describe('createLayerDescriptor', () => {
query: 'processor.event:"transaction"',
},
sourceDescriptor: {
applyGlobalQuery: true,
geoField: 'client.geo.location',
id: '12345',
indexPatternId: 'apm_static_index_pattern_id',

View file

@ -21,7 +21,7 @@ jest.mock('uuid/v4', () => {
import { createSecurityLayerDescriptors } from './create_layer_descriptors';
describe('createLayerDescriptor', () => {
test('amp index', () => {
test('apm index', () => {
expect(createSecurityLayerDescriptors('id', 'apm-*-transaction*')).toEqual([
{
__dataRequests: [],
@ -32,6 +32,7 @@ describe('createLayerDescriptor', () => {
maxZoom: 24,
minZoom: 0,
sourceDescriptor: {
applyGlobalQuery: true,
filterByMapBounds: true,
geoField: 'client.geo.location',
id: '12345',
@ -138,6 +139,7 @@ describe('createLayerDescriptor', () => {
maxZoom: 24,
minZoom: 0,
sourceDescriptor: {
applyGlobalQuery: true,
filterByMapBounds: true,
geoField: 'server.geo.location',
id: '12345',
@ -244,6 +246,7 @@ describe('createLayerDescriptor', () => {
maxZoom: 24,
minZoom: 0,
sourceDescriptor: {
applyGlobalQuery: true,
destGeoField: 'server.geo.location',
id: '12345',
indexPatternId: 'id',
@ -362,6 +365,7 @@ describe('createLayerDescriptor', () => {
maxZoom: 24,
minZoom: 0,
sourceDescriptor: {
applyGlobalQuery: true,
filterByMapBounds: true,
geoField: 'source.geo.location',
id: '12345',
@ -468,6 +472,7 @@ describe('createLayerDescriptor', () => {
maxZoom: 24,
minZoom: 0,
sourceDescriptor: {
applyGlobalQuery: true,
filterByMapBounds: true,
geoField: 'destination.geo.location',
id: '12345',
@ -574,6 +579,7 @@ describe('createLayerDescriptor', () => {
maxZoom: 24,
minZoom: 0,
sourceDescriptor: {
applyGlobalQuery: true,
destGeoField: 'destination.geo.location',
id: '12345',
indexPatternId: 'id',

View file

@ -46,18 +46,20 @@ import {
DynamicStylePropertyOptions,
MapFilters,
MapQuery,
VectorJoinSourceRequestMeta,
VectorLayerDescriptor,
VectorSourceRequestMeta,
VectorStyleRequestMeta,
} from '../../../../common/descriptor_types';
import { IVectorSource } from '../../sources/vector_source';
import { CustomIconAndTooltipContent, ILayer } from '../layer';
import { IJoin, PropertiesMap } from '../../joins/join';
import { IJoin } from '../../joins/join';
import { IField } from '../../fields/field';
import { DataRequestContext } from '../../../actions';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
import { IESSource } from '../../sources/es_source';
import { PropertiesMap } from '../../../../common/elasticsearch_util';
interface SourceResult {
refreshed: boolean;
@ -239,7 +241,7 @@ export class VectorLayer extends AbstractLayer {
}
const requestToken = Symbol(`${SOURCE_BOUNDS_DATA_REQUEST_ID}-${this.getId()}`);
const searchFilters = this._getSearchFilters(
const searchFilters: VectorSourceRequestMeta = this._getSearchFilters(
dataFilters,
this.getSource(),
this.getCurrentStyle()
@ -324,7 +326,7 @@ export class VectorLayer extends AbstractLayer {
const joinSource = join.getRightJoinSource();
const sourceDataId = join.getSourceDataRequestId();
const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`);
const searchFilters = {
const searchFilters: VectorJoinSourceRequestMeta = {
...dataFilters,
fieldNames: joinSource.getFieldNames(),
sourceQuery: joinSource.getWhereQuery(),
@ -386,9 +388,11 @@ export class VectorLayer extends AbstractLayer {
source: IVectorSource,
style: IVectorStyle
): VectorSourceRequestMeta {
const styleFieldNames =
style.getType() === LAYER_STYLE_TYPE.VECTOR ? style.getSourceFieldNames() : [];
const fieldNames = [
...source.getFieldNames(),
...(style.getType() === LAYER_STYLE_TYPE.VECTOR ? style.getSourceFieldNames() : []),
...styleFieldNames,
...this.getValidJoins().map((join) => join.getLeftField().getName()),
];
@ -464,7 +468,11 @@ export class VectorLayer extends AbstractLayer {
} = syncContext;
const dataRequestId = SOURCE_DATA_REQUEST_ID;
const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`);
const searchFilters = this._getSearchFilters(dataFilters, source, style);
const searchFilters: VectorSourceRequestMeta = this._getSearchFilters(
dataFilters,
source,
style
);
const prevDataRequest = this.getSourceDataRequest();
const canSkipFetch = await canSkipSourceUpdate({
source,

View file

@ -11,7 +11,12 @@ import { Adapters } from 'src/plugins/inspector/public';
import { FileLayer } from '@elastic/ems-client';
import { Attribution, ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { AbstractVectorSource, GeoJsonWithMeta, IVectorSource } from '../vector_source';
import { SOURCE_TYPES, FIELD_ORIGIN, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
import {
SOURCE_TYPES,
FIELD_ORIGIN,
VECTOR_SHAPE_TYPE,
FORMAT_TYPE,
} from '../../../../common/constants';
import { getEmsFileLayers } from '../../../meta';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { UpdateSourceEditor } from './update_source_editor';
@ -30,11 +35,9 @@ export const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', {
});
export class EMSFileSource extends AbstractVectorSource implements IEmsFileSource {
static type = SOURCE_TYPES.EMS_FILE;
static createDescriptor({ id, tooltipProperties = [] }: Partial<EMSFileSourceDescriptor>) {
return {
type: EMSFileSource.type,
type: SOURCE_TYPES.EMS_FILE,
id: id!,
tooltipProperties,
};
@ -99,7 +102,7 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc
const emsFileLayer = await this.getEMSFileLayer();
// @ts-ignore
const featureCollection = await AbstractVectorSource.getGeoJson({
format: emsFileLayer.getDefaultFormatType(),
format: emsFileLayer.getDefaultFormatType() as FORMAT_TYPE,
featureCollectionPath: 'data',
fetchUrl: emsFileLayer.getDefaultFormatUrl(),
});

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import React from 'react';
import { AbstractTMSSource } from '../tms_source';
import { getEmsTmsServices } from '../../../meta';
@ -20,25 +19,18 @@ export const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', {
});
export class EMSTMSSource extends AbstractTMSSource {
static type = SOURCE_TYPES.EMS_TMS;
static createDescriptor(sourceConfig) {
static createDescriptor(descriptor) {
return {
type: EMSTMSSource.type,
id: sourceConfig.id,
isAutoSelect: sourceConfig.isAutoSelect,
type: SOURCE_TYPES.EMS_TMS,
id: descriptor.id,
isAutoSelect:
typeof descriptor.isAutoSelect !== 'undefined' ? !!descriptor.isAutoSelect : false,
};
}
constructor(descriptor, inspectorAdapters) {
super(
{
id: descriptor.id,
type: EMSTMSSource.type,
isAutoSelect: _.get(descriptor, 'isAutoSelect', false),
},
inspectorAdapters
);
descriptor = EMSTMSSource.createDescriptor(descriptor);
super(descriptor, inspectorAdapters);
}
renderSourceSettingsEditor({ onChange }) {

View file

@ -33,9 +33,21 @@ export abstract class AbstractESAggSource extends AbstractESSource {
private readonly _metricFields: IESAggField[];
private readonly _canReadFromGeoJson: boolean;
static createDescriptor(
descriptor: Partial<AbstractESAggSourceDescriptor>
): AbstractESAggSourceDescriptor {
const normalizedDescriptor = AbstractESSource.createDescriptor(descriptor);
return {
...normalizedDescriptor,
type: descriptor.type ? descriptor.type : '',
metrics:
descriptor.metrics && descriptor.metrics.length > 0 ? descriptor.metrics : [DEFAULT_METRIC],
};
}
constructor(
descriptor: AbstractESAggSourceDescriptor,
inspectorAdapters: Adapters,
inspectorAdapters?: Adapters,
canReadFromGeoJson = true
) {
super(descriptor, inspectorAdapters);
@ -55,7 +67,7 @@ export abstract class AbstractESAggSource extends AbstractESSource {
}
}
getFieldByName(fieldName: string) {
getFieldByName(fieldName: string): IField | null {
return this.getMetricFieldForName(fieldName);
}
@ -113,7 +125,7 @@ export abstract class AbstractESAggSource extends AbstractESSource {
}
}
async getFields() {
async getFields(): Promise<IField[]> {
return this.getMetricFields();
}
@ -128,7 +140,7 @@ export abstract class AbstractESAggSource extends AbstractESSource {
return valueAggsDsl;
}
async getTooltipProperties(properties: GeoJsonProperties) {
async getTooltipProperties(properties: GeoJsonProperties): Promise<ITooltipProperty[]> {
const metricFields = await this.getFields();
const promises: Array<Promise<ITooltipProperty>> = [];
metricFields.forEach((metricField) => {

View file

@ -1,46 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { AbstractESAggSource } from '../es_agg_source';
import {
ESGeoGridSourceDescriptor,
MapFilters,
MapQuery,
VectorSourceSyncMeta,
VectorSourceRequestMeta,
} from '../../../../common/descriptor_types';
import { GRID_RESOLUTION } from '../../../../common/constants';
import { IField } from '../../fields/field';
import { ITiledSingleLayerVectorSource } from '../vector_source';
export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingleLayerVectorSource {
static createDescriptor({
indexPatternId,
geoField,
requestType,
resolution,
}: Partial<ESGeoGridSourceDescriptor>): ESGeoGridSourceDescriptor;
constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown);
readonly _descriptor: ESGeoGridSourceDescriptor;
getFieldNames(): string[];
getGridResolution(): GRID_RESOLUTION;
getGeoGridPrecision(zoom: number): number;
createField({ fieldName }: { fieldName: string }): IField;
getLayerName(): string;
getUrlTemplateWithMeta(
searchFilters: VectorSourceRequestMeta
): Promise<{
layerName: string;
urlTemplate: string;
minSourceZoom: number;
maxSourceZoom: number;
}>;
}

View file

@ -4,37 +4,51 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import uuid from 'uuid/v4';
import React, { ReactElement } from 'react';
import { i18n } from '@kbn/i18n';
import rison from 'rison-node';
import { Feature } from 'geojson';
import { SearchResponse } from 'elasticsearch';
import {
convertCompositeRespToGeoJson,
convertRegularRespToGeoJson,
makeESBbox,
} from '../../../../common/elasticsearch_util';
// @ts-expect-error
import { UpdateSourceEditor } from './update_source_editor';
import {
SOURCE_TYPES,
DEFAULT_MAX_BUCKETS_LIMIT,
RENDER_AS,
GRID_RESOLUTION,
VECTOR_SHAPE_TYPE,
MVT_SOURCE_LAYER_NAME,
GIS_API_PATH,
MVT_GETGRIDTILE_API_PATH,
GEOTILE_GRID_AGG_NAME,
GEOCENTROID_AGG_NAME,
ES_GEO_FIELD_TYPE,
GEOCENTROID_AGG_NAME,
GEOTILE_GRID_AGG_NAME,
GIS_API_PATH,
GRID_RESOLUTION,
MVT_GETGRIDTILE_API_PATH,
MVT_SOURCE_LAYER_NAME,
RENDER_AS,
SOURCE_TYPES,
VECTOR_SHAPE_TYPE,
} from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source';
import { AbstractESAggSource } from '../es_agg_source';
import { DataRequestAbortError } from '../../util/data_request';
import { registerSource } from '../source_registry';
import { LICENSED_FEATURES } from '../../../licensed_features';
import rison from 'rison-node';
import { getHttp } from '../../../kibana_services';
import { GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source';
import {
ESGeoGridSourceDescriptor,
MapExtent,
VectorSourceRequestMeta,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { ISearchSource } from '../../../../../../../src/plugins/data/common/search/search_source';
import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
import { isValidStringConfig } from '../../util/valid_string_config';
export const MAX_GEOTILE_LEVEL = 29;
@ -46,31 +60,41 @@ export const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle
defaultMessage: 'Heat map',
});
export class ESGeoGridSource extends AbstractESAggSource {
static type = SOURCE_TYPES.ES_GEO_GRID;
static createDescriptor({ indexPatternId, geoField, metrics, requestType, resolution }) {
export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingleLayerVectorSource {
static createDescriptor(
descriptor: Partial<ESGeoGridSourceDescriptor>
): ESGeoGridSourceDescriptor {
const normalizedDescriptor = AbstractESAggSource.createDescriptor(descriptor);
if (!isValidStringConfig(normalizedDescriptor.geoField)) {
throw new Error('Cannot create an ESGeoGridSourceDescriptor without a geoField');
}
return {
type: ESGeoGridSource.type,
id: uuid(),
indexPatternId,
geoField,
metrics: metrics ? metrics : [DEFAULT_METRIC],
requestType,
resolution: resolution ? resolution : GRID_RESOLUTION.COARSE,
...normalizedDescriptor,
type: SOURCE_TYPES.ES_GEO_GRID,
geoField: normalizedDescriptor.geoField!,
requestType: descriptor.requestType || RENDER_AS.POINT,
resolution: descriptor.resolution ? descriptor.resolution : GRID_RESOLUTION.COARSE,
};
}
constructor(descriptor, inspectorAdapters) {
super(descriptor, inspectorAdapters, descriptor.resolution !== GRID_RESOLUTION.SUPER_FINE);
readonly _descriptor: ESGeoGridSourceDescriptor;
constructor(descriptor: Partial<ESGeoGridSourceDescriptor>, inspectorAdapters?: Adapters) {
const sourceDescriptor = ESGeoGridSource.createDescriptor(descriptor);
super(
sourceDescriptor,
inspectorAdapters,
descriptor.resolution !== GRID_RESOLUTION.SUPER_FINE
);
this._descriptor = sourceDescriptor;
}
renderSourceSettingsEditor({ onChange, currentLayerType }) {
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> {
return (
<UpdateSourceEditor
currentLayerType={currentLayerType}
currentLayerType={sourceEditorArgs.currentLayerType}
indexPatternId={this.getIndexPatternId()}
onChange={onChange}
onChange={sourceEditorArgs.onChange}
metrics={this._descriptor.metrics}
renderAs={this._descriptor.requestType}
resolution={this._descriptor.resolution}
@ -78,17 +102,17 @@ export class ESGeoGridSource extends AbstractESAggSource {
);
}
getSyncMeta() {
getSyncMeta(): VectorSourceSyncMeta {
return {
requestType: this._descriptor.requestType,
};
}
async getImmutableProperties() {
let indexPatternTitle = this.getIndexPatternId();
async getImmutableProperties(): Promise<ImmutableSourceProperty[]> {
let indexPatternName = this.getIndexPatternId();
try {
const indexPattern = await this.getIndexPattern();
indexPatternTitle = indexPattern.title;
indexPatternName = indexPattern.title;
} catch (error) {
// ignore error, title will just default to id
}
@ -102,7 +126,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
label: i18n.translate('xpack.maps.source.esGrid.indexPatternLabel', {
defaultMessage: 'Index pattern',
}),
value: indexPatternTitle,
value: indexPatternName,
},
{
label: i18n.translate('xpack.maps.source.esGrid.geospatialFieldLabel', {
@ -117,7 +141,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName());
}
isGeoGridPrecisionAware() {
isGeoGridPrecisionAware(): boolean {
if (this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE) {
// MVT gridded data should not bootstrap each time the precision changes
// mapbox-gl needs to handle this
@ -128,15 +152,15 @@ export class ESGeoGridSource extends AbstractESAggSource {
}
}
showJoinEditor() {
showJoinEditor(): boolean {
return false;
}
getGridResolution() {
getGridResolution(): GRID_RESOLUTION {
return this._descriptor.resolution;
}
getGeoGridPrecision(zoom) {
getGeoGridPrecision(zoom: number): number {
if (this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE) {
// The target-precision needs to be determined server side.
return NaN;
@ -178,9 +202,18 @@ export class ESGeoGridSource extends AbstractESAggSource {
bucketsPerGrid,
isRequestStillActive,
bufferedExtent,
}: {
searchSource: ISearchSource;
indexPattern: IndexPattern;
precision: number;
layerName: string;
registerCancelCallback: (callback: () => void) => void;
bucketsPerGrid: number;
isRequestStillActive: () => boolean;
bufferedExtent: MapExtent;
}) {
const gridsPerRequest = Math.floor(DEFAULT_MAX_BUCKETS_LIMIT / bucketsPerGrid);
const aggs = {
const gridsPerRequest: number = Math.floor(DEFAULT_MAX_BUCKETS_LIMIT / bucketsPerGrid);
const aggs: any = {
compositeSplit: {
composite: {
size: gridsPerRequest,
@ -232,8 +265,10 @@ export class ESGeoGridSource extends AbstractESAggSource {
aggs.compositeSplit.composite.after = afterKey;
}
searchSource.setField('aggs', aggs);
const requestId = afterKey ? `${this.getId()} afterKey ${afterKey.geoSplit}` : this.getId();
const esResponse = await this._runEsQuery({
const requestId: string = afterKey
? `${this.getId()} afterKey ${afterKey.geoSplit}`
: this.getId();
const esResponse: SearchResponse<unknown> = await this._runEsQuery({
requestId,
requestName: `${layerName} (${requestCount})`,
searchSource,
@ -259,7 +294,12 @@ export class ESGeoGridSource extends AbstractESAggSource {
return features;
}
_addNonCompositeAggsToSearchSource(searchSource, indexPattern, precision, bufferedExtent) {
_addNonCompositeAggsToSearchSource(
searchSource: ISearchSource,
indexPattern: IndexPattern,
precision: number | null,
bufferedExtent?: MapExtent | null
) {
searchSource.setField('aggs', {
[GEOTILE_GRID_AGG_NAME]: {
geotile_grid: {
@ -290,7 +330,14 @@ export class ESGeoGridSource extends AbstractESAggSource {
layerName,
registerCancelCallback,
bufferedExtent,
}) {
}: {
searchSource: ISearchSource;
indexPattern: IndexPattern;
precision: number;
layerName: string;
registerCancelCallback: (callback: () => void) => void;
bufferedExtent?: MapExtent;
}): Promise<Feature[]> {
this._addNonCompositeAggsToSearchSource(searchSource, indexPattern, precision, bufferedExtent);
const esResponse = await this._runEsQuery({
@ -306,52 +353,69 @@ export class ESGeoGridSource extends AbstractESAggSource {
return convertRegularRespToGeoJson(esResponse, this._descriptor.requestType);
}
async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback, isRequestStillActive) {
const indexPattern = await this.getIndexPattern();
const searchSource = await this.makeSearchSource(searchFilters, 0);
async getGeoJsonWithMeta(
layerName: string,
searchFilters: VectorSourceRequestMeta,
registerCancelCallback: (callback: () => void) => void,
isRequestStillActive: () => boolean
): Promise<GeoJsonWithMeta> {
const indexPattern: IndexPattern = await this.getIndexPattern();
const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0);
let bucketsPerGrid = 1;
this.getMetricFields().forEach((metricField) => {
bucketsPerGrid += metricField.getBucketCount();
});
const features =
bucketsPerGrid === 1
? await this._nonCompositeAggRequest({
searchSource,
indexPattern,
precision: searchFilters.geogridPrecision,
layerName,
registerCancelCallback,
bufferedExtent: searchFilters.buffer,
})
: await this._compositeAggRequest({
searchSource,
indexPattern,
precision: searchFilters.geogridPrecision,
layerName,
registerCancelCallback,
bucketsPerGrid,
isRequestStillActive,
bufferedExtent: searchFilters.buffer,
});
let features: Feature[];
if (searchFilters.buffer) {
features =
bucketsPerGrid === 1
? await this._nonCompositeAggRequest({
searchSource,
indexPattern,
precision: searchFilters.geogridPrecision || 0,
layerName,
registerCancelCallback,
bufferedExtent: searchFilters.buffer,
})
: await this._compositeAggRequest({
searchSource,
indexPattern,
precision: searchFilters.geogridPrecision || 0,
layerName,
registerCancelCallback,
bucketsPerGrid,
isRequestStillActive,
bufferedExtent: searchFilters.buffer,
});
} else {
throw new Error('Cannot get GeoJson without searchFilter.buffer');
}
return {
data: {
type: 'FeatureCollection',
features: features,
features,
},
meta: {
areResultsTrimmed: false,
},
};
} as GeoJsonWithMeta;
}
getLayerName() {
getLayerName(): string {
return MVT_SOURCE_LAYER_NAME;
}
async getUrlTemplateWithMeta(searchFilters) {
async getUrlTemplateWithMeta(
searchFilters: VectorSourceRequestMeta
): Promise<{
layerName: string;
urlTemplate: string;
minSourceZoom: number;
maxSourceZoom: number;
}> {
const indexPattern = await this.getIndexPattern();
const searchSource = await this.makeSearchSource(searchFilters, 0);
@ -376,25 +440,25 @@ export class ESGeoGridSource extends AbstractESAggSource {
layerName: this.getLayerName(),
minSourceZoom: this.getMinZoom(),
maxSourceZoom: this.getMaxZoom(),
urlTemplate: urlTemplate,
urlTemplate,
};
}
isFilterByMapBounds() {
isFilterByMapBounds(): boolean {
if (this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE) {
//MVT gridded data. Should exclude bounds-filter from ES-DSL
// MVT gridded data. Should exclude bounds-filter from ES-DSL
return false;
} else {
//Should include bounds-filter from ES-DSL
// Should include bounds-filter from ES-DSL
return true;
}
}
canFormatFeatureProperties() {
canFormatFeatureProperties(): boolean {
return true;
}
async getSupportedShapeTypes() {
async getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]> {
if (this._descriptor.requestType === RENDER_AS.GRID) {
return [VECTOR_SHAPE_TYPE.POLYGON];
}
@ -402,7 +466,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
return [VECTOR_SHAPE_TYPE.POINT];
}
async getLicensedFeatures() {
async getLicensedFeatures(): Promise<LICENSED_FEATURES[]> {
const geoField = await this._getGeoField();
return geoField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE
? [LICENSED_FEATURES.GEO_SHAPE_AGGS_GEO_TILE]

View file

@ -5,7 +5,6 @@
*/
import React from 'react';
import uuid from 'uuid/v4';
import turfBbox from '@turf/bbox';
import { multiPoint } from '@turf/helpers';
@ -14,7 +13,7 @@ import { i18n } from '@kbn/i18n';
import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { convertToLines } from './convert_to_lines';
import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source';
import { AbstractESAggSource } from '../es_agg_source';
import { registerSource } from '../source_registry';
import { turfBboxToBounds } from '../../../../common/elasticsearch_util';
import { DataRequestAbortError } from '../../util/data_request';
@ -28,14 +27,14 @@ export const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', {
export class ESPewPewSource extends AbstractESAggSource {
static type = SOURCE_TYPES.ES_PEW_PEW;
static createDescriptor({ indexPatternId, sourceGeoField, destGeoField, metrics }) {
static createDescriptor(descriptor) {
const normalizedDescriptor = AbstractESAggSource.createDescriptor(descriptor);
return {
...normalizedDescriptor,
type: ESPewPewSource.type,
id: uuid(),
indexPatternId: indexPatternId,
sourceGeoField,
destGeoField,
metrics: metrics ? metrics : [DEFAULT_METRIC],
indexPatternId: descriptor.indexPatternId,
sourceGeoField: descriptor.sourceGeoField,
destGeoField: descriptor.destGeoField,
};
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const DEFAULT_FILTER_BY_MAP_BOUNDS = true;
export const DEFAULT_FILTER_BY_MAP_BOUNDS: boolean = true;

View file

@ -16,8 +16,15 @@ import { VectorLayer } from '../../layers/vector_layer/vector_layer';
import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants';
import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer';
import { EsDocumentsLayerIcon } from './es_documents_layer_icon';
import {
ESSearchSourceDescriptor,
VectorLayerDescriptor,
} from '../../../../common/descriptor_types';
export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) {
export function createDefaultLayerDescriptor(
sourceConfig: Partial<ESSearchSourceDescriptor>,
mapColors: string[]
): VectorLayerDescriptor {
const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig);
if (sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS) {
@ -36,7 +43,7 @@ export const esDocumentsLayerWizardConfig: LayerWizard = {
}),
icon: EsDocumentsLayerIcon,
renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: unknown) => {
const onSourceConfigChange = (sourceConfig: Partial<ESSearchSourceDescriptor>) => {
if (!sourceConfig) {
previewLayers([]);
return;

View file

@ -1,26 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { AbstractESSource } from '../es_source';
import { ESSearchSourceDescriptor, MapFilters } from '../../../../common/descriptor_types';
import { ITiledSingleLayerVectorSource } from '../vector_source';
export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource {
static createDescriptor(sourceConfig: unknown): ESSearchSourceDescriptor;
constructor(sourceDescriptor: Partial<ESSearchSourceDescriptor>, inspectorAdapters: unknown);
getFieldNames(): string[];
getUrlTemplateWithMeta(
searchFilters: MapFilters
): Promise<{
layerName: string;
urlTemplate: string;
minSourceZoom: number;
maxSourceZoom: number;
}>;
getLayerName(): string;
}

View file

@ -11,21 +11,22 @@ jest.mock('./load_index_settings');
import { getIndexPatternService, getSearchService, getHttp } from '../../../kibana_services';
import { SearchSource } from 'src/plugins/data/public';
// @ts-expect-error
import { loadIndexSettings } from './load_index_settings';
import { ESSearchSource } from './es_search_source';
import { VectorSourceRequestMeta } from '../../../../common/descriptor_types';
const mockDescriptor = { indexPatternId: 'foo', geoField: 'bar' };
describe('ESSearchSource', () => {
it('constructor', () => {
const esSearchSource = new ESSearchSource({}, null);
const esSearchSource = new ESSearchSource(mockDescriptor);
expect(esSearchSource instanceof ESSearchSource).toBe(true);
});
describe('ITiledSingleLayerVectorSource', () => {
it('mb-source params', () => {
const esSearchSource = new ESSearchSource({}, null);
const esSearchSource = new ESSearchSource(mockDescriptor);
expect(esSearchSource.getMinZoom()).toBe(0);
expect(esSearchSource.getMaxZoom()).toBe(24);
expect(esSearchSource.getLayerName()).toBe('source_layer');
@ -72,6 +73,7 @@ describe('ESSearchSource', () => {
getIndexPatternService.mockReturnValue(mockIndexPatternService);
// @ts-expect-error
getSearchService.mockReturnValue(mockSearchService);
// @ts-expect-error
loadIndexSettings.mockReturnValue({
maxResultWindow: 1000,
});
@ -104,10 +106,10 @@ describe('ESSearchSource', () => {
};
it('Should only include required props', async () => {
const esSearchSource = new ESSearchSource(
{ geoField: geoFieldName, indexPatternId: 'ipId' },
null
);
const esSearchSource = new ESSearchSource({
geoField: geoFieldName,
indexPatternId: 'ipId',
});
const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters);
expect(urlTemplateWithMeta.urlTemplate).toBe(
`rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fields,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape`
@ -118,22 +120,28 @@ describe('ESSearchSource', () => {
describe('isFilterByMapBounds', () => {
it('default', () => {
const esSearchSource = new ESSearchSource({}, null);
const esSearchSource = new ESSearchSource(mockDescriptor);
expect(esSearchSource.isFilterByMapBounds()).toBe(true);
});
it('mvt', () => {
const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null);
const esSearchSource = new ESSearchSource({
...mockDescriptor,
scalingType: SCALING_TYPES.MVT,
});
expect(esSearchSource.isFilterByMapBounds()).toBe(false);
});
});
describe('getJoinsDisabledReason', () => {
it('default', () => {
const esSearchSource = new ESSearchSource({}, null);
const esSearchSource = new ESSearchSource(mockDescriptor);
expect(esSearchSource.getJoinsDisabledReason()).toBe(null);
});
it('mvt', () => {
const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null);
const esSearchSource = new ESSearchSource({
...mockDescriptor,
scalingType: SCALING_TYPES.MVT,
});
expect(esSearchSource.getJoinsDisabledReason()).toBe(
'Joins are not supported when scaling by mvt vector tiles'
);
@ -142,12 +150,15 @@ describe('ESSearchSource', () => {
describe('getFields', () => {
it('default', () => {
const esSearchSource = new ESSearchSource({}, null);
const esSearchSource = new ESSearchSource(mockDescriptor);
const docField = esSearchSource.createField({ fieldName: 'prop1' });
expect(docField.canReadFromGeoJson()).toBe(true);
});
it('mvt', () => {
const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null);
const esSearchSource = new ESSearchSource({
...mockDescriptor,
scalingType: SCALING_TYPES.MVT,
});
const docField = esSearchSource.createField({ fieldName: 'prop1' });
expect(docField.canReadFromGeoJson()).toBe(false);
});

View file

@ -5,50 +5,82 @@
*/
import _ from 'lodash';
import React from 'react';
import React, { ReactElement } from 'react';
import rison from 'rison-node';
import { i18n } from '@kbn/i18n';
import { IFieldType, IndexPattern } from 'src/plugins/data/public';
import { FeatureCollection, GeoJsonProperties } from 'geojson';
import { AbstractESSource } from '../es_source';
import { getSearchService, getHttp } from '../../../kibana_services';
import { hitsToGeoJson, getField, addFieldToDSL } from '../../../../common/elasticsearch_util';
import { getHttp, getSearchService } from '../../../kibana_services';
import { addFieldToDSL, getField, hitsToGeoJson } from '../../../../common/elasticsearch_util';
// @ts-expect-error
import { UpdateSourceEditor } from './update_source_editor';
import {
SOURCE_TYPES,
ES_GEO_FIELD_TYPE,
DEFAULT_MAX_BUCKETS_LIMIT,
SORT_ORDER,
SCALING_TYPES,
VECTOR_SHAPE_TYPE,
MVT_SOURCE_LAYER_NAME,
ES_GEO_FIELD_TYPE,
FIELD_ORIGIN,
GIS_API_PATH,
MVT_GETTILE_API_PATH,
MVT_SOURCE_LAYER_NAME,
SCALING_TYPES,
SOURCE_TYPES,
VECTOR_SHAPE_TYPE,
} from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { getSourceFields } from '../../../index_pattern_util';
import { loadIndexSettings } from './load_index_settings';
import uuid from 'uuid/v4';
import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
import { ESDocField } from '../../fields/es_doc_field';
import { registerSource } from '../source_registry';
import {
ESSearchSourceDescriptor,
VectorSourceRequestMeta,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
import { ImmutableSourceProperty, PreIndexedShape, SourceEditorArgs } from '../source';
import { IField } from '../../fields/field';
import {
GeoJsonWithMeta,
ITiledSingleLayerVectorSource,
SourceTooltipConfig,
} from '../vector_source';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
import { DataRequest } from '../../util/data_request';
import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common';
import { isValidStringConfig } from '../../util/valid_string_config';
export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', {
defaultMessage: 'Documents',
});
function getDocValueAndSourceFields(indexPattern, fieldNames) {
const docValueFields = [];
const sourceOnlyFields = [];
const scriptFields = {};
export interface ScriptField {
source: string;
lang: string;
}
function getDocValueAndSourceFields(
indexPattern: IndexPattern,
fieldNames: string[]
): {
docValueFields: Array<string | { format: string; field: string }>;
sourceOnlyFields: string[];
scriptFields: Record<string, { script: ScriptField }>;
} {
const docValueFields: Array<string | { format: string; field: string }> = [];
const sourceOnlyFields: string[] = [];
const scriptFields: Record<string, { script: ScriptField }> = {};
fieldNames.forEach((fieldName) => {
const field = getField(indexPattern, fieldName);
if (field.scripted) {
scriptFields[field.name] = {
script: {
source: field.script,
lang: field.lang,
source: field.script || '',
lang: field.lang || '',
},
};
} else if (field.readFromDocValues) {
@ -68,43 +100,64 @@ function getDocValueAndSourceFields(indexPattern, fieldNames) {
return { docValueFields, sourceOnlyFields, scriptFields };
}
export class ESSearchSource extends AbstractESSource {
static type = SOURCE_TYPES.ES_SEARCH;
export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource {
readonly _descriptor: ESSearchSourceDescriptor;
protected readonly _tooltipFields: ESDocField[];
static createDescriptor(descriptor) {
static createDescriptor(descriptor: Partial<ESSearchSourceDescriptor>): ESSearchSourceDescriptor {
const normalizedDescriptor = AbstractESSource.createDescriptor(descriptor);
if (!isValidStringConfig(normalizedDescriptor.geoField)) {
throw new Error('Cannot create an ESSearchSourceDescriptor without a geoField');
}
return {
...descriptor,
id: descriptor.id ? descriptor.id : uuid(),
type: ESSearchSource.type,
indexPatternId: descriptor.indexPatternId,
geoField: descriptor.geoField,
filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS),
tooltipProperties: _.get(descriptor, 'tooltipProperties', []),
sortField: _.get(descriptor, 'sortField', ''),
sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC),
scalingType: _.get(descriptor, 'scalingType', SCALING_TYPES.LIMIT),
topHitsSplitField: descriptor.topHitsSplitField,
topHitsSize: _.get(descriptor, 'topHitsSize', 1),
...normalizedDescriptor,
type: SOURCE_TYPES.ES_SEARCH,
geoField: normalizedDescriptor.geoField!,
filterByMapBounds:
typeof descriptor.filterByMapBounds === 'boolean'
? descriptor.filterByMapBounds
: DEFAULT_FILTER_BY_MAP_BOUNDS,
tooltipProperties: Array.isArray(descriptor.tooltipProperties)
? descriptor.tooltipProperties
: [],
sortField: isValidStringConfig(descriptor.sortField) ? (descriptor.sortField as string) : '',
sortOrder: isValidStringConfig(descriptor.sortOrder)
? descriptor.sortOrder!
: SortDirection.desc,
scalingType: isValidStringConfig(descriptor.scalingType)
? descriptor.scalingType!
: SCALING_TYPES.LIMIT,
topHitsSplitField: isValidStringConfig(descriptor.topHitsSplitField)
? descriptor.topHitsSplitField!
: '',
topHitsSize:
typeof descriptor.topHitsSize === 'number' && descriptor.topHitsSize > 0
? descriptor.topHitsSize
: 1,
};
}
constructor(descriptor, inspectorAdapters) {
super(ESSearchSource.createDescriptor(descriptor), inspectorAdapters);
this._tooltipFields = this._descriptor.tooltipProperties.map((property) =>
this.createField({ fieldName: property })
);
constructor(descriptor: Partial<ESSearchSourceDescriptor>, inspectorAdapters?: Adapters) {
const sourceDescriptor = ESSearchSource.createDescriptor(descriptor);
super(sourceDescriptor, inspectorAdapters);
this._descriptor = sourceDescriptor;
this._tooltipFields = this._descriptor.tooltipProperties
? this._descriptor.tooltipProperties.map((property) => {
return this.createField({ fieldName: property });
})
: [];
}
createField({ fieldName }) {
createField({ fieldName }: { fieldName: string }): ESDocField {
return new ESDocField({
fieldName,
source: this,
origin: FIELD_ORIGIN.SOURCE,
canReadFromGeoJson: this._descriptor.scalingType !== SCALING_TYPES.MVT,
});
}
renderSourceSettingsEditor({ onChange }) {
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> | null {
const getGeoField = () => {
return this._getGeoField();
};
@ -113,7 +166,7 @@ export class ESSearchSource extends AbstractESSource {
source={this}
indexPatternId={this.getIndexPatternId()}
getGeoField={getGeoField}
onChange={onChange}
onChange={sourceEditorArgs.onChange}
tooltipFields={this._tooltipFields}
sortField={this._descriptor.sortField}
sortOrder={this._descriptor.sortOrder}
@ -125,34 +178,36 @@ export class ESSearchSource extends AbstractESSource {
);
}
async getFields() {
async getFields(): Promise<IField[]> {
try {
const indexPattern = await this.getIndexPattern();
return indexPattern.fields
.filter((field) => {
// Ensure fielddata is enabled for field.
// Search does not request _source
return field.aggregatable;
})
.map((field) => {
const fields: IFieldType[] = indexPattern.fields.filter((field) => {
// Ensure fielddata is enabled for field.
// Search does not request _source
return field.aggregatable;
});
return fields.map(
(field): IField => {
return this.createField({ fieldName: field.name });
});
}
);
} catch (error) {
// failed index-pattern retrieval will show up as error-message in the layer-toc-entry
return [];
}
}
getFieldNames() {
getFieldNames(): string[] {
return [this._descriptor.geoField];
}
async getImmutableProperties() {
let indexPatternTitle = this.getIndexPatternId();
async getImmutableProperties(): Promise<ImmutableSourceProperty[]> {
let indexPatternName = this.getIndexPatternId();
let geoFieldType = '';
try {
const indexPattern = await this.getIndexPattern();
indexPatternTitle = indexPattern.title;
indexPatternName = indexPattern.title;
const geoField = await this._getGeoField();
geoFieldType = geoField.type;
} catch (error) {
@ -168,7 +223,7 @@ export class ESSearchSource extends AbstractESSource {
label: i18n.translate('xpack.maps.source.esSearch.indexPatternLabel', {
defaultMessage: `Index pattern`,
}),
value: indexPatternTitle,
value: indexPatternName,
},
{
label: i18n.translate('xpack.maps.source.esSearch.geoFieldLabel', {
@ -186,8 +241,12 @@ export class ESSearchSource extends AbstractESSource {
}
// Returns sort content for an Elasticsearch search body
_buildEsSort() {
_buildEsSort(): Array<Record<string, SortDirectionNumeric>> {
const { sortField, sortOrder } = this._descriptor;
if (!sortField) {
throw new Error('Cannot build sort');
}
return [
{
[sortField]: {
@ -197,16 +256,30 @@ export class ESSearchSource extends AbstractESSource {
];
}
async _getTopHits(layerName, searchFilters, registerCancelCallback) {
async _getTopHits(
layerName: string,
searchFilters: VectorSourceRequestMeta,
registerCancelCallback: (callback: () => void) => void
) {
const { topHitsSplitField: topHitsSplitFieldName, topHitsSize } = this._descriptor;
const indexPattern = await this.getIndexPattern();
if (!topHitsSplitFieldName) {
throw new Error('Cannot _getTopHits without topHitsSplitField');
}
const indexPattern: IndexPattern = await this.getIndexPattern();
const { docValueFields, sourceOnlyFields, scriptFields } = getDocValueAndSourceFields(
indexPattern,
searchFilters.fieldNames
);
const topHits = {
const topHits: {
size: number;
script_fields: Record<string, { script: ScriptField }>;
docvalue_fields: Array<string | { format: string; field: string }>;
_source?: boolean | { includes: string[] };
sort?: Array<Record<string, SortDirectionNumeric>>;
} = {
size: topHitsSize,
script_fields: scriptFields,
docvalue_fields: docValueFields,
@ -215,6 +288,7 @@ export class ESSearchSource extends AbstractESSource {
if (this._hasSort()) {
topHits.sort = this._buildEsSort();
}
if (sourceOnlyFields.length === 0) {
topHits._source = false;
} else {
@ -223,7 +297,7 @@ export class ESSearchSource extends AbstractESSource {
};
}
const topHitsSplitField = getField(indexPattern, topHitsSplitFieldName);
const topHitsSplitField: IFieldType = getField(indexPattern, topHitsSplitFieldName);
const cardinalityAgg = { precision_threshold: 1 };
const termsAgg = {
size: DEFAULT_MAX_BUCKETS_LIMIT,
@ -253,13 +327,13 @@ export class ESSearchSource extends AbstractESSource {
requestDescription: 'Elasticsearch document top hits request',
});
const allHits = [];
const allHits: any[] = [];
const entityBuckets = _.get(resp, 'aggregations.entitySplit.buckets', []);
const totalEntities = _.get(resp, 'aggregations.totalEntities.value', 0);
// can not compare entityBuckets.length to totalEntities because totalEntities is an approximate
const areEntitiesTrimmed = entityBuckets.length >= DEFAULT_MAX_BUCKETS_LIMIT;
let areTopHitsTrimmed = false;
entityBuckets.forEach((entityBucket) => {
entityBuckets.forEach((entityBucket: any) => {
const total = _.get(entityBucket, 'entityHits.hits.total', 0);
const hits = _.get(entityBucket, 'entityHits.hits.hits', []);
// Reverse hits list so top documents by sort are drawn on top
@ -282,7 +356,12 @@ export class ESSearchSource extends AbstractESSource {
// searchFilters.fieldNames contains geo field and any fields needed for styling features
// Performs Elasticsearch search request being careful to pull back only required fields to minimize response size
async _getSearchHits(layerName, searchFilters, maxResultWindow, registerCancelCallback) {
async _getSearchHits(
layerName: string,
searchFilters: VectorSourceRequestMeta,
maxResultWindow: number,
registerCancelCallback: (callback: () => void) => void
) {
const indexPattern = await this.getIndexPattern();
const { docValueFields, sourceOnlyFields } = getDocValueAndSourceFields(
@ -322,23 +401,28 @@ export class ESSearchSource extends AbstractESSource {
};
}
_isTopHits() {
_isTopHits(): boolean {
const { scalingType, topHitsSplitField } = this._descriptor;
return !!(scalingType === SCALING_TYPES.TOP_HITS && topHitsSplitField);
}
_hasSort() {
_hasSort(): boolean {
const { sortField, sortOrder } = this._descriptor;
return !!sortField && !!sortOrder;
}
async getMaxResultWindow() {
async getMaxResultWindow(): Promise<number> {
const indexPattern = await this.getIndexPattern();
const indexSettings = await loadIndexSettings(indexPattern.title);
return indexSettings.maxResultWindow;
}
async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) {
async getGeoJsonWithMeta(
layerName: string,
searchFilters: VectorSourceRequestMeta,
registerCancelCallback: (callback: () => void) => void,
isRequestStillActive: () => boolean
): Promise<GeoJsonWithMeta> {
const indexPattern = await this.getIndexPattern();
const indexSettings = await loadIndexSettings(indexPattern.title);
@ -355,7 +439,7 @@ export class ESSearchSource extends AbstractESSource {
const unusedMetaFields = indexPattern.metaFields.filter((metaField) => {
return !['_id', '_index'].includes(metaField);
});
const flattenHit = (hit) => {
const flattenHit = (hit: Record<string, any>) => {
const properties = indexPattern.flattenHit(hit);
// remove metaFields
unusedMetaFields.forEach((metaField) => {
@ -375,7 +459,7 @@ export class ESSearchSource extends AbstractESSource {
hits,
flattenHit,
geoField.name,
geoField.type,
geoField.type as ES_GEO_FIELD_TYPE,
epochMillisFields
);
} catch (error) {
@ -394,11 +478,11 @@ export class ESSearchSource extends AbstractESSource {
};
}
canFormatFeatureProperties() {
canFormatFeatureProperties(): boolean {
return this._tooltipFields.length > 0;
}
async _loadTooltipProperties(docId, index, indexPattern) {
async _loadTooltipProperties(docId: string | number, index: string, indexPattern: IndexPattern) {
if (this._tooltipFields.length === 0) {
return {};
}
@ -430,7 +514,7 @@ export class ESSearchSource extends AbstractESSource {
}
const properties = indexPattern.flattenHit(hit);
indexPattern.metaFields.forEach((metaField) => {
indexPattern.metaFields.forEach((metaField: string) => {
if (!this._getTooltipPropertyNames().includes(metaField)) {
delete properties[metaField];
}
@ -438,7 +522,14 @@ export class ESSearchSource extends AbstractESSource {
return properties;
}
async getTooltipProperties(properties) {
_getTooltipPropertyNames(): string[] {
return this._tooltipFields.map((field: IField) => field.getName());
}
async getTooltipProperties(properties: GeoJsonProperties): Promise<ITooltipProperty[]> {
if (properties === null) {
throw new Error('properties cannot be null');
}
const indexPattern = await this.getIndexPattern();
const propertyValues = await this._loadTooltipProperties(
properties._id,
@ -452,25 +543,27 @@ export class ESSearchSource extends AbstractESSource {
return Promise.all(tooltipProperties);
}
isFilterByMapBounds() {
if (this._descriptor.scalingType === SCALING_TYPES.CLUSTER) {
isFilterByMapBounds(): boolean {
if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) {
return true;
} else if (this._descriptor.scalingType === SCALING_TYPES.MVT) {
return false;
} else {
return this._descriptor.filterByMapBounds;
return !!this._descriptor.filterByMapBounds;
}
}
async getLeftJoinFields() {
async getLeftJoinFields(): Promise<IField[]> {
const indexPattern = await this.getIndexPattern();
// Left fields are retrieved from _source.
return getSourceFields(indexPattern.fields).map((field) =>
this.createField({ fieldName: field.name })
return getSourceFields(indexPattern.fields).map(
(field): IField => {
return this.createField({ fieldName: field.name });
}
);
}
async getSupportedShapeTypes() {
async getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]> {
let geoFieldType;
try {
const geoField = await this._getGeoField();
@ -486,8 +579,10 @@ export class ESSearchSource extends AbstractESSource {
return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
}
getSourceTooltipContent(sourceDataRequest) {
const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null;
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig {
const featureCollection: FeatureCollection | null = sourceDataRequest
? (sourceDataRequest.getData() as FeatureCollection)
: null;
const meta = sourceDataRequest ? sourceDataRequest.getMeta() : null;
if (!featureCollection || !meta) {
// no tooltip content needed when there is no feature collection or meta
@ -519,7 +614,7 @@ export class ESSearchSource extends AbstractESSource {
tooltipContent: `${entitiesFoundMsg} ${docsPerEntityMsg}`,
// Used to show trimmed icon in legend
// user only needs to be notified of trimmed results when entities are trimmed
areResultsTrimmed: meta.areEntitiesTrimmed,
areResultsTrimmed: !!meta.areEntitiesTrimmed,
};
}
@ -542,7 +637,7 @@ export class ESSearchSource extends AbstractESSource {
};
}
getSyncMeta() {
getSyncMeta(): VectorSourceSyncMeta | null {
return {
sortField: this._descriptor.sortField,
sortOrder: this._descriptor.sortOrder,
@ -552,7 +647,10 @@ export class ESSearchSource extends AbstractESSource {
};
}
async getPreIndexedShape(properties) {
async getPreIndexedShape(properties: GeoJsonProperties): Promise<PreIndexedShape | null> {
if (properties === null) {
return null;
}
const geoField = await this._getGeoField();
return {
index: properties._index, // Can not use index pattern title because it may reference many indices
@ -561,7 +659,7 @@ export class ESSearchSource extends AbstractESSource {
};
}
getJoinsDisabledReason() {
getJoinsDisabledReason(): string | null {
let reason;
if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) {
reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', {
@ -577,11 +675,18 @@ export class ESSearchSource extends AbstractESSource {
return reason;
}
getLayerName() {
getLayerName(): string {
return MVT_SOURCE_LAYER_NAME;
}
async getUrlTemplateWithMeta(searchFilters) {
async getUrlTemplateWithMeta(
searchFilters: VectorSourceRequestMeta
): Promise<{
layerName: string;
urlTemplate: string;
minSourceZoom: number;
maxSourceZoom: number;
}> {
const indexPattern = await this.getIndexPattern();
const indexSettings = await loadIndexSettings(indexPattern.title);
@ -621,7 +726,7 @@ export class ESSearchSource extends AbstractESSource {
layerName: this.getLayerName(),
minSourceZoom: this.getMinZoom(),
maxSourceZoom: this.getMaxZoom(),
urlTemplate: urlTemplate,
urlTemplate,
};
}
}

View file

@ -4,20 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import {
DEFAULT_MAX_RESULT_WINDOW,
DEFAULT_MAX_INNER_RESULT_WINDOW,
INDEX_SETTINGS_API_PATH,
} from '../../../../common/constants';
import { getHttp, getToasts } from '../../../kibana_services';
import { i18n } from '@kbn/i18n';
let toastDisplayed = false;
const indexSettings = new Map();
const indexSettings = new Map<string, Promise<INDEX_SETTINGS>>();
export async function loadIndexSettings(indexPatternTitle) {
export interface INDEX_SETTINGS {
maxResultWindow: number;
maxInnerResultWindow: number;
}
export async function loadIndexSettings(indexPatternTitle: string): Promise<INDEX_SETTINGS> {
if (indexSettings.has(indexPatternTitle)) {
return indexSettings.get(indexPatternTitle);
return indexSettings.get(indexPatternTitle)!;
}
const fetchPromise = fetchIndexSettings(indexPatternTitle);
@ -25,7 +30,7 @@ export async function loadIndexSettings(indexPatternTitle) {
return fetchPromise;
}
async function fetchIndexSettings(indexPatternTitle) {
async function fetchIndexSettings(indexPatternTitle: string): Promise<INDEX_SETTINGS> {
const http = getHttp();
const toasts = getToasts();
try {
@ -50,6 +55,7 @@ async function fetchIndexSettings(indexPatternTitle) {
toastDisplayed = true;
toasts.addWarning(warningMsg);
}
// eslint-disable-next-line no-console
console.warn(warningMsg);
return {
maxResultWindow: DEFAULT_MAX_RESULT_WINDOW,

View file

@ -18,10 +18,10 @@ import {
getSourceFields,
supportsGeoTileAgg,
} from '../../../index_pattern_util';
import { SORT_ORDER } from '../../../../common/constants';
import { SortDirection, indexPatterns } from '../../../../../../../src/plugins/data/public';
import { ESDocField } from '../../fields/es_doc_field';
import { FormattedMessage } from '@kbn/i18n/react';
import { indexPatterns } from '../../../../../../../src/plugins/data/public';
import { ScalingForm } from './scaling_form';
export class UpdateSourceEditor extends Component {
@ -183,13 +183,13 @@ export class UpdateSourceEditor extends Component {
text: i18n.translate('xpack.maps.source.esSearch.ascendingLabel', {
defaultMessage: 'ascending',
}),
value: SORT_ORDER.ASC,
value: SortDirection.asc,
},
{
text: i18n.translate('xpack.maps.source.esSearch.descendingLabel', {
defaultMessage: 'descending',
}),
value: SORT_ORDER.DESC,
value: SortDirection.desc,
},
]}
value={this.props.sortOrder}

View file

@ -1,86 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { AbstractVectorSource } from '../vector_source';
import { IVectorSource } from '../vector_source';
import { TimeRange } from '../../../../../../../src/plugins/data/common';
import { IndexPattern, ISearchSource } from '../../../../../../../src/plugins/data/public';
import {
DynamicStylePropertyOptions,
MapQuery,
VectorSourceRequestMeta,
} from '../../../../common/descriptor_types';
import { IVectorStyle } from '../../styles/vector/vector_style';
import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
export interface IESSource extends IVectorSource {
getId(): string;
getIndexPattern(): Promise<IndexPattern>;
getIndexPatternId(): string;
getGeoFieldName(): string;
getMaxResultWindow(): Promise<number>;
makeSearchSource(
searchFilters: VectorSourceRequestMeta,
limit: number,
initialSearchContext?: object
): Promise<ISearchSource>;
loadStylePropsMeta({
layerName,
style,
dynamicStyleProps,
registerCancelCallback,
sourceQuery,
timeFilters,
}: {
layerName: string;
style: IVectorStyle;
dynamicStyleProps: Array<IDynamicStyleProperty<DynamicStylePropertyOptions>>;
registerCancelCallback: (callback: () => void) => void;
sourceQuery?: MapQuery;
timeFilters: TimeRange;
}): Promise<object>;
}
export class AbstractESSource extends AbstractVectorSource implements IESSource {
getId(): string;
getIndexPattern(): Promise<IndexPattern>;
getIndexPatternId(): string;
getGeoFieldName(): string;
getMaxResultWindow(): Promise<number>;
makeSearchSource(
searchFilters: VectorSourceRequestMeta,
limit: number,
initialSearchContext?: object
): Promise<ISearchSource>;
loadStylePropsMeta({
layerName,
style,
dynamicStyleProps,
registerCancelCallback,
sourceQuery,
timeFilters,
}: {
layerName: string;
style: IVectorStyle;
dynamicStyleProps: Array<IDynamicStyleProperty<DynamicStylePropertyOptions>>;
registerCancelCallback: (callback: () => void) => void;
sourceQuery?: MapQuery;
timeFilters: TimeRange;
}): Promise<object>;
_runEsQuery: ({
requestId,
requestName,
requestDescription,
searchSource,
registerCancelCallback,
}: {
requestId: string;
requestName: string;
requestDescription: string;
searchSource: ISearchSource;
registerCancelCallback: () => void;
}) => Promise<unknown>;
}

View file

@ -4,7 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AbstractVectorSource } from '../vector_source';
import { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
import { Filter, IFieldType, IndexPattern, ISearchSource } from 'src/plugins/data/public';
import { AbstractVectorSource, BoundsFilters } from '../vector_source';
import {
getAutocompleteService,
getIndexPatternService,
@ -12,62 +15,122 @@ import {
getSearchService,
} from '../../../kibana_services';
import { createExtentFilter } from '../../../../common/elasticsearch_util';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
import { copyPersistentState } from '../../../reducers/util';
import { DataRequestAbortError } from '../../util/data_request';
import { expandToTileBoundaries } from '../../../../common/geo_tile_utils';
import { search } from '../../../../../../../src/plugins/data/public';
import { IVectorSource } from '../vector_source';
import { TimeRange } from '../../../../../../../src/plugins/data/common';
import {
AbstractESSourceDescriptor,
AbstractSourceDescriptor,
DynamicStylePropertyOptions,
MapExtent,
MapQuery,
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 { ES_GEO_FIELD_TYPE, FieldFormatter } from '../../../../common/constants';
import {
Adapters,
RequestResponder,
} from '../../../../../../../src/plugins/inspector/common/adapters';
import { isValidStringConfig } from '../../util/valid_string_config';
export class AbstractESSource extends AbstractVectorSource {
constructor(descriptor, inspectorAdapters) {
super(
{
...descriptor,
applyGlobalQuery: _.get(descriptor, 'applyGlobalQuery', true),
},
inspectorAdapters
);
export interface IESSource extends IVectorSource {
isESSource(): true;
getId(): string;
getIndexPattern(): Promise<IndexPattern>;
getIndexPatternId(): string;
getGeoFieldName(): string;
loadStylePropsMeta({
layerName,
style,
dynamicStyleProps,
registerCancelCallback,
sourceQuery,
timeFilters,
}: {
layerName: string;
style: IVectorStyle;
dynamicStyleProps: Array<IDynamicStyleProperty<DynamicStylePropertyOptions>>;
registerCancelCallback: (callback: () => void) => void;
sourceQuery?: MapQuery;
timeFilters: TimeRange;
}): Promise<object>;
}
export class AbstractESSource extends AbstractVectorSource implements IESSource {
indexPattern?: IndexPattern;
readonly _descriptor: AbstractESSourceDescriptor;
static createDescriptor(
descriptor: Partial<AbstractESSourceDescriptor>
): AbstractESSourceDescriptor {
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:
// backfill old _.get usage
typeof descriptor.applyGlobalQuery !== 'undefined' ? !!descriptor.applyGlobalQuery : true,
};
}
getId() {
constructor(descriptor: AbstractESSourceDescriptor, inspectorAdapters?: Adapters) {
super(AbstractESSource.createDescriptor(descriptor), inspectorAdapters);
this._descriptor = descriptor;
}
getId(): string {
return this._descriptor.id;
}
isFieldAware() {
isFieldAware(): boolean {
return true;
}
isRefreshTimerAware() {
isRefreshTimerAware(): boolean {
return true;
}
isQueryAware() {
isQueryAware(): boolean {
return true;
}
getIndexPatternIds() {
getIndexPatternIds(): string[] {
return [this.getIndexPatternId()];
}
getQueryableIndexPatternIds() {
getQueryableIndexPatternIds(): string[] {
if (this.getApplyGlobalQuery()) {
return [this.getIndexPatternId()];
}
return [];
}
isESSource() {
isESSource(): true {
return true;
}
destroy() {
this._inspectorAdapters.requests.resetRequest(this.getId());
const inspectorAdapters = this.getInspectorAdapters();
if (inspectorAdapters) {
inspectorAdapters.requests.resetRequest(this.getId());
}
}
cloneDescriptor() {
cloneDescriptor(): AbstractSourceDescriptor {
const clonedDescriptor = copyPersistentState(this._descriptor);
// id used as uuid to track requests in inspector
clonedDescriptor.id = uuid();
@ -80,26 +143,45 @@ export class AbstractESSource extends AbstractVectorSource {
requestDescription,
searchSource,
registerCancelCallback,
}) {
}: {
requestId: string;
requestName: string;
requestDescription: string;
searchSource: ISearchSource;
registerCancelCallback: (callback: () => void) => void;
}): Promise<any> {
const abortController = new AbortController();
registerCancelCallback(() => abortController.abort());
const inspectorRequest = this._inspectorAdapters.requests.start(requestName, {
id: requestId,
description: requestDescription,
});
const inspectorAdapters = this.getInspectorAdapters();
let inspectorRequest: RequestResponder | undefined;
if (inspectorAdapters) {
inspectorRequest = inspectorAdapters.requests.start(requestName, {
id: requestId,
description: requestDescription,
});
}
let resp;
try {
inspectorRequest.stats(search.getRequestInspectorStats(searchSource));
searchSource.getSearchRequestBody().then((body) => {
inspectorRequest.json(body);
});
if (inspectorRequest) {
const requestStats = search.getRequestInspectorStats(searchSource);
inspectorRequest.stats(requestStats);
searchSource.getSearchRequestBody().then((body) => {
if (inspectorRequest) {
inspectorRequest.json(body);
}
});
}
resp = await searchSource.fetch({ abortSignal: abortController.signal });
inspectorRequest
.stats(search.getResponseInspectorStats(resp, searchSource))
.ok({ json: resp });
if (inspectorRequest) {
const responseStats = search.getResponseInspectorStats(resp, searchSource);
inspectorRequest.stats(responseStats).ok({ json: resp });
}
} catch (error) {
inspectorRequest.error({ error });
if (inspectorRequest) {
inspectorRequest.error(error);
}
if (error.name === 'AbortError') {
throw new DataRequestAbortError();
}
@ -115,22 +197,40 @@ export class AbstractESSource extends AbstractVectorSource {
return resp;
}
async makeSearchSource(searchFilters, limit, initialSearchContext) {
async makeSearchSource(
searchFilters: VectorSourceRequestMeta | VectorJoinSourceRequestMeta | BoundsFilters,
limit: number,
initialSearchContext?: object
): Promise<ISearchSource> {
const indexPattern = await this.getIndexPattern();
const isTimeAware = await this.isTimeAware();
const applyGlobalQuery = _.get(searchFilters, 'applyGlobalQuery', true);
const globalFilters = applyGlobalQuery ? searchFilters.filters : [];
const allFilters = [...globalFilters];
if (this.isFilterByMapBounds() && searchFilters.buffer) {
//buffer can be empty
const applyGlobalQuery =
typeof searchFilters.applyGlobalQuery === 'boolean' ? searchFilters.applyGlobalQuery : true;
const globalFilters: Filter[] = applyGlobalQuery ? searchFilters.filters : [];
const allFilters: Filter[] = [...globalFilters];
if (this.isFilterByMapBounds() && 'buffer' in searchFilters && searchFilters.buffer) {
// buffer can be empty
const geoField = await this._getGeoField();
const buffer = this.isGeoGridPrecisionAware()
? expandToTileBoundaries(searchFilters.buffer, searchFilters.geogridPrecision)
: searchFilters.buffer;
allFilters.push(createExtentFilter(buffer, geoField.name, geoField.type));
const buffer: MapExtent =
this.isGeoGridPrecisionAware() &&
'geogridPrecision' in searchFilters &&
typeof searchFilters.geogridPrecision === 'number'
? expandToTileBoundaries(searchFilters.buffer, searchFilters.geogridPrecision)
: searchFilters.buffer;
const extentFilter = createExtentFilter(
buffer,
geoField.name,
geoField.type as ES_GEO_FIELD_TYPE
);
// @ts-expect-error
allFilters.push(extentFilter);
}
if (isTimeAware) {
allFilters.push(getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters));
const filter = getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters);
if (filter) {
allFilters.push(filter);
}
}
const searchService = getSearchService();
const searchSource = await searchService.searchSource.create(initialSearchContext);
@ -153,7 +253,10 @@ export class AbstractESSource extends AbstractVectorSource {
return searchSource;
}
async getBoundsForFilters(boundsFilters, registerCancelCallback) {
async getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (callback: () => void) => void
): Promise<MapExtent | null> {
const searchSource = await this.makeSearchSource(boundsFilters, 0);
searchSource.setField('aggs', {
fitToBounds: {
@ -184,14 +287,14 @@ export class AbstractESSource extends AbstractVectorSource {
const minLon = esBounds.top_left.lon;
const maxLon = esBounds.bottom_right.lon;
return {
minLon: minLon > maxLon ? minLon - 360 : minLon, //fixes an ES bbox to straddle dateline
minLon: minLon > maxLon ? minLon - 360 : minLon, // fixes an ES bbox to straddle dateline
maxLon,
minLat: esBounds.bottom_right.lat,
maxLat: esBounds.top_left.lat,
};
}
async isTimeAware() {
async isTimeAware(): Promise<boolean> {
try {
const indexPattern = await this.getIndexPattern();
const timeField = indexPattern.timeFieldName;
@ -201,15 +304,19 @@ export class AbstractESSource extends AbstractVectorSource {
}
}
getIndexPatternId() {
getIndexPatternId(): string {
return this._descriptor.indexPatternId;
}
getGeoFieldName() {
getGeoFieldName(): string {
if (!this._descriptor.geoField) {
throw new Error('Should not call');
}
return this._descriptor.geoField;
}
async getIndexPattern() {
async getIndexPattern(): Promise<IndexPattern> {
// Do we need this cache? Doesn't the IndexPatternService take care of this?
if (this.indexPattern) {
return this.indexPattern;
}
@ -227,16 +334,16 @@ export class AbstractESSource extends AbstractVectorSource {
}
}
async supportsFitToBounds() {
async supportsFitToBounds(): Promise<boolean> {
try {
const geoField = await this._getGeoField();
return geoField.aggregatable;
return !!geoField.aggregatable;
} catch (error) {
return false;
}
}
async _getGeoField() {
async _getGeoField(): Promise<IFieldType> {
const indexPattern = await this.getIndexPattern();
const geoField = indexPattern.fields.getByName(this.getGeoFieldName());
if (!geoField) {
@ -250,7 +357,7 @@ export class AbstractESSource extends AbstractVectorSource {
return geoField;
}
async getDisplayName() {
async getDisplayName(): Promise<string> {
try {
const indexPattern = await this.getIndexPattern();
return indexPattern.title;
@ -260,15 +367,11 @@ export class AbstractESSource extends AbstractVectorSource {
}
}
isBoundsAware() {
isBoundsAware(): boolean {
return true;
}
getId() {
return this._descriptor.id;
}
async createFieldFormatter(field) {
async createFieldFormatter(field: IField): Promise<FieldFormatter | null> {
let indexPattern;
try {
indexPattern = await this.getIndexPattern();
@ -291,15 +394,25 @@ export class AbstractESSource extends AbstractVectorSource {
registerCancelCallback,
sourceQuery,
timeFilters,
}) {
}: {
layerName: string;
style: IVectorStyle;
dynamicStyleProps: Array<IDynamicStyleProperty<DynamicStylePropertyOptions>>;
registerCancelCallback: (callback: () => void) => void;
sourceQuery?: MapQuery;
timeFilters: TimeRange;
}): Promise<object> {
const promises = dynamicStyleProps.map((dynamicStyleProp) => {
return dynamicStyleProp.getFieldMetaRequest();
});
const fieldAggRequests = await Promise.all(promises);
const aggs = fieldAggRequests.reduce((aggs, fieldAggRequest) => {
return fieldAggRequest ? { ...aggs, ...fieldAggRequest } : aggs;
}, {});
const allAggs: Record<string, any> = fieldAggRequests.reduce(
(aggs: Record<string, any>, fieldAggRequest: unknown | null) => {
return fieldAggRequest ? { ...aggs, ...(fieldAggRequest as Record<string, any>) } : aggs;
},
{}
);
const indexPattern = await this.getIndexPattern();
const searchService = getSearchService();
@ -307,12 +420,15 @@ export class AbstractESSource extends AbstractVectorSource {
searchSource.setField('index', indexPattern);
searchSource.setField('size', 0);
searchSource.setField('aggs', aggs);
searchSource.setField('aggs', allAggs);
if (sourceQuery) {
searchSource.setField('query', sourceQuery);
}
if (style.isTimeAware() && (await this.isTimeAware())) {
searchSource.setField('filter', [getTimeFilter().createFilter(indexPattern, timeFilters)]);
const timeFilter = getTimeFilter().createFilter(indexPattern, timeFilters);
if (timeFilter) {
searchSource.setField('filter', [timeFilter]);
}
}
const resp = await this._runEsQuery({
@ -335,15 +451,17 @@ export class AbstractESSource extends AbstractVectorSource {
return resp.aggregations;
}
getValueSuggestions = async (field, query) => {
getValueSuggestions = async (field: IField, query: string): Promise<string[]> => {
try {
const indexPattern = await this.getIndexPattern();
const indexPatternField = indexPattern.fields.getByName(field.getRootName())!;
return await getAutocompleteService().getValueSuggestions({
indexPattern,
field: indexPattern.fields.getByName(field.getRootName()),
field: indexPatternField,
query,
});
} catch (error) {
// eslint-disable-next-line no-console
console.warn(
`Unable to fetch suggestions for field: ${field.getRootName()}, query: ${query}, error: ${
error.message

View file

@ -1,22 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { MapQuery, VectorJoinSourceRequestMeta } from '../../../../common/descriptor_types';
import { IField } from '../../fields/field';
import { IESAggSource } from '../es_agg_source';
import { PropertiesMap } from '../../joins/join';
export interface IESTermSource extends IESAggSource {
getTermField: () => IField;
hasCompleteConfig: () => boolean;
getWhereQuery: () => MapQuery;
getPropertiesMap: (
searchFilters: VectorJoinSourceRequestMeta,
leftSourceName: string,
leftFieldName: string,
registerCancelCallback: (callback: () => void) => void
) => PropertiesMap;
}

View file

@ -34,6 +34,7 @@ describe('getMetricFields', () => {
id: '1234',
indexPatternTitle: indexPatternTitle,
term: termFieldName,
indexPatternId: 'foobar',
});
const metrics = source.getMetricFields();
expect(metrics[0].getName()).toEqual('__kbnjoin__count__1234');
@ -46,6 +47,7 @@ describe('getMetricFields', () => {
indexPatternTitle: indexPatternTitle,
term: termFieldName,
metrics: metricExamples,
indexPatternId: 'foobar',
});
const metrics = source.getMetricFields();
expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed__1234');

View file

@ -5,8 +5,8 @@
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { ISearchSource, Query } from 'src/plugins/data/public';
import {
AGG_TYPE,
DEFAULT_MAX_BUCKETS_LIMIT,
@ -20,15 +20,22 @@ import {
getField,
addFieldToDSL,
extractPropertiesFromBucket,
BucketProperties,
} from '../../../../common/elasticsearch_util';
import {
ESTermSourceDescriptor,
VectorJoinSourceRequestMeta,
} from '../../../../common/descriptor_types';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
import { PropertiesMap } from '../../../../common/elasticsearch_util';
const TERMS_AGG_NAME = 'join';
const TERMS_BUCKET_KEYS_TO_IGNORE = ['key', 'doc_count'];
export function extractPropertiesMap(rawEsData, countPropertyName) {
const propertiesMap = new Map();
_.get(rawEsData, ['aggregations', TERMS_AGG_NAME, 'buckets'], []).forEach((termBucket) => {
export function extractPropertiesMap(rawEsData: any, countPropertyName: string): PropertiesMap {
const propertiesMap: PropertiesMap = new Map<string, BucketProperties>();
const buckets: any[] = _.get(rawEsData, ['aggregations', TERMS_AGG_NAME, 'buckets'], []);
buckets.forEach((termBucket: any) => {
const properties = extractPropertiesFromBucket(termBucket, TERMS_BUCKET_KEYS_TO_IGNORE);
if (countPropertyName) {
properties[countPropertyName] = termBucket.doc_count;
@ -41,37 +48,36 @@ export function extractPropertiesMap(rawEsData, countPropertyName) {
export class ESTermSource extends AbstractESAggSource {
static type = SOURCE_TYPES.ES_TERM_SOURCE;
constructor(descriptor, inspectorAdapters) {
super(descriptor, inspectorAdapters);
private readonly _termField: ESDocField;
readonly _descriptor: ESTermSourceDescriptor;
constructor(descriptor: ESTermSourceDescriptor, inspectorAdapters: Adapters) {
super(AbstractESAggSource.createDescriptor(descriptor), inspectorAdapters);
this._descriptor = descriptor;
this._termField = new ESDocField({
fieldName: descriptor.term,
fieldName: this._descriptor.term,
source: this,
origin: this.getOriginForField(),
});
}
static renderEditor({}) {
//no need to localize. this editor is never rendered.
return `<div>editor details</div>`;
}
hasCompleteConfig() {
return _.has(this._descriptor, 'indexPatternId') && _.has(this._descriptor, 'term');
}
getTermField() {
getTermField(): ESDocField {
return this._termField;
}
getOriginForField() {
getOriginForField(): FIELD_ORIGIN {
return FIELD_ORIGIN.JOIN;
}
getWhereQuery() {
getWhereQuery(): Query | undefined {
return this._descriptor.whereQuery;
}
getAggKey(aggType, fieldName) {
getAggKey(aggType: AGG_TYPE, fieldName?: string): string {
return getJoinAggKey({
aggType,
aggFieldName: fieldName,
@ -79,7 +85,7 @@ export class ESTermSource extends AbstractESAggSource {
});
}
getAggLabel(aggType, fieldName) {
getAggLabel(aggType: AGG_TYPE, fieldName: string) {
return aggType === AGG_TYPE.COUNT
? i18n.translate('xpack.maps.source.esJoin.countLabel', {
defaultMessage: `Count of {indexPatternTitle}`,
@ -88,13 +94,18 @@ export class ESTermSource extends AbstractESAggSource {
: super.getAggLabel(aggType, fieldName);
}
async getPropertiesMap(searchFilters, leftSourceName, leftFieldName, registerCancelCallback) {
async getPropertiesMap(
searchFilters: VectorJoinSourceRequestMeta,
leftSourceName: string,
leftFieldName: string,
registerCancelCallback: (callback: () => void) => void
): Promise<PropertiesMap> {
if (!this.hasCompleteConfig()) {
return [];
return new Map<string, BucketProperties>();
}
const indexPattern = await this.getIndexPattern();
const searchSource = await this.makeSearchSource(searchFilters, 0);
const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0);
const termsField = getField(indexPattern, this._termField.getName());
const termsAgg = { size: DEFAULT_MAX_BUCKETS_LIMIT };
searchSource.setField('aggs', {
@ -122,16 +133,16 @@ export class ESTermSource extends AbstractESAggSource {
return extractPropertiesMap(rawEsData, countPropertyName);
}
isFilterByMapBounds() {
isFilterByMapBounds(): boolean {
return false;
}
async getDisplayName() {
//no need to localize. this is never rendered.
async getDisplayName(): Promise<string> {
// no need to localize. this is never rendered.
return `es_table ${this.getIndexPatternId()}`;
}
getFieldNames() {
getFieldNames(): string[] {
return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName());
}
}

View file

@ -5,10 +5,11 @@
*/
import { Feature, FeatureCollection } from 'geojson';
import { AbstractVectorSource } from '../vector_source';
import { AbstractVectorSource, GeoJsonWithMeta } from '../vector_source';
import { EMPTY_FEATURE_COLLECTION, SOURCE_TYPES } from '../../../../common/constants';
import { GeojsonFileSourceDescriptor } from '../../../../common/descriptor_types';
import { registerSource } from '../source_registry';
import { IField } from '../../fields/field';
function getFeatureCollection(geoJson: Feature | FeatureCollection | null): FeatureCollection {
if (!geoJson) {
@ -30,26 +31,28 @@ function getFeatureCollection(geoJson: Feature | FeatureCollection | null): Feat
}
export class GeojsonFileSource extends AbstractVectorSource {
static type = SOURCE_TYPES.GEOJSON_FILE;
static createDescriptor(
geoJson: Feature | FeatureCollection | null,
name: string
): GeojsonFileSourceDescriptor {
return {
type: GeojsonFileSource.type,
type: SOURCE_TYPES.GEOJSON_FILE,
__featureCollection: getFeatureCollection(geoJson),
name,
};
}
async getGeoJsonWithMeta() {
async getGeoJsonWithMeta(): Promise<GeoJsonWithMeta> {
return {
data: (this._descriptor as GeojsonFileSourceDescriptor).__featureCollection,
meta: {},
};
}
createField({ fieldName }: { fieldName: string }): IField {
throw new Error('Not implemented');
}
async getDisplayName() {
return (this._descriptor as GeojsonFileSourceDescriptor).name;
}

View file

@ -26,7 +26,7 @@ export const kibanaRegionMapLayerWizardConfig: LayerWizard = {
}),
icon: 'logoKibana',
renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: unknown) => {
const onSourceConfigChange = (sourceConfig: { name: string }) => {
const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig);
const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
previewLayers([layerDescriptor]);

View file

@ -1,15 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { AbstractVectorSource, IVectorSource } from '../vector_source';
export interface IKibanaRegionSource extends IVectorSource {
getVectorFileMeta(): Promise<unknown>;
}
export class KibanaRegionSource extends AbstractVectorSource implements IKibanaRegionSource {
getVectorFileMeta(): Promise<unknown>;
}

View file

@ -4,29 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AbstractVectorSource } from '../vector_source';
import { getKibanaRegionList } from '../../../meta';
import { i18n } from '@kbn/i18n';
import { AbstractVectorSource, GeoJsonWithMeta } from '../vector_source';
import { getKibanaRegionList } from '../../../meta';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { FIELD_ORIGIN, SOURCE_TYPES } from '../../../../common/constants';
import { FIELD_ORIGIN, FORMAT_TYPE, SOURCE_TYPES } from '../../../../common/constants';
import { KibanaRegionField } from '../../fields/kibana_region_field';
import { registerSource } from '../source_registry';
import { KibanaRegionmapSourceDescriptor } from '../../../../common/descriptor_types/source_descriptor_types';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
import { IField } from '../../fields/field';
import { LayerConfig } from '../../../../../../../src/plugins/region_map/config';
export const sourceTitle = i18n.translate('xpack.maps.source.kbnRegionMapTitle', {
defaultMessage: 'Configured GeoJSON',
});
export class KibanaRegionmapSource extends AbstractVectorSource {
static type = SOURCE_TYPES.REGIONMAP_FILE;
readonly _descriptor: KibanaRegionmapSourceDescriptor;
static createDescriptor({ name }) {
static createDescriptor({ name }: { name: string }): KibanaRegionmapSourceDescriptor {
return {
type: KibanaRegionmapSource.type,
name: name,
type: SOURCE_TYPES.REGIONMAP_FILE,
name,
};
}
createField({ fieldName }) {
constructor(descriptor: KibanaRegionmapSourceDescriptor, inspectorAdapters?: Adapters) {
super(descriptor, inspectorAdapters);
this._descriptor = descriptor;
}
createField({ fieldName }: { fieldName: string }): KibanaRegionField {
return new KibanaRegionField({
fieldName,
source: this,
@ -49,10 +58,12 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
];
}
async getVectorFileMeta() {
const regionList = getKibanaRegionList();
const meta = regionList.find((source) => source.name === this._descriptor.name);
if (!meta) {
async getVectorFileMeta(): Promise<LayerConfig> {
const regionList: LayerConfig[] = getKibanaRegionList();
const layerConfig: LayerConfig | undefined = regionList.find(
(regionConfig: LayerConfig) => regionConfig.name === this._descriptor.name
);
if (!layerConfig) {
throw new Error(
i18n.translate('xpack.maps.source.kbnRegionMap.noConfigErrorMessage', {
defaultMessage: `Unable to find map.regionmap configuration for {name}`,
@ -62,13 +73,13 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
})
);
}
return meta;
return layerConfig;
}
async getGeoJsonWithMeta() {
async getGeoJsonWithMeta(): Promise<GeoJsonWithMeta> {
const vectorFileMeta = await this.getVectorFileMeta();
const featureCollection = await AbstractVectorSource.getGeoJson({
format: vectorFileMeta.format.type,
format: vectorFileMeta.format.type as FORMAT_TYPE,
featureCollectionPath: vectorFileMeta.meta.feature_collection_path,
fetchUrl: vectorFileMeta.url,
});
@ -78,12 +89,16 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
};
}
async getLeftJoinFields() {
const vectorFileMeta = await this.getVectorFileMeta();
return vectorFileMeta.fields.map((f) => this.createField({ fieldName: f.name }));
async getLeftJoinFields(): Promise<IField[]> {
const vectorFileMeta: LayerConfig = await this.getVectorFileMeta();
return vectorFileMeta.fields.map(
(field): KibanaRegionField => {
return this.createField({ fieldName: field.name });
}
);
}
async getDisplayName() {
async getDisplayName(): Promise<string> {
return this._descriptor.name;
}

View file

@ -28,6 +28,7 @@ import {
import { MVTField } from '../../fields/mvt_field';
import { UpdateSourceEditor } from './update_source_editor';
import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
export const sourceTitle = i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle',
@ -66,7 +67,7 @@ export class MVTSingleLayerVectorSource
constructor(
sourceDescriptor: TiledSingleLayerVectorSourceDescriptor,
inspectorAdapters?: object
inspectorAdapters?: Adapters
) {
super(sourceDescriptor, inspectorAdapters);
this._descriptor = MVTSingleLayerVectorSource.createDescriptor(sourceDescriptor);
@ -165,22 +166,22 @@ export class MVTSingleLayerVectorSource
return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
}
canFormatFeatureProperties() {
canFormatFeatureProperties(): boolean {
return !!this._tooltipFields.length;
}
getMinZoom() {
getMinZoom(): number {
return this._descriptor.minSourceZoom;
}
getMaxZoom() {
getMaxZoom(): number {
return this._descriptor.maxSourceZoom;
}
getBoundsForFilters(
async getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (callback: () => void) => void
): MapExtent | null {
): Promise<MapExtent | null> {
return null;
}

View file

@ -9,6 +9,7 @@
import { ReactElement } from 'react';
import { Adapters } from 'src/plugins/inspector/public';
import { GeoJsonProperties } from 'geojson';
import { copyPersistentState } from '../../reducers/util';
import { IField } from '../fields/field';
@ -62,7 +63,7 @@ export interface ISource {
getIndexPatternIds(): string[];
getQueryableIndexPatternIds(): string[];
getGeoGridPrecision(zoom: number): number;
getPreIndexedShape(): Promise<PreIndexedShape | null>;
getPreIndexedShape(properties: GeoJsonProperties): Promise<PreIndexedShape | null>;
createFieldFormatter(field: IField): Promise<FieldFormatter | null>;
getValueSuggestions(field: IField, query: string): Promise<string[]>;
getMinZoom(): number;
@ -72,7 +73,7 @@ export interface ISource {
export class AbstractSource implements ISource {
readonly _descriptor: AbstractSourceDescriptor;
readonly _inspectorAdapters?: Adapters | undefined;
private readonly _inspectorAdapters?: Adapters;
constructor(descriptor: AbstractSourceDescriptor, inspectorAdapters?: Adapters) {
this._descriptor = descriptor;
@ -153,7 +154,7 @@ export class AbstractSource implements ISource {
return false;
}
getJoinsDisabledReason() {
getJoinsDisabledReason(): string | null {
return null;
}
@ -162,7 +163,7 @@ export class AbstractSource implements ISource {
}
// Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes
async getPreIndexedShape(/* properties */): Promise<PreIndexedShape | null> {
async getPreIndexedShape(properties: GeoJsonProperties): Promise<PreIndexedShape | null> {
return null;
}
@ -183,11 +184,11 @@ export class AbstractSource implements ISource {
return false;
}
getMinZoom() {
getMinZoom(): number {
return MIN_ZOOM;
}
getMaxZoom() {
getMaxZoom(): number {
return MAX_ZOOM;
}

View file

@ -6,11 +6,12 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { ISource } from './source';
import { Adapters } from '../../../../../../src/plugins/inspector/common/adapters';
export type SourceRegistryEntry = {
ConstructorFunction: new (
sourceDescriptor: any, // this is the source-descriptor that corresponds specifically to the particular ISource instance
inspectorAdapters?: object
inspectorAdapters?: Adapters
) => ISource;
type: string;
};

View file

@ -1,108 +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;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';
import { Filter, TimeRange } from 'src/plugins/data/public';
import { AbstractSource, ISource } from '../source';
import { IField } from '../../fields/field';
import {
ESSearchSourceResponseMeta,
MapExtent,
MapFilters,
MapQuery,
VectorSourceRequestMeta,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { VECTOR_SHAPE_TYPE } from '../../../../common/constants';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
import { DataRequest } from '../../util/data_request';
export interface SourceTooltipConfig {
tooltipContent: string | null;
areResultsTrimmed: boolean;
}
export type GeoJsonFetchMeta = ESSearchSourceResponseMeta;
export type GeoJsonWithMeta = {
data: FeatureCollection;
meta?: GeoJsonFetchMeta;
};
export type BoundsFilters = {
applyGlobalQuery: boolean;
filters: Filter[];
query?: MapQuery;
sourceQuery?: MapQuery;
timeFilters: TimeRange;
};
export interface IVectorSource extends ISource {
getTooltipProperties(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (callback: () => void) => void
): MapExtent | null;
getGeoJsonWithMeta(
layerName: string,
searchFilters: MapFilters,
registerCancelCallback: (callback: () => void) => void,
isRequestStillActive: () => boolean
): Promise<GeoJsonWithMeta>;
getFields(): Promise<IField[]>;
getFieldByName(fieldName: string): IField | null;
getLeftJoinFields(): Promise<IField[]>;
getSyncMeta(): VectorSourceSyncMeta;
getFieldNames(): string[];
getApplyGlobalQuery(): boolean;
createField({ fieldName }: { fieldName: string }): IField;
canFormatFeatureProperties(): boolean;
getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]>;
isBoundsAware(): boolean;
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig;
}
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
getTooltipProperties(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (callback: () => void) => void
): MapExtent | null;
getGeoJsonWithMeta(
layerName: string,
searchFilters: VectorSourceRequestMeta,
registerCancelCallback: (callback: () => void) => void,
isRequestStillActive: () => boolean
): Promise<GeoJsonWithMeta>;
getFields(): Promise<IField[]>;
getFieldByName(fieldName: string): IField | null;
getLeftJoinFields(): Promise<IField[]>;
getSyncMeta(): VectorSourceSyncMeta;
getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]>;
canFormatFeatureProperties(): boolean;
getApplyGlobalQuery(): boolean;
getFieldNames(): string[];
createField({ fieldName }: { fieldName: string }): IField;
isBoundsAware(): boolean;
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig;
}
export interface ITiledSingleLayerVectorSource extends IVectorSource {
getUrlTemplateWithMeta(
searchFilters: VectorSourceRequestMeta
): Promise<{
layerName: string;
urlTemplate: string;
minSourceZoom: number;
maxSourceZoom: number;
}>;
getMinZoom(): number;
getMaxZoom(): number;
getLayerName(): string;
}

View file

@ -1,140 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { TooltipProperty } from '../../tooltips/tooltip_property';
import { AbstractSource } from './../source';
import * as topojson from 'topojson-client';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { VECTOR_SHAPE_TYPE } from '../../../../common/constants';
export class AbstractVectorSource extends AbstractSource {
static async getGeoJson({ format, featureCollectionPath, fetchUrl }) {
let fetchedJson;
try {
// TODO proxy map.regionmap url requests through kibana server and then use kfetch
// Can not use kfetch because fetchUrl may point to external URL. (map.regionmap)
const response = await fetch(fetchUrl);
if (!response.ok) {
throw new Error('Request failed');
}
fetchedJson = await response.json();
} catch (e) {
throw new Error(
i18n.translate('xpack.maps.source.vetorSource.requestFailedErrorMessage', {
defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`,
values: { fetchUrl },
})
);
}
if (format === 'geojson') {
return fetchedJson;
}
if (format === 'topojson') {
const features = _.get(fetchedJson, `objects.${featureCollectionPath}`);
return topojson.feature(fetchedJson, features);
}
throw new Error(
i18n.translate('xpack.maps.source.vetorSource.formatErrorMessage', {
defaultMessage: `Unable to fetch vector shapes from url: {format}`,
values: { format },
})
);
}
/**
* factory function creating a new field-instance
* @param fieldName
* @param label
* @returns {IField}
*/
createField() {
throw new Error(`Should implemement ${this.constructor.type} ${this}`);
}
getFieldNames() {
return [];
}
/**
* Retrieves a field. This may be an existing instance.
* @param fieldName
* @param label
* @returns {IField}
*/
getFieldByName(name) {
return this.createField({ fieldName: name });
}
_getTooltipPropertyNames() {
return this._tooltipFields.map((field) => field.getName());
}
isFilterByMapBounds() {
return false;
}
isBoundsAware() {
return false;
}
async getBoundsForFilters() {
console.warn('Should implement AbstractVectorSource#getBoundsForFilters');
return null;
}
async getFields() {
return [];
}
async getLeftJoinFields() {
return [];
}
async getGeoJsonWithMeta() {
throw new Error('Should implement VectorSource#getGeoJson');
}
canFormatFeatureProperties() {
return false;
}
// Allow source to filter and format feature properties before displaying to user
async getTooltipProperties(properties) {
const tooltipProperties = [];
for (const key in properties) {
if (key.startsWith('__kbn')) {
//these are system properties and should be ignored
continue;
}
tooltipProperties.push(new TooltipProperty(key, key, properties[key]));
}
return tooltipProperties;
}
async isTimeAware() {
return false;
}
showJoinEditor() {
return true;
}
async getSupportedShapeTypes() {
return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
}
getSourceTooltipContent(/* sourceDataRequest */) {
return { tooltipContent: null, areResultsTrimmed: false };
}
getSyncMeta() {
return {};
}
}

View file

@ -0,0 +1,209 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-expect-error
import * as topojson from 'topojson-client';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { FeatureCollection, GeoJsonProperties } from 'geojson';
import { Filter, TimeRange } from 'src/plugins/data/public';
import { FORMAT_TYPE, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
import { TooltipProperty, ITooltipProperty } from '../../tooltips/tooltip_property';
import { AbstractSource, ISource } from '../source';
import { IField } from '../../fields/field';
import {
ESSearchSourceResponseMeta,
MapExtent,
MapQuery,
VectorSourceRequestMeta,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { DataRequest } from '../../util/data_request';
export interface SourceTooltipConfig {
tooltipContent: string | null;
areResultsTrimmed: boolean;
}
export type GeoJsonFetchMeta = ESSearchSourceResponseMeta;
export interface GeoJsonWithMeta {
data: FeatureCollection;
meta?: GeoJsonFetchMeta;
}
export interface BoundsFilters {
applyGlobalQuery: boolean;
filters: Filter[];
query?: MapQuery;
sourceQuery?: MapQuery;
timeFilters: TimeRange;
}
export interface IVectorSource extends ISource {
getTooltipProperties(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (callback: () => void) => void
): Promise<MapExtent | null>;
getGeoJsonWithMeta(
layerName: string,
searchFilters: VectorSourceRequestMeta,
registerCancelCallback: (callback: () => void) => void,
isRequestStillActive: () => boolean
): Promise<GeoJsonWithMeta>;
getFields(): Promise<IField[]>;
getFieldByName(fieldName: string): IField | null;
getLeftJoinFields(): Promise<IField[]>;
getSyncMeta(): VectorSourceSyncMeta | null;
getFieldNames(): string[];
getApplyGlobalQuery(): boolean;
createField({ fieldName }: { fieldName: string }): IField;
canFormatFeatureProperties(): boolean;
getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]>;
isBoundsAware(): boolean;
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig;
}
export interface ITiledSingleLayerVectorSource extends IVectorSource {
getUrlTemplateWithMeta(
searchFilters: VectorSourceRequestMeta
): Promise<{
layerName: string;
urlTemplate: string;
minSourceZoom: number;
maxSourceZoom: number;
}>;
getMinZoom(): number;
getMaxZoom(): number;
getLayerName(): string;
}
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
static async getGeoJson({
format,
featureCollectionPath,
fetchUrl,
}: {
format: FORMAT_TYPE;
featureCollectionPath: string;
fetchUrl: string;
}) {
let fetchedJson;
try {
const response = await fetch(fetchUrl);
if (!response.ok) {
throw new Error('Request failed');
}
fetchedJson = await response.json();
} catch (e) {
throw new Error(
i18n.translate('xpack.maps.source.vetorSource.requestFailedErrorMessage', {
defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`,
values: { fetchUrl },
})
);
}
if (format === FORMAT_TYPE.GEOJSON) {
return fetchedJson;
}
if (format === FORMAT_TYPE.TOPOJSON) {
const features = _.get(fetchedJson, `objects.${featureCollectionPath}`);
return topojson.feature(fetchedJson, features);
}
throw new Error(
i18n.translate('xpack.maps.source.vetorSource.formatErrorMessage', {
defaultMessage: `Unable to fetch vector shapes from url: {format}`,
values: { format },
})
);
}
getFieldNames(): string[] {
return [];
}
createField({ fieldName }: { fieldName: string }): IField {
throw new Error('Not implemented');
}
getFieldByName(fieldName: string): IField | null {
return this.createField({ fieldName });
}
isFilterByMapBounds() {
return false;
}
isBoundsAware(): boolean {
return false;
}
async getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (callback: () => void) => void
): Promise<MapExtent | null> {
return null;
}
async getFields(): Promise<IField[]> {
return [];
}
async getLeftJoinFields(): Promise<IField[]> {
return [];
}
async getGeoJsonWithMeta(
layerName: string,
searchFilters: VectorSourceRequestMeta,
registerCancelCallback: (callback: () => void) => void,
isRequestStillActive: () => boolean
): Promise<GeoJsonWithMeta> {
throw new Error('Should implement VectorSource#getGeoJson');
}
canFormatFeatureProperties() {
return false;
}
// Allow source to filter and format feature properties before displaying to user
async getTooltipProperties(properties: GeoJsonProperties): Promise<ITooltipProperty[]> {
const tooltipProperties: ITooltipProperty[] = [];
for (const key in properties) {
if (key.startsWith('__kbn')) {
// these are system properties and should be ignored
continue;
}
tooltipProperties.push(new TooltipProperty(key, key, properties[key]));
}
return tooltipProperties;
}
async isTimeAware() {
return false;
}
showJoinEditor() {
return true;
}
async getSupportedShapeTypes() {
return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
}
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig {
return { tooltipContent: null, areResultsTrimmed: false };
}
getSyncMeta(): VectorSourceSyncMeta | null {
return null;
}
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/**
* Validate user-generated data (e.g. descriptors). Possibly dirty or of wrong type.
* @param value
*/
export function isValidStringConfig(value: any): boolean {
return typeof value === 'string' && value !== '';
}

View file

@ -29,8 +29,9 @@ import {
getKibanaVersion,
} from './kibana_services';
import { getLicenseId } from './licensed_features';
import { LayerConfig } from '../../../../src/plugins/region_map/config';
export function getKibanaRegionList(): unknown[] {
export function getKibanaRegionList(): LayerConfig[] {
return getRegionmapLayers();
}

View file

@ -34,6 +34,7 @@ import {
import { extractFeaturesFromFilters } from '../../common/elasticsearch_util';
import { MapStoreState } from '../reducers/store';
import {
AbstractSourceDescriptor,
DataRequestDescriptor,
DrawState,
Goto,
@ -94,7 +95,13 @@ export function createLayerInstance(
}
}
function createSourceInstance(sourceDescriptor: any, inspectorAdapters?: Adapters): ISource {
function createSourceInstance(
sourceDescriptor: AbstractSourceDescriptor | null,
inspectorAdapters?: Adapters
): ISource {
if (sourceDescriptor === null) {
throw new Error('Source-descriptor should be initialized');
}
const source = getSourceByType(sourceDescriptor.type);
if (!source) {
throw new Error(`Unrecognized sourceType ${sourceDescriptor.type}`);