mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Maps] top hits layer wizard (#95678)
* [Maps] top hits source * add icon * top hits form * clean up icon * create descriptor * add update editor * update docs * tslint and i18n fixes * update jest test * add sort inputs to create UI * review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
39ebc8068c
commit
a70bd991ab
26 changed files with 671 additions and 285 deletions
|
@ -76,9 +76,8 @@ then accumulates the most relevant documents based on sort order for each entry
|
|||
|
||||
To enable top hits:
|
||||
|
||||
. Click *Add layer*, then select the *Documents* layer.
|
||||
. Click *Add layer*, then select the *Top hits per entity* layer.
|
||||
. Configure *Index pattern* and *Geospatial field*.
|
||||
. In *Scaling*, select *Show top hits per entity*.
|
||||
. Set *Entity* to the field that identifies entities in your documents.
|
||||
This field will be used in the terms aggregation to group your documents into entity buckets.
|
||||
. Set *Documents per entity* to configure the maximum number of documents accumulated per entity.
|
||||
|
|
|
@ -23,8 +23,6 @@ Select the appropriate *Scaling* option for your use case.
|
|||
* *Limit results to 10000.* The layer displays features from the first `index.max_result_window` documents.
|
||||
Results exceeding `index.max_result_window` are not displayed.
|
||||
|
||||
* *Show top hits per entity.* The layer displays the <<maps-top-hits-aggregation, most relevant documents per entity>>.
|
||||
|
||||
* *Show clusters when results exceed 10000.* When results exceed `index.max_result_window`, the layer uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into clusters and displays metrics for each cluster. When results are less then `index.max_result_window`, the layer displays features from individual documents.
|
||||
|
||||
* *Use vector tiles.* Vector tiles partition your map into 6 to 8 tiles.
|
||||
|
@ -36,6 +34,9 @@ Tiles exceeding `index.max_result_window` have a visual indicator when there are
|
|||
*Point to point*:: Aggregated data paths between the source and destination.
|
||||
The index must contain at least 2 fields mapped as {ref}/geo-point.html[geo_point], source and destination.
|
||||
|
||||
*Top hits per entity*:: The layer displays the <<maps-top-hits-aggregation, most relevant documents per entity>>.
|
||||
The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape].
|
||||
|
||||
*Tracks*:: Create lines from points.
|
||||
The index must contain at least one field mapped as {ref}/geo-point.html[geo_point].
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ export type MapFilters = {
|
|||
};
|
||||
|
||||
type ESSearchSourceSyncMeta = {
|
||||
filterByMapBounds: boolean;
|
||||
sortField: string;
|
||||
sortOrder: SortDirection;
|
||||
scalingType: SCALING_TYPES;
|
||||
|
|
|
@ -95,8 +95,8 @@ export type ESGeoLineSourceDescriptor = AbstractESAggSourceDescriptor & {
|
|||
|
||||
export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & {
|
||||
geoField: string;
|
||||
filterByMapBounds?: boolean;
|
||||
tooltipProperties?: string[];
|
||||
filterByMapBounds: boolean;
|
||||
tooltipProperties: string[];
|
||||
sortField: string;
|
||||
sortOrder: SortDirection;
|
||||
scalingType: SCALING_TYPES;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
export const TopHitsLayerIcon: FunctionComponent = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="49" height="25" fill="none" viewBox="0 0 49 25">
|
||||
<circle cx="24.939" cy="12.409" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="35.849" cy="12.409" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="45.561" cy="12.409" r="3.273" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="5.197" cy="18.954" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="13.485" cy="12.409" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
</svg>
|
||||
);
|
|
@ -13,9 +13,9 @@ export const TracksLayerIcon: FunctionComponent = () => (
|
|||
className="mapLayersWizardIcon__highlight"
|
||||
d="M12.733 12.136h32.283v.545H12.935L4.452 19.98l-.356-.413 8.637-7.43z"
|
||||
/>
|
||||
<circle cx="24.939" cy="12.409" r="3.273" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="24.939" cy="12.409" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="35.849" cy="12.409" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="45.561" cy="12.409" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="45.561" cy="12.409" r="3.273" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="5.197" cy="18.954" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="13.485" cy="12.409" r="2.182" className="mapLayersWizardIcon__highlight" />
|
||||
</svg>
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { registerLayerWizard } from './layer_wizard_registry';
|
||||
import { uploadLayerWizardConfig } from './file_upload_wizard';
|
||||
// @ts-ignore
|
||||
import { esDocumentsLayerWizardConfig } from '../sources/es_search_source';
|
||||
import {
|
||||
esDocumentsLayerWizardConfig,
|
||||
esTopHitsLayerWizardConfig,
|
||||
} from '../sources/es_search_source';
|
||||
import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from '../sources/es_geo_grid_source';
|
||||
import { geoLineLayerWizardConfig } from '../sources/es_geo_line_source';
|
||||
// @ts-ignore
|
||||
|
@ -37,13 +39,13 @@ export function registerLayerWizards() {
|
|||
|
||||
// Registration order determines display order
|
||||
registerLayerWizard(uploadLayerWizardConfig);
|
||||
// @ts-ignore
|
||||
registerLayerWizard(esDocumentsLayerWizardConfig);
|
||||
// @ts-ignore
|
||||
registerLayerWizard(choroplethLayerWizardConfig);
|
||||
registerLayerWizard(clustersLayerWizardConfig);
|
||||
// @ts-ignore
|
||||
registerLayerWizard(heatmapLayerWizardConfig);
|
||||
registerLayerWizard(esTopHitsLayerWizardConfig);
|
||||
registerLayerWizard(geoLineLayerWizardConfig);
|
||||
// @ts-ignore
|
||||
registerLayerWizard(point2PointLayerWizardConfig);
|
||||
|
|
|
@ -31,12 +31,6 @@ exports[`scaling form should disable clusters option when clustering is not supp
|
|||
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"
|
||||
|
@ -123,12 +117,6 @@ exports[`scaling form should render 1`] = `
|
|||
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}
|
||||
|
@ -177,110 +165,3 @@ exports[`scaling form should render 1`] = `
|
|||
</EuiFormRow>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`scaling form 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"
|
||||
>
|
||||
<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]}
|
||||
/>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<React.Fragment>
|
||||
<EuiBetaBadge
|
||||
label="beta"
|
||||
/>
|
||||
<EuiHorizontalRule
|
||||
margin="xs"
|
||||
/>
|
||||
Use vector tiles for faster display of large datasets.
|
||||
</React.Fragment>
|
||||
}
|
||||
delay="regular"
|
||||
position="left"
|
||||
>
|
||||
<EuiRadio
|
||||
checked={false}
|
||||
id="MVT"
|
||||
label="Use vector tiles"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
</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 []}
|
||||
isClearable={false}
|
||||
onChange={[Function]}
|
||||
placeholder="Select entity field"
|
||||
value={null}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
`;
|
||||
|
|
|
@ -98,9 +98,6 @@ exports[`should enable sort order select when sort field provided 1`] = `
|
|||
onChange={[Function]}
|
||||
scalingType="LIMIT"
|
||||
supportsClustering={false}
|
||||
termFields={null}
|
||||
topHitsSize={1}
|
||||
topHitsSplitField="trackId"
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
|
@ -206,9 +203,6 @@ exports[`should render update source editor 1`] = `
|
|||
onChange={[Function]}
|
||||
scalingType="LIMIT"
|
||||
supportsClustering={false}
|
||||
termFields={null}
|
||||
topHitsSize={1}
|
||||
topHitsSplitField="trackId"
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
|
|
|
@ -17,7 +17,6 @@ import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
|
|||
import { ScalingForm } from './scaling_form';
|
||||
import {
|
||||
getGeoFields,
|
||||
getTermsFields,
|
||||
getGeoTileAggNotSupportedReason,
|
||||
supportsGeoTileAgg,
|
||||
} from '../../../index_pattern_util';
|
||||
|
@ -34,8 +33,6 @@ const RESET_INDEX_PATTERN_STATE = {
|
|||
geoFieldName: undefined,
|
||||
filterByMapBounds: DEFAULT_FILTER_BY_MAP_BOUNDS,
|
||||
scalingType: SCALING_TYPES.CLUSTERS, // turn on clusting by default
|
||||
topHitsSplitField: undefined,
|
||||
topHitsSize: 1,
|
||||
};
|
||||
|
||||
export class CreateSourceEditor extends Component {
|
||||
|
@ -97,14 +94,7 @@ export class CreateSourceEditor extends Component {
|
|||
};
|
||||
|
||||
_previewLayer = () => {
|
||||
const {
|
||||
indexPattern,
|
||||
geoFieldName,
|
||||
filterByMapBounds,
|
||||
scalingType,
|
||||
topHitsSplitField,
|
||||
topHitsSize,
|
||||
} = this.state;
|
||||
const { indexPattern, geoFieldName, filterByMapBounds, scalingType } = this.state;
|
||||
|
||||
const sourceConfig =
|
||||
indexPattern && geoFieldName
|
||||
|
@ -113,8 +103,6 @@ export class CreateSourceEditor extends Component {
|
|||
geoField: geoFieldName,
|
||||
filterByMapBounds,
|
||||
scalingType,
|
||||
topHitsSplitField,
|
||||
topHitsSize,
|
||||
}
|
||||
: null;
|
||||
this.props.onSourceConfigChange(sourceConfig);
|
||||
|
@ -167,9 +155,6 @@ export class CreateSourceEditor extends Component {
|
|||
)
|
||||
: null
|
||||
}
|
||||
termFields={getTermsFields(this.state.indexPattern.fields)}
|
||||
topHitsSplitField={this.state.topHitsSplitField}
|
||||
topHitsSize={this.state.topHitsSize}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -10,7 +10,6 @@ import React from 'react';
|
|||
// @ts-ignore
|
||||
import { CreateSourceEditor } from './create_source_editor';
|
||||
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
|
||||
// @ts-ignore
|
||||
import { ESSearchSource, sourceTitle } from './es_search_source';
|
||||
import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer';
|
||||
import { VectorLayer } from '../../layers/vector_layer';
|
||||
|
|
|
@ -60,6 +60,7 @@ import { ITooltipProperty } from '../../tooltips/tooltip_property';
|
|||
import { DataRequest } from '../../util/data_request';
|
||||
import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common';
|
||||
import { isValidStringConfig } from '../../util/valid_string_config';
|
||||
import { TopHitsUpdateSourceEditor } from './top_hits';
|
||||
|
||||
export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', {
|
||||
defaultMessage: 'Documents',
|
||||
|
@ -166,6 +167,22 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
|
|||
}
|
||||
|
||||
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> | null {
|
||||
if (this._isTopHits()) {
|
||||
return (
|
||||
<TopHitsUpdateSourceEditor
|
||||
source={this}
|
||||
indexPatternId={this.getIndexPatternId()}
|
||||
onChange={sourceEditorArgs.onChange}
|
||||
tooltipFields={this._tooltipFields}
|
||||
sortField={this._descriptor.sortField}
|
||||
sortOrder={this._descriptor.sortOrder}
|
||||
filterByMapBounds={this.isFilterByMapBounds()}
|
||||
topHitsSplitField={this._descriptor.topHitsSplitField}
|
||||
topHitsSize={this._descriptor.topHitsSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getGeoField = () => {
|
||||
return this._getGeoField();
|
||||
};
|
||||
|
@ -180,8 +197,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
|
|||
sortOrder={this._descriptor.sortOrder}
|
||||
scalingType={this._descriptor.scalingType}
|
||||
filterByMapBounds={this.isFilterByMapBounds()}
|
||||
topHitsSplitField={this._descriptor.topHitsSplitField}
|
||||
topHitsSize={this._descriptor.topHitsSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -658,6 +673,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
|
|||
|
||||
getSyncMeta(): VectorSourceSyncMeta | null {
|
||||
return {
|
||||
filterByMapBounds: this._descriptor.filterByMapBounds,
|
||||
sortField: this._descriptor.sortField,
|
||||
sortOrder: this._descriptor.sortOrder,
|
||||
scalingType: this._descriptor.scalingType,
|
||||
|
|
|
@ -11,3 +11,4 @@ export {
|
|||
createDefaultLayerDescriptor,
|
||||
esDocumentsLayerWizardConfig,
|
||||
} from './es_documents_layer_wizard';
|
||||
export { esTopHitsLayerWizardConfig } from './top_hits';
|
||||
|
|
|
@ -26,8 +26,6 @@ const defaultProps = {
|
|||
scalingType: SCALING_TYPES.LIMIT,
|
||||
supportsClustering: true,
|
||||
termFields: [],
|
||||
topHitsSplitField: null,
|
||||
topHitsSize: 1,
|
||||
};
|
||||
|
||||
describe('scaling form', () => {
|
||||
|
@ -48,12 +46,4 @@ describe('scaling form', () => {
|
|||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,19 +19,9 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { SingleFieldSelect } from '../../../components/single_field_select';
|
||||
import { getIndexPatternService } from '../../../kibana_services';
|
||||
// @ts-ignore
|
||||
import { ValidatedRange } from '../../../components/validated_range';
|
||||
import {
|
||||
DEFAULT_MAX_INNER_RESULT_WINDOW,
|
||||
DEFAULT_MAX_RESULT_WINDOW,
|
||||
LAYER_TYPE,
|
||||
SCALING_TYPES,
|
||||
} from '../../../../common/constants';
|
||||
// @ts-ignore
|
||||
import { DEFAULT_MAX_RESULT_WINDOW, LAYER_TYPE, SCALING_TYPES } from '../../../../common/constants';
|
||||
import { loadIndexSettings } from './load_index_settings';
|
||||
import { IFieldType } from '../../../../../../../src/plugins/data/public';
|
||||
import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view';
|
||||
|
||||
interface Props {
|
||||
|
@ -41,19 +31,14 @@ interface Props {
|
|||
scalingType: SCALING_TYPES;
|
||||
supportsClustering: boolean;
|
||||
clusteringDisabledReason?: string | null;
|
||||
termFields: IFieldType[];
|
||||
topHitsSplitField: string | null;
|
||||
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;
|
||||
|
@ -70,11 +55,9 @@ export class ScalingForm extends Component<Props, State> {
|
|||
async loadIndexSettings() {
|
||||
try {
|
||||
const indexPattern = await getIndexPatternService().get(this.props.indexPatternId);
|
||||
const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings(
|
||||
indexPattern!.title
|
||||
);
|
||||
const { maxResultWindow } = await loadIndexSettings(indexPattern!.title);
|
||||
if (this._isMounted) {
|
||||
this.setState({ maxInnerResultWindow, maxResultWindow });
|
||||
this.setState({ maxResultWindow });
|
||||
}
|
||||
} catch (err) {
|
||||
return;
|
||||
|
@ -98,71 +81,6 @@ export class ScalingForm extends Component<Props, State> {
|
|||
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
|
||||
};
|
||||
|
||||
_onTopHitsSplitFieldChange = (topHitsSplitField?: string) => {
|
||||
if (!topHitsSplitField) {
|
||||
return;
|
||||
}
|
||||
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}
|
||||
isClearable={false}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{sizeSlider}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
_renderClusteringRadio() {
|
||||
const clusteringRadio = (
|
||||
<EuiRadio
|
||||
|
@ -218,10 +136,7 @@ export class ScalingForm extends Component<Props, State> {
|
|||
|
||||
render() {
|
||||
let filterByBoundsSwitch;
|
||||
if (
|
||||
this.props.scalingType === SCALING_TYPES.TOP_HITS ||
|
||||
this.props.scalingType === SCALING_TYPES.LIMIT
|
||||
) {
|
||||
if (this.props.scalingType === SCALING_TYPES.LIMIT) {
|
||||
filterByBoundsSwitch = (
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
|
@ -236,16 +151,6 @@ export class ScalingForm extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
let topHitsOptionsForm = null;
|
||||
if (this.props.scalingType === SCALING_TYPES.TOP_HITS) {
|
||||
topHitsOptionsForm = (
|
||||
<Fragment>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
{this._renderTopHitsForm()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="xs">
|
||||
|
@ -267,21 +172,12 @@ export class ScalingForm extends Component<Props, State> {
|
|||
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()}
|
||||
{this._renderMVTRadio()}
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
|
||||
{filterByBoundsSwitch}
|
||||
{topHitsOptionsForm}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
|
||||
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 { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types';
|
||||
import {
|
||||
IndexPattern,
|
||||
IFieldType,
|
||||
SortDirection,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { TopHitsForm } from './top_hits_form';
|
||||
import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view';
|
||||
|
||||
interface Props {
|
||||
onSourceConfigChange: (sourceConfig: Partial<ESSearchSourceDescriptor> | null) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
indexPattern: IndexPattern | null;
|
||||
geoFields: IFieldType[];
|
||||
geoFieldName: string | null;
|
||||
sortField: string | null;
|
||||
sortFields: IFieldType[];
|
||||
sortOrder: SortDirection;
|
||||
termFields: IFieldType[];
|
||||
topHitsSplitField: string | null;
|
||||
topHitsSize: number;
|
||||
}
|
||||
|
||||
export class CreateSourceEditor extends Component<Props, State> {
|
||||
state: State = {
|
||||
indexPattern: null,
|
||||
geoFields: [],
|
||||
geoFieldName: null,
|
||||
sortField: null,
|
||||
sortFields: [],
|
||||
sortOrder: SortDirection.desc,
|
||||
termFields: [],
|
||||
topHitsSplitField: null,
|
||||
topHitsSize: 1,
|
||||
};
|
||||
|
||||
_onIndexPatternSelect = (indexPattern: IndexPattern) => {
|
||||
const geoFields = getGeoFields(indexPattern.fields);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
indexPattern,
|
||||
geoFields,
|
||||
geoFieldName: geoFields.length ? geoFields[0].name : null,
|
||||
sortField: indexPattern.timeFieldName ? indexPattern.timeFieldName : null,
|
||||
sortFields: getSortFields(indexPattern.fields),
|
||||
termFields: getTermsFields(indexPattern.fields),
|
||||
topHitsSplitField: null,
|
||||
},
|
||||
this._previewLayer
|
||||
);
|
||||
};
|
||||
|
||||
_onGeoFieldSelect = (geoFieldName?: string) => {
|
||||
this.setState({ geoFieldName: geoFieldName ? geoFieldName : null }, this._previewLayer);
|
||||
};
|
||||
|
||||
_onTopHitsPropChange = ({ propName, value }: OnSourceChangeArgs) => {
|
||||
this.setState(
|
||||
// @ts-expect-error
|
||||
{ [propName]: value },
|
||||
this._previewLayer
|
||||
);
|
||||
};
|
||||
|
||||
_previewLayer = () => {
|
||||
const {
|
||||
indexPattern,
|
||||
geoFieldName,
|
||||
sortField,
|
||||
sortOrder,
|
||||
topHitsSplitField,
|
||||
topHitsSize,
|
||||
} = this.state;
|
||||
|
||||
const tooltipProperties: string[] = [];
|
||||
if (topHitsSplitField) {
|
||||
tooltipProperties.push(topHitsSplitField);
|
||||
}
|
||||
if (indexPattern && indexPattern.timeFieldName) {
|
||||
tooltipProperties.push(indexPattern.timeFieldName);
|
||||
}
|
||||
|
||||
const sourceConfig =
|
||||
indexPattern && geoFieldName && sortField && topHitsSplitField
|
||||
? {
|
||||
indexPatternId: indexPattern.id,
|
||||
geoField: geoFieldName,
|
||||
scalingType: SCALING_TYPES.TOP_HITS,
|
||||
sortField,
|
||||
sortOrder,
|
||||
tooltipProperties,
|
||||
topHitsSplitField,
|
||||
topHitsSize,
|
||||
}
|
||||
: null;
|
||||
this.props.onSourceConfigChange(sourceConfig);
|
||||
};
|
||||
|
||||
_renderGeoSelect() {
|
||||
return this.state.indexPattern ? (
|
||||
<GeoFieldSelect
|
||||
value={this.state.geoFieldName ? this.state.geoFieldName : ''}
|
||||
onChange={this._onGeoFieldSelect}
|
||||
geoFields={this.state.geoFields}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
_renderTopHitsPanel() {
|
||||
if (!this.state.indexPattern || !this.state.indexPattern.id || !this.state.geoFieldName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TopHitsForm
|
||||
indexPatternId={this.state.indexPattern.id}
|
||||
isColumnCompressed={false}
|
||||
onChange={this._onTopHitsPropChange}
|
||||
sortField={this.state.sortField ? this.state.sortField : ''}
|
||||
sortFields={this.state.sortFields}
|
||||
sortOrder={this.state.sortOrder}
|
||||
termFields={this.state.termFields}
|
||||
topHitsSplitField={this.state.topHitsSplitField}
|
||||
topHitsSize={this.state.topHitsSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPanel>
|
||||
<GeoIndexPatternSelect
|
||||
value={
|
||||
this.state.indexPattern && this.state.indexPattern.id ? this.state.indexPattern.id : ''
|
||||
}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
/>
|
||||
|
||||
{this._renderGeoSelect()}
|
||||
|
||||
{this._renderTopHitsPanel()}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { TopHitsUpdateSourceEditor } from './update_source_editor';
|
||||
export { esTopHitsLayerWizardConfig } from './wizard';
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, Component, Fragment } from 'react';
|
||||
import { EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SingleFieldSelect } from '../../../../components/single_field_select';
|
||||
import { getIndexPatternService } from '../../../../kibana_services';
|
||||
// @ts-expect-error
|
||||
import { ValidatedRange } from '../../../../components/validated_range';
|
||||
import { DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../../../../common/constants';
|
||||
import { loadIndexSettings } from '../load_index_settings';
|
||||
import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view';
|
||||
import { IFieldType, SortDirection } from '../../../../../../../../src/plugins/data/public';
|
||||
|
||||
interface Props {
|
||||
indexPatternId: string;
|
||||
isColumnCompressed?: boolean;
|
||||
onChange: (args: OnSourceChangeArgs) => void;
|
||||
sortField: string;
|
||||
sortFields: IFieldType[];
|
||||
sortOrder: SortDirection;
|
||||
termFields: IFieldType[];
|
||||
topHitsSplitField: string | null;
|
||||
topHitsSize: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
maxInnerResultWindow: number;
|
||||
}
|
||||
|
||||
export class TopHitsForm extends Component<Props, State> {
|
||||
state = {
|
||||
maxInnerResultWindow: DEFAULT_MAX_INNER_RESULT_WINDOW,
|
||||
};
|
||||
_isMounted = false;
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.loadIndexSettings();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
_onTopHitsSplitFieldChange = (topHitsSplitField?: string) => {
|
||||
if (!topHitsSplitField) {
|
||||
return;
|
||||
}
|
||||
this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField });
|
||||
};
|
||||
|
||||
_onTopHitsSizeChange = (size: number) => {
|
||||
this.props.onChange({ propName: 'topHitsSize', value: size });
|
||||
};
|
||||
|
||||
_onSortFieldChange = (sortField?: string) => {
|
||||
this.props.onChange({ propName: 'sortField', value: sortField });
|
||||
};
|
||||
|
||||
_onSortOrderChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.props.onChange({ propName: 'sortOrder', value: event.target.value });
|
||||
};
|
||||
|
||||
async loadIndexSettings() {
|
||||
try {
|
||||
const indexPattern = await getIndexPatternService().get(this.props.indexPatternId);
|
||||
const { maxInnerResultWindow } = await loadIndexSettings(indexPattern!.title);
|
||||
if (this._isMounted) {
|
||||
this.setState({ maxInnerResultWindow });
|
||||
}
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
)}
|
||||
value={this.props.topHitsSplitField}
|
||||
onChange={this._onTopHitsSplitFieldChange}
|
||||
fields={this.props.termFields}
|
||||
isClearable={false}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{sizeSlider}
|
||||
|
||||
{sortField}
|
||||
|
||||
{sortOrder}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { EuiFormRow, EuiTitle, EuiPanel, EuiSpacer, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
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 { SortDirection, IFieldType } from '../../../../../../../../src/plugins/data/public';
|
||||
import { ESDocField } from '../../../fields/es_doc_field';
|
||||
import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view';
|
||||
import { TopHitsForm } from './top_hits_form';
|
||||
import { ESSearchSource } from '../es_search_source';
|
||||
import { IField } from '../../../fields/field';
|
||||
|
||||
interface Props {
|
||||
filterByMapBounds: boolean;
|
||||
indexPatternId: string;
|
||||
onChange: (args: OnSourceChangeArgs) => void;
|
||||
tooltipFields: IField[];
|
||||
topHitsSplitField: string;
|
||||
topHitsSize: number;
|
||||
sortField: string;
|
||||
sortOrder: SortDirection;
|
||||
source: ESSearchSource;
|
||||
}
|
||||
|
||||
interface State {
|
||||
loadError?: string;
|
||||
sourceFields: IField[];
|
||||
termFields: IFieldType[];
|
||||
sortFields: IFieldType[];
|
||||
}
|
||||
|
||||
export class TopHitsUpdateSourceEditor extends Component<Props, State> {
|
||||
private _isMounted = false;
|
||||
|
||||
state: State = {
|
||||
sourceFields: [],
|
||||
termFields: [],
|
||||
sortFields: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.loadFields();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
async loadFields() {
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await getIndexPatternService().get(this.props.indexPatternId);
|
||||
} catch (err) {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
loadError: i18n.translate('xpack.maps.source.esSearch.loadErrorMessage', {
|
||||
defaultMessage: `Unable to find Index pattern {id}`,
|
||||
values: {
|
||||
id: this.props.indexPatternId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawTooltipFields = getSourceFields(indexPattern.fields);
|
||||
const sourceFields = rawTooltipFields.map((field) => {
|
||||
return new ESDocField({
|
||||
fieldName: field.name,
|
||||
source: this.props.source,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({
|
||||
sourceFields,
|
||||
termFields: getTermsFields(indexPattern.fields),
|
||||
sortFields: getSortFields(indexPattern.fields),
|
||||
});
|
||||
}
|
||||
_onTooltipPropertiesChange = (propertyNames: string[]) => {
|
||||
this.props.onChange({ propName: 'tooltipProperties', value: propertyNames });
|
||||
};
|
||||
|
||||
_onFilterByMapBoundsChange = (event: EuiSwitchEvent) => {
|
||||
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.esSearch.tooltipsTitle"
|
||||
defaultMessage="Tooltip fields"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<TooltipSelector
|
||||
tooltipFields={this.props.tooltipFields}
|
||||
onChange={this._onTooltipPropertiesChange}
|
||||
fields={this.state.sourceFields}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.source.topHitsPanelLabel"
|
||||
defaultMessage="Top hits"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<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}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { CreateSourceEditor } from './create_source_editor';
|
||||
import { LayerWizard, RenderWizardArguments } from '../../../layers/layer_wizard_registry';
|
||||
import { VectorLayer } from '../../../layers/vector_layer';
|
||||
import { LAYER_WIZARD_CATEGORY } from '../../../../../common/constants';
|
||||
import { TopHitsLayerIcon } from '../../../layers/icons/top_hits_layer_icon';
|
||||
import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types';
|
||||
import { ESSearchSource } from '../es_search_source';
|
||||
|
||||
export const esTopHitsLayerWizardConfig: LayerWizard = {
|
||||
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
|
||||
description: i18n.translate('xpack.maps.source.topHitsDescription', {
|
||||
defaultMessage:
|
||||
'Display the most relevant documents per entity, e.g. the most recent GPS hits per vehicle.',
|
||||
}),
|
||||
icon: TopHitsLayerIcon,
|
||||
renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
|
||||
const onSourceConfigChange = (sourceConfig: Partial<ESSearchSourceDescriptor> | null) => {
|
||||
if (!sourceConfig) {
|
||||
previewLayers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig);
|
||||
const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
|
||||
previewLayers([layerDescriptor]);
|
||||
};
|
||||
return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />;
|
||||
},
|
||||
title: i18n.translate('xpack.maps.source.topHitsTitle', {
|
||||
defaultMessage: 'Top hits per entity',
|
||||
}),
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
import React, { Fragment, Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiFormRow, EuiSelect, EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { FIELD_ORIGIN } from '../../../../common/constants';
|
||||
import { SingleFieldSelect } from '../../../components/single_field_select';
|
||||
import { TooltipSelector } from '../../../components/tooltip_selector';
|
||||
|
||||
|
@ -15,7 +16,6 @@ import { getIndexPatternService } from '../../../kibana_services';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
getGeoTileAggNotSupportedReason,
|
||||
getTermsFields,
|
||||
getSourceFields,
|
||||
supportsGeoTileAgg,
|
||||
} from '../../../index_pattern_util';
|
||||
|
@ -33,14 +33,11 @@ export class UpdateSourceEditor extends Component {
|
|||
sortField: PropTypes.string,
|
||||
sortOrder: PropTypes.string.isRequired,
|
||||
scalingType: PropTypes.string.isRequired,
|
||||
topHitsSplitField: PropTypes.string,
|
||||
topHitsSize: PropTypes.number.isRequired,
|
||||
source: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
sourceFields: null,
|
||||
termFields: null,
|
||||
sortFields: null,
|
||||
supportsClustering: false,
|
||||
mvtDisabledReason: null,
|
||||
|
@ -94,6 +91,7 @@ export class UpdateSourceEditor extends Component {
|
|||
return new ESDocField({
|
||||
fieldName: field.name,
|
||||
source: this.props.source,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -102,7 +100,6 @@ export class UpdateSourceEditor extends Component {
|
|||
clusteringDisabledReason: getGeoTileAggNotSupportedReason(geoField),
|
||||
mvtDisabledReason: null,
|
||||
sourceFields: sourceFields,
|
||||
termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields
|
||||
sortFields: indexPattern.fields.filter(
|
||||
(field) => field.sortable && !indexPatterns.isNestedField(field)
|
||||
), //todo change sort fields to use fields
|
||||
|
@ -212,9 +209,6 @@ export class UpdateSourceEditor extends Component {
|
|||
scalingType={this.props.scalingType}
|
||||
supportsClustering={this.state.supportsClustering}
|
||||
clusteringDisabledReason={this.state.clusteringDisabledReason}
|
||||
termFields={this.state.termFields}
|
||||
topHitsSplitField={this.props.topHitsSplitField}
|
||||
topHitsSize={this.props.topHitsSize}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
|
|
@ -26,8 +26,6 @@ const defaultProps = {
|
|||
tooltipFields: [],
|
||||
sortOrder: 'DESC',
|
||||
scalingType: SCALING_TYPES.LIMIT,
|
||||
topHitsSplitField: 'trackId',
|
||||
topHitsSize: 1,
|
||||
};
|
||||
|
||||
test('should render update source editor', async () => {
|
||||
|
|
37
x-pack/plugins/maps/public/components/geo_field_select.tsx
Normal file
37
x-pack/plugins/maps/public/components/geo_field_select.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { SingleFieldSelect } from './single_field_select';
|
||||
import { IFieldType } from '../../../../../src/plugins/data/common';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
geoFields: IFieldType[];
|
||||
onChange: (geoFieldName?: string) => void;
|
||||
}
|
||||
|
||||
export function GeoFieldSelect(props: Props) {
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.geofieldLabel', {
|
||||
defaultMessage: 'Geospatial field',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.source.selectLabel', {
|
||||
defaultMessage: 'Select geo field',
|
||||
})}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
fields={props.geoFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -56,6 +56,12 @@ export function getTermsFields(fields: IFieldType[]): IFieldType[] {
|
|||
});
|
||||
}
|
||||
|
||||
export function getSortFields(fields: IFieldType[]): IFieldType[] {
|
||||
return fields.filter((field) => {
|
||||
return field.sortable && !indexPatterns.isNestedField(field);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAggregatableGeoFieldTypes(): string[] {
|
||||
const aggregatableFieldTypes = [ES_GEO_FIELD_TYPE.GEO_POINT];
|
||||
if (getIsGoldPlus()) {
|
||||
|
|
|
@ -12762,7 +12762,6 @@
|
|||
"xpack.maps.source.esSearch.topHitsSplitFieldLabel": "エンティティ",
|
||||
"xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択",
|
||||
"xpack.maps.source.esSearch.useMVTVectorTiles": "ベクトルタイルを使用",
|
||||
"xpack.maps.source.esSearch.useTopHitsLabel": "エンティティごとにトップヒットを表示。",
|
||||
"xpack.maps.source.esSearchDescription": "Elasticsearch の点、線、多角形",
|
||||
"xpack.maps.source.esSearchTitle": "ドキュメント",
|
||||
"xpack.maps.source.esSource.noGeoFieldErrorMessage": "インデックスパターン {indexPatternTitle} には現在ジオフィールド {geoField} が含まれていません",
|
||||
|
|
|
@ -12930,7 +12930,6 @@
|
|||
"xpack.maps.source.esSearch.topHitsSplitFieldLabel": "实体",
|
||||
"xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder": "选择实体字段",
|
||||
"xpack.maps.source.esSearch.useMVTVectorTiles": "使用矢量磁贴",
|
||||
"xpack.maps.source.esSearch.useTopHitsLabel": "显示每个实体最高命中结果。",
|
||||
"xpack.maps.source.esSearchDescription": "Elasticsearch 的点、线和多边形",
|
||||
"xpack.maps.source.esSearchTitle": "文档",
|
||||
"xpack.maps.source.esSource.noGeoFieldErrorMessage": "索引模式“{indexPatternTitle}”不再包含地理字段 {geoField}",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue