mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[maps] time series geo line (#159267)
Part of https://github.com/elastic/kibana/issues/141978 PR updates tracks layer with "group by time series" logic. When true, geo_line metric aggregation is proceeded by `time_series` bucket aggregation instead of `filters` bucket aggregation (used by existing terms split). ### UI when creating tracks layer with time series data view <img width="481" alt="Screen Shot 2023-06-22 at 12 35 46 PM" src="ccfeb6ef
-c714-49a3-a6d6-f6b52cce80be"> <img width="469" alt="Screen Shot 2023-06-22 at 12 35 55 PM" src="55cba2dc
-6326-4141-bde5-7a6cc0f0b333"> <img width="542" alt="Screen Shot 2023-06-22 at 12 49 22 PM" src="694ce621
-2b6e-4a20-ba20-b9f9d20da8ef"> ### UI when editing tracks layer with time series data view <img width="447" alt="Screen Shot 2023-06-22 at 12 36 17 PM" src="96cbb3f3
-4ca5-430f-91b3-71b5013ca6e9"> <img width="457" alt="Screen Shot 2023-06-22 at 12 36 24 PM" src="4d603809
-7e6a-4b72-98d7-d3a516b2c809"> ### Test instructions * clone https://github.com/thomasneirynck/faketracks * cd into `faketracks` * run `npm install` * run `node ./generate_tracks.js --isTimeSeries` * In Kibana, create `tracks` data view * In Maps, create new map and add `Tracks` layer. Select `Tracks` data view. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
bfb07386b2
commit
7340007718
22 changed files with 630 additions and 173 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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 & {
|
||||
|
|
|
@ -19,7 +19,7 @@ export function ShowAsLabel(props: Props) {
|
|||
return (
|
||||
<EuiToolTip
|
||||
content={
|
||||
<EuiText>
|
||||
<EuiText size="s">
|
||||
<dl>
|
||||
<dt>{CLUSTER_LABEL}</dt>
|
||||
<dd>
|
||||
|
|
|
@ -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;
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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<ESGeoLineSourceDescriptor> | null) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
indexPattern: DataView | null;
|
||||
pointFields: DataViewField[];
|
||||
geoField: string;
|
||||
groupByTimeseries: boolean;
|
||||
splitField: string;
|
||||
sortField: string;
|
||||
lineSimplificationSize: number;
|
||||
}
|
||||
|
||||
export class CreateSourceEditor extends Component<Props, State> {
|
||||
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<Props, State> {
|
|||
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<Props, State> {
|
|||
);
|
||||
};
|
||||
|
||||
_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<Props, State> {
|
|||
};
|
||||
|
||||
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<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.esGeoLine.geofieldLabel', {
|
||||
defaultMessage: 'Geospatial field',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.source.esGeoLine.geofieldPlaceholder', {
|
||||
defaultMessage: 'Select geo field',
|
||||
})}
|
||||
value={this.state.geoField}
|
||||
onChange={this._onGeoFieldSelect}
|
||||
fields={getGeoPointFields(this.state.indexPattern.fields)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<GeoFieldSelect
|
||||
value={this.state.geoField}
|
||||
onChange={this._onGeoFieldSelect}
|
||||
geoFields={this.state.pointFields}
|
||||
isClearable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -125,9 +153,14 @@ export class CreateSourceEditor extends Component<Props, State> {
|
|||
|
||||
return (
|
||||
<GeoLineForm
|
||||
isColumnCompressed={false}
|
||||
indexPattern={this.state.indexPattern}
|
||||
onGroupByTimeseriesChange={this._onGroupByTimeseriesChange}
|
||||
onLineSimplificationSizeChange={this._onLineSimplificationSizeChange}
|
||||
onSortFieldChange={this._onSortFieldSelect}
|
||||
onSplitFieldChange={this._onSplitFieldSelect}
|
||||
groupByTimeseries={this.state.groupByTimeseries}
|
||||
lineSimplificationSize={this.state.lineSimplificationSize}
|
||||
sortField={this.state.sortField}
|
||||
splitField={this.state.splitField}
|
||||
/>
|
||||
|
|
|
@ -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<ESGeoLineSourceDescriptor, 'splitField' | 'sortField'>;
|
||||
|
||||
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<ESGeoLineSource>({
|
||||
fieldName: TIME_SERIES_ID_FIELD_NAME,
|
||||
label: TIME_SERIES_ID_FIELD_NAME,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
dataType: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
async getFields(): Promise<IField[]> {
|
||||
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<GeoJsonWithMeta> {
|
||||
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<GeoJsonWithMeta> {
|
||||
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<ITooltipProperty[]> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.esGeoLine.splitFieldLabel', {
|
||||
defaultMessage: 'Entity',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.source.esGeoLine.splitFieldPlaceholder', {
|
||||
defaultMessage: 'Select entity field',
|
||||
})}
|
||||
value={props.splitField}
|
||||
onChange={onSplitFieldChange}
|
||||
fields={getTermsFields(props.indexPattern.fields)}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.esGeoLine.sortFieldLabel', {
|
||||
defaultMessage: 'Sort',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.source.esGeoLine.sortFieldPlaceholder', {
|
||||
defaultMessage: 'Select sort field',
|
||||
})}
|
||||
value={props.sortField}
|
||||
onChange={onSortFieldChange}
|
||||
fields={props.indexPattern.fields.filter((field) => {
|
||||
const isSplitField = props.splitField ? field.name === props.splitField : false;
|
||||
return (
|
||||
!isSplitField &&
|
||||
field.sortable &&
|
||||
!indexPatterns.isNestedField(field) &&
|
||||
['number', 'date'].includes(field.type)
|
||||
);
|
||||
})}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 && (
|
||||
<EuiFormRow
|
||||
label={<GroupByLabel />}
|
||||
display={props.isColumnCompressed ? 'columnCompressed' : 'row'}
|
||||
>
|
||||
<GroupByButtonGroup
|
||||
groupByTimeseries={props.groupByTimeseries}
|
||||
onGroupByTimeseriesChange={props.onGroupByTimeseriesChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{props.groupByTimeseries ? (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.esGeoLine.lineSImplificationSizeLabel', {
|
||||
defaultMessage: 'Simplification threshold',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.maps.esGeoLine.lineSImplificationSizeHelpText', {
|
||||
defaultMessage:
|
||||
'The maximum number of points for each track. Track is simplifed when threshold is exceeded. Use smaller values for better performance.',
|
||||
})}
|
||||
display={props.isColumnCompressed ? 'columnCompressed' : 'row'}
|
||||
>
|
||||
<SizeSlider
|
||||
value={props.lineSimplificationSize}
|
||||
onChange={props.onLineSimplificationSizeChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={ENTITY_INPUT_LABEL}
|
||||
display={props.isColumnCompressed ? 'columnCompressed' : 'row'}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.source.esGeoLine.splitFieldPlaceholder', {
|
||||
defaultMessage: 'Select entity field',
|
||||
})}
|
||||
value={props.splitField}
|
||||
onChange={onSplitFieldChange}
|
||||
fields={getTermsFields(props.indexPattern.fields)}
|
||||
isClearable={false}
|
||||
compressed={props.isColumnCompressed}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={SORT_INPUT_LABEL}
|
||||
display={props.isColumnCompressed ? 'columnCompressed' : 'row'}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.source.esGeoLine.sortFieldPlaceholder', {
|
||||
defaultMessage: 'Select sort field',
|
||||
})}
|
||||
value={props.sortField}
|
||||
onChange={onSortFieldChange}
|
||||
fields={props.indexPattern.fields.filter((field) => {
|
||||
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}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<EuiButtonGroup
|
||||
type="single"
|
||||
legend={i18n.translate('xpack.maps.source.esGeoLine.groupByButtonGroupLegend', {
|
||||
defaultMessage: 'Choose group by method',
|
||||
})}
|
||||
options={GROUP_BY_OPTIONS}
|
||||
idSelected={groupByTimeseries ? GROUP_BY_TIME_SERIES : GROUP_BY_TERM}
|
||||
onChange={(id: string) => {
|
||||
onGroupByTimeseriesChange(id === GROUP_BY_TIME_SERIES);
|
||||
}}
|
||||
isFullWidth={true}
|
||||
buttonSize="compressed"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<EuiToolTip
|
||||
content={
|
||||
<EuiText size="s">
|
||||
<dl>
|
||||
<dt>{TIME_SERIES_LABEL}</dt>
|
||||
<dd>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.source.esGeoLine.groupBy.timeseriesDescription"
|
||||
defaultMessage="Create a track for each unique time series. Track is simplifed when number of points exceeds limit."
|
||||
/>
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>{TERMS_LABEL}</dt>
|
||||
<dd>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.source.esGeoGrid.groupBy.termsDescription"
|
||||
defaultMessage="Create a track for top {maxTermsTracks} terms. Track is truncated when number of points exceeds limit."
|
||||
values={{ maxTermsTracks: MAX_TERMS_TRACKS }}
|
||||
/>
|
||||
</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage id="xpack.maps.source.esGeoGrid.groupByLabel" defaultMessage="Group by" />{' '}
|
||||
<EuiIcon type="questionInCircle" color="subdued" />
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
});
|
|
@ -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';
|
|
@ -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 (
|
||||
<EuiRange
|
||||
value={size}
|
||||
onChange={(event) => {
|
||||
setSize(parseInt(event.currentTarget.value, 10));
|
||||
}}
|
||||
min={100}
|
||||
max={10000}
|
||||
showLabels
|
||||
showValue
|
||||
step={100}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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<ESGeoLineSourceDescriptor> | null) => {
|
||||
if (!sourceConfig) {
|
||||
previewLayers([]);
|
||||
return;
|
||||
|
|
|
@ -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<Props, State> {
|
|||
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<Props, State> {
|
|||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<GeoLineForm
|
||||
isColumnCompressed={true}
|
||||
indexPattern={this.state.indexPattern}
|
||||
onGroupByTimeseriesChange={this._onGroupByTimeseriesChange}
|
||||
onLineSimplificationSizeChange={this._onLineSimplificationSizeChange}
|
||||
onSortFieldChange={this._onSortFieldChange}
|
||||
onSplitFieldChange={this._onSplitFieldChange}
|
||||
groupByTimeseries={this.props.groupByTimeseries}
|
||||
lineSimplificationSize={this.props.lineSimplificationSize}
|
||||
sortField={this.props.sortField}
|
||||
splitField={this.props.splitField}
|
||||
/>
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "ソートフィールドを選択",
|
||||
|
|
|
@ -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": "选择排序字段",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue