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 3094596a3f13b..30f02a7a9c4c7 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 @@ -104,6 +104,7 @@ export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & { sortField: string; sortOrder: SortDirection; scalingType: SCALING_TYPES; + topHitsGroupByTimeseries: boolean; topHitsSplitField: string; topHitsSize: number; }; diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.test.ts b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.test.ts index 22534c6a55658..be5c7503facec 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.test.ts @@ -60,6 +60,7 @@ describe('createLayerDescriptor', () => { 'client.geo.country_iso_code', 'client.as.organization.name', ], + topHitsGroupByTimeseries: false, topHitsSize: 1, topHitsSplitField: 'client.ip', type: 'ES_SEARCH', @@ -138,6 +139,7 @@ describe('createLayerDescriptor', () => { 'server.geo.country_iso_code', 'server.as.organization.name', ], + topHitsGroupByTimeseries: false, topHitsSize: 1, topHitsSplitField: 'server.ip', type: 'ES_SEARCH', @@ -290,6 +292,7 @@ describe('createLayerDescriptor', () => { 'source.geo.country_iso_code', 'source.as.organization.name', ], + topHitsGroupByTimeseries: false, topHitsSize: 1, topHitsSplitField: 'source.ip', type: 'ES_SEARCH', @@ -368,6 +371,7 @@ describe('createLayerDescriptor', () => { 'destination.geo.country_iso_code', 'destination.as.organization.name', ], + topHitsGroupByTimeseries: false, topHitsSize: 1, topHitsSplitField: 'destination.ip', type: 'ES_SEARCH', @@ -514,6 +518,7 @@ describe('createLayerDescriptor', () => { 'client.geo.country_iso_code', 'client.as.organization.name', ], + topHitsGroupByTimeseries: false, topHitsSize: 1, topHitsSplitField: 'client.ip', type: 'ES_SEARCH', @@ -592,6 +597,7 @@ describe('createLayerDescriptor', () => { 'server.geo.country_iso_code', 'server.as.organization.name', ], + topHitsGroupByTimeseries: false, topHitsSize: 1, topHitsSplitField: 'server.ip', type: 'ES_SEARCH', diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.test.ts index 97b0d2b188ab5..d2941ec168ddc 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.test.ts @@ -44,6 +44,7 @@ test('Should create layer descriptor', () => { sortField: '', sortOrder: 'desc', tooltipProperties: [], + topHitsGroupByTimeseries: false, topHitsSize: 1, topHitsSplitField: '', type: 'ES_SEARCH', diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index cfe1a3c872b86..4bb67e8b38099 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -48,6 +48,7 @@ import { loadIndexSettings } from './util/load_index_settings'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; import { + AbstractESSourceDescriptor, DataRequestMeta, ESSearchSourceDescriptor, Timeslice, @@ -83,6 +84,7 @@ type ESSearchSourceSyncMeta = Pick< | 'sortField' | 'sortOrder' | 'scalingType' + | 'topHitsGroupByTimeseries' | 'topHitsSplitField' | 'topHitsSize' >; @@ -106,7 +108,9 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource protected readonly _tooltipFields: ESDocField[]; static createDescriptor(descriptor: Partial): ESSearchSourceDescriptor { - const normalizedDescriptor = AbstractESSource.createDescriptor(descriptor); + const normalizedDescriptor = AbstractESSource.createDescriptor( + descriptor + ) as AbstractESSourceDescriptor & Partial; if (!isValidStringConfig(normalizedDescriptor.geoField)) { throw new Error('Cannot create an ESSearchSourceDescriptor without a geoField'); } @@ -128,6 +132,10 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource scalingType: isValidStringConfig(descriptor.scalingType) ? descriptor.scalingType! : SCALING_TYPES.MVT, + topHitsGroupByTimeseries: + typeof normalizedDescriptor.topHitsGroupByTimeseries === 'boolean' + ? normalizedDescriptor.topHitsGroupByTimeseries + : false, topHitsSplitField: isValidStringConfig(descriptor.topHitsSplitField) ? descriptor.topHitsSplitField! : '', @@ -168,6 +176,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource sortField={this._descriptor.sortField} sortOrder={this._descriptor.sortOrder} filterByMapBounds={this.isFilterByMapBounds()} + topHitsGroupByTimeseries={this._descriptor.topHitsGroupByTimeseries} topHitsSplitField={this._descriptor.topHitsSplitField} topHitsSize={this._descriptor.topHitsSize} /> @@ -271,9 +280,13 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource registerCancelCallback: (callback: () => void) => void, inspectorAdapters: Adapters ) { - const { topHitsSplitField: topHitsSplitFieldName, topHitsSize } = this._descriptor; + const { + topHitsGroupByTimeseries, + topHitsSplitField: topHitsSplitFieldName, + topHitsSize, + } = this._descriptor; - if (!topHitsSplitFieldName) { + if (!topHitsGroupByTimeseries && !topHitsSplitFieldName) { throw new Error('Cannot _getTopHits without topHitsSplitField'); } @@ -310,7 +323,6 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource }; } - const topHitsSplitField: DataViewField = getField(indexPattern, topHitsSplitFieldName); const cardinalityAgg = { precision_threshold: 1 }; const termsAgg = { size: DEFAULT_MAX_BUCKETS_LIMIT, @@ -319,26 +331,50 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource const searchSource = await this.makeSearchSource(requestMeta, 0); searchSource.setField('trackTotalHits', false); - searchSource.setField('aggs', { - totalEntities: { - cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField), - }, - entitySplit: { - terms: addFieldToDSL(termsAgg, topHitsSplitField), - aggs: { - entityHits: { - top_hits: topHits, + + if (topHitsGroupByTimeseries) { + searchSource.setField('aggs', { + totalEntities: { + cardinality: { + ...cardinalityAgg, + field: '_tsid', }, }, - }, - }); - if (topHitsSplitField.type === 'string') { - const entityIsNotEmptyFilter = buildPhraseFilter(topHitsSplitField, '', indexPattern); - entityIsNotEmptyFilter.meta.negate = true; - searchSource.setField('filter', [ - ...(searchSource.getField('filter') as Filter[]), - entityIsNotEmptyFilter, - ]); + entitySplit: { + terms: { + ...termsAgg, + field: '_tsid', + }, + aggs: { + entityHits: { + top_hits: topHits, + }, + }, + }, + }); + } else { + const topHitsSplitField: DataViewField = getField(indexPattern, topHitsSplitFieldName); + searchSource.setField('aggs', { + totalEntities: { + cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField), + }, + entitySplit: { + terms: addFieldToDSL(termsAgg, topHitsSplitField), + aggs: { + entityHits: { + top_hits: topHits, + }, + }, + }, + }); + if (topHitsSplitField.type === 'string') { + const entityIsNotEmptyFilter = buildPhraseFilter(topHitsSplitField, '', indexPattern); + entityIsNotEmptyFilter.meta.negate = true; + searchSource.setField('filter', [ + ...(searchSource.getField('filter') as Filter[]), + entityIsNotEmptyFilter, + ]); + } } const resp = await this._runEsQuery({ @@ -354,7 +390,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource 'Get top hits from data view: {dataViewName}, entities: {entitiesFieldName}, geospatial field: {geoFieldName}', values: { dataViewName: indexPattern.getName(), - entitiesFieldName: topHitsSplitFieldName, + entitiesFieldName: topHitsGroupByTimeseries ? '_tsid' : topHitsSplitFieldName, geoFieldName: this._descriptor.geoField, }, }), @@ -475,8 +511,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource } _isTopHits(): boolean { - const { scalingType, topHitsSplitField } = this._descriptor; - return !!(scalingType === SCALING_TYPES.TOP_HITS && topHitsSplitField); + return this._descriptor.scalingType === SCALING_TYPES.TOP_HITS; } async _getSourceIndexList(): Promise { @@ -794,6 +829,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource sortField: this._descriptor.sortField, sortOrder: this._descriptor.sortOrder, scalingType: this._descriptor.scalingType, + topHitsGroupByTimeseries: this._descriptor.topHitsGroupByTimeseries, topHitsSplitField: this._descriptor.topHitsSplitField, topHitsSize: this._descriptor.topHitsSize, }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx index a3fa9a9745cb7..a047f2bb9297b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx @@ -13,7 +13,12 @@ import { SortDirection } from '@kbn/data-plugin/public'; import { SCALING_TYPES } from '../../../../../common/constants'; import { GeoFieldSelect } from '../../../../components/geo_field_select'; import { GeoIndexPatternSelect } from '../../../../components/geo_index_pattern_select'; -import { getGeoFields, getTermsFields, getSortFields } from '../../../../index_pattern_util'; +import { + getGeoFields, + getTermsFields, + getSortFields, + getIsTimeseries, +} from '../../../../index_pattern_util'; import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types'; import { TopHitsForm } from './top_hits_form'; import { OnSourceChangeArgs } from '../../source'; @@ -27,12 +32,14 @@ interface Props { interface State { indexPattern: DataView | null; + isTimeseries: boolean; geoFields: DataViewField[]; geoFieldName: string | null; sortField: string | null; sortFields: DataViewField[]; sortOrder: SortDirection; termFields: DataViewField[]; + topHitsGroupByTimeseries: boolean; topHitsSplitField: string | null; topHitsSize: number; } @@ -40,27 +47,32 @@ interface State { export class CreateSourceEditor extends Component { state: State = { indexPattern: null, + isTimeseries: false, geoFields: [], geoFieldName: null, sortField: null, sortFields: [], sortOrder: SortDirection.desc, termFields: [], + topHitsGroupByTimeseries: false, topHitsSplitField: null, topHitsSize: 1, }; _onIndexPatternSelect = (indexPattern: DataView) => { const geoFields = getGeoFields(indexPattern.fields); + const isTimeseries = getIsTimeseries(indexPattern); this.setState( { indexPattern, + isTimeseries, geoFields, geoFieldName: geoFields.length ? geoFields[0].name : null, sortField: indexPattern.timeFieldName ? indexPattern.timeFieldName : null, sortFields: getSortFields(indexPattern.fields), termFields: getTermsFields(indexPattern.fields), + topHitsGroupByTimeseries: isTimeseries, topHitsSplitField: null, }, this._previewLayer @@ -80,11 +92,27 @@ export class CreateSourceEditor extends Component { }; _previewLayer = () => { - const { indexPattern, geoFieldName, sortField, sortOrder, topHitsSplitField, topHitsSize } = - this.state; + const { + indexPattern, + geoFieldName, + sortField, + sortOrder, + topHitsGroupByTimeseries, + topHitsSplitField, + topHitsSize, + } = this.state; const tooltipProperties: string[] = []; - if (topHitsSplitField) { + if (topHitsGroupByTimeseries) { + const timeSeriesDimensionFieldNames = (indexPattern?.fields ?? []) + .filter((field) => { + return field.timeSeriesDimension; + }) + .map((field) => { + return field.name; + }); + tooltipProperties.push(...timeSeriesDimensionFieldNames); + } else if (topHitsSplitField) { tooltipProperties.push(topHitsSplitField); } if (indexPattern && indexPattern.timeFieldName) { @@ -94,7 +122,7 @@ export class CreateSourceEditor extends Component { const field = geoFieldName && indexPattern?.getFieldByName(geoFieldName); const sourceConfig = - indexPattern && geoFieldName && sortField && topHitsSplitField + indexPattern && geoFieldName && sortField && (topHitsGroupByTimeseries || topHitsSplitField) ? { indexPatternId: indexPattern.id, geoField: geoFieldName, @@ -102,7 +130,8 @@ export class CreateSourceEditor extends Component { sortField, sortOrder, tooltipProperties, - topHitsSplitField, + topHitsGroupByTimeseries, + topHitsSplitField: topHitsSplitField ? topHitsSplitField : undefined, topHitsSize, } : null; @@ -129,11 +158,13 @@ export class CreateSourceEditor extends Component { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx index 54b09ed831c8e..60ce58e9345ac 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx @@ -16,15 +16,18 @@ import { ValidatedRange } from '../../../../components/validated_range'; import { DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../../../../common/constants'; import { loadIndexSettings } from '../util/load_index_settings'; import { OnSourceChangeArgs } from '../../source'; +import { GroupByButtonGroup } from '../../es_geo_line_source/geo_line_form/group_by_button_group'; interface Props { indexPatternId: string; isColumnCompressed?: boolean; + isTimeseries: boolean; onChange: (args: OnSourceChangeArgs) => void; sortField: string; sortFields: DataViewField[]; sortOrder: SortDirection; termFields: DataViewField[]; + topHitsGroupByTimeseries: boolean; topHitsSplitField: string | null; topHitsSize: number; } @@ -48,6 +51,10 @@ export class TopHitsForm extends Component { this._isMounted = false; } + _onGroupByTimeseriesChange = (topHitsGroupByTimeseries: boolean) => { + this.props.onChange({ propName: 'topHitsGroupByTimeseries', value: topHitsGroupByTimeseries }); + }; + _onTopHitsSplitFieldChange = (topHitsSplitField?: string) => { if (!topHitsSplitField) { return; @@ -80,110 +87,120 @@ export class TopHitsForm extends Component { } render() { - let sizeSlider; - let sortField; - let sortOrder; - if (this.props.topHitsSplitField) { - sizeSlider = ( - - - - ); - - sortField = ( - - - - ); - - sortOrder = ( - - - - ); - } - return ( - - + + + )} + + {!this.props.topHitsGroupByTimeseries && ( + + + + )} + + {(this.props.topHitsSplitField || this.props.topHitsGroupByTimeseries) && ( + <> + - - - {sizeSlider} - - {sortField} - - {sortOrder} + display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'} + > + + + + + + + + + + )} ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx index 2e26cbea33b52..6158ed56b7bd5 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx @@ -6,7 +6,15 @@ */ import React, { Component, Fragment } from 'react'; -import { EuiFormRow, EuiTitle, EuiPanel, EuiSpacer, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { + EuiFormRow, + EuiTitle, + EuiPanel, + EuiSkeletonText, + EuiSpacer, + EuiSwitch, + EuiSwitchEvent, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { DataViewField } from '@kbn/data-views-plugin/public'; @@ -16,7 +24,12 @@ import { FIELD_ORIGIN } from '../../../../../common/constants'; import { TooltipSelector } from '../../../../components/tooltip_selector'; import { getIndexPatternService } from '../../../../kibana_services'; -import { getTermsFields, getSortFields, getSourceFields } from '../../../../index_pattern_util'; +import { + getTermsFields, + getIsTimeseries, + getSortFields, + getSourceFields, +} from '../../../../index_pattern_util'; import { ESDocField } from '../../../fields/es_doc_field'; import { OnSourceChangeArgs } from '../../source'; import { TopHitsForm } from './top_hits_form'; @@ -28,6 +41,7 @@ interface Props { indexPatternId: string; onChange: (args: OnSourceChangeArgs) => void; tooltipFields: IField[]; + topHitsGroupByTimeseries: boolean; topHitsSplitField: string; topHitsSize: number; sortField: string; @@ -36,6 +50,8 @@ interface Props { } interface State { + isLoading: boolean; + isTimeseries: boolean; loadError?: string; sourceFields: IField[]; termFields: DataViewField[]; @@ -46,6 +62,8 @@ export class TopHitsUpdateSourceEditor extends Component { private _isMounted = false; state: State = { + isLoading: false, + isTimeseries: false, sourceFields: [], termFields: [], sortFields: [], @@ -61,12 +79,15 @@ export class TopHitsUpdateSourceEditor extends Component { } async loadFields() { + this.setState({ isLoading: true }); + let indexPattern; try { indexPattern = await getIndexPatternService().get(this.props.indexPatternId); } catch (err) { if (this._isMounted) { this.setState({ + isLoading: false, loadError: getDataViewNotFoundMessage(this.props.indexPatternId), }); } @@ -87,6 +108,8 @@ export class TopHitsUpdateSourceEditor extends Component { }); this.setState({ + isLoading: false, + isTimeseries: getIsTimeseries(indexPattern), sourceFields, termFields: getTermsFields(indexPattern.fields), sortFields: getSortFields(indexPattern.fields), @@ -115,11 +138,13 @@ export class TopHitsUpdateSourceEditor extends Component { - + + + @@ -135,17 +160,21 @@ export class TopHitsUpdateSourceEditor extends Component { - + + +