diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts
index 91943b7d87790..6c42664c90ffb 100644
--- a/src/plugins/data_views/common/types.ts
+++ b/src/plugins/data_views/common/types.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { SavedObject } from '@kbn/core/server';
import type { ErrorToastOptions, ToastInputFields } from '@kbn/core-notifications-browser';
@@ -437,7 +438,7 @@ export type FieldSpec = DataViewFieldBase & {
/**
* set if field is a TSDB metric field
*/
- timeSeriesMetric?: 'histogram' | 'summary' | 'gauge' | 'counter';
+ timeSeriesMetric?: estypes.MappingTimeSeriesMetricType;
// not persisted
diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts
index dc73cebb8855d..3511cefd34d87 100644
--- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts
+++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ElasticsearchClient } from '@kbn/core/server';
import { keyBy } from 'lodash';
import type { QueryDslQueryContainer } from '../../common/types';
@@ -27,7 +28,7 @@ export interface FieldDescriptor {
metadata_field?: boolean;
fixedInterval?: string[];
timeZone?: string[];
- timeSeriesMetric?: 'histogram' | 'summary' | 'counter' | 'gauge';
+ timeSeriesMetric?: estypes.MappingTimeSeriesMetricType;
timeSeriesDimension?: boolean;
}
diff --git a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.ts
index 0cf2e460d35f5..ee029f1271297 100644
--- a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.ts
+++ b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_caps_response.ts
@@ -122,13 +122,17 @@ export function readFieldCapsResponse(
return agg;
}
- let timeSeriesMetricType: 'gauge' | 'counter' | undefined;
+ let timeSeriesMetricType: 'gauge' | 'counter' | 'position' | undefined;
if (timeSeriesMetricProp.length === 1 && timeSeriesMetricProp[0] === 'gauge') {
timeSeriesMetricType = 'gauge';
}
if (timeSeriesMetricProp.length === 1 && timeSeriesMetricProp[0] === 'counter') {
timeSeriesMetricType = 'counter';
}
+ // @ts-expect-error MappingTimeSeriesMetricType does not contain 'position'
+ if (timeSeriesMetricProp.length === 1 && timeSeriesMetricProp[0] === 'position') {
+ timeSeriesMetricType = 'position';
+ }
const esType = types[0];
const field = {
name: fieldName,
@@ -144,7 +148,9 @@ export function readFieldCapsResponse(
timeSeriesDimension: capsByType[types[0]].time_series_dimension,
};
// This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes
+ // @ts-expect-error MappingTimeSeriesMetricType does not contain 'position'
agg.array.push(field);
+ // @ts-expect-error MappingTimeSeriesMetricType does not contain 'position'
agg.hash[fieldName] = field;
return agg;
},
diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts
index 3ac86807ff7f6..3094596a3f13b 100644
--- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts
@@ -91,8 +91,10 @@ export type ESGeoGridSourceDescriptor = AbstractESAggSourceDescriptor & {
export type ESGeoLineSourceDescriptor = AbstractESAggSourceDescriptor & {
geoField: string;
- splitField: string;
- sortField: string;
+ groupByTimeseries: boolean;
+ lineSimplificationSize: number;
+ splitField?: string;
+ sortField?: string;
};
export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & {
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/show_as_label.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/show_as_label.tsx
index e16bc1cb8bcff..22c4417308a73 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/show_as_label.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/show_as_label.tsx
@@ -19,7 +19,7 @@ export function ShowAsLabel(props: Props) {
return (
+
- {CLUSTER_LABEL}
-
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/constants.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/constants.ts
new file mode 100644
index 0000000000000..a95c779b85a9a
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/constants.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// geo_line aggregation without time series buckets uses lots of resources
+// limit resource consumption by limiting number of tracks to smaller amount
+export const MAX_TERMS_TRACKS = 250;
+
+// Constant is used to identify time series id field in UIs, tooltips, and styling.
+// Constant is not passed to Elasticsearch APIs and is not related to '_tsid' document metadata field.
+// Constant value of '_tsid' is arbitrary.
+export const TIME_SERIES_ID_FIELD_NAME = '_tsid';
+
+export const DEFAULT_LINE_SIMPLIFICATION_SIZE = 500;
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/convert_to_geojson.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/convert_to_geojson.ts
index de571dd69f3c6..2323078c2c09c 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/convert_to_geojson.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/convert_to_geojson.ts
@@ -20,7 +20,9 @@ export function convertToGeoJson(esResponse: any, entitySplitFieldName: string)
for (let i = 0; i < entityKeys.length; i++) {
const entityKey = entityKeys[i];
const bucket = buckets[entityKey];
- const feature = bucket.path as Feature;
+ const feature = {
+ ...(bucket.path as Feature),
+ };
if (!feature.properties!.complete) {
numTrimmedTracks++;
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/create_source_editor.tsx
index 25f946d88db55..f847db00b5210 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/create_source_editor.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/create_source_editor.tsx
@@ -7,39 +7,38 @@
import React, { Component } from 'react';
-import { DataView } from '@kbn/data-plugin/common';
-import { i18n } from '@kbn/i18n';
-import { EuiFormRow, EuiPanel } from '@elastic/eui';
-import { SingleFieldSelect } from '../../../components/single_field_select';
+import type { DataView, DataViewField } from '@kbn/data-plugin/common';
+import { EuiPanel } from '@elastic/eui';
import { GeoIndexPatternSelect } from '../../../components/geo_index_pattern_select';
-
-import { getGeoPointFields } from '../../../index_pattern_util';
+import { GeoFieldSelect } from '../../../components/geo_field_select';
+import { ESGeoLineSourceDescriptor } from '../../../../common/descriptor_types';
+import { getGeoPointFields, getIsTimeseries } from '../../../index_pattern_util';
import { GeoLineForm } from './geo_line_form';
+import { DEFAULT_LINE_SIMPLIFICATION_SIZE } from './constants';
interface Props {
- onSourceConfigChange: (
- sourceConfig: {
- indexPatternId: string;
- geoField: string;
- splitField: string;
- sortField: string;
- } | null
- ) => void;
+ onSourceConfigChange: (sourceConfig: Partial | null) => void;
}
interface State {
indexPattern: DataView | null;
+ pointFields: DataViewField[];
geoField: string;
+ groupByTimeseries: boolean;
splitField: string;
sortField: string;
+ lineSimplificationSize: number;
}
export class CreateSourceEditor extends Component {
state: State = {
indexPattern: null,
+ pointFields: [],
geoField: '',
+ groupByTimeseries: false,
splitField: '',
sortField: '',
+ lineSimplificationSize: DEFAULT_LINE_SIMPLIFICATION_SIZE,
};
_onIndexPatternSelect = (indexPattern: DataView) => {
@@ -47,6 +46,8 @@ export class CreateSourceEditor extends Component {
this.setState(
{
indexPattern,
+ pointFields,
+ groupByTimeseries: getIsTimeseries(indexPattern),
geoField: pointFields.length ? pointFields[0].name : '',
sortField: indexPattern.timeFieldName ? indexPattern.timeFieldName : '',
},
@@ -67,6 +68,24 @@ export class CreateSourceEditor extends Component {
);
};
+ _onGroupByTimeseriesChange = (groupByTimeseries: boolean) => {
+ this.setState(
+ {
+ groupByTimeseries,
+ },
+ this.previewLayer
+ );
+ };
+
+ _onLineSimplificationSizeChange = (lineSimplificationSize: number) => {
+ this.setState(
+ {
+ lineSimplificationSize,
+ },
+ this.previewLayer
+ );
+ };
+
_onSplitFieldSelect = (newValue: string) => {
this.setState(
{
@@ -86,11 +105,28 @@ export class CreateSourceEditor extends Component {
};
previewLayer = () => {
- const { indexPattern, geoField, splitField, sortField } = this.state;
+ const {
+ indexPattern,
+ geoField,
+ groupByTimeseries,
+ splitField,
+ sortField,
+ lineSimplificationSize,
+ } = this.state;
const sourceConfig =
- indexPattern && indexPattern.id && geoField && splitField && sortField
- ? { indexPatternId: indexPattern.id, geoField, splitField, sortField }
+ indexPattern &&
+ indexPattern.id &&
+ geoField &&
+ (groupByTimeseries || (splitField && sortField))
+ ? {
+ indexPatternId: indexPattern.id,
+ geoField,
+ groupByTimeseries,
+ lineSimplificationSize,
+ splitField,
+ sortField,
+ }
: null;
this.props.onSourceConfigChange(sourceConfig);
};
@@ -101,20 +137,12 @@ export class CreateSourceEditor extends Component {
}
return (
-
-
-
+
);
}
@@ -125,9 +153,14 @@ export class CreateSourceEditor extends Component {
return (
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx
index 65a4d5f2fb5b3..24cafe66de35c 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx
@@ -30,6 +30,7 @@ import { AbstractESAggSource, ESAggsSourceSyncMeta } from '../es_agg_source';
import { DataRequest } from '../../util/data_request';
import { convertToGeoJson } from './convert_to_geojson';
import { ESDocField } from '../../fields/es_doc_field';
+import { InlineField } from '../../fields/inline_field';
import { UpdateSourceEditor } from './update_source_editor';
import { ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { GeoJsonWithMeta } from '../vector_source';
@@ -39,11 +40,18 @@ import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_proper
import { getIsGoldPlus } from '../../../licensed_features';
import { LICENSED_FEATURES } from '../../../licensed_features';
import { mergeExecutionContext } from '../execution_context_utils';
+import { ENTITY_INPUT_LABEL, SORT_INPUT_LABEL } from './geo_line_form';
+import {
+ DEFAULT_LINE_SIMPLIFICATION_SIZE,
+ MAX_TERMS_TRACKS,
+ TIME_SERIES_ID_FIELD_NAME,
+} from './constants';
type ESGeoLineSourceSyncMeta = ESAggsSourceSyncMeta &
- Pick;
-
-const MAX_TRACKS = 250;
+ Pick<
+ ESGeoLineSourceDescriptor,
+ 'groupByTimeseries' | 'lineSimplificationSize' | 'splitField' | 'sortField'
+ >;
export const geoLineTitle = i18n.translate('xpack.maps.source.esGeoLineTitle', {
defaultMessage: 'Tracks',
@@ -64,21 +72,27 @@ export class ESGeoLineSource extends AbstractESAggSource {
const normalizedDescriptor = AbstractESAggSource.createDescriptor(
descriptor
) as ESGeoLineSourceDescriptor;
+
if (!isValidStringConfig(normalizedDescriptor.geoField)) {
throw new Error('Cannot create an ESGeoLineSource without a geoField');
}
- if (!isValidStringConfig(normalizedDescriptor.splitField)) {
- throw new Error('Cannot create an ESGeoLineSource without a splitField');
- }
- if (!isValidStringConfig(normalizedDescriptor.sortField)) {
- throw new Error('Cannot create an ESGeoLineSource without a sortField');
- }
+
+ const groupByTimeseries =
+ typeof normalizedDescriptor.groupByTimeseries === 'boolean'
+ ? normalizedDescriptor.groupByTimeseries
+ : false;
+
return {
...normalizedDescriptor,
type: SOURCE_TYPES.ES_GEO_LINE,
+ groupByTimeseries,
+ lineSimplificationSize:
+ typeof normalizedDescriptor.lineSimplificationSize === 'number'
+ ? normalizedDescriptor.lineSimplificationSize
+ : DEFAULT_LINE_SIMPLIFICATION_SIZE,
geoField: normalizedDescriptor.geoField!,
- splitField: normalizedDescriptor.splitField!,
- sortField: normalizedDescriptor.sortField!,
+ splitField: normalizedDescriptor.splitField,
+ sortField: normalizedDescriptor.sortField,
};
}
@@ -103,8 +117,10 @@ export class ESGeoLineSource extends AbstractESAggSource {
indexPatternId={this.getIndexPatternId()}
onChange={onChange}
metrics={this._descriptor.metrics}
- splitField={this._descriptor.splitField}
- sortField={this._descriptor.sortField}
+ groupByTimeseries={this._descriptor.groupByTimeseries}
+ lineSimplificationSize={this._descriptor.lineSimplificationSize}
+ splitField={this._descriptor.splitField ?? ''}
+ sortField={this._descriptor.sortField ?? ''}
/>
);
}
@@ -112,6 +128,8 @@ export class ESGeoLineSource extends AbstractESAggSource {
getSyncMeta(dataFilters: DataFilters): ESGeoLineSourceSyncMeta {
return {
...super.getSyncMeta(dataFilters),
+ groupByTimeseries: this._descriptor.groupByTimeseries,
+ lineSimplificationSize: this._descriptor.lineSimplificationSize,
splitField: this._descriptor.splitField,
sortField: this._descriptor.sortField,
};
@@ -136,22 +154,43 @@ export class ESGeoLineSource extends AbstractESAggSource {
];
}
- _createSplitField(): IField {
- return new ESDocField({
- fieldName: this._descriptor.splitField,
+ _createSplitField(): IField | null {
+ return this._descriptor.splitField
+ ? new ESDocField({
+ fieldName: this._descriptor.splitField,
+ source: this,
+ origin: FIELD_ORIGIN.SOURCE,
+ })
+ : null;
+ }
+
+ _createTsidField(): IField | null {
+ return new InlineField({
+ fieldName: TIME_SERIES_ID_FIELD_NAME,
+ label: TIME_SERIES_ID_FIELD_NAME,
source: this,
origin: FIELD_ORIGIN.SOURCE,
+ dataType: 'string',
});
}
async getFields(): Promise {
- return [...this.getMetricFields(), this._createSplitField()];
+ const groupByField = this._descriptor.groupByTimeseries
+ ? this._createTsidField()
+ : this._createSplitField();
+ return groupByField ? [...this.getMetricFields(), groupByField] : this.getMetricFields();
}
getFieldByName(name: string): IField | null {
- return name === this._descriptor.splitField
- ? this._createSplitField()
- : this.getMetricFieldForName(name);
+ if (name === this._descriptor.splitField) {
+ return this._createSplitField();
+ }
+
+ if (name === TIME_SERIES_ID_FIELD_NAME) {
+ return this._createTsidField();
+ }
+
+ return this.getMetricFieldForName(name);
}
isGeoGridPrecisionAware() {
@@ -173,6 +212,126 @@ export class ESGeoLineSource extends AbstractESAggSource {
throw new Error(REQUIRES_GOLD_LICENSE_MSG);
}
+ return this._descriptor.groupByTimeseries
+ ? this._getGeoLineByTimeseries(
+ layerName,
+ requestMeta,
+ registerCancelCallback,
+ isRequestStillActive,
+ inspectorAdapters
+ )
+ : this._getGeoLineByTerms(
+ layerName,
+ requestMeta,
+ registerCancelCallback,
+ isRequestStillActive,
+ inspectorAdapters
+ );
+ }
+
+ async _getGeoLineByTimeseries(
+ layerName: string,
+ requestMeta: VectorSourceRequestMeta,
+ registerCancelCallback: (callback: () => void) => void,
+ isRequestStillActive: () => boolean,
+ inspectorAdapters: Adapters
+ ): Promise {
+ const indexPattern = await this.getIndexPattern();
+ const searchSource = await this.makeSearchSource(requestMeta, 0);
+ searchSource.setField('trackTotalHits', false);
+ searchSource.setField('aggs', {
+ totalEntities: {
+ cardinality: {
+ field: '_tsid',
+ },
+ },
+ tracks: {
+ time_series: {},
+ aggs: {
+ path: {
+ geo_line: {
+ point: {
+ field: this._descriptor.geoField,
+ },
+ size: this._descriptor.lineSimplificationSize,
+ },
+ },
+ ...this.getValueAggsDsl(indexPattern),
+ },
+ },
+ });
+
+ const resp = await this._runEsQuery({
+ requestId: `${this.getId()}_tracks`,
+ requestName: i18n.translate('xpack.maps.source.esGeoLine.timeSeriesTrackRequestName', {
+ defaultMessage: `'{layerName}' tracks request (time series)`,
+ values: {
+ layerName,
+ },
+ }),
+ searchSource,
+ registerCancelCallback,
+ requestDescription: i18n.translate(
+ 'xpack.maps.source.esGeoLine.timeSeriesTrackRequestDescription',
+ {
+ defaultMessage:
+ 'Get tracks from data view: {dataViewName}, geospatial field: {geoFieldName}',
+ values: {
+ dataViewName: indexPattern.getName(),
+ geoFieldName: this._descriptor.geoField,
+ },
+ }
+ ),
+ searchSessionId: requestMeta.searchSessionId,
+ executionContext: mergeExecutionContext(
+ { description: 'es_geo_line:time_series_tracks' },
+ requestMeta.executionContext
+ ),
+ requestsAdapter: inspectorAdapters.requests,
+ });
+
+ const { featureCollection } = convertToGeoJson(resp, TIME_SERIES_ID_FIELD_NAME);
+
+ const entityCount = featureCollection.features.length;
+ const areEntitiesTrimmed = entityCount >= 10000; // 10000 is max buckets created by time_series aggregation
+
+ return {
+ data: featureCollection,
+ meta: {
+ areResultsTrimmed: areEntitiesTrimmed,
+ areEntitiesTrimmed,
+ entityCount,
+ numTrimmedTracks: 0, // geo_line by time series never truncates tracks and instead simplifies tracks
+ totalEntities: resp?.aggregations?.totalEntities?.value ?? 0,
+ } as ESGeoLineSourceResponseMeta,
+ };
+ }
+
+ async _getGeoLineByTerms(
+ layerName: string,
+ requestMeta: VectorSourceRequestMeta,
+ registerCancelCallback: (callback: () => void) => void,
+ isRequestStillActive: () => boolean,
+ inspectorAdapters: Adapters
+ ): Promise {
+ if (!this._descriptor.splitField) {
+ throw new Error(
+ i18n.translate('xpack.maps.source.esGeoLine.missingConfigurationError', {
+ defaultMessage: `Unable to create tracks. Provide a value for required configuration '{inputLabel}'`,
+ values: { inputLabel: ENTITY_INPUT_LABEL },
+ })
+ );
+ }
+
+ if (!this._descriptor.sortField) {
+ throw new Error(
+ i18n.translate('xpack.maps.source.esGeoLine.missingConfigurationError', {
+ defaultMessage: `Unable to create tracks. Provide a value for required configuration '{inputLabel}'`,
+ values: { inputLabel: SORT_INPUT_LABEL },
+ })
+ );
+ }
+
const indexPattern = await this.getIndexPattern();
// Request is broken into 2 requests
@@ -186,8 +345,8 @@ export class ESGeoLineSource extends AbstractESAggSource {
const entitySearchSource = await this.makeSearchSource(requestMeta, 0);
entitySearchSource.setField('trackTotalHits', false);
const splitField = getField(indexPattern, this._descriptor.splitField);
- const cardinalityAgg = { precision_threshold: 1 };
- const termsAgg = { size: MAX_TRACKS };
+ const cardinalityAgg = { precision_threshold: MAX_TERMS_TRACKS };
+ const termsAgg = { size: MAX_TERMS_TRACKS };
entitySearchSource.setField('aggs', {
totalEntities: {
cardinality: addFieldToDSL(cardinalityAgg, splitField),
@@ -208,7 +367,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
const entityResp = await this._runEsQuery({
requestId: `${this.getId()}_entities`,
requestName: i18n.translate('xpack.maps.source.esGeoLine.entityRequestName', {
- defaultMessage: '{layerName} entities request',
+ defaultMessage: `'{layerName}' entities request`,
values: {
layerName,
},
@@ -236,7 +395,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
[]
);
const totalEntities = _.get(entityResp, 'aggregations.totalEntities.value', 0);
- const areEntitiesTrimmed = entityBuckets.length >= MAX_TRACKS;
+ const areEntitiesTrimmed = entityBuckets.length >= MAX_TERMS_TRACKS;
if (totalEntities === 0) {
return {
data: EMPTY_FEATURE_COLLECTION,
@@ -288,7 +447,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
const tracksResp = await this._runEsQuery({
requestId: `${this.getId()}_tracks`,
requestName: i18n.translate('xpack.maps.source.esGeoLine.trackRequestName', {
- defaultMessage: '{layerName} tracks request',
+ defaultMessage: `'{layerName}' tracks request (terms)`,
values: {
layerName,
},
@@ -306,7 +465,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
}),
searchSessionId: requestMeta.searchSessionId,
executionContext: mergeExecutionContext(
- { description: 'es_geo_line:tracks' },
+ { description: 'es_geo_line:terms_tracks' },
requestMeta.executionContext
),
requestsAdapter: inspectorAdapters.requests,
@@ -392,15 +551,21 @@ export class ESGeoLineSource extends AbstractESAggSource {
async getTooltipProperties(properties: GeoJsonProperties): Promise {
const tooltipProperties = await super.getTooltipProperties(properties);
- tooltipProperties.push(
- new TooltipProperty(
- 'isTrackComplete',
- i18n.translate('xpack.maps.source.esGeoLine.isTrackCompleteLabel', {
- defaultMessage: 'track is complete',
- }),
- properties!.complete.toString()
- )
- );
+ if (properties && typeof properties!.complete === 'boolean') {
+ tooltipProperties.push(
+ new TooltipProperty(
+ '__kbn__track__complete',
+ this._descriptor.groupByTimeseries
+ ? i18n.translate('xpack.maps.source.esGeoLine.isTrackSimplifiedLabel', {
+ defaultMessage: 'track is simplified',
+ })
+ : i18n.translate('xpack.maps.source.esGeoLine.isTrackTruncatedLabel', {
+ defaultMessage: 'track is truncated',
+ }),
+ (!properties.complete).toString()
+ )
+ );
+ }
return tooltipProperties;
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form.tsx
deleted file mode 100644
index e079d25a4c628..0000000000000
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { DataView } from '@kbn/data-plugin/common';
-import { EuiFormRow } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { indexPatterns } from '@kbn/data-plugin/public';
-import { SingleFieldSelect } from '../../../components/single_field_select';
-import { getTermsFields } from '../../../index_pattern_util';
-
-interface Props {
- indexPattern: DataView;
- onSortFieldChange: (fieldName: string) => void;
- onSplitFieldChange: (fieldName: string) => void;
- sortField: string;
- splitField: string;
-}
-
-export function GeoLineForm(props: Props) {
- function onSortFieldChange(fieldName: string | undefined) {
- if (fieldName !== undefined) {
- props.onSortFieldChange(fieldName);
- }
- }
- function onSplitFieldChange(fieldName: string | undefined) {
- if (fieldName !== undefined) {
- props.onSplitFieldChange(fieldName);
- }
- }
- return (
- <>
-
-
-
-
-
- {
- const isSplitField = props.splitField ? field.name === props.splitField : false;
- return (
- !isSplitField &&
- field.sortable &&
- !indexPatterns.isNestedField(field) &&
- ['number', 'date'].includes(field.type)
- );
- })}
- isClearable={false}
- />
-
- >
- );
-}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/geo_line_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/geo_line_form.tsx
new file mode 100644
index 0000000000000..49e377731709e
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/geo_line_form.tsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import { DataView } from '@kbn/data-plugin/common';
+import { EuiFormRow } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { indexPatterns } from '@kbn/data-plugin/public';
+import { SingleFieldSelect } from '../../../../components/single_field_select';
+import { getTermsFields, getIsTimeseries } from '../../../../index_pattern_util';
+import { ENTITY_INPUT_LABEL, SORT_INPUT_LABEL } from './i18n_constants';
+import { GroupByButtonGroup } from './group_by_button_group';
+import { GroupByLabel } from './group_by_label';
+import { SizeSlider } from './size_slider';
+
+interface Props {
+ isColumnCompressed: boolean;
+ indexPattern: DataView;
+ groupByTimeseries: boolean;
+ lineSimplificationSize: number;
+ onGroupByTimeseriesChange: (groupByTimeseries: boolean) => void;
+ onLineSimplificationSizeChange: (lineSimplificationSize: number) => void;
+ onSortFieldChange: (fieldName: string) => void;
+ onSplitFieldChange: (fieldName: string) => void;
+ sortField: string;
+ splitField: string;
+}
+
+export function GeoLineForm(props: Props) {
+ const isTimeseries = useMemo(() => {
+ return getIsTimeseries(props.indexPattern);
+ }, [props.indexPattern]);
+
+ function onSortFieldChange(fieldName: string | undefined) {
+ if (fieldName !== undefined) {
+ props.onSortFieldChange(fieldName);
+ }
+ }
+ function onSplitFieldChange(fieldName: string | undefined) {
+ if (fieldName !== undefined) {
+ props.onSplitFieldChange(fieldName);
+ }
+ }
+
+ return (
+ <>
+ {isTimeseries && (
+ }
+ display={props.isColumnCompressed ? 'columnCompressed' : 'row'}
+ >
+
+
+ )}
+ {props.groupByTimeseries ? (
+
+
+
+ ) : (
+ <>
+
+
+
+
+
+ {
+ const isSplitField = props.splitField ? field.name === props.splitField : false;
+ return (
+ !isSplitField &&
+ field.sortable &&
+ !indexPatterns.isNestedField(field) &&
+ ['number', 'date'].includes(field.type)
+ );
+ })}
+ isClearable={false}
+ compressed={props.isColumnCompressed}
+ />
+
+ >
+ )}
+ >
+ );
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/group_by_button_group.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/group_by_button_group.tsx
new file mode 100644
index 0000000000000..55133463b413a
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/group_by_button_group.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiButtonGroup } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { TIME_SERIES_LABEL, TERMS_LABEL } from './i18n_constants';
+
+const GROUP_BY_TIME_SERIES = 'timeseries';
+const GROUP_BY_TERM = 'terms';
+const GROUP_BY_OPTIONS = [
+ {
+ id: GROUP_BY_TIME_SERIES,
+ label: TIME_SERIES_LABEL,
+ },
+ {
+ id: GROUP_BY_TERM,
+ label: TERMS_LABEL,
+ },
+];
+
+interface Props {
+ groupByTimeseries: boolean;
+ onGroupByTimeseriesChange: (groupByTimeseries: boolean) => void;
+}
+
+export function GroupByButtonGroup({ groupByTimeseries, onGroupByTimeseriesChange }: Props) {
+ return (
+ {
+ onGroupByTimeseriesChange(id === GROUP_BY_TIME_SERIES);
+ }}
+ isFullWidth={true}
+ buttonSize="compressed"
+ />
+ );
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/group_by_label.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/group_by_label.tsx
new file mode 100644
index 0000000000000..455d6ee3f3913
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/group_by_label.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { TIME_SERIES_LABEL, TERMS_LABEL } from './i18n_constants';
+import { MAX_TERMS_TRACKS } from '../constants';
+
+export function GroupByLabel() {
+ return (
+
+
+ - {TIME_SERIES_LABEL}
+ -
+
+
+
+
+
+ - {TERMS_LABEL}
+ -
+
+
+
+
+
+
+ }
+ >
+
+ {' '}
+
+
+
+ );
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/i18n_constants.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/i18n_constants.ts
new file mode 100644
index 0000000000000..784e1e5b40900
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/i18n_constants.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const TIME_SERIES_LABEL = i18n.translate(
+ 'xpack.maps.source.esGeoLine.groupBy.timeseriesLabel',
+ {
+ defaultMessage: 'Time series',
+ }
+);
+
+export const TERMS_LABEL = i18n.translate('xpack.maps.source.esGeoLine.groupBy.termsLabel', {
+ defaultMessage: 'Top terms',
+});
+
+export const ENTITY_INPUT_LABEL = i18n.translate('xpack.maps.source.esGeoLine.splitFieldLabel', {
+ defaultMessage: 'Entity',
+});
+
+export const SORT_INPUT_LABEL = i18n.translate('xpack.maps.source.esGeoLine.sortFieldLabel', {
+ defaultMessage: 'Sort',
+});
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/index.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/index.ts
new file mode 100644
index 0000000000000..1f8f65e9feb04
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { ENTITY_INPUT_LABEL, SORT_INPUT_LABEL } from './i18n_constants';
+export { GeoLineForm } from './geo_line_form';
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/size_slider.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/size_slider.tsx
new file mode 100644
index 0000000000000..4d8dd73903e63
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form/size_slider.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import useDebounce from 'react-use/lib/useDebounce';
+import { EuiRange } from '@elastic/eui';
+
+interface Props {
+ value: number;
+ onChange: (size: number) => void;
+}
+
+export function SizeSlider({ onChange, value }: Props) {
+ const [size, setSize] = useState(value);
+
+ const [, cancel] = useDebounce(
+ () => {
+ onChange(size);
+ },
+ 150,
+ [size]
+ );
+
+ useEffect(() => {
+ return () => {
+ cancel();
+ };
+ }, [cancel]);
+
+ return (
+ {
+ setSize(parseInt(event.currentTarget.value, 10));
+ }}
+ min={100}
+ max={10000}
+ showLabels
+ showValue
+ step={100}
+ />
+ );
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx
index 2957235602d7b..5392232feb26e 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx
@@ -16,6 +16,7 @@ import {
VECTOR_STYLES,
WIZARD_ID,
} from '../../../../common/constants';
+import { ESGeoLineSourceDescriptor } from '../../../../common/descriptor_types';
import { VectorStyle } from '../../styles/vector/vector_style';
import { GeoJsonVectorLayer } from '../../layers/vector_layer';
import { getIsGoldPlus } from '../../../licensed_features';
@@ -34,14 +35,7 @@ export const geoLineLayerWizardConfig: LayerWizard = {
return !getIsGoldPlus();
},
renderWizard: ({ previewLayers }: RenderWizardArguments) => {
- const onSourceConfigChange = (
- sourceConfig: {
- indexPatternId: string;
- geoField: string;
- splitField: string;
- sortField: string;
- } | null
- ) => {
+ const onSourceConfigChange = (sourceConfig: Partial | null) => {
if (!sourceConfig) {
previewLayers([]);
return;
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx
index 1201358225ca3..d411b64734718 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx
@@ -19,6 +19,8 @@ import type { OnSourceChangeArgs } from '../source';
interface Props {
bucketsName: string;
indexPatternId: string;
+ groupByTimeseries: boolean;
+ lineSimplificationSize: number;
splitField: string;
sortField: string;
metrics: AggDescriptor[];
@@ -69,12 +71,20 @@ export class UpdateSourceEditor extends Component {
this.props.onChange({ propName: 'metrics', value: metrics });
};
- _onSplitFieldChange = (fieldName: string) => {
- this.props.onChange({ propName: 'splitField', value: fieldName });
+ _onGroupByTimeseriesChange = (value: boolean) => {
+ this.props.onChange({ propName: 'groupByTimeseries', value });
};
- _onSortFieldChange = (fieldName: string) => {
- this.props.onChange({ propName: 'sortField', value: fieldName });
+ _onLineSimplificationSizeChange = (value: number) => {
+ this.props.onChange({ propName: 'lineSimplificationSize', value });
+ };
+
+ _onSplitFieldChange = (value: string) => {
+ this.props.onChange({ propName: 'splitField', value });
+ };
+
+ _onSortFieldChange = (value: string) => {
+ this.props.onChange({ propName: 'sortField', value });
};
render() {
@@ -116,9 +126,14 @@ export class UpdateSourceEditor extends Component {
diff --git a/x-pack/plugins/maps/public/index_pattern_util.ts b/x-pack/plugins/maps/public/index_pattern_util.ts
index fc31b62f79a7b..c27b7285fd3eb 100644
--- a/x-pack/plugins/maps/public/index_pattern_util.ts
+++ b/x-pack/plugins/maps/public/index_pattern_util.ts
@@ -13,6 +13,12 @@ import { getIndexPatternService } from './kibana_services';
import { ES_GEO_FIELD_TYPE, ES_GEO_FIELD_TYPES } from '../common/constants';
import { getIsGoldPlus } from './licensed_features';
+export function getIsTimeseries(dataView: DataView): boolean {
+ return dataView.fields.some((field) => {
+ return field.timeSeriesDimension || field.timeSeriesMetric;
+ });
+}
+
export function getGeoTileAggNotSupportedReason(field: DataViewField): string | null {
if (!field.aggregatable) {
return i18n.translate('xpack.maps.geoTileAgg.disabled.docValues', {
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index e5924a78dcbbb..f1f5ffece0a0c 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -21810,10 +21810,7 @@
"xpack.maps.source.esGeoGrid.showAsLabel": "Afficher en tant que",
"xpack.maps.source.esGeoGrid.showAsSelector": "Sélectionner la méthode d’affichage",
"xpack.maps.source.esGeoLine.bucketsName": "pistes",
- "xpack.maps.source.esGeoLine.geofieldLabel": "Champ géospatial",
- "xpack.maps.source.esGeoLine.geofieldPlaceholder": "Sélectionner un champ géographique",
"xpack.maps.source.esGeoLine.geospatialFieldLabel": "Champ géospatial",
- "xpack.maps.source.esGeoLine.isTrackCompleteLabel": "la piste est complète",
"xpack.maps.source.esGeoLine.metricsLabel": "Indicateurs de piste",
"xpack.maps.source.esGeoLine.sortFieldLabel": "Trier",
"xpack.maps.source.esGeoLine.sortFieldPlaceholder": "Sélectionner le champ de tri",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index ede24d36d52c3..5888ea9cf64f6 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -21810,10 +21810,7 @@
"xpack.maps.source.esGeoGrid.showAsLabel": "表示形式",
"xpack.maps.source.esGeoGrid.showAsSelector": "表示方法を選択",
"xpack.maps.source.esGeoLine.bucketsName": "追跡",
- "xpack.maps.source.esGeoLine.geofieldLabel": "地理空間フィールド",
- "xpack.maps.source.esGeoLine.geofieldPlaceholder": "ジオフィールドを選択",
"xpack.maps.source.esGeoLine.geospatialFieldLabel": "地理空間フィールド",
- "xpack.maps.source.esGeoLine.isTrackCompleteLabel": "トラックは完了しました",
"xpack.maps.source.esGeoLine.metricsLabel": "トラックメトリック",
"xpack.maps.source.esGeoLine.sortFieldLabel": "並べ替え",
"xpack.maps.source.esGeoLine.sortFieldPlaceholder": "ソートフィールドを選択",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 2a4f31a69af74..2845576f68992 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -21810,10 +21810,7 @@
"xpack.maps.source.esGeoGrid.showAsLabel": "显示为",
"xpack.maps.source.esGeoGrid.showAsSelector": "选择显示方法",
"xpack.maps.source.esGeoLine.bucketsName": "轨迹",
- "xpack.maps.source.esGeoLine.geofieldLabel": "地理空间字段",
- "xpack.maps.source.esGeoLine.geofieldPlaceholder": "选择地理字段",
"xpack.maps.source.esGeoLine.geospatialFieldLabel": "地理空间字段",
- "xpack.maps.source.esGeoLine.isTrackCompleteLabel": "轨迹完整",
"xpack.maps.source.esGeoLine.metricsLabel": "轨迹指标",
"xpack.maps.source.esGeoLine.sortFieldLabel": "排序",
"xpack.maps.source.esGeoLine.sortFieldPlaceholder": "选择排序字段",