mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Maps] surface geo_shape clustering gold feature (#68666)
* [Maps] surface geo_shape clustering gold feature * show gold in scaling form * tslint * more tslint changes * fix jest test * fix functional test by handling fields prop being undefined * tslint fixes - that thing is slow to run * review feedback * update doc_values missing copy Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
ae8f6e3195
commit
7d9378aa22
12 changed files with 376 additions and 242 deletions
|
@ -99,6 +99,9 @@ export enum ES_GEO_FIELD_TYPE {
|
|||
GEO_SHAPE = 'geo_shape',
|
||||
}
|
||||
|
||||
// Using strings instead of ES_GEO_FIELD_TYPE enum to avoid typeing errors where IFieldType.type is compared to value
|
||||
export const ES_GEO_FIELD_TYPES = ['geo_point', 'geo_shape'];
|
||||
|
||||
export enum ES_SPATIAL_RELATIONS {
|
||||
INTERSECTS = 'INTERSECTS',
|
||||
DISJOINT = 'DISJOINT',
|
||||
|
|
|
@ -8,15 +8,25 @@ import _ from 'lodash';
|
|||
import React, { Fragment, Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ES_GEO_FIELD_TYPES } from '../../../../common/constants';
|
||||
import { SingleFieldSelect } from '../../../components/single_field_select';
|
||||
import { getIndexPatternService, getIndexPatternSelectComponent } from '../../../kibana_services';
|
||||
import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
import { getAggregatableGeoFieldTypes, getFieldsWithGeoTileAgg } from '../../../index_pattern_util';
|
||||
import {
|
||||
getFieldsWithGeoTileAgg,
|
||||
getGeoFields,
|
||||
getGeoTileAggNotSupportedReason,
|
||||
supportsGeoTileAgg,
|
||||
} from '../../../index_pattern_util';
|
||||
import { RenderAsSelect } from './render_as_select';
|
||||
|
||||
function doesNotSupportGeoTileAgg(field) {
|
||||
return !supportsGeoTileAgg(field);
|
||||
}
|
||||
|
||||
export class CreateSourceEditor extends Component {
|
||||
static propTypes = {
|
||||
onSourceConfigChange: PropTypes.func.isRequired,
|
||||
|
@ -87,9 +97,9 @@ export class CreateSourceEditor extends Component {
|
|||
});
|
||||
|
||||
//make default selection
|
||||
const geoFields = getFieldsWithGeoTileAgg(indexPattern.fields);
|
||||
if (geoFields[0]) {
|
||||
this._onGeoFieldSelect(geoFields[0].name);
|
||||
const geoFieldsWithGeoTileAgg = getFieldsWithGeoTileAgg(indexPattern.fields);
|
||||
if (geoFieldsWithGeoTileAgg[0]) {
|
||||
this._onGeoFieldSelect(geoFieldsWithGeoTileAgg[0].name);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
|
@ -141,10 +151,10 @@ export class CreateSourceEditor extends Component {
|
|||
value={this.state.geoField}
|
||||
onChange={this._onGeoFieldSelect}
|
||||
fields={
|
||||
this.state.indexPattern
|
||||
? getFieldsWithGeoTileAgg(this.state.indexPattern.fields)
|
||||
: undefined
|
||||
this.state.indexPattern ? getGeoFields(this.state.indexPattern.fields) : undefined
|
||||
}
|
||||
isFieldDisabled={doesNotSupportGeoTileAgg}
|
||||
getFieldDisabledReason={getGeoTileAggNotSupportedReason}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
@ -176,7 +186,7 @@ export class CreateSourceEditor extends Component {
|
|||
placeholder={i18n.translate('xpack.maps.source.esGeoGrid.indexPatternPlaceholder', {
|
||||
defaultMessage: 'Select index pattern',
|
||||
})}
|
||||
fieldTypes={getAggregatableGeoFieldTypes()}
|
||||
fieldTypes={ES_GEO_FIELD_TYPES}
|
||||
onNoIndexPatterns={this._onNoIndexPatterns}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should not render clusters option when clustering is not supported 1`] = `
|
||||
exports[`should disable clusters option when clustering is not supported 1`] = `
|
||||
<Fragment>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
|
@ -24,22 +24,33 @@ exports[`should not render clusters option when clustering is not supported 1`]
|
|||
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.",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<EuiRadio
|
||||
checked={true}
|
||||
id="LIMIT"
|
||||
label="Limit results to 10000."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<EuiRadio
|
||||
checked={false}
|
||||
id="TOP_HITS"
|
||||
label="Show top hits per entity."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<EuiToolTip
|
||||
content="Simulated clustering disabled"
|
||||
delay="regular"
|
||||
position="left"
|
||||
>
|
||||
<EuiRadio
|
||||
checked={false}
|
||||
disabled={true}
|
||||
id="CLUSTERS"
|
||||
label="Show clusters when results exceed 10000."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
|
@ -83,26 +94,27 @@ exports[`should render 1`] = `
|
|||
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.",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<EuiRadio
|
||||
checked={true}
|
||||
id="LIMIT"
|
||||
label="Limit results to 10000."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<EuiRadio
|
||||
checked={false}
|
||||
id="TOP_HITS"
|
||||
label="Show top hits per entity."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<EuiRadio
|
||||
checked={false}
|
||||
disabled={false}
|
||||
id="CLUSTERS"
|
||||
label="Show clusters when results exceed 10000."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
|
@ -146,26 +158,27 @@ exports[`should render top hits form when scaling type is TOP_HITS 1`] = `
|
|||
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.",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<EuiRadio
|
||||
checked={false}
|
||||
id="LIMIT"
|
||||
label="Limit results to 10000."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<EuiRadio
|
||||
checked={true}
|
||||
id="TOP_HITS"
|
||||
label="Show top hits per entity."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<EuiRadio
|
||||
checked={false}
|
||||
disabled={false}
|
||||
id="CLUSTERS"
|
||||
label="Show clusters when results exceed 10000."
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
|
|
|
@ -13,20 +13,15 @@ import { SingleFieldSelect } from '../../../components/single_field_select';
|
|||
import { getIndexPatternService, getIndexPatternSelectComponent } from '../../../kibana_services';
|
||||
import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants';
|
||||
import { ES_GEO_FIELD_TYPES, 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, supportsGeoTileAgg } from '../../../index_pattern_util';
|
||||
|
||||
function getGeoFields(fields) {
|
||||
return fields.filter((field) => {
|
||||
return (
|
||||
!indexPatterns.isNestedField(field) &&
|
||||
[ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE].includes(field.type)
|
||||
);
|
||||
});
|
||||
}
|
||||
import {
|
||||
getGeoFields,
|
||||
getTermsFields,
|
||||
getGeoTileAggNotSupportedReason,
|
||||
supportsGeoTileAgg,
|
||||
} from '../../../index_pattern_util';
|
||||
|
||||
function doesGeoFieldSupportGeoTileAgg(indexPattern, geoFieldName) {
|
||||
return indexPattern ? supportsGeoTileAgg(indexPattern.fields.getByName(geoFieldName)) : false;
|
||||
|
@ -217,6 +212,13 @@ export class CreateSourceEditor extends Component {
|
|||
this.state.indexPattern,
|
||||
this.state.geoFieldName
|
||||
)}
|
||||
clusteringDisabledReason={
|
||||
this.state.indexPattern
|
||||
? getGeoTileAggNotSupportedReason(
|
||||
this.state.indexPattern.fields.getByName(this.state.geoFieldName)
|
||||
)
|
||||
: null
|
||||
}
|
||||
termFields={getTermsFields(this.state.indexPattern.fields)}
|
||||
topHitsSplitField={this.state.topHitsSplitField}
|
||||
topHitsSize={this.state.topHitsSize}
|
||||
|
@ -260,7 +262,7 @@ export class CreateSourceEditor extends Component {
|
|||
defaultMessage: 'Select index pattern',
|
||||
}
|
||||
)}
|
||||
fieldTypes={[ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]}
|
||||
fieldTypes={ES_GEO_FIELD_TYPES}
|
||||
onNoIndexPatterns={this._onNoIndexPatterns}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -34,8 +34,14 @@ test('should render', async () => {
|
|||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not render clusters option when clustering is not supported', async () => {
|
||||
const component = shallow(<ScalingForm {...defaultProps} supportsClustering={false} />);
|
||||
test('should disable clusters option when clustering is not supported', async () => {
|
||||
const component = shallow(
|
||||
<ScalingForm
|
||||
{...defaultProps}
|
||||
supportsClustering={false}
|
||||
clusteringDisabledReason="Simulated clustering disabled"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -12,11 +12,11 @@ import {
|
|||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiHorizontalRule,
|
||||
EuiRadioGroup,
|
||||
EuiRadio,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
// @ts-ignore
|
||||
import { SingleFieldSelect } from '../../../components/single_field_select';
|
||||
import { getIndexPatternService } from '../../../kibana_services';
|
||||
// @ts-ignore
|
||||
|
@ -38,6 +38,7 @@ interface Props {
|
|||
onChange: (args: OnSourceChangeArgs) => void;
|
||||
scalingType: SCALING_TYPES;
|
||||
supportsClustering: boolean;
|
||||
clusteringDisabledReason?: string | null;
|
||||
termFields: IFieldType[];
|
||||
topHitsSplitField?: string;
|
||||
topHitsSize: number;
|
||||
|
@ -88,7 +89,7 @@ export class ScalingForm extends Component<Props, State> {
|
|||
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
|
||||
};
|
||||
|
||||
_onTopHitsSplitFieldChange = (topHitsSplitField: string) => {
|
||||
_onTopHitsSplitFieldChange = (topHitsSplitField?: string) => {
|
||||
this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField });
|
||||
};
|
||||
|
||||
|
@ -149,32 +150,30 @@ export class ScalingForm extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
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', {
|
||||
_renderClusteringRadio() {
|
||||
const clusteringRadio = (
|
||||
<EuiRadio
|
||||
id={SCALING_TYPES.CLUSTERS}
|
||||
label={i18n.translate('xpack.maps.source.esSearch.clusterScalingLabel', {
|
||||
defaultMessage: 'Show clusters when results exceed {maxResultWindow}.',
|
||||
values: { maxResultWindow: this.state.maxResultWindow },
|
||||
}),
|
||||
});
|
||||
}
|
||||
})}
|
||||
checked={this.props.scalingType === SCALING_TYPES.CLUSTERS}
|
||||
onChange={() => this._onScalingTypeChange(SCALING_TYPES.CLUSTERS)}
|
||||
disabled={!this.props.supportsClustering}
|
||||
/>
|
||||
);
|
||||
|
||||
return this.props.clusteringDisabledReason ? (
|
||||
<EuiToolTip position="left" content={this.props.clusteringDisabledReason}>
|
||||
{clusteringRadio}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
clusteringRadio
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let filterByBoundsSwitch;
|
||||
if (this.props.scalingType !== SCALING_TYPES.CLUSTERS) {
|
||||
filterByBoundsSwitch = (
|
||||
|
@ -212,11 +211,26 @@ export class ScalingForm extends Component<Props, State> {
|
|||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow>
|
||||
<EuiRadioGroup
|
||||
options={scalingOptions}
|
||||
idSelected={this.props.scalingType}
|
||||
onChange={this._onScalingTypeChange}
|
||||
/>
|
||||
<div>
|
||||
<EuiRadio
|
||||
id={SCALING_TYPES.LIMIT}
|
||||
label={i18n.translate('xpack.maps.source.esSearch.limitScalingLabel', {
|
||||
defaultMessage: 'Limit results to {maxResultWindow}.',
|
||||
values: { maxResultWindow: this.state.maxResultWindow },
|
||||
})}
|
||||
checked={this.props.scalingType === SCALING_TYPES.LIMIT}
|
||||
onChange={() => this._onScalingTypeChange(SCALING_TYPES.LIMIT)}
|
||||
/>
|
||||
<EuiRadio
|
||||
id={SCALING_TYPES.TOP_HITS}
|
||||
label={i18n.translate('xpack.maps.source.esSearch.useTopHitsLabel', {
|
||||
defaultMessage: 'Show top hits per entity.',
|
||||
})}
|
||||
checked={this.props.scalingType === SCALING_TYPES.TOP_HITS}
|
||||
onChange={() => this._onScalingTypeChange(SCALING_TYPES.TOP_HITS)}
|
||||
/>
|
||||
{this._renderClusteringRadio()}
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
|
||||
{filterByBoundsSwitch}
|
||||
|
|
|
@ -12,7 +12,12 @@ import { TooltipSelector } from '../../../components/tooltip_selector';
|
|||
|
||||
import { getIndexPatternService } from '../../../kibana_services';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getTermsFields, getSourceFields, supportsGeoTileAgg } from '../../../index_pattern_util';
|
||||
import {
|
||||
getGeoTileAggNotSupportedReason,
|
||||
getTermsFields,
|
||||
getSourceFields,
|
||||
supportsGeoTileAgg,
|
||||
} from '../../../index_pattern_util';
|
||||
import { SORT_ORDER } from '../../../../common/constants';
|
||||
import { ESDocField } from '../../fields/es_doc_field';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -91,6 +96,7 @@ export class UpdateSourceEditor extends Component {
|
|||
|
||||
this.setState({
|
||||
supportsClustering: supportsGeoTileAgg(geoField),
|
||||
clusteringDisabledReason: getGeoTileAggNotSupportedReason(geoField),
|
||||
sourceFields: sourceFields,
|
||||
termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields
|
||||
sortFields: indexPattern.fields.filter(
|
||||
|
@ -201,6 +207,7 @@ export class UpdateSourceEditor extends Component {
|
|||
onChange={this.props.onChange}
|
||||
scalingType={this.props.scalingType}
|
||||
supportsClustering={this.state.supportsClustering}
|
||||
clusteringDisabledReason={this.state.clusteringDisabledReason}
|
||||
termFields={this.state.termFields}
|
||||
topHitsSplitField={this.props.topHitsSplitField}
|
||||
topHitsSize={this.props.topHitsSize}
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { EuiComboBox, EuiHighlight, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FieldIcon } from '../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
function fieldsToOptions(fields) {
|
||||
if (!fields) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fields
|
||||
.map((field) => {
|
||||
return {
|
||||
value: field,
|
||||
label: 'label' in field ? field.label : field.name,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
function renderOption(option, searchValue, contentClassName) {
|
||||
return (
|
||||
<EuiFlexGroup className={contentClassName} gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={null}>
|
||||
<FieldIcon type={option.value.type} fill="none" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export function SingleFieldSelect({ fields, onChange, value, placeholder, ...rest }) {
|
||||
const onSelection = (selectedOptions) => {
|
||||
onChange(_.get(selectedOptions, '0.value.name'));
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiComboBox
|
||||
placeholder={placeholder}
|
||||
singleSelection={true}
|
||||
options={fieldsToOptions(fields)}
|
||||
selectedOptions={value ? [{ value: value, label: value }] : []}
|
||||
onChange={onSelection}
|
||||
isDisabled={!fields}
|
||||
renderOption={renderOption}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SingleFieldSelect.propTypes = {
|
||||
placeholder: PropTypes.string,
|
||||
fields: PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string, // fieldName
|
||||
};
|
118
x-pack/plugins/maps/public/components/single_field_select.tsx
Normal file
118
x-pack/plugins/maps/public/components/single_field_select.tsx
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiComboBoxProps,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiHighlight,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { IFieldType } from 'src/plugins/data/public';
|
||||
import { FieldIcon } from '../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
function fieldsToOptions(
|
||||
fields?: IFieldType[],
|
||||
isFieldDisabled?: (field: IFieldType) => boolean
|
||||
): Array<EuiComboBoxOptionOption<IFieldType>> {
|
||||
if (!fields) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fields
|
||||
.map((field) => {
|
||||
const option: EuiComboBoxOptionOption<IFieldType> = {
|
||||
value: field,
|
||||
label: field.name,
|
||||
};
|
||||
if (isFieldDisabled && isFieldDisabled(field)) {
|
||||
option.disabled = true;
|
||||
}
|
||||
return option;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
type Props = Omit<
|
||||
EuiComboBoxProps<IFieldType>,
|
||||
'isDisabled' | 'onChange' | 'options' | 'renderOption' | 'selectedOptions' | 'singleSelection'
|
||||
> & {
|
||||
fields?: IFieldType[];
|
||||
onChange: (fieldName?: string) => void;
|
||||
value?: string; // index pattern field name
|
||||
isFieldDisabled?: (field: IFieldType) => boolean;
|
||||
getFieldDisabledReason?: (field: IFieldType) => string | null;
|
||||
};
|
||||
|
||||
export function SingleFieldSelect({
|
||||
fields,
|
||||
getFieldDisabledReason,
|
||||
isFieldDisabled,
|
||||
onChange,
|
||||
value,
|
||||
...rest
|
||||
}: Props) {
|
||||
function renderOption(
|
||||
option: EuiComboBoxOptionOption<IFieldType>,
|
||||
searchValue: string,
|
||||
contentClassName: string
|
||||
) {
|
||||
const content = (
|
||||
<EuiFlexGroup className={contentClassName} gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={null}>
|
||||
<FieldIcon type={option.value!.type} fill="none" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const disabledReason =
|
||||
option.disabled && getFieldDisabledReason ? getFieldDisabledReason(option.value!) : null;
|
||||
|
||||
return disabledReason ? (
|
||||
<EuiToolTip position="left" content={disabledReason}>
|
||||
{content}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
const onSelection = (selectedOptions: Array<EuiComboBoxOptionOption<IFieldType>>) => {
|
||||
onChange(_.get(selectedOptions, '0.value.name'));
|
||||
};
|
||||
|
||||
const selectedOptions: Array<EuiComboBoxOptionOption<IFieldType>> = [];
|
||||
if (value && fields) {
|
||||
const selectedField = fields.find((field: IFieldType) => {
|
||||
return field.name === value;
|
||||
});
|
||||
if (selectedField) {
|
||||
selectedOptions.push({ value: selectedField, label: value });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiComboBox
|
||||
singleSelection={true}
|
||||
options={fieldsToOptions(fields, isFieldDisabled)}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onSelection}
|
||||
isDisabled={!fields}
|
||||
renderOption={renderOption}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getIndexPatternService, getIsGoldPlus } from './kibana_services';
|
||||
import { indexPatterns } from '../../../../src/plugins/data/public';
|
||||
import { ES_GEO_FIELD_TYPE } from '../common/constants';
|
||||
|
||||
export async function getIndexPatternsFromIds(indexPatternIds = []) {
|
||||
const promises = [];
|
||||
indexPatternIds.forEach((id) => {
|
||||
const indexPatternPromise = getIndexPatternService().get(id);
|
||||
if (indexPatternPromise) {
|
||||
promises.push(indexPatternPromise);
|
||||
}
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
export function getTermsFields(fields) {
|
||||
return fields.filter((field) => {
|
||||
return (
|
||||
field.aggregatable &&
|
||||
!indexPatterns.isNestedField(field) &&
|
||||
['number', 'boolean', 'date', 'ip', 'string'].includes(field.type)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAggregatableGeoFieldTypes() {
|
||||
const aggregatableFieldTypes = [ES_GEO_FIELD_TYPE.GEO_POINT];
|
||||
if (getIsGoldPlus()) {
|
||||
aggregatableFieldTypes.push(ES_GEO_FIELD_TYPE.GEO_SHAPE);
|
||||
}
|
||||
return aggregatableFieldTypes;
|
||||
}
|
||||
|
||||
export function getFieldsWithGeoTileAgg(fields) {
|
||||
return fields.filter(supportsGeoTileAgg);
|
||||
}
|
||||
|
||||
export function supportsGeoTileAgg(field) {
|
||||
return (
|
||||
field &&
|
||||
field.aggregatable &&
|
||||
!indexPatterns.isNestedField(field) &&
|
||||
getAggregatableGeoFieldTypes().includes(field.type)
|
||||
);
|
||||
}
|
||||
|
||||
// Returns filtered fields list containing only fields that exist in _source.
|
||||
export function getSourceFields(fields) {
|
||||
return fields.filter((field) => {
|
||||
// Multi fields are not stored in _source and only exist in index.
|
||||
const isMultiField = field.subType && field.subType.multi;
|
||||
return !isMultiField && !indexPatterns.isNestedField(field);
|
||||
});
|
||||
}
|
|
@ -18,6 +18,7 @@ describe('getSourceFields', () => {
|
|||
const fields = [
|
||||
{
|
||||
name: 'agent',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'agent.keyword',
|
||||
|
@ -26,10 +27,11 @@ describe('getSourceFields', () => {
|
|||
parent: 'agent',
|
||||
},
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
];
|
||||
const sourceFields = getSourceFields(fields);
|
||||
expect(sourceFields).toEqual([{ name: 'agent' }]);
|
||||
expect(sourceFields).toEqual([{ name: 'agent', type: 'string' }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -37,6 +39,7 @@ describe('Gold+ licensing', () => {
|
|||
const testStubs = [
|
||||
{
|
||||
field: {
|
||||
name: 'location',
|
||||
type: 'geo_point',
|
||||
aggregatable: true,
|
||||
},
|
||||
|
@ -45,6 +48,7 @@ describe('Gold+ licensing', () => {
|
|||
},
|
||||
{
|
||||
field: {
|
||||
name: 'location',
|
||||
type: 'geo_shape',
|
||||
aggregatable: false,
|
||||
},
|
||||
|
@ -53,6 +57,7 @@ describe('Gold+ licensing', () => {
|
|||
},
|
||||
{
|
||||
field: {
|
||||
name: 'location',
|
||||
type: 'geo_shape',
|
||||
aggregatable: true,
|
||||
},
|
85
x-pack/plugins/maps/public/index_pattern_util.ts
Normal file
85
x-pack/plugins/maps/public/index_pattern_util.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { IFieldType, IndexPattern } from 'src/plugins/data/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getIndexPatternService, getIsGoldPlus } from './kibana_services';
|
||||
import { indexPatterns } from '../../../../src/plugins/data/public';
|
||||
import { ES_GEO_FIELD_TYPE, ES_GEO_FIELD_TYPES } from '../common/constants';
|
||||
|
||||
export function getGeoTileAggNotSupportedReason(field: IFieldType): string | null {
|
||||
if (!field.aggregatable) {
|
||||
return i18n.translate('xpack.maps.geoTileAgg.disabled.docValues', {
|
||||
defaultMessage:
|
||||
'Clustering requires aggregations. Enable aggregations by setting doc_values to true.',
|
||||
});
|
||||
}
|
||||
|
||||
if (field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE && !getIsGoldPlus()) {
|
||||
return i18n.translate('xpack.maps.geoTileAgg.disabled.license', {
|
||||
defaultMessage: 'Geo_shape clustering requires a Gold license.',
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getIndexPatternsFromIds(
|
||||
indexPatternIds: string[] = []
|
||||
): Promise<IndexPattern[]> {
|
||||
const promises: Array<Promise<IndexPattern>> = [];
|
||||
indexPatternIds.forEach((id) => {
|
||||
promises.push(getIndexPatternService().get(id));
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
export function getTermsFields(fields: IFieldType[]): IFieldType[] {
|
||||
return fields.filter((field) => {
|
||||
return (
|
||||
field.aggregatable &&
|
||||
!indexPatterns.isNestedField(field) &&
|
||||
['number', 'boolean', 'date', 'ip', 'string'].includes(field.type)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAggregatableGeoFieldTypes(): string[] {
|
||||
const aggregatableFieldTypes = [ES_GEO_FIELD_TYPE.GEO_POINT];
|
||||
if (getIsGoldPlus()) {
|
||||
aggregatableFieldTypes.push(ES_GEO_FIELD_TYPE.GEO_SHAPE);
|
||||
}
|
||||
return aggregatableFieldTypes;
|
||||
}
|
||||
|
||||
export function getGeoFields(fields: IFieldType[]): IFieldType[] {
|
||||
return fields.filter((field) => {
|
||||
return !indexPatterns.isNestedField(field) && ES_GEO_FIELD_TYPES.includes(field.type);
|
||||
});
|
||||
}
|
||||
|
||||
export function getFieldsWithGeoTileAgg(fields: IFieldType[]): IFieldType[] {
|
||||
return fields.filter(supportsGeoTileAgg);
|
||||
}
|
||||
|
||||
export function supportsGeoTileAgg(field?: IFieldType): boolean {
|
||||
return (
|
||||
!!field &&
|
||||
!!field.aggregatable &&
|
||||
!indexPatterns.isNestedField(field) &&
|
||||
getAggregatableGeoFieldTypes().includes(field.type)
|
||||
);
|
||||
}
|
||||
|
||||
// Returns filtered fields list containing only fields that exist in _source.
|
||||
export function getSourceFields(fields: IFieldType[]): IFieldType[] {
|
||||
return fields.filter((field) => {
|
||||
// Multi fields are not stored in _source and only exist in index.
|
||||
const isMultiField = field.subType && field.subType.multi;
|
||||
return !isMultiField && !indexPatterns.isNestedField(field);
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue