[Maps] Default ES document layer scaling type to clusters and show scaling UI in the create wizard (#60668)

* [Maps] show scaling panel in ES documents create wizard

* minor fix

* remove unused async state

* update create editor to use ScalingForm

* default geo field

* ts lint errors

* remove old dynamic filter behavior

* update jest tests

* eslint

* remove indexCount route

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-03-23 16:40:43 -06:00 committed by GitHub
parent 13baa51561
commit dc31736dd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 636 additions and 616 deletions

View file

@ -5,6 +5,7 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { LAYER_TYPE } from '../../common/constants';
import { DataMeta, MapFilters } from '../../common/data_request_descriptor_types';
export type SyncContext = {
@ -16,3 +17,10 @@ export type SyncContext = {
registerCancelCallback(requestToken: symbol, callback: () => void): void;
dataFilters: MapFilters;
};
export function updateSourceProp(
layerId: string,
propName: string,
value: unknown,
newLayerType?: LAYER_TYPE
): void;

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { LAYER_TYPE } from '../../../common/constants';
export type OnSourceChangeArgs = {
propName: string;
value: unknown;
newLayerType?: LAYER_TYPE;
};

View file

@ -0,0 +1,205 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should not render clusters option when clustering is not supported 1`] = `
<Fragment>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Scaling"
id="xpack.maps.esSearch.scaleTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiRadioGroup
idSelected="LIMIT"
onChange={[Function]}
options={
Array [
Object {
"id": "LIMIT",
"label": "Limit results to 10000.",
},
Object {
"id": "TOP_HITS",
"label": "Show top hits per entity.",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Dynamically filter for data in the visible map area"
onChange={[Function]}
/>
</EuiFormRow>
</Fragment>
`;
exports[`should render 1`] = `
<Fragment>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Scaling"
id="xpack.maps.esSearch.scaleTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiRadioGroup
idSelected="LIMIT"
onChange={[Function]}
options={
Array [
Object {
"id": "LIMIT",
"label": "Limit results to 10000.",
},
Object {
"id": "TOP_HITS",
"label": "Show top hits per entity.",
},
Object {
"id": "CLUSTERS",
"label": "Show clusters when results exceed 10000.",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Dynamically filter for data in the visible map area"
onChange={[Function]}
/>
</EuiFormRow>
</Fragment>
`;
exports[`should render top hits form when scaling type is TOP_HITS 1`] = `
<Fragment>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Scaling"
id="xpack.maps.esSearch.scaleTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiRadioGroup
idSelected="TOP_HITS"
onChange={[Function]}
options={
Array [
Object {
"id": "LIMIT",
"label": "Limit results to 10000.",
},
Object {
"id": "TOP_HITS",
"label": "Show top hits per entity.",
},
Object {
"id": "CLUSTERS",
"label": "Show clusters when results exceed 10000.",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Dynamically filter for data in the visible map area"
onChange={[Function]}
/>
</EuiFormRow>
<EuiHorizontalRule
margin="xs"
/>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Entity"
labelType="label"
>
<SingleFieldSelect
compressed={true}
fields={Array []}
onChange={[Function]}
placeholder="Select entity field"
/>
</EuiFormRow>
</Fragment>
`;

View file

@ -91,257 +91,20 @@ exports[`should enable sort order select when sort field provided 1`] = `
size="s"
/>
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Scaling"
id="xpack.maps.esSearch.scaleTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiRadioGroup
idSelected="LIMIT"
onChange={[Function]}
options={
Array [
Object {
"id": "LIMIT",
"label": "Limit results to 10000.",
},
Object {
"id": "TOP_HITS",
"label": "Show top hits per entity.",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Dynamically filter for data in the visible map area"
onChange={[Function]}
/>
</EuiFormRow>
</EuiPanel>
<EuiSpacer
size="s"
/>
</Fragment>
`;
exports[`should render top hits form when scaling type is TOP_HITS 1`] = `
<Fragment>
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Tooltip fields"
id="xpack.maps.esSearch.tooltipsTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<TooltipSelector
fields={null}
<ScalingForm
filterByMapBounds={true}
indexPatternId="indexPattern1"
onChange={[Function]}
tooltipFields={Array []}
scalingType="LIMIT"
supportsClustering={false}
termFields={null}
topHitsSize={1}
topHitsSplitField="trackId"
/>
</EuiPanel>
<EuiSpacer
size="s"
/>
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Sorting"
id="xpack.maps.esSearch.sortTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Field"
labelType="label"
>
<SingleFieldSelect
compressed={true}
fields={null}
onChange={[Function]}
placeholder="Select sort field"
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Order"
labelType="label"
>
<EuiSelect
compressed={true}
disabled={true}
onChange={[Function]}
options={
Array [
Object {
"text": "ascending",
"value": "asc",
},
Object {
"text": "descending",
"value": "desc",
},
]
}
value="DESC"
/>
</EuiFormRow>
</EuiPanel>
<EuiSpacer
size="s"
/>
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Scaling"
id="xpack.maps.esSearch.scaleTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiRadioGroup
idSelected="TOP_HITS"
onChange={[Function]}
options={
Array [
Object {
"id": "LIMIT",
"label": "Limit results to 10000.",
},
Object {
"id": "TOP_HITS",
"label": "Show top hits per entity.",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Dynamically filter for data in the visible map area"
onChange={[Function]}
/>
</EuiFormRow>
<EuiHorizontalRule
margin="xs"
/>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Entity"
labelType="label"
>
<SingleFieldSelect
compressed={true}
fields={null}
onChange={[Function]}
placeholder="Select entity field"
value="trackId"
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Documents per entity"
labelType="label"
>
<ValidatedRange
compressed={true}
data-test-subj="layerPanelTopHitsSize"
max={100}
min={1}
onChange={[Function]}
showInput={true}
showLabels={true}
showRange={true}
step={1}
value={1}
/>
</EuiFormRow>
</EuiPanel>
<EuiSpacer
size="s"
/>
</Fragment>
`;
@ -435,60 +198,16 @@ exports[`should render update source editor 1`] = `
size="s"
/>
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Scaling"
id="xpack.maps.esSearch.scaleTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
<ScalingForm
filterByMapBounds={true}
indexPatternId="indexPattern1"
onChange={[Function]}
scalingType="LIMIT"
supportsClustering={false}
termFields={null}
topHitsSize={1}
topHitsSplitField="trackId"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiRadioGroup
idSelected="LIMIT"
onChange={[Function]}
options={
Array [
Object {
"id": "LIMIT",
"label": "Limit results to 10000.",
},
Object {
"id": "TOP_HITS",
"label": "Show top hits per entity.",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Dynamically filter for data in the visible map area"
onChange={[Function]}
/>
</EuiFormRow>
</EuiPanel>
<EuiSpacer
size="s"

View file

@ -7,24 +7,17 @@
import _ from 'lodash';
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import { EuiFormRow, EuiSpacer, EuiSwitch, EuiCallOut } from '@elastic/eui';
import { EuiFormRow, EuiSpacer } from '@elastic/eui';
import { SingleFieldSelect } from '../../../components/single_field_select';
import {
getIndexPatternService,
getIndexPatternSelectComponent,
getHttp,
} from '../../../kibana_services';
import { getIndexPatternService, getIndexPatternSelectComponent } from '../../../kibana_services';
import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
ES_GEO_FIELD_TYPE,
GIS_API_PATH,
DEFAULT_MAX_RESULT_WINDOW,
} from '../../../../common/constants';
import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants';
import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
import { indexPatterns } from '../../../../../../../../src/plugins/data/public';
import { ScalingForm } from './scaling_form';
import { getTermsFields } from '../../../index_pattern_util';
function getGeoFields(fields) {
return fields.filter(field => {
@ -34,11 +27,26 @@ function getGeoFields(fields) {
);
});
}
function isGeoFieldAggregatable(indexPattern, geoFieldName) {
if (!indexPattern) {
return false;
}
const geoField = indexPattern.fields.getByName(geoFieldName);
return geoField && geoField.aggregatable;
}
const RESET_INDEX_PATTERN_STATE = {
indexPattern: undefined,
geoField: undefined,
geoFields: undefined,
// ES search source descriptor state
geoFieldName: undefined,
filterByMapBounds: DEFAULT_FILTER_BY_MAP_BOUNDS,
showFilterByBoundsSwitch: false,
scalingType: SCALING_TYPES.CLUSTERS, // turn on clusting by default
topHitsSplitField: undefined,
topHitsSize: 1,
};
export class CreateSourceEditor extends Component {
@ -58,41 +66,28 @@ export class CreateSourceEditor extends Component {
componentDidMount() {
this._isMounted = true;
this.loadIndexPattern(this.state.indexPatternId);
}
onIndexPatternSelect = indexPatternId => {
_onIndexPatternSelect = indexPatternId => {
this.setState(
{
indexPatternId,
},
this.loadIndexPattern(indexPatternId)
this._loadIndexPattern(indexPatternId)
);
};
loadIndexPattern = indexPatternId => {
_loadIndexPattern = indexPatternId => {
this.setState(
{
isLoadingIndexPattern: true,
...RESET_INDEX_PATTERN_STATE,
},
this.debouncedLoad.bind(null, indexPatternId)
this._debouncedLoad.bind(null, indexPatternId)
);
};
loadIndexDocCount = async indexPatternTitle => {
const http = getHttp();
const { count } = await http.fetch(`../${GIS_API_PATH}/indexCount`, {
method: 'GET',
credentials: 'same-origin',
query: {
index: indexPatternTitle,
},
});
return count;
};
debouncedLoad = _.debounce(async indexPatternId => {
_debouncedLoad = _.debounce(async indexPatternId => {
if (!indexPatternId || indexPatternId.length === 0) {
return;
}
@ -105,15 +100,6 @@ export class CreateSourceEditor extends Component {
return;
}
let indexHasSmallDocCount = false;
try {
const indexDocCount = await this.loadIndexDocCount(indexPattern.title);
indexHasSmallDocCount = indexDocCount <= DEFAULT_MAX_RESULT_WINDOW;
} catch (error) {
// retrieving index count is a nice to have and is not essential
// do not interrupt user flow if unable to retrieve count
}
if (!this._isMounted) {
return;
}
@ -124,43 +110,71 @@ export class CreateSourceEditor extends Component {
return;
}
const geoFields = getGeoFields(indexPattern.fields);
this.setState({
isLoadingIndexPattern: false,
indexPattern: indexPattern,
filterByMapBounds: !indexHasSmallDocCount, // Turn off filterByMapBounds when index contains a limited number of documents
showFilterByBoundsSwitch: indexHasSmallDocCount,
geoFields,
});
//make default selection
const geoFields = getGeoFields(indexPattern.fields);
if (geoFields[0]) {
this.onGeoFieldSelect(geoFields[0].name);
if (geoFields.length) {
// make default selection, prefer aggregatable field over the first available
const firstAggregatableGeoField = geoFields.find(geoField => {
return geoField.aggregatable;
});
const defaultGeoFieldName = firstAggregatableGeoField
? firstAggregatableGeoField
: geoFields[0];
this._onGeoFieldSelect(defaultGeoFieldName.name);
}
}, 300);
onGeoFieldSelect = geoField => {
_onGeoFieldSelect = geoFieldName => {
// Respect previous scaling type selection unless newly selected geo field does not support clustering.
const scalingType =
this.state.scalingType === SCALING_TYPES.CLUSTERS &&
!isGeoFieldAggregatable(this.state.indexPattern, geoFieldName)
? SCALING_TYPES.LIMIT
: this.state.scalingType;
this.setState(
{
geoField,
geoFieldName,
scalingType,
},
this.previewLayer
this._previewLayer
);
};
onFilterByMapBoundsChange = event => {
_onScalingPropChange = ({ propName, value }) => {
this.setState(
{
filterByMapBounds: event.target.checked,
[propName]: value,
},
this.previewLayer
this._previewLayer
);
};
previewLayer = () => {
const { indexPatternId, geoField, filterByMapBounds } = this.state;
_previewLayer = () => {
const {
indexPatternId,
geoFieldName,
filterByMapBounds,
scalingType,
topHitsSplitField,
topHitsSize,
} = this.state;
const sourceConfig =
indexPatternId && geoField ? { indexPatternId, geoField, filterByMapBounds } : null;
indexPatternId && geoFieldName
? {
indexPatternId,
geoField: geoFieldName,
filterByMapBounds,
scalingType,
topHitsSplitField,
topHitsSize,
}
: null;
this.props.onSourceConfigChange(sourceConfig);
};
@ -183,56 +197,35 @@ export class CreateSourceEditor extends Component {
placeholder={i18n.translate('xpack.maps.source.esSearch.selectLabel', {
defaultMessage: 'Select geo field',
})}
value={this.state.geoField}
onChange={this.onGeoFieldSelect}
fields={
this.state.indexPattern ? getGeoFields(this.state.indexPattern.fields) : undefined
}
value={this.state.geoFieldName}
onChange={this._onGeoFieldSelect}
fields={this.state.geoFields}
/>
</EuiFormRow>
);
}
_renderFilterByMapBounds() {
if (!this.state.showFilterByBoundsSwitch) {
_renderScalingPanel() {
if (!this.state.indexPattern || !this.state.geoFieldName) {
return null;
}
return (
<Fragment>
<EuiCallOut
title={i18n.translate('xpack.maps.source.esSearch.disableFilterByMapBoundsTitle', {
defaultMessage: `Dynamic data filter disabled`,
})}
>
<p>
<FormattedMessage
id="xpack.maps.source.esSearch.disableFilterByMapBoundsExplainMsg"
defaultMessage="Index '{indexPatternTitle}' has a small number of documents and does not require dynamic filtering."
values={{
indexPatternTitle: this.state.indexPattern
? this.state.indexPattern.title
: this.state.indexPatternId,
}}
/>
</p>
<p>
<FormattedMessage
id="xpack.maps.source.esSearch.disableFilterByMapBoundsTurnOnMsg"
defaultMessage="Turn on dynamic filtering if you expect the number of documents to increase."
/>
</p>
</EuiCallOut>
<EuiSpacer size="s" />
<EuiFormRow>
<EuiSwitch
label={i18n.translate('xpack.maps.source.esSearch.extentFilterLabel', {
defaultMessage: `Dynamically filter for data in the visible map area`,
})}
checked={this.state.filterByMapBounds}
onChange={this.onFilterByMapBoundsChange}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<ScalingForm
filterByMapBounds={this.state.filterByMapBounds}
indexPatternId={this.state.indexPatternId}
onChange={this._onScalingPropChange}
scalingType={this.state.scalingType}
supportsClustering={isGeoFieldAggregatable(
this.state.indexPattern,
this.state.geoFieldName
)}
termFields={getTermsFields(this.state.indexPattern.fields)}
topHitsSplitField={this.state.topHitsSplitField}
topHitsSize={this.state.topHitsSize}
/>
</Fragment>
);
}
@ -265,7 +258,7 @@ export class CreateSourceEditor extends Component {
<IndexPatternSelect
isDisabled={this.state.noGeoIndexPatternsExist}
indexPatternId={this.state.indexPatternId}
onChange={this.onIndexPatternSelect}
onChange={this._onIndexPatternSelect}
placeholder={i18n.translate(
'xpack.maps.source.esSearch.selectIndexPatternPlaceholder',
{
@ -279,7 +272,7 @@ export class CreateSourceEditor extends Component {
{this._renderGeoSelect()}
{this._renderFilterByMapBounds()}
{this._renderScalingPanel()}
</Fragment>
);
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('../../../kibana_services', () => ({}));
jest.mock('./load_index_settings', () => ({
loadIndexSettings: async () => {
return { maxInnerResultWindow: 100, maxResultWindow: 10000 };
},
}));
import React from 'react';
import { shallow } from 'enzyme';
import { ScalingForm } from './scaling_form';
import { SCALING_TYPES } from '../../../../common/constants';
const defaultProps = {
filterByMapBounds: true,
indexPatternId: 'myIndexPattern',
onChange: () => {},
scalingType: SCALING_TYPES.LIMIT,
supportsClustering: true,
termFields: [],
topHitsSize: 1,
};
test('should render', async () => {
const component = shallow(<ScalingForm {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should not render clusters option when clustering is not supported', async () => {
const component = shallow(<ScalingForm {...defaultProps} supportsClustering={false} />);
expect(component).toMatchSnapshot();
});
test('should render top hits form when scaling type is TOP_HITS', async () => {
const component = shallow(<ScalingForm {...defaultProps} scalingType={SCALING_TYPES.TOP_HITS} />);
expect(component).toMatchSnapshot();
});

View file

@ -0,0 +1,230 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, Component } from 'react';
import {
EuiFormRow,
EuiSwitch,
EuiSwitchEvent,
EuiTitle,
EuiSpacer,
EuiHorizontalRule,
EuiRadioGroup,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
// @ts-ignore
import { SingleFieldSelect } from '../../../components/single_field_select';
// @ts-ignore
import { indexPatternService } from '../../../kibana_services';
// @ts-ignore
import { getTermsFields, getSourceFields } from '../../../index_pattern_util';
// @ts-ignore
import { ValidatedRange } from '../../../components/validated_range';
import {
DEFAULT_MAX_INNER_RESULT_WINDOW,
DEFAULT_MAX_RESULT_WINDOW,
SCALING_TYPES,
LAYER_TYPE,
} from '../../../../common/constants';
// @ts-ignore
import { loadIndexSettings } from './load_index_settings';
import { IFieldType } from '../../../../../../../../src/plugins/data/public';
import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view';
interface Props {
filterByMapBounds: boolean;
indexPatternId: string;
onChange: (args: OnSourceChangeArgs) => void;
scalingType: SCALING_TYPES;
supportsClustering: boolean;
termFields: IFieldType[];
topHitsSplitField?: string;
topHitsSize: number;
}
interface State {
maxInnerResultWindow: number;
maxResultWindow: number;
}
export class ScalingForm extends Component<Props, State> {
state = {
maxInnerResultWindow: DEFAULT_MAX_INNER_RESULT_WINDOW,
maxResultWindow: DEFAULT_MAX_RESULT_WINDOW,
};
_isMounted = false;
componentDidMount() {
this._isMounted = true;
this.loadIndexSettings();
}
componentWillUnmount() {
this._isMounted = false;
}
async loadIndexSettings() {
try {
const indexPattern = await indexPatternService.get(this.props.indexPatternId);
const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings(indexPattern.title);
if (this._isMounted) {
this.setState({ maxInnerResultWindow, maxResultWindow });
}
} catch (err) {
return;
}
}
_onScalingTypeChange = (optionId: string): void => {
const layerType =
optionId === SCALING_TYPES.CLUSTERS ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.VECTOR;
this.props.onChange({ propName: 'scalingType', value: optionId, newLayerType: layerType });
};
_onFilterByMapBoundsChange = (event: EuiSwitchEvent) => {
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
};
_onTopHitsSplitFieldChange = (topHitsSplitField: string) => {
this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField });
};
_onTopHitsSizeChange = (size: number) => {
this.props.onChange({ propName: 'topHitsSize', value: size });
};
_renderTopHitsForm() {
let sizeSlider;
if (this.props.topHitsSplitField) {
sizeSlider = (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsSizeLabel', {
defaultMessage: 'Documents per entity',
})}
display="columnCompressed"
>
<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>
);
}
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsSplitFieldLabel', {
defaultMessage: 'Entity',
})}
display="columnCompressed"
>
<SingleFieldSelect
placeholder={i18n.translate(
'xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder',
{
defaultMessage: 'Select entity field',
}
)}
value={this.props.topHitsSplitField}
onChange={this._onTopHitsSplitFieldChange}
fields={this.props.termFields}
compressed
/>
</EuiFormRow>
{sizeSlider}
</Fragment>
);
}
render() {
const scalingOptions = [
{
id: SCALING_TYPES.LIMIT,
label: i18n.translate('xpack.maps.source.esSearch.limitScalingLabel', {
defaultMessage: 'Limit results to {maxResultWindow}.',
values: { maxResultWindow: this.state.maxResultWindow },
}),
},
{
id: SCALING_TYPES.TOP_HITS,
label: i18n.translate('xpack.maps.source.esSearch.useTopHitsLabel', {
defaultMessage: 'Show top hits per entity.',
}),
},
];
if (this.props.supportsClustering) {
scalingOptions.push({
id: SCALING_TYPES.CLUSTERS,
label: i18n.translate('xpack.maps.source.esSearch.clusterScalingLabel', {
defaultMessage: 'Show clusters when results exceed {maxResultWindow}.',
values: { maxResultWindow: this.state.maxResultWindow },
}),
});
}
let filterByBoundsSwitch;
if (this.props.scalingType !== SCALING_TYPES.CLUSTERS) {
filterByBoundsSwitch = (
<EuiFormRow>
<EuiSwitch
label={i18n.translate('xpack.maps.source.esSearch.extentFilterLabel', {
defaultMessage: 'Dynamically filter for data in the visible map area',
})}
checked={this.props.filterByMapBounds}
onChange={this._onFilterByMapBoundsChange}
compressed
/>
</EuiFormRow>
);
}
let scalingForm = null;
if (this.props.scalingType === SCALING_TYPES.TOP_HITS) {
scalingForm = (
<Fragment>
<EuiHorizontalRule margin="xs" />
{this._renderTopHitsForm()}
</Fragment>
);
}
return (
<Fragment>
<EuiTitle size="xs">
<h5>
<FormattedMessage id="xpack.maps.esSearch.scaleTitle" defaultMessage="Scaling" />
</h5>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFormRow>
<EuiRadioGroup
options={scalingOptions}
idSelected={this.props.scalingType}
onChange={this._onScalingTypeChange}
/>
</EuiFormRow>
{filterByBoundsSwitch}
{scalingForm}
</Fragment>
);
}
}

View file

@ -6,34 +6,18 @@
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import {
EuiFormRow,
EuiSwitch,
EuiSelect,
EuiTitle,
EuiPanel,
EuiSpacer,
EuiHorizontalRule,
EuiRadioGroup,
} from '@elastic/eui';
import { EuiFormRow, EuiSelect, EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui';
import { SingleFieldSelect } from '../../../components/single_field_select';
import { TooltipSelector } from '../../../components/tooltip_selector';
import { getIndexPatternService } from '../../../kibana_services';
import { i18n } from '@kbn/i18n';
import { getTermsFields, getSourceFields } from '../../../index_pattern_util';
import { ValidatedRange } from '../../../components/validated_range';
import {
DEFAULT_MAX_INNER_RESULT_WINDOW,
DEFAULT_MAX_RESULT_WINDOW,
SORT_ORDER,
SCALING_TYPES,
LAYER_TYPE,
} from '../../../../common/constants';
import { SORT_ORDER } from '../../../../common/constants';
import { ESDocField } from '../../fields/es_doc_field';
import { FormattedMessage } from '@kbn/i18n/react';
import { loadIndexSettings } from './load_index_settings';
import { indexPatterns } from '../../../../../../../../src/plugins/data/public';
import { ScalingForm } from './scaling_form';
export class UpdateSourceEditor extends Component {
static propTypes = {
@ -52,33 +36,18 @@ export class UpdateSourceEditor extends Component {
sourceFields: null,
termFields: null,
sortFields: null,
maxInnerResultWindow: DEFAULT_MAX_INNER_RESULT_WINDOW,
maxResultWindow: DEFAULT_MAX_RESULT_WINDOW,
supportsClustering: false,
};
componentDidMount() {
this._isMounted = true;
this.loadFields();
this.loadIndexSettings();
}
componentWillUnmount() {
this._isMounted = false;
}
async loadIndexSettings() {
try {
const indexPattern = await getIndexPatternService().get(this.props.indexPatternId);
const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings(indexPattern.title);
if (this._isMounted) {
this.setState({ maxInnerResultWindow, maxResultWindow });
}
} catch (err) {
return;
}
}
async loadFields() {
let indexPattern;
try {
@ -133,85 +102,14 @@ export class UpdateSourceEditor extends Component {
this.props.onChange({ propName: 'tooltipProperties', value: propertyNames });
};
_onScalingTypeChange = optionId => {
const layerType =
optionId === SCALING_TYPES.CLUSTERS ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.VECTOR;
this.props.onChange({ propName: 'scalingType', value: optionId, newLayerType: layerType });
};
_onFilterByMapBoundsChange = event => {
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
};
onTopHitsSplitFieldChange = topHitsSplitField => {
this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField });
};
onSortFieldChange = sortField => {
_onSortFieldChange = sortField => {
this.props.onChange({ propName: 'sortField', value: sortField });
};
onSortOrderChange = e => {
_onSortOrderChange = e => {
this.props.onChange({ propName: 'sortOrder', value: e.target.value });
};
onTopHitsSizeChange = size => {
this.props.onChange({ propName: 'topHitsSize', value: size });
};
_renderTopHitsForm() {
let sizeSlider;
if (this.props.topHitsSplitField) {
sizeSlider = (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsSizeLabel', {
defaultMessage: 'Documents per entity',
})}
display="columnCompressed"
>
<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>
);
}
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsSplitFieldLabel', {
defaultMessage: 'Entity',
})}
display="columnCompressed"
>
<SingleFieldSelect
placeholder={i18n.translate(
'xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder',
{
defaultMessage: 'Select entity field',
}
)}
value={this.props.topHitsSplitField}
onChange={this.onTopHitsSplitFieldChange}
fields={this.state.termFields}
compressed
/>
</EuiFormRow>
{sizeSlider}
</Fragment>
);
}
_renderTooltipsPanel() {
return (
<EuiPanel>
@ -257,7 +155,7 @@ export class UpdateSourceEditor extends Component {
defaultMessage: 'Select sort field',
})}
value={this.props.sortField}
onChange={this.onSortFieldChange}
onChange={this._onSortFieldChange}
fields={this.state.sortFields}
compressed
/>
@ -286,7 +184,7 @@ export class UpdateSourceEditor extends Component {
},
]}
value={this.props.sortOrder}
onChange={this.onSortOrderChange}
onChange={this._onSortOrderChange}
compressed
/>
</EuiFormRow>
@ -295,78 +193,18 @@ export class UpdateSourceEditor extends Component {
}
_renderScalingPanel() {
const scalingOptions = [
{
id: SCALING_TYPES.LIMIT,
label: i18n.translate('xpack.maps.source.esSearch.limitScalingLabel', {
defaultMessage: 'Limit results to {maxResultWindow}.',
values: { maxResultWindow: this.state.maxResultWindow },
}),
},
{
id: SCALING_TYPES.TOP_HITS,
label: i18n.translate('xpack.maps.source.esSearch.useTopHitsLabel', {
defaultMessage: 'Show top hits per entity.',
}),
},
];
if (this.state.supportsClustering) {
scalingOptions.push({
id: SCALING_TYPES.CLUSTERS,
label: i18n.translate('xpack.maps.source.esSearch.clusterScalingLabel', {
defaultMessage: 'Show clusters when results exceed {maxResultWindow}.',
values: { maxResultWindow: this.state.maxResultWindow },
}),
});
}
let filterByBoundsSwitch;
if (this.props.scalingType !== SCALING_TYPES.CLUSTERS) {
filterByBoundsSwitch = (
<EuiFormRow>
<EuiSwitch
label={i18n.translate('xpack.maps.source.esSearch.extentFilterLabel', {
defaultMessage: 'Dynamically filter for data in the visible map area',
})}
checked={this.props.filterByMapBounds}
onChange={this._onFilterByMapBoundsChange}
compressed
/>
</EuiFormRow>
);
}
let scalingForm = null;
if (this.props.scalingType === SCALING_TYPES.TOP_HITS) {
scalingForm = (
<Fragment>
<EuiHorizontalRule margin="xs" />
{this._renderTopHitsForm()}
</Fragment>
);
}
return (
<EuiPanel>
<EuiTitle size="xs">
<h5>
<FormattedMessage id="xpack.maps.esSearch.scaleTitle" defaultMessage="Scaling" />
</h5>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFormRow>
<EuiRadioGroup
options={scalingOptions}
idSelected={this.props.scalingType}
onChange={this._onScalingTypeChange}
/>
</EuiFormRow>
{filterByBoundsSwitch}
{scalingForm}
<ScalingForm
filterByMapBounds={this.props.filterByMapBounds}
indexPatternId={this.props.indexPatternId}
onChange={this.props.onChange}
scalingType={this.props.scalingType}
supportsClustering={this.state.supportsClustering}
termFields={this.state.termFields}
topHitsSplitField={this.props.topHitsSplitField}
topHitsSize={this.props.topHitsSize}
/>
</EuiPanel>
);
}

View file

@ -40,11 +40,3 @@ test('should enable sort order select when sort field provided', async () => {
expect(component).toMatchSnapshot();
});
test('should render top hits form when scaling type is TOP_HITS', async () => {
const component = shallow(
<UpdateSourceEditor {...defaultProps} scalingType={SCALING_TYPES.TOP_HITS} />
);
expect(component).toMatchSnapshot();
});

View file

@ -409,26 +409,6 @@ export function initRoutes(server, licenseUid) {
},
});
server.route({
method: 'GET',
path: `${ROOT}/indexCount`,
handler: async (request, h) => {
const { server, query } = request;
if (!query.index) {
return h.response().code(400);
}
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
try {
const { count } = await callWithRequest(request, 'count', { index: query.index });
return { count };
} catch (error) {
return h.response().code(400);
}
},
});
server.route({
method: 'GET',
path: `/${INDEX_SETTINGS_API_PATH}`,

View file

@ -43,13 +43,13 @@ export function createMapPath(id: string) {
return `${MAP_BASE_URL}/${id}`;
}
export const LAYER_TYPE = {
TILE: 'TILE',
VECTOR: 'VECTOR',
VECTOR_TILE: 'VECTOR_TILE',
HEATMAP: 'HEATMAP',
BLENDED_VECTOR: 'BLENDED_VECTOR',
};
export enum LAYER_TYPE {
TILE = 'TILE',
VECTOR = 'VECTOR',
VECTOR_TILE = 'VECTOR_TILE',
HEATMAP = 'HEATMAP',
BLENDED_VECTOR = 'BLENDED_VECTOR',
}
export enum SORT_ORDER {
ASC = 'asc',

View file

@ -7170,9 +7170,6 @@
"xpack.maps.source.esGridDescription": "それぞれのグリッド付きセルのメトリックでグリッドにグループ分けされた地理空間データです。",
"xpack.maps.source.esGridTitle": "グリッド集約",
"xpack.maps.source.esSearch.convertToGeoJsonErrorMsg": "検索への応答を geoJson 機能コレクションに変換できません。エラー: {errorMsg}",
"xpack.maps.source.esSearch.disableFilterByMapBoundsExplainMsg": "インデックス「{indexPatternTitle}」はドキュメント数が少なく、ダイナミックフィルターが必要ありません。",
"xpack.maps.source.esSearch.disableFilterByMapBoundsTitle": "ダイナミックデータフィルターは無効です",
"xpack.maps.source.esSearch.disableFilterByMapBoundsTurnOnMsg": "ドキュメント数が増えると思われる場合はダイナミックフィルターをオンにしてください。",
"xpack.maps.source.esSearch.extentFilterLabel": "マップの表示範囲でデータを動的にフィルタリング",
"xpack.maps.source.esSearch.geofieldLabel": "地理空間フィールド",
"xpack.maps.source.esSearch.geoFieldLabel": "地理空間フィールド",

View file

@ -7170,9 +7170,6 @@
"xpack.maps.source.esGridDescription": "地理空间数据在网格中进行分组,每个网格单元格都具有指标",
"xpack.maps.source.esGridTitle": "网格聚合",
"xpack.maps.source.esSearch.convertToGeoJsonErrorMsg": "无法将搜索响应转换成 geoJson 功能集合,错误:{errorMsg}",
"xpack.maps.source.esSearch.disableFilterByMapBoundsExplainMsg": "索引“{indexPatternTitle}”具有很少数量的文档,不需要动态筛选。",
"xpack.maps.source.esSearch.disableFilterByMapBoundsTitle": "动态数据筛选已禁用",
"xpack.maps.source.esSearch.disableFilterByMapBoundsTurnOnMsg": "如果预期文档数量会增加,请打开动态筛选。",
"xpack.maps.source.esSearch.extentFilterLabel": "在可见地图区域中动态筛留数据",
"xpack.maps.source.esSearch.geofieldLabel": "地理空间字段",
"xpack.maps.source.esSearch.geoFieldLabel": "地理空间字段",