[maps] Support time series split for top hits per entity source (#161799)

Closes https://github.com/elastic/kibana/issues/141978

<img width="600" alt="Screen Shot 2023-07-12 at 1 51 45 PM"
src="a71fc82f-31e0-49b2-9178-c70d890a9912">

### 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 `Top hits` layer. Select `Tracks` data
view.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-07-14 13:45:49 -06:00 committed by GitHub
parent d5bb2adcc8
commit 2259e91250
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 271 additions and 150 deletions

View file

@ -104,6 +104,7 @@ export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & {
sortField: string;
sortOrder: SortDirection;
scalingType: SCALING_TYPES;
topHitsGroupByTimeseries: boolean;
topHitsSplitField: string;
topHitsSize: number;
};

View file

@ -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',

View file

@ -44,6 +44,7 @@ test('Should create layer descriptor', () => {
sortField: '',
sortOrder: 'desc',
tooltipProperties: [],
topHitsGroupByTimeseries: false,
topHitsSize: 1,
topHitsSplitField: '',
type: 'ES_SEARCH',

View file

@ -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>): ESSearchSourceDescriptor {
const normalizedDescriptor = AbstractESSource.createDescriptor(descriptor);
const normalizedDescriptor = AbstractESSource.createDescriptor(
descriptor
) as AbstractESSourceDescriptor & Partial<ESSearchSourceDescriptor>;
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<string[]> {
@ -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,
};

View file

@ -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<Props, State> {
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<Props, State> {
};
_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<Props, State> {
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<Props, State> {
sortField,
sortOrder,
tooltipProperties,
topHitsSplitField,
topHitsGroupByTimeseries,
topHitsSplitField: topHitsSplitField ? topHitsSplitField : undefined,
topHitsSize,
}
: null;
@ -129,11 +158,13 @@ export class CreateSourceEditor extends Component<Props, State> {
<TopHitsForm
indexPatternId={this.state.indexPattern.id}
isColumnCompressed={false}
isTimeseries={this.state.isTimeseries}
onChange={this._onTopHitsPropChange}
sortField={this.state.sortField ? this.state.sortField : ''}
sortFields={this.state.sortFields}
sortOrder={this.state.sortOrder}
termFields={this.state.termFields}
topHitsGroupByTimeseries={this.state.topHitsGroupByTimeseries}
topHitsSplitField={this.state.topHitsSplitField}
topHitsSize={this.state.topHitsSize}
/>

View file

@ -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<Props, State> {
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<Props, State> {
}
render() {
let sizeSlider;
let sortField;
let sortOrder;
if (this.props.topHitsSplitField) {
sizeSlider = (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsSizeLabel', {
defaultMessage: 'Documents per entity',
})}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<ValidatedRange
min={1}
max={this.state.maxInnerResultWindow}
step={1}
value={this.props.topHitsSize}
onChange={this._onTopHitsSizeChange}
showLabels
showInput
showRange
data-test-subj="layerPanelTopHitsSize"
compressed
/>
</EuiFormRow>
);
sortField = (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esTopHitsSearch.sortFieldLabel', {
defaultMessage: 'Sort field',
})}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.maps.source.esSearch.sortFieldSelectPlaceholder', {
defaultMessage: 'Select sort field',
})}
value={this.props.sortField}
onChange={this._onSortFieldChange}
fields={this.props.sortFields}
compressed
/>
</EuiFormRow>
);
sortOrder = (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esTopHitsSearch.sortOrderLabel', {
defaultMessage: 'Sort order',
})}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<EuiSelect
disabled={!this.props.sortField}
options={[
{
text: i18n.translate('xpack.maps.source.esSearch.ascendingLabel', {
defaultMessage: 'ascending',
}),
value: SortDirection.asc,
},
{
text: i18n.translate('xpack.maps.source.esSearch.descendingLabel', {
defaultMessage: 'descending',
}),
value: SortDirection.desc,
},
]}
value={this.props.sortOrder}
onChange={this._onSortOrderChange}
compressed
/>
</EuiFormRow>
);
}
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsSplitFieldLabel', {
defaultMessage: 'Entity',
})}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<SingleFieldSelect
placeholder={i18n.translate(
'xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder',
{
defaultMessage: 'Select entity field',
{this.props.isTimeseries && (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsGroupByLabel', {
defaultMessage: 'Group by',
})}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<GroupByButtonGroup
groupByTimeseries={this.props.topHitsGroupByTimeseries}
onGroupByTimeseriesChange={this._onGroupByTimeseriesChange}
/>
</EuiFormRow>
)}
{!this.props.topHitsGroupByTimeseries && (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsSplitFieldLabel', {
defaultMessage: 'Entity',
})}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<SingleFieldSelect
placeholder={i18n.translate(
'xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder',
{
defaultMessage: 'Select entity field',
}
)}
value={this.props.topHitsSplitField}
onChange={this._onTopHitsSplitFieldChange}
fields={this.props.termFields}
isClearable={false}
compressed
/>
</EuiFormRow>
)}
{(this.props.topHitsSplitField || this.props.topHitsGroupByTimeseries) && (
<>
<EuiFormRow
label={
this.props.topHitsGroupByTimeseries
? i18n.translate('xpack.maps.source.esSearch.topHitsTimeseriesSizeLabel', {
defaultMessage: 'Documents per time series',
})
: i18n.translate('xpack.maps.source.esSearch.topHitsSizeLabel', {
defaultMessage: 'Documents per entity',
})
}
)}
value={this.props.topHitsSplitField}
onChange={this._onTopHitsSplitFieldChange}
fields={this.props.termFields}
isClearable={false}
compressed
/>
</EuiFormRow>
{sizeSlider}
{sortField}
{sortOrder}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<ValidatedRange
min={1}
max={this.state.maxInnerResultWindow}
step={1}
value={this.props.topHitsSize}
onChange={this._onTopHitsSizeChange}
showLabels
showInput
showRange
data-test-subj="layerPanelTopHitsSize"
compressed
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.maps.source.esTopHitsSearch.sortFieldLabel', {
defaultMessage: 'Sort field',
})}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<SingleFieldSelect
placeholder={i18n.translate(
'xpack.maps.source.esSearch.sortFieldSelectPlaceholder',
{
defaultMessage: 'Select sort field',
}
)}
value={this.props.sortField}
onChange={this._onSortFieldChange}
fields={this.props.sortFields}
compressed
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.maps.source.esTopHitsSearch.sortOrderLabel', {
defaultMessage: 'Sort order',
})}
display={this.props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<EuiSelect
disabled={!this.props.sortField}
options={[
{
text: i18n.translate('xpack.maps.source.esSearch.ascendingLabel', {
defaultMessage: 'ascending',
}),
value: SortDirection.asc,
},
{
text: i18n.translate('xpack.maps.source.esSearch.descendingLabel', {
defaultMessage: 'descending',
}),
value: SortDirection.desc,
},
]}
value={this.props.sortOrder}
onChange={this._onSortOrderChange}
compressed
/>
</EuiFormRow>
</>
)}
</Fragment>
);
}

View file

@ -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<Props, State> {
private _isMounted = false;
state: State = {
isLoading: false,
isTimeseries: false,
sourceFields: [],
termFields: [],
sortFields: [],
@ -61,12 +79,15 @@ export class TopHitsUpdateSourceEditor extends Component<Props, State> {
}
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<Props, State> {
});
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<Props, State> {
<EuiSpacer size="m" />
<TooltipSelector
tooltipFields={this.props.tooltipFields}
onChange={this._onTooltipPropertiesChange}
fields={this.state.sourceFields}
/>
<EuiSkeletonText lines={3} size="s" isLoading={this.state.isLoading}>
<TooltipSelector
tooltipFields={this.props.tooltipFields}
onChange={this._onTooltipPropertiesChange}
fields={this.state.sourceFields}
/>
</EuiSkeletonText>
</EuiPanel>
<EuiSpacer size="s" />
@ -135,17 +160,21 @@ export class TopHitsUpdateSourceEditor extends Component<Props, State> {
<EuiSpacer size="m" />
<TopHitsForm
indexPatternId={this.props.indexPatternId}
isColumnCompressed={true}
onChange={this.props.onChange}
sortField={this.props.sortField}
sortFields={this.state.sortFields}
sortOrder={this.props.sortOrder}
termFields={this.state.termFields}
topHitsSplitField={this.props.topHitsSplitField}
topHitsSize={this.props.topHitsSize}
/>
<EuiSkeletonText lines={3} size="s" isLoading={this.state.isLoading}>
<TopHitsForm
indexPatternId={this.props.indexPatternId}
isColumnCompressed={true}
isTimeseries={this.state.isTimeseries}
onChange={this.props.onChange}
sortField={this.props.sortField}
sortFields={this.state.sortFields}
sortOrder={this.props.sortOrder}
termFields={this.state.termFields}
topHitsGroupByTimeseries={this.props.topHitsGroupByTimeseries}
topHitsSplitField={this.props.topHitsSplitField}
topHitsSize={this.props.topHitsSize}
/>
</EuiSkeletonText>
<EuiFormRow>
<EuiSwitch