mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
d5bb2adcc8
commit
2259e91250
7 changed files with 271 additions and 150 deletions
|
@ -104,6 +104,7 @@ export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & {
|
|||
sortField: string;
|
||||
sortOrder: SortDirection;
|
||||
scalingType: SCALING_TYPES;
|
||||
topHitsGroupByTimeseries: boolean;
|
||||
topHitsSplitField: string;
|
||||
topHitsSize: number;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -44,6 +44,7 @@ test('Should create layer descriptor', () => {
|
|||
sortField: '',
|
||||
sortOrder: 'desc',
|
||||
tooltipProperties: [],
|
||||
topHitsGroupByTimeseries: false,
|
||||
topHitsSize: 1,
|
||||
topHitsSplitField: '',
|
||||
type: 'ES_SEARCH',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue