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": "选择排序字段",