mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Maps] choropleth layer wizard (#69699)
* [Maps] choropleth layer wizard * add boundaries radio group * geo_index_pattern_select * consolidate more logic into geo_index_pattern_select * small clean-up * left geo field and join field * move EuiPanel into render wizard * cleanup * right panel * createEmsChoroplethLayerDescriptor * createEsChoroplethLayerDescriptor * i18n cleanup * tslint * snapshot update * review feedback * review feedback * update snapshot * make EMS default source * tslint Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
7db95a1691
commit
917598141f
34 changed files with 1343 additions and 446 deletions
|
@ -0,0 +1,107 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should render EMS UI when left source is BOUNDARIES_SOURCE.EMS 1`] = `
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
defaultMessage="Boundaries source"
|
||||
id="xpack.maps.choropleth.boundariesLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiRadioGroup
|
||||
idSelected="EMS"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"id": "EMS",
|
||||
"label": "Administrative boundaries from Elastic Maps Service",
|
||||
},
|
||||
Object {
|
||||
"id": "ELASTICSEARCH",
|
||||
"label": "Points, lines, and polygons from Elasticsearch",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EMSFileSelect
|
||||
onChange={[Function]}
|
||||
value={null}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`should render elasticsearch UI when left source is BOUNDARIES_SOURCE.ELASTICSEARCH 1`] = `
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
defaultMessage="Boundaries source"
|
||||
id="xpack.maps.choropleth.boundariesLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiRadioGroup
|
||||
idSelected="ELASTICSEARCH"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"id": "EMS",
|
||||
"label": "Administrative boundaries from Elastic Maps Service",
|
||||
},
|
||||
Object {
|
||||
"id": "ELASTICSEARCH",
|
||||
"label": "Points, lines, and polygons from Elasticsearch",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<GeoIndexPatternSelect
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
|
||||
import { LayerWizard, RenderWizardArguments } from '../layer_wizard_registry';
|
||||
import { LayerTemplate } from './layer_template';
|
||||
|
||||
export const choroplethLayerWizardConfig: LayerWizard = {
|
||||
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
|
||||
description: i18n.translate('xpack.maps.choropleth.desc', {
|
||||
defaultMessage: 'Shaded areas to compare statistics across boundaries',
|
||||
}),
|
||||
icon: 'logoElasticsearch',
|
||||
renderWizard: (renderWizardArguments: RenderWizardArguments) => {
|
||||
return <LayerTemplate {...renderWizardArguments} />;
|
||||
},
|
||||
title: i18n.translate('xpack.maps.choropleth.title', {
|
||||
defaultMessage: 'Choropleth',
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 uuid from 'uuid/v4';
|
||||
import {
|
||||
AGG_TYPE,
|
||||
COLOR_MAP_TYPE,
|
||||
FIELD_ORIGIN,
|
||||
SCALING_TYPES,
|
||||
SOURCE_TYPES,
|
||||
STYLE_TYPE,
|
||||
VECTOR_STYLES,
|
||||
} from '../../../../common/constants';
|
||||
import { getJoinAggKey } from '../../../../common/get_agg_key';
|
||||
import {
|
||||
AggDescriptor,
|
||||
ColorDynamicOptions,
|
||||
EMSFileSourceDescriptor,
|
||||
ESSearchSourceDescriptor,
|
||||
} from '../../../../common/descriptor_types';
|
||||
import { VectorStyle } from '../../styles/vector/vector_style';
|
||||
import { VectorLayer } from '../vector_layer/vector_layer';
|
||||
import { EMSFileSource } from '../../sources/ems_file_source';
|
||||
// @ts-ignore
|
||||
import { ESSearchSource } from '../../sources/es_search_source';
|
||||
import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults';
|
||||
|
||||
const defaultDynamicProperties = getDefaultDynamicProperties();
|
||||
|
||||
function createChoroplethLayerDescriptor({
|
||||
sourceDescriptor,
|
||||
leftField,
|
||||
rightIndexPatternId,
|
||||
rightIndexPatternTitle,
|
||||
rightTermField,
|
||||
}: {
|
||||
sourceDescriptor: EMSFileSourceDescriptor | ESSearchSourceDescriptor;
|
||||
leftField: string;
|
||||
rightIndexPatternId: string;
|
||||
rightIndexPatternTitle: string;
|
||||
rightTermField: string;
|
||||
}) {
|
||||
const metricsDescriptor: AggDescriptor = { type: AGG_TYPE.COUNT };
|
||||
const joinId = uuid();
|
||||
const joinKey = getJoinAggKey({
|
||||
aggType: metricsDescriptor.type,
|
||||
aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '',
|
||||
rightSourceId: joinId,
|
||||
});
|
||||
return VectorLayer.createDescriptor({
|
||||
joins: [
|
||||
{
|
||||
leftField,
|
||||
right: {
|
||||
type: SOURCE_TYPES.ES_TERM_SOURCE,
|
||||
id: joinId,
|
||||
indexPatternId: rightIndexPatternId,
|
||||
indexPatternTitle: rightIndexPatternTitle,
|
||||
term: rightTermField,
|
||||
metrics: [metricsDescriptor],
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceDescriptor,
|
||||
style: VectorStyle.createDescriptor({
|
||||
[VECTOR_STYLES.FILL_COLOR]: {
|
||||
type: STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions),
|
||||
field: {
|
||||
name: joinKey,
|
||||
origin: FIELD_ORIGIN.JOIN,
|
||||
},
|
||||
color: 'Yellow to Red',
|
||||
type: COLOR_MAP_TYPE.ORDINAL,
|
||||
},
|
||||
},
|
||||
[VECTOR_STYLES.LINE_COLOR]: {
|
||||
type: STYLE_TYPE.STATIC,
|
||||
options: {
|
||||
color: '#3d3d3d',
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function createEmsChoroplethLayerDescriptor({
|
||||
leftEmsFileId,
|
||||
leftEmsField,
|
||||
rightIndexPatternId,
|
||||
rightIndexPatternTitle,
|
||||
rightTermField,
|
||||
}: {
|
||||
leftEmsFileId: string;
|
||||
leftEmsField: string;
|
||||
rightIndexPatternId: string;
|
||||
rightIndexPatternTitle: string;
|
||||
rightTermField: string;
|
||||
}) {
|
||||
return createChoroplethLayerDescriptor({
|
||||
sourceDescriptor: EMSFileSource.createDescriptor({
|
||||
id: leftEmsFileId,
|
||||
tooltipProperties: [leftEmsField],
|
||||
}),
|
||||
leftField: leftEmsField,
|
||||
rightIndexPatternId,
|
||||
rightIndexPatternTitle,
|
||||
rightTermField,
|
||||
});
|
||||
}
|
||||
|
||||
export function createEsChoroplethLayerDescriptor({
|
||||
leftIndexPatternId,
|
||||
leftGeoField,
|
||||
leftJoinField,
|
||||
rightIndexPatternId,
|
||||
rightIndexPatternTitle,
|
||||
rightTermField,
|
||||
}: {
|
||||
leftIndexPatternId: string;
|
||||
leftGeoField: string;
|
||||
leftJoinField: string;
|
||||
rightIndexPatternId: string;
|
||||
rightIndexPatternTitle: string;
|
||||
rightTermField: string;
|
||||
}) {
|
||||
return createChoroplethLayerDescriptor({
|
||||
sourceDescriptor: ESSearchSource.createDescriptor({
|
||||
indexPatternId: leftIndexPatternId,
|
||||
geoField: leftGeoField,
|
||||
scalingType: SCALING_TYPES.LIMIT,
|
||||
tooltipProperties: [leftJoinField],
|
||||
applyGlobalQuery: false,
|
||||
}),
|
||||
leftField: leftJoinField,
|
||||
rightIndexPatternId,
|
||||
rightIndexPatternTitle,
|
||||
rightTermField,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { choroplethLayerWizardConfig } from './choropleth_layer_wizard';
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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', () => {
|
||||
const MockIndexPatternSelect = (props: unknown) => {
|
||||
return <div />;
|
||||
};
|
||||
return {
|
||||
getIndexPatternSelectComponent: () => {
|
||||
return MockIndexPatternSelect;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { BOUNDARIES_SOURCE, LayerTemplate } from './layer_template';
|
||||
|
||||
const renderWizardArguments = {
|
||||
previewLayers: () => {},
|
||||
mapColors: [],
|
||||
currentStepId: null,
|
||||
enableNextBtn: () => {},
|
||||
disableNextBtn: () => {},
|
||||
startStepLoading: () => {},
|
||||
stopStepLoading: () => {},
|
||||
advanceToNextStep: () => {},
|
||||
};
|
||||
|
||||
test('should render elasticsearch UI when left source is BOUNDARIES_SOURCE.ELASTICSEARCH', async () => {
|
||||
const component = shallow(<LayerTemplate {...renderWizardArguments} />);
|
||||
component.setState({ leftSource: BOUNDARIES_SOURCE.ELASTICSEARCH });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render EMS UI when left source is BOUNDARIES_SOURCE.EMS', async () => {
|
||||
const component = shallow(<LayerTemplate {...renderWizardArguments} />);
|
||||
component.setState({ leftSource: BOUNDARIES_SOURCE.EMS });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -0,0 +1,459 @@
|
|||
/*
|
||||
* 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, { Component } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { FileLayer } from '@elastic/ems-client';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiRadioGroup,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { IFieldType, IndexPattern } from 'src/plugins/data/public';
|
||||
import { RenderWizardArguments } from '../layer_wizard_registry';
|
||||
import { EMSFileSelect } from '../../../components/ems_file_select';
|
||||
import { GeoIndexPatternSelect } from '../../../components/geo_index_pattern_select';
|
||||
import { SingleFieldSelect } from '../../../components/single_field_select';
|
||||
import { getGeoFields, getSourceFields, getTermsFields } from '../../../index_pattern_util';
|
||||
import { getEmsFileLayers } from '../../../meta';
|
||||
import { getIndexPatternSelectComponent, getIndexPatternService } from '../../../kibana_services';
|
||||
import {
|
||||
createEmsChoroplethLayerDescriptor,
|
||||
createEsChoroplethLayerDescriptor,
|
||||
} from './create_choropleth_layer_descriptor';
|
||||
|
||||
export enum BOUNDARIES_SOURCE {
|
||||
ELASTICSEARCH = 'ELASTICSEARCH',
|
||||
EMS = 'EMS',
|
||||
}
|
||||
|
||||
const BOUNDARIES_OPTIONS = [
|
||||
{
|
||||
id: BOUNDARIES_SOURCE.EMS,
|
||||
label: i18n.translate('xpack.maps.choropleth.boundaries.ems', {
|
||||
defaultMessage: 'Administrative boundaries from Elastic Maps Service',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: BOUNDARIES_SOURCE.ELASTICSEARCH,
|
||||
label: i18n.translate('xpack.maps.choropleth.boundaries.elasticsearch', {
|
||||
defaultMessage: 'Points, lines, and polygons from Elasticsearch',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
interface State {
|
||||
leftSource: BOUNDARIES_SOURCE;
|
||||
leftEmsFileId: string | null;
|
||||
leftEmsFields: Array<EuiComboBoxOptionOption<string>>;
|
||||
leftIndexPattern: IndexPattern | null;
|
||||
leftGeoFields: IFieldType[];
|
||||
leftJoinFields: IFieldType[];
|
||||
leftGeoField: string | null;
|
||||
leftEmsJoinField: string | null;
|
||||
leftElasticsearchJoinField: string | null;
|
||||
rightIndexPatternId: string | null;
|
||||
rightIndexPatternTitle: string | null;
|
||||
rightTermsFields: IFieldType[];
|
||||
rightJoinField: string | null;
|
||||
}
|
||||
|
||||
export class LayerTemplate extends Component<RenderWizardArguments, State> {
|
||||
private _isMounted: boolean = false;
|
||||
|
||||
state = {
|
||||
leftSource: BOUNDARIES_SOURCE.EMS,
|
||||
leftEmsFileId: null,
|
||||
leftEmsFields: [],
|
||||
leftIndexPattern: null,
|
||||
leftGeoFields: [],
|
||||
leftJoinFields: [],
|
||||
leftGeoField: null,
|
||||
leftEmsJoinField: null,
|
||||
leftElasticsearchJoinField: null,
|
||||
rightIndexPatternId: null,
|
||||
rightIndexPatternTitle: null,
|
||||
rightTermsFields: [],
|
||||
rightJoinField: null,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
_loadRightFields = async (indexPatternId: string) => {
|
||||
this.setState({ rightTermsFields: [], rightIndexPatternTitle: null });
|
||||
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await getIndexPatternService().get(indexPatternId);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
// method may be called again before 'get' returns
|
||||
// ignore response when fetched index pattern does not match active index pattern
|
||||
if (!this._isMounted || indexPatternId !== this.state.rightIndexPatternId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
rightTermsFields: getTermsFields(indexPattern.fields),
|
||||
rightIndexPatternTitle: indexPattern.title,
|
||||
});
|
||||
};
|
||||
|
||||
_loadEmsFileFields = async () => {
|
||||
const emsFileLayers = await getEmsFileLayers();
|
||||
const emsFileLayer = emsFileLayers.find((fileLayer: FileLayer) => {
|
||||
return fileLayer.getId() === this.state.leftEmsFileId;
|
||||
});
|
||||
|
||||
if (!this._isMounted || !emsFileLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftEmsFields = emsFileLayer
|
||||
.getFieldsInLanguage()
|
||||
.filter((field) => {
|
||||
return field.type === 'id';
|
||||
})
|
||||
.map((field) => {
|
||||
return {
|
||||
value: field.name,
|
||||
label: field.description,
|
||||
};
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
leftEmsFields,
|
||||
leftEmsJoinField: leftEmsFields.length ? leftEmsFields[0].value : null,
|
||||
},
|
||||
this._previewLayer
|
||||
);
|
||||
};
|
||||
|
||||
_onLeftSourceChange = (optionId: string) => {
|
||||
this.setState(
|
||||
{ leftSource: optionId as BOUNDARIES_SOURCE, rightJoinField: null },
|
||||
this._previewLayer
|
||||
);
|
||||
};
|
||||
|
||||
_onLeftIndexPatternChange = (indexPattern: IndexPattern) => {
|
||||
this.setState(
|
||||
{
|
||||
leftIndexPattern: indexPattern,
|
||||
leftGeoFields: getGeoFields(indexPattern.fields),
|
||||
leftJoinFields: getSourceFields(indexPattern.fields),
|
||||
leftGeoField: null,
|
||||
leftElasticsearchJoinField: null,
|
||||
rightJoinField: null,
|
||||
},
|
||||
() => {
|
||||
// make default geo field selection
|
||||
if (this.state.leftGeoFields.length) {
|
||||
// @ts-expect-error - avoid wrong "Property 'name' does not exist on type 'never'." compile error
|
||||
this._onLeftGeoFieldSelect(this.state.leftGeoFields[0].name);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
_onLeftGeoFieldSelect = (geoField?: string) => {
|
||||
if (!geoField) {
|
||||
return;
|
||||
}
|
||||
this.setState({ leftGeoField: geoField }, this._previewLayer);
|
||||
};
|
||||
|
||||
_onLeftJoinFieldSelect = (joinField?: string) => {
|
||||
if (!joinField) {
|
||||
return;
|
||||
}
|
||||
this.setState({ leftElasticsearchJoinField: joinField }, this._previewLayer);
|
||||
};
|
||||
|
||||
_onLeftEmsFileChange = (emFileId: string) => {
|
||||
this.setState({ leftEmsFileId: emFileId, leftEmsJoinField: null, rightJoinField: null }, () => {
|
||||
this._previewLayer();
|
||||
this._loadEmsFileFields();
|
||||
});
|
||||
};
|
||||
|
||||
_onLeftEmsFieldChange = (selectedOptions: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
if (selectedOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ leftEmsJoinField: selectedOptions[0].value! }, this._previewLayer);
|
||||
};
|
||||
|
||||
_onRightIndexPatternChange = (indexPatternId: string) => {
|
||||
if (!indexPatternId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
rightIndexPatternId: indexPatternId,
|
||||
rightJoinField: null,
|
||||
},
|
||||
() => {
|
||||
this._previewLayer();
|
||||
this._loadRightFields(indexPatternId);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
_onRightJoinFieldSelect = (joinField?: string) => {
|
||||
if (!joinField) {
|
||||
return;
|
||||
}
|
||||
this.setState({ rightJoinField: joinField }, this._previewLayer);
|
||||
};
|
||||
|
||||
_isLeftConfigComplete() {
|
||||
if (this.state.leftSource === BOUNDARIES_SOURCE.ELASTICSEARCH) {
|
||||
return (
|
||||
!!this.state.leftIndexPattern &&
|
||||
!!this.state.leftGeoField &&
|
||||
!!this.state.leftElasticsearchJoinField
|
||||
);
|
||||
} else {
|
||||
return !!this.state.leftEmsFileId && !!this.state.leftEmsJoinField;
|
||||
}
|
||||
}
|
||||
|
||||
_isRightConfigComplete() {
|
||||
return !!this.state.rightIndexPatternId && !!this.state.rightJoinField;
|
||||
}
|
||||
|
||||
_previewLayer() {
|
||||
if (!this._isLeftConfigComplete() || !this._isRightConfigComplete()) {
|
||||
this.props.previewLayers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const layerDescriptor =
|
||||
this.state.leftSource === BOUNDARIES_SOURCE.ELASTICSEARCH
|
||||
? createEsChoroplethLayerDescriptor({
|
||||
// @ts-expect-error - avoid wrong "Property 'id' does not exist on type 'never'." compile error
|
||||
leftIndexPatternId: this.state.leftIndexPattern!.id,
|
||||
leftGeoField: this.state.leftGeoField!,
|
||||
leftJoinField: this.state.leftElasticsearchJoinField!,
|
||||
rightIndexPatternId: this.state.rightIndexPatternId!,
|
||||
rightIndexPatternTitle: this.state.rightIndexPatternTitle!,
|
||||
rightTermField: this.state.rightJoinField!,
|
||||
})
|
||||
: createEmsChoroplethLayerDescriptor({
|
||||
leftEmsFileId: this.state.leftEmsFileId!,
|
||||
leftEmsField: this.state.leftEmsJoinField!,
|
||||
rightIndexPatternId: this.state.rightIndexPatternId!,
|
||||
rightIndexPatternTitle: this.state.rightIndexPatternTitle!,
|
||||
rightTermField: this.state.rightJoinField!,
|
||||
});
|
||||
|
||||
this.props.previewLayers([layerDescriptor]);
|
||||
}
|
||||
|
||||
_renderLeftSourceForm() {
|
||||
if (this.state.leftSource === BOUNDARIES_SOURCE.ELASTICSEARCH) {
|
||||
let geoFieldSelect;
|
||||
if (this.state.leftGeoFields.length) {
|
||||
geoFieldSelect = (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.choropleth.geofieldLabel', {
|
||||
defaultMessage: 'Geospatial field',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.choropleth.geofieldPlaceholder', {
|
||||
defaultMessage: 'Select geo field',
|
||||
})}
|
||||
value={this.state.leftGeoField}
|
||||
onChange={this._onLeftGeoFieldSelect}
|
||||
fields={this.state.leftGeoFields}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
let joinFieldSelect;
|
||||
if (this.state.leftJoinFields.length) {
|
||||
joinFieldSelect = (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.choropleth.joinFieldLabel', {
|
||||
defaultMessage: 'Join field',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.choropleth.joinFieldPlaceholder', {
|
||||
defaultMessage: 'Select field',
|
||||
})}
|
||||
value={this.state.leftElasticsearchJoinField}
|
||||
onChange={this._onLeftJoinFieldSelect}
|
||||
fields={this.state.leftJoinFields}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<GeoIndexPatternSelect
|
||||
// @ts-expect-error - avoid wrong "Property 'id' does not exist on type 'never'." compile error
|
||||
value={this.state.leftIndexPattern ? this.state.leftIndexPattern!.id : ''}
|
||||
onChange={this._onLeftIndexPatternChange}
|
||||
/>
|
||||
{geoFieldSelect}
|
||||
{joinFieldSelect}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
let emsFieldSelect;
|
||||
if (this.state.leftEmsFields.length) {
|
||||
let selectedOption;
|
||||
if (this.state.leftEmsJoinField) {
|
||||
selectedOption = this.state.leftEmsFields.find(
|
||||
(option: EuiComboBoxOptionOption<string>) => {
|
||||
return this.state.leftEmsJoinField === option.value;
|
||||
}
|
||||
);
|
||||
}
|
||||
emsFieldSelect = (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.choropleth.joinFieldLabel', {
|
||||
defaultMessage: 'Join field',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
singleSelection={true}
|
||||
isClearable={false}
|
||||
options={this.state.leftEmsFields}
|
||||
selectedOptions={selectedOption ? [selectedOption] : []}
|
||||
onChange={this._onLeftEmsFieldChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EMSFileSelect value={this.state.leftEmsFileId} onChange={this._onLeftEmsFileChange} />
|
||||
{emsFieldSelect}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_renderLeftPanel() {
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.choropleth.boundariesLabel"
|
||||
defaultMessage="Boundaries source"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow>
|
||||
<EuiRadioGroup
|
||||
options={BOUNDARIES_OPTIONS}
|
||||
idSelected={this.state.leftSource}
|
||||
onChange={this._onLeftSourceChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{this._renderLeftSourceForm()}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
_renderRightPanel() {
|
||||
if (!this._isLeftConfigComplete()) {
|
||||
return null;
|
||||
}
|
||||
const IndexPatternSelect = getIndexPatternSelectComponent();
|
||||
|
||||
let joinFieldSelect;
|
||||
if (this.state.rightTermsFields.length) {
|
||||
joinFieldSelect = (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.choropleth.joinFieldLabel', {
|
||||
defaultMessage: 'Join field',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.choropleth.joinFieldPlaceholder', {
|
||||
defaultMessage: 'Select field',
|
||||
})}
|
||||
value={this.state.rightJoinField}
|
||||
onChange={this._onRightJoinFieldSelect}
|
||||
fields={this.state.rightTermsFields}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.choropleth.statisticsLabel"
|
||||
defaultMessage="Statistics source"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.choropleth.rightSourceLabel', {
|
||||
defaultMessage: 'Index pattern',
|
||||
})}
|
||||
>
|
||||
<IndexPatternSelect
|
||||
placeholder={i18n.translate('xpack.maps.maps.choropleth.rightSourcePlaceholder', {
|
||||
defaultMessage: 'Select index pattern',
|
||||
})}
|
||||
indexPatternId={this.state.rightIndexPatternId}
|
||||
onChange={this._onRightIndexPatternChange}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{joinFieldSelect}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this._renderLeftPanel()}
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{this._renderRightPanel()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import { wmsLayerWizardConfig } from '../sources/wms_source';
|
|||
import { mvtVectorSourceWizardConfig } from '../sources/mvt_single_layer_vector_source';
|
||||
import { ObservabilityLayerWizardConfig } from './solution_layers/observability';
|
||||
import { SecurityLayerWizardConfig } from './solution_layers/security';
|
||||
import { choroplethLayerWizardConfig } from './choropleth_layer_wizard';
|
||||
import { getEnableVectorTiles } from '../../kibana_services';
|
||||
|
||||
let registered = false;
|
||||
|
@ -41,6 +42,7 @@ export function registerLayerWizards() {
|
|||
// @ts-ignore
|
||||
registerLayerWizard(esDocumentsLayerWizardConfig);
|
||||
// @ts-ignore
|
||||
registerLayerWizard(choroplethLayerWizardConfig);
|
||||
registerLayerWizard(clustersLayerWizardConfig);
|
||||
// @ts-ignore
|
||||
registerLayerWizard(heatmapLayerWizardConfig);
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { RenderWizardArguments } from '../../layer_wizard_registry';
|
||||
import { LayerSelect, OBSERVABILITY_LAYER_TYPE } from './layer_select';
|
||||
import { getMetricOptionsForLayer, MetricSelect, OBSERVABILITY_METRIC_TYPE } from './metric_select';
|
||||
|
@ -63,7 +64,7 @@ export class ObservabilityLayerTemplate extends Component<RenderWizardArguments,
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
<LayerSelect value={this.state.layer} onChange={this._onLayerChange} />
|
||||
<MetricSelect
|
||||
layer={this.state.layer}
|
||||
|
@ -75,7 +76,7 @@ export class ObservabilityLayerTemplate extends Component<RenderWizardArguments,
|
|||
value={this.state.display}
|
||||
onChange={this._onDisplayChange}
|
||||
/>
|
||||
</Fragment>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { RenderWizardArguments } from '../../layer_wizard_registry';
|
||||
import { IndexPatternSelect } from './index_pattern_select';
|
||||
import { createSecurityLayerDescriptors } from './create_layer_descriptors';
|
||||
|
@ -44,12 +45,12 @@ export class SecurityLayerTemplate extends Component<RenderWizardArguments, Stat
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
<IndexPatternSelect
|
||||
value={this.state.indexPatternId ? this.state.indexPatternId! : ''}
|
||||
onChange={this._onIndexPatternChange}
|
||||
/>
|
||||
</Fragment>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { IFieldType } from 'src/plugins/data/public';
|
||||
import {
|
||||
ES_GEO_FIELD_TYPE,
|
||||
|
@ -146,15 +147,17 @@ export class ClientFileCreateSourceEditor extends Component<RenderWizardArgument
|
|||
render() {
|
||||
const FileUpload = getFileUploadComponent();
|
||||
return (
|
||||
<FileUpload
|
||||
appName={'Maps'}
|
||||
isIndexingTriggered={this.state.indexingStage === INDEXING_STAGE.TRIGGERED}
|
||||
onFileUpload={this._onFileUpload}
|
||||
onFileRemove={this._onFileRemove}
|
||||
onIndexReady={this._onIndexReady}
|
||||
transformDetails={'geo'}
|
||||
onIndexingComplete={this._onIndexingComplete}
|
||||
/>
|
||||
<EuiPanel>
|
||||
<FileUpload
|
||||
appName={'Maps'}
|
||||
isIndexingTriggered={this.state.indexingStage === INDEXING_STAGE.TRIGGERED}
|
||||
onFileUpload={this._onFileUpload}
|
||||
onFileRemove={this._onFileRemove}
|
||||
onIndexReady={this._onIndexReady}
|
||||
transformDetails={'geo'}
|
||||
onIndexingComplete={this._onIndexingComplete}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,95 +5,33 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FileLayer } from '@elastic/ems-client';
|
||||
import { getEmsFileLayers } from '../../../meta';
|
||||
import { getEmsUnavailableMessage } from '../ems_unavailable_message';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types';
|
||||
import { EMSFileSelect } from '../../../components/ems_file_select';
|
||||
|
||||
interface Props {
|
||||
onSourceConfigChange: (sourceConfig: Partial<EMSFileSourceDescriptor>) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasLoadedOptions: boolean;
|
||||
emsFileOptions: Array<EuiComboBoxOptionOption<string>>;
|
||||
selectedOption: EuiComboBoxOptionOption<string> | null;
|
||||
emsFileId: string | null;
|
||||
}
|
||||
|
||||
export class EMSFileCreateSourceEditor extends Component<Props, State> {
|
||||
private _isMounted: boolean = false;
|
||||
|
||||
state = {
|
||||
hasLoadedOptions: false,
|
||||
emsFileOptions: [],
|
||||
selectedOption: null,
|
||||
emsFileId: null,
|
||||
};
|
||||
|
||||
_loadFileOptions = async () => {
|
||||
const fileLayers: FileLayer[] = await getEmsFileLayers();
|
||||
const options = fileLayers.map((fileLayer) => {
|
||||
return {
|
||||
value: fileLayer.getId(),
|
||||
label: fileLayer.getDisplayName(),
|
||||
};
|
||||
});
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
hasLoadedOptions: true,
|
||||
emsFileOptions: options,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._loadFileOptions();
|
||||
}
|
||||
|
||||
_onChange = (selectedOptions: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
if (selectedOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ selectedOption: selectedOptions[0] });
|
||||
|
||||
const emsFileId = selectedOptions[0].value;
|
||||
_onChange = (emsFileId: string) => {
|
||||
this.setState({ emsFileId });
|
||||
this.props.onSourceConfigChange({ id: emsFileId });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.hasLoadedOptions) {
|
||||
// TODO display loading message
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.emsFile.layerLabel', {
|
||||
defaultMessage: 'Layer',
|
||||
})}
|
||||
helpText={this.state.emsFileOptions.length === 0 ? getEmsUnavailableMessage() : null}
|
||||
>
|
||||
<EuiComboBox
|
||||
placeholder={i18n.translate('xpack.maps.source.emsFile.selectPlaceholder', {
|
||||
defaultMessage: 'Select EMS vector shapes',
|
||||
})}
|
||||
options={this.state.emsFileOptions}
|
||||
selectedOptions={this.state.selectedOption ? [this.state.selectedOption!] : []}
|
||||
onChange={this._onChange}
|
||||
isClearable={false}
|
||||
singleSelection={true}
|
||||
isDisabled={this.state.emsFileOptions.length === 0}
|
||||
data-test-subj="emsVectorComboBox"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiPanel>
|
||||
<EMSFileSelect value={this.state.emsFileId} onChange={this._onChange} />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSelect, EuiFormRow } from '@elastic/eui';
|
||||
import { EuiSelect, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||
|
||||
import { getEmsTmsServices } from '../../../meta';
|
||||
import { getEmsUnavailableMessage } from '../ems_unavailable_message';
|
||||
import { getEmsUnavailableMessage } from '../../../components/ems_unavailable_message';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AUTO_SELECT = 'auto_select';
|
||||
|
@ -71,23 +71,25 @@ export class TileServiceSelect extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.emsTile.label', {
|
||||
defaultMessage: 'Tile service',
|
||||
})}
|
||||
helpText={helpText}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiSelect
|
||||
hasNoInitialSelection={!selectedId}
|
||||
value={selectedId}
|
||||
options={this.state.emsTmsOptions}
|
||||
onChange={this._onChange}
|
||||
isLoading={!this.state.hasLoaded}
|
||||
disabled={this.state.hasLoaded && this.state.emsTmsOptions.length === 0}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.emsTile.label', {
|
||||
defaultMessage: 'Tile service',
|
||||
})}
|
||||
helpText={helpText}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiSelect
|
||||
hasNoInitialSelection={!selectedId}
|
||||
value={selectedId}
|
||||
options={this.state.emsTmsOptions}
|
||||
onChange={this._onChange}
|
||||
isLoading={!this.state.hasLoaded}
|
||||
disabled={this.state.hasLoaded && this.state.emsTmsOptions.length === 0}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { Fragment, Component } from 'react';
|
||||
import React, { 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 { GeoIndexPatternSelect } from '../../../components/geo_index_pattern_select';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||
import {
|
||||
getFieldsWithGeoTileAgg,
|
||||
getGeoFields,
|
||||
|
@ -33,76 +30,26 @@ export class CreateSourceEditor extends Component {
|
|||
};
|
||||
|
||||
state = {
|
||||
isLoadingIndexPattern: false,
|
||||
indexPatternId: '',
|
||||
indexPattern: null,
|
||||
geoField: '',
|
||||
requestType: this.props.requestType,
|
||||
noGeoIndexPatternsExist: false,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
onIndexPatternSelect = (indexPatternId) => {
|
||||
onIndexPatternSelect = (indexPattern) => {
|
||||
this.setState(
|
||||
{
|
||||
indexPatternId,
|
||||
indexPattern,
|
||||
},
|
||||
this.loadIndexPattern.bind(null, indexPatternId)
|
||||
() => {
|
||||
//make default selection
|
||||
const geoFieldsWithGeoTileAgg = getFieldsWithGeoTileAgg(indexPattern.fields);
|
||||
if (geoFieldsWithGeoTileAgg[0]) {
|
||||
this._onGeoFieldSelect(geoFieldsWithGeoTileAgg[0].name);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
loadIndexPattern = (indexPatternId) => {
|
||||
this.setState(
|
||||
{
|
||||
isLoadingIndexPattern: true,
|
||||
indexPattern: undefined,
|
||||
geoField: undefined,
|
||||
},
|
||||
this.debouncedLoad.bind(null, indexPatternId)
|
||||
);
|
||||
};
|
||||
|
||||
debouncedLoad = _.debounce(async (indexPatternId) => {
|
||||
if (!indexPatternId || indexPatternId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await getIndexPatternService().get(indexPatternId);
|
||||
} catch (err) {
|
||||
// index pattern no longer exists
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// props.indexPatternId may be updated before getIndexPattern returns
|
||||
// ignore response when fetched index pattern does not match active index pattern
|
||||
if (indexPattern.id !== indexPatternId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isLoadingIndexPattern: false,
|
||||
indexPattern: indexPattern,
|
||||
});
|
||||
|
||||
//make default selection
|
||||
const geoFieldsWithGeoTileAgg = getFieldsWithGeoTileAgg(indexPattern.fields);
|
||||
if (geoFieldsWithGeoTileAgg[0]) {
|
||||
this._onGeoFieldSelect(geoFieldsWithGeoTileAgg[0].name);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
_onGeoFieldSelect = (geoField) => {
|
||||
this.setState(
|
||||
{
|
||||
|
@ -122,17 +69,13 @@ export class CreateSourceEditor extends Component {
|
|||
};
|
||||
|
||||
previewLayer = () => {
|
||||
const { indexPatternId, geoField, requestType } = this.state;
|
||||
const { indexPattern, geoField, requestType } = this.state;
|
||||
|
||||
const sourceConfig =
|
||||
indexPatternId && geoField ? { indexPatternId, geoField, requestType } : null;
|
||||
indexPattern && geoField ? { indexPatternId: indexPattern.id, geoField, requestType } : null;
|
||||
this.props.onSourceConfigChange(sourceConfig);
|
||||
};
|
||||
|
||||
_onNoIndexPatterns = () => {
|
||||
this.setState({ noGeoIndexPatternsExist: true });
|
||||
};
|
||||
|
||||
_renderGeoSelect() {
|
||||
if (!this.state.indexPattern) {
|
||||
return null;
|
||||
|
@ -170,50 +113,16 @@ export class CreateSourceEditor extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderIndexPatternSelect() {
|
||||
const IndexPatternSelect = getIndexPatternSelectComponent();
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.esGeoGrid.indexPatternLabel', {
|
||||
defaultMessage: 'Index pattern',
|
||||
})}
|
||||
>
|
||||
<IndexPatternSelect
|
||||
isDisabled={this.state.noGeoIndexPatternsExist}
|
||||
indexPatternId={this.state.indexPatternId}
|
||||
onChange={this.onIndexPatternSelect}
|
||||
placeholder={i18n.translate('xpack.maps.source.esGeoGrid.indexPatternPlaceholder', {
|
||||
defaultMessage: 'Select index pattern',
|
||||
})}
|
||||
fieldTypes={ES_GEO_FIELD_TYPES}
|
||||
onNoIndexPatterns={this._onNoIndexPatterns}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
_renderNoIndexPatternWarning() {
|
||||
if (!this.state.noGeoIndexPatternsExist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<NoIndexPatternCallout />
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
{this._renderNoIndexPatternWarning()}
|
||||
{this._renderIndexPatternSelect()}
|
||||
<EuiPanel>
|
||||
<GeoIndexPatternSelect
|
||||
value={this.state.indexPattern ? this.state.indexPattern.id : ''}
|
||||
onChange={this.onIndexPatternSelect}
|
||||
/>
|
||||
{this._renderGeoSelect()}
|
||||
{this._renderRenderAsSelect()}
|
||||
</Fragment>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getIndexPatternService, getIndexPatternSelectComponent } from '../../..
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { EuiFormRow, EuiCallOut } from '@elastic/eui';
|
||||
import { EuiFormRow, EuiCallOut, EuiPanel } from '@elastic/eui';
|
||||
import { getFieldsWithGeoTileAgg } from '../../../index_pattern_util';
|
||||
import { ES_GEO_FIELD_TYPE } from '../../../../common/constants';
|
||||
|
||||
|
@ -200,11 +200,11 @@ export class CreateSourceEditor extends Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
{callout}
|
||||
{this._renderIndexPatternSelect()}
|
||||
{this._renderGeoSelects()}
|
||||
</Fragment>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -210,8 +210,10 @@ exports[`should render top hits form when scaling type is TOP_HITS 1`] = `
|
|||
<SingleFieldSelect
|
||||
compressed={true}
|
||||
fields={Array []}
|
||||
isClearable={false}
|
||||
onChange={[Function]}
|
||||
placeholder="Select entity field"
|
||||
value={null}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
|
|
|
@ -4,16 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { Fragment, Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { SingleFieldSelect } from '../../../components/single_field_select';
|
||||
import { getIndexPatternService, getIndexPatternSelectComponent } from '../../../kibana_services';
|
||||
import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout';
|
||||
import { GeoIndexPatternSelect } from '../../../components/geo_index_pattern_select';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ES_GEO_FIELD_TYPES, SCALING_TYPES } from '../../../../common/constants';
|
||||
import { SCALING_TYPES } from '../../../../common/constants';
|
||||
import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
|
||||
import { ScalingForm } from './scaling_form';
|
||||
import {
|
||||
|
@ -45,79 +43,32 @@ export class CreateSourceEditor extends Component {
|
|||
};
|
||||
|
||||
state = {
|
||||
isLoadingIndexPattern: false,
|
||||
noGeoIndexPatternsExist: false,
|
||||
...RESET_INDEX_PATTERN_STATE,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
_onIndexPatternSelect = (indexPatternId) => {
|
||||
this.setState(
|
||||
{
|
||||
indexPatternId,
|
||||
},
|
||||
this._loadIndexPattern(indexPatternId)
|
||||
);
|
||||
};
|
||||
|
||||
_loadIndexPattern = (indexPatternId) => {
|
||||
this.setState(
|
||||
{
|
||||
isLoadingIndexPattern: true,
|
||||
...RESET_INDEX_PATTERN_STATE,
|
||||
},
|
||||
this._debouncedLoad.bind(null, indexPatternId)
|
||||
);
|
||||
};
|
||||
|
||||
_debouncedLoad = _.debounce(async (indexPatternId) => {
|
||||
if (!indexPatternId || indexPatternId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await getIndexPatternService().get(indexPatternId);
|
||||
} catch (err) {
|
||||
// index pattern no longer exists
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// props.indexPatternId may be updated before getIndexPattern returns
|
||||
// ignore response when fetched index pattern does not match active index pattern
|
||||
if (indexPattern.id !== indexPatternId) {
|
||||
return;
|
||||
}
|
||||
|
||||
_onIndexPatternSelect = (indexPattern) => {
|
||||
const geoFields = getGeoFields(indexPattern.fields);
|
||||
this.setState({
|
||||
isLoadingIndexPattern: false,
|
||||
indexPattern: indexPattern,
|
||||
geoFields,
|
||||
});
|
||||
|
||||
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);
|
||||
this.setState(
|
||||
{
|
||||
...RESET_INDEX_PATTERN_STATE,
|
||||
indexPattern,
|
||||
geoFields,
|
||||
},
|
||||
() => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
_onGeoFieldSelect = (geoFieldName) => {
|
||||
// Respect previous scaling type selection unless newly selected geo field does not support clustering.
|
||||
|
@ -146,7 +97,7 @@ export class CreateSourceEditor extends Component {
|
|||
|
||||
_previewLayer = () => {
|
||||
const {
|
||||
indexPatternId,
|
||||
indexPattern,
|
||||
geoFieldName,
|
||||
filterByMapBounds,
|
||||
scalingType,
|
||||
|
@ -155,9 +106,9 @@ export class CreateSourceEditor extends Component {
|
|||
} = this.state;
|
||||
|
||||
const sourceConfig =
|
||||
indexPatternId && geoFieldName
|
||||
indexPattern && geoFieldName
|
||||
? {
|
||||
indexPatternId,
|
||||
indexPatternId: indexPattern.id,
|
||||
geoField: geoFieldName,
|
||||
filterByMapBounds,
|
||||
scalingType,
|
||||
|
@ -168,10 +119,6 @@ export class CreateSourceEditor extends Component {
|
|||
this.props.onSourceConfigChange(sourceConfig);
|
||||
};
|
||||
|
||||
_onNoIndexPatterns = () => {
|
||||
this.setState({ noGeoIndexPatternsExist: true });
|
||||
};
|
||||
|
||||
_renderGeoSelect() {
|
||||
if (!this.state.indexPattern) {
|
||||
return;
|
||||
|
@ -205,7 +152,7 @@ export class CreateSourceEditor extends Component {
|
|||
<EuiSpacer size="m" />
|
||||
<ScalingForm
|
||||
filterByMapBounds={this.state.filterByMapBounds}
|
||||
indexPatternId={this.state.indexPatternId}
|
||||
indexPatternId={this.state.indexPattern ? this.state.indexPattern.id : ''}
|
||||
onChange={this._onScalingPropChange}
|
||||
scalingType={this.state.scalingType}
|
||||
supportsClustering={doesGeoFieldSupportGeoTileAgg(
|
||||
|
@ -227,50 +174,18 @@ export class CreateSourceEditor extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderNoIndexPatternWarning() {
|
||||
if (!this.state.noGeoIndexPatternsExist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<NoIndexPatternCallout />
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const IndexPatternSelect = getIndexPatternSelectComponent();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{this._renderNoIndexPatternWarning()}
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.esSearch.indexPatternLabel', {
|
||||
defaultMessage: 'Index pattern',
|
||||
})}
|
||||
>
|
||||
<IndexPatternSelect
|
||||
isDisabled={this.state.noGeoIndexPatternsExist}
|
||||
indexPatternId={this.state.indexPatternId}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.maps.source.esSearch.selectIndexPatternPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select index pattern',
|
||||
}
|
||||
)}
|
||||
fieldTypes={ES_GEO_FIELD_TYPES}
|
||||
onNoIndexPatterns={this._onNoIndexPatterns}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiPanel>
|
||||
<GeoIndexPatternSelect
|
||||
value={this.state.indexPattern ? this.state.indexPattern.id : ''}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
/>
|
||||
|
||||
{this._renderGeoSelect()}
|
||||
|
||||
{this._renderScalingPanel()}
|
||||
</Fragment>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: s
|
|||
export const esDocumentsLayerWizardConfig: LayerWizard = {
|
||||
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
|
||||
description: i18n.translate('xpack.maps.source.esSearchDescription', {
|
||||
defaultMessage: 'Vector data from a Kibana index pattern',
|
||||
defaultMessage: 'Points, lines, and polygons from Elasticsearch',
|
||||
}),
|
||||
icon: 'logoElasticsearch',
|
||||
renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
|
||||
|
|
|
@ -25,6 +25,7 @@ const defaultProps = {
|
|||
scalingType: SCALING_TYPES.LIMIT,
|
||||
supportsClustering: true,
|
||||
termFields: [],
|
||||
topHitsSplitField: null,
|
||||
topHitsSize: 1,
|
||||
};
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ interface Props {
|
|||
supportsClustering: boolean;
|
||||
clusteringDisabledReason?: string | null;
|
||||
termFields: IFieldType[];
|
||||
topHitsSplitField?: string;
|
||||
topHitsSplitField: string | null;
|
||||
topHitsSize: number;
|
||||
}
|
||||
|
||||
|
@ -90,6 +90,9 @@ export class ScalingForm extends Component<Props, State> {
|
|||
};
|
||||
|
||||
_onTopHitsSplitFieldChange = (topHitsSplitField?: string) => {
|
||||
if (!topHitsSplitField) {
|
||||
return;
|
||||
}
|
||||
this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField });
|
||||
};
|
||||
|
||||
|
@ -141,6 +144,7 @@ export class ScalingForm extends Component<Props, State> {
|
|||
value={this.props.topHitsSplitField}
|
||||
onChange={this._onTopHitsSplitFieldChange}
|
||||
fields={this.props.termFields}
|
||||
isClearable={false}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiSelect, EuiFormRow } from '@elastic/eui';
|
||||
import { EuiSelect, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||
import { getKibanaRegionList } from '../../../meta';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
@ -31,19 +31,21 @@ export function CreateSourceEditor({ onSourceConfigChange }) {
|
|||
: null;
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.kbnRegionMap.vectorLayerLabel', {
|
||||
defaultMessage: 'Vector layer',
|
||||
})}
|
||||
helpText={helpText}
|
||||
>
|
||||
<EuiSelect
|
||||
hasNoInitialSelection
|
||||
options={regionmapOptions}
|
||||
onChange={onChange}
|
||||
disabled={regionmapOptions.length === 0}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.kbnRegionMap.vectorLayerLabel', {
|
||||
defaultMessage: 'Vector layer',
|
||||
})}
|
||||
helpText={helpText}
|
||||
>
|
||||
<EuiSelect
|
||||
hasNoInitialSelection
|
||||
options={regionmapOptions}
|
||||
onChange={onChange}
|
||||
disabled={regionmapOptions.length === 0}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
|
||||
import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||
|
||||
import { getKibanaTileMap } from '../../../meta';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -19,21 +19,23 @@ export function CreateSourceEditor({ onSourceConfigChange }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.kbnTMS.kbnTMS.urlLabel', {
|
||||
defaultMessage: 'Tilemap url',
|
||||
})}
|
||||
helpText={
|
||||
tilemap.url
|
||||
? null
|
||||
: i18n.translate('xpack.maps.source.kbnTMS.noLayerAvailableHelptext', {
|
||||
defaultMessage:
|
||||
'No tilemap layer is available. Ask your system administrator to set "map.tilemap.url" in kibana.yml.',
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiFieldText readOnly value={tilemap.url} />
|
||||
</EuiFormRow>
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.kbnTMS.kbnTMS.urlLabel', {
|
||||
defaultMessage: 'Tilemap url',
|
||||
})}
|
||||
helpText={
|
||||
tilemap.url
|
||||
? null
|
||||
: i18n.translate('xpack.maps.source.kbnTMS.noLayerAvailableHelptext', {
|
||||
defaultMessage:
|
||||
'No tilemap layer is available. Ask your system administrator to set "map.tilemap.url" in kibana.yml.',
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiFieldText readOnly value={tilemap.url} />
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
*/
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
import React, { Fragment, Component, ChangeEvent } from 'react';
|
||||
import React, { Component, ChangeEvent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
|
||||
import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants';
|
||||
import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
@ -85,7 +85,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage', {
|
||||
defaultMessage: 'Url',
|
||||
|
@ -122,7 +122,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
|
|||
}
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
EuiComboBox,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiForm,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { WmsClient } from './wms_client';
|
||||
|
@ -289,7 +289,7 @@ export class WMSCreateSourceEditor extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.wms.urlLabel', {
|
||||
defaultMessage: 'Url',
|
||||
|
@ -303,7 +303,7 @@ export class WMSCreateSourceEditor extends Component {
|
|||
{this._renderLayerAndStyleInputs()}
|
||||
|
||||
{this._renderAttributionInputs()}
|
||||
</EuiForm>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
*/
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
import React, { Fragment, Component, ChangeEvent } from 'react';
|
||||
import React, { Component, ChangeEvent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
|
||||
import { EuiFormRow, EuiFieldText, EuiPanel } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AttributionDescriptor } from '../../../../common/descriptor_types';
|
||||
|
||||
|
@ -77,7 +77,7 @@ export class XYZTMSEditor extends Component<Props, State> {
|
|||
render() {
|
||||
const { attributionText, attributionUrl } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
<EuiFormRow label="Url">
|
||||
<EuiFieldText
|
||||
placeholder={'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'}
|
||||
|
@ -116,7 +116,7 @@ export class XYZTMSEditor extends Component<Props, State> {
|
|||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
104
x-pack/plugins/maps/public/components/__snapshots__/geo_index_pattern_select.test.tsx.snap
generated
Normal file
104
x-pack/plugins/maps/public/components/__snapshots__/geo_index_pattern_select.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,104 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should render 1`] = `
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Index pattern"
|
||||
labelType="label"
|
||||
>
|
||||
<MockIndexPatternSelect
|
||||
fieldTypes={
|
||||
Array [
|
||||
"geo_point",
|
||||
"geo_shape",
|
||||
]
|
||||
}
|
||||
indexPatternId="indexPatternId"
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
onChange={[Function]}
|
||||
onNoIndexPatterns={[Function]}
|
||||
placeholder="Select index pattern"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`should render no index pattern warning when there are no matching index patterns 1`] = `
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title="Couldn't find any index patterns with geospatial fields"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="You'll need to "
|
||||
id="xpack.maps.noIndexPattern.doThisPrefixDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
<EuiLink
|
||||
href="abc//app/management/kibana/indexPatterns"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="create an index pattern"
|
||||
id="xpack.maps.noIndexPattern.doThisLinkTextDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>
|
||||
<FormattedMessage
|
||||
defaultMessage=" with geospatial fields."
|
||||
id="xpack.maps.noIndexPattern.doThisSuffixDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Don't have any geospatial data sets? "
|
||||
id="xpack.maps.noIndexPattern.hintDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
<EuiLink
|
||||
href="abc//app/home#/tutorial_directory/sampleData"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Get started with some sample data sets."
|
||||
id="xpack.maps.noIndexPattern.getStartedLinkText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Index pattern"
|
||||
labelType="label"
|
||||
>
|
||||
<MockIndexPatternSelect
|
||||
fieldTypes={
|
||||
Array [
|
||||
"geo_point",
|
||||
"geo_shape",
|
||||
]
|
||||
}
|
||||
indexPatternId="indexPatternId"
|
||||
isClearable={false}
|
||||
isDisabled={true}
|
||||
onChange={[Function]}
|
||||
onNoIndexPatterns={[Function]}
|
||||
placeholder="Select index pattern"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
`;
|
105
x-pack/plugins/maps/public/components/ems_file_select.tsx
Normal file
105
x-pack/plugins/maps/public/components/ems_file_select.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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, { Component } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FileLayer } from '@elastic/ems-client';
|
||||
import { getEmsFileLayers } from '../meta';
|
||||
import { getEmsUnavailableMessage } from './ems_unavailable_message';
|
||||
|
||||
interface Props {
|
||||
onChange: (emsFileId: string) => void;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasLoadedOptions: boolean;
|
||||
emsFileOptions: Array<EuiComboBoxOptionOption<string>>;
|
||||
}
|
||||
|
||||
export class EMSFileSelect extends Component<Props, State> {
|
||||
private _isMounted: boolean = false;
|
||||
|
||||
state = {
|
||||
hasLoadedOptions: false,
|
||||
emsFileOptions: [],
|
||||
};
|
||||
|
||||
_loadFileOptions = async () => {
|
||||
const fileLayers: FileLayer[] = await getEmsFileLayers();
|
||||
const options = fileLayers.map((fileLayer) => {
|
||||
return {
|
||||
value: fileLayer.getId(),
|
||||
label: fileLayer.getDisplayName(),
|
||||
};
|
||||
});
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
hasLoadedOptions: true,
|
||||
emsFileOptions: options,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._loadFileOptions();
|
||||
}
|
||||
|
||||
_onChange = (selectedOptions: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
if (selectedOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange(selectedOptions[0].value!);
|
||||
};
|
||||
|
||||
_renderSelect() {
|
||||
if (!this.state.hasLoadedOptions) {
|
||||
return <EuiSelect isLoading />;
|
||||
}
|
||||
|
||||
const selectedOption = this.state.emsFileOptions.find(
|
||||
(option: EuiComboBoxOptionOption<string>) => {
|
||||
return option.value === this.props.value;
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiComboBox
|
||||
placeholder={i18n.translate('xpack.maps.emsFileSelect.selectPlaceholder', {
|
||||
defaultMessage: 'Select EMS layer',
|
||||
})}
|
||||
options={this.state.emsFileOptions}
|
||||
selectedOptions={selectedOption ? [selectedOption!] : []}
|
||||
onChange={this._onChange}
|
||||
isClearable={false}
|
||||
singleSelection={true}
|
||||
isDisabled={this.state.emsFileOptions.length === 0}
|
||||
data-test-subj="emsVectorComboBox"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.emsFileSelect.selectLabel', {
|
||||
defaultMessage: 'Layer',
|
||||
})}
|
||||
helpText={this.state.emsFileOptions.length === 0 ? getEmsUnavailableMessage() : null}
|
||||
>
|
||||
{this._renderSelect()}
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-ignore
|
||||
import { getIsEmsEnabled } from '../../kibana_services';
|
||||
import { getIsEmsEnabled } from '../kibana_services';
|
||||
|
||||
export function getEmsUnavailableMessage(): string {
|
||||
const isEmsEnabled = getIsEmsEnabled();
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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', () => {
|
||||
const MockIndexPatternSelect = (props: unknown) => {
|
||||
return <div />;
|
||||
};
|
||||
const MockHttp = {
|
||||
basePath: {
|
||||
prepend: (path: string) => {
|
||||
return `abc/${path}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
getIndexPatternSelectComponent: () => {
|
||||
return MockIndexPatternSelect;
|
||||
},
|
||||
getHttp: () => {
|
||||
return MockHttp;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { GeoIndexPatternSelect } from './geo_index_pattern_select';
|
||||
|
||||
test('should render', async () => {
|
||||
const component = shallow(<GeoIndexPatternSelect onChange={() => {}} value={'indexPatternId'} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render no index pattern warning when there are no matching index patterns', async () => {
|
||||
const component = shallow(<GeoIndexPatternSelect onChange={() => {}} value={'indexPatternId'} />);
|
||||
component.setState({ noGeoIndexPatternsExist: true });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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, { Component } from 'react';
|
||||
import { EuiCallOut, EuiFormRow, EuiLink, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { IndexPattern } from 'src/plugins/data/public';
|
||||
import {
|
||||
getIndexPatternSelectComponent,
|
||||
getIndexPatternService,
|
||||
getHttp,
|
||||
} from '../kibana_services';
|
||||
import { ES_GEO_FIELD_TYPES } from '../../common/constants';
|
||||
|
||||
interface Props {
|
||||
onChange: (indexPattern: IndexPattern) => void;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
interface State {
|
||||
noGeoIndexPatternsExist: boolean;
|
||||
}
|
||||
|
||||
export class GeoIndexPatternSelect extends Component<Props, State> {
|
||||
private _isMounted: boolean = false;
|
||||
|
||||
state = {
|
||||
noGeoIndexPatternsExist: false,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
_onIndexPatternSelect = async (indexPatternId: string) => {
|
||||
if (!indexPatternId || indexPatternId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await getIndexPatternService().get(indexPatternId);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
// method may be called again before 'get' returns
|
||||
// ignore response when fetched index pattern does not match active index pattern
|
||||
if (this._isMounted && indexPattern.id === indexPatternId) {
|
||||
this.props.onChange(indexPattern);
|
||||
}
|
||||
};
|
||||
|
||||
_onNoIndexPatterns = () => {
|
||||
this.setState({ noGeoIndexPatternsExist: true });
|
||||
};
|
||||
|
||||
_renderNoIndexPatternWarning() {
|
||||
if (!this.state.noGeoIndexPatternsExist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.maps.noIndexPattern.messageTitle', {
|
||||
defaultMessage: `Couldn't find any index patterns with geospatial fields`,
|
||||
})}
|
||||
color="warning"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.doThisPrefixDescription"
|
||||
defaultMessage="You'll need to "
|
||||
/>
|
||||
<EuiLink href={getHttp().basePath.prepend(`/app/management/kibana/indexPatterns`)}>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.doThisLinkTextDescription"
|
||||
defaultMessage="create an index pattern"
|
||||
/>
|
||||
</EuiLink>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.doThisSuffixDescription"
|
||||
defaultMessage=" with geospatial fields."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.hintDescription"
|
||||
defaultMessage="Don't have any geospatial data sets? "
|
||||
/>
|
||||
<EuiLink href={getHttp().basePath.prepend('/app/home#/tutorial_directory/sampleData')}>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.getStartedLinkText"
|
||||
defaultMessage="Get started with some sample data sets."
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const IndexPatternSelect = getIndexPatternSelectComponent();
|
||||
return (
|
||||
<>
|
||||
{this._renderNoIndexPatternWarning()}
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.indexPatternSelectLabel', {
|
||||
defaultMessage: 'Index pattern',
|
||||
})}
|
||||
>
|
||||
<IndexPatternSelect
|
||||
isDisabled={this.state.noGeoIndexPatternsExist}
|
||||
indexPatternId={this.props.value}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
placeholder={i18n.translate('xpack.maps.indexPatternSelectPlaceholder', {
|
||||
defaultMessage: 'Select index pattern',
|
||||
})}
|
||||
fieldTypes={ES_GEO_FIELD_TYPES}
|
||||
onNoIndexPatterns={this._onNoIndexPatterns}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,52 +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 { getHttp } from '../kibana_services';
|
||||
import React from 'react';
|
||||
import { EuiCallOut, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
export function NoIndexPatternCallout() {
|
||||
const http = getHttp();
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.maps.noIndexPattern.messageTitle', {
|
||||
defaultMessage: `Couldn't find any index patterns with geospatial fields`,
|
||||
})}
|
||||
color="warning"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.doThisPrefixDescription"
|
||||
defaultMessage="You'll need to "
|
||||
/>
|
||||
<EuiLink href={http.basePath.prepend(`/app/management/kibana/indexPatterns`)}>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.doThisLinkTextDescription"
|
||||
defaultMessage="create an index pattern"
|
||||
/>
|
||||
</EuiLink>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.doThisSuffixDescription"
|
||||
defaultMessage=" with geospatial fields."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.hintDescription"
|
||||
defaultMessage="Don't have any geospatial data sets? "
|
||||
/>
|
||||
<EuiLink href={http.basePath.prepend('/app/home#/tutorial_directory/sampleData')}>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.noIndexPattern.getStartedLinkText"
|
||||
defaultMessage="Get started with some sample data sets."
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
|
@ -49,7 +49,7 @@ type Props = Omit<
|
|||
> & {
|
||||
fields?: IFieldType[];
|
||||
onChange: (fieldName?: string) => void;
|
||||
value?: string; // index pattern field name
|
||||
value: string | null; // index pattern field name
|
||||
isFieldDisabled?: (field: IFieldType) => boolean;
|
||||
getFieldDisabledReason?: (field: IFieldType) => string | null;
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiButtonEmpty, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { LayerWizardSelect } from './layer_wizard_select';
|
||||
import { LayerWizard, RenderWizardArguments } from '../../../classes/layers/layer_wizard_registry';
|
||||
|
@ -50,7 +50,7 @@ export const FlyoutBody = (props: Props) => {
|
|||
return (
|
||||
<Fragment>
|
||||
{backButton}
|
||||
<EuiPanel>{props.layerWizard.renderWizard(renderWizardArgs)}</EuiPanel>
|
||||
{props.layerWizard.renderWizard(renderWizardArgs)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9141,7 +9141,6 @@
|
|||
"xpack.maps.source.ems.disabledDescription": "Elastic Maps Service へのアクセスが無効になっています。システム管理者に問い合わせるか、kibana.yml で「map.includeElasticMapsService」を設定してください。",
|
||||
"xpack.maps.source.ems.noAccessDescription": "Kibana が Elastic Maps Service にアクセスできません。システム管理者にお問い合わせください",
|
||||
"xpack.maps.source.emsFile.layerLabel": "レイヤー",
|
||||
"xpack.maps.source.emsFile.selectPlaceholder": "EMS ベクターシェイプを選択",
|
||||
"xpack.maps.source.emsFile.unableToFindIdErrorMessage": "ID {id} の EMS ベクターシェイプが見つかりません",
|
||||
"xpack.maps.source.emsFileDescription": "Elastic Maps Service の行政区画のベクターシェイプ",
|
||||
"xpack.maps.source.emsFileTitle": "ベクターシェイプ",
|
||||
|
@ -9157,8 +9156,6 @@
|
|||
"xpack.maps.source.esGeoGrid.geofieldLabel": "地理空間フィールド",
|
||||
"xpack.maps.source.esGeoGrid.geofieldPlaceholder": "ジオフィールドを選択",
|
||||
"xpack.maps.source.esGeoGrid.gridRectangleDropdownOption": "グリッド",
|
||||
"xpack.maps.source.esGeoGrid.indexPatternLabel": "インデックスパターン",
|
||||
"xpack.maps.source.esGeoGrid.indexPatternPlaceholder": "インデックスパターンを選択",
|
||||
"xpack.maps.source.esGeoGrid.pointsDropdownOption": "クラスター",
|
||||
"xpack.maps.source.esGeoGrid.showAsLabel": "表示形式",
|
||||
"xpack.maps.source.esGrid.coarseDropdownOption": "粗い",
|
||||
|
@ -9192,7 +9189,6 @@
|
|||
"xpack.maps.source.esSearch.limitScalingLabel": "結果を {maxResultWindow} に限定。",
|
||||
"xpack.maps.source.esSearch.loadErrorMessage": "インデックスパターン {id} が見つかりません",
|
||||
"xpack.maps.source.esSearch.loadTooltipPropertiesErrorMsg": "ドキュメントが見つかりません。_id: {docId}",
|
||||
"xpack.maps.source.esSearch.selectIndexPatternPlaceholder": "インデックスパターンを選択",
|
||||
"xpack.maps.source.esSearch.selectLabel": "ジオフィールドを選択",
|
||||
"xpack.maps.source.esSearch.sortFieldLabel": "フィールド",
|
||||
"xpack.maps.source.esSearch.sortFieldSelectPlaceholder": "ソートフィールドを選択",
|
||||
|
|
|
@ -9146,7 +9146,6 @@
|
|||
"xpack.maps.source.ems.disabledDescription": "已禁用对 Elastic 地图服务的访问。让您的系统管理员在 kibana.yml 中设置“map.includeElasticMapsService”。",
|
||||
"xpack.maps.source.ems.noAccessDescription": "Kibana 无法访问 Elastic 地图服务。请联系您的系统管理员",
|
||||
"xpack.maps.source.emsFile.layerLabel": "图层",
|
||||
"xpack.maps.source.emsFile.selectPlaceholder": "选择 EMS 矢量形状",
|
||||
"xpack.maps.source.emsFile.unableToFindIdErrorMessage": "找不到 ID {id} 的 EMS 矢量形状",
|
||||
"xpack.maps.source.emsFileDescription": "来自 Elastic 地图服务的管理边界的矢量形状",
|
||||
"xpack.maps.source.emsFileTitle": "矢量形状",
|
||||
|
@ -9162,8 +9161,6 @@
|
|||
"xpack.maps.source.esGeoGrid.geofieldLabel": "地理空间字段",
|
||||
"xpack.maps.source.esGeoGrid.geofieldPlaceholder": "选择地理字段",
|
||||
"xpack.maps.source.esGeoGrid.gridRectangleDropdownOption": "网格",
|
||||
"xpack.maps.source.esGeoGrid.indexPatternLabel": "索引模式",
|
||||
"xpack.maps.source.esGeoGrid.indexPatternPlaceholder": "选择索引模式",
|
||||
"xpack.maps.source.esGeoGrid.pointsDropdownOption": "集群",
|
||||
"xpack.maps.source.esGeoGrid.showAsLabel": "显示为",
|
||||
"xpack.maps.source.esGrid.coarseDropdownOption": "粗糙",
|
||||
|
@ -9197,7 +9194,6 @@
|
|||
"xpack.maps.source.esSearch.limitScalingLabel": "将结果数限制到 {maxResultWindow}。",
|
||||
"xpack.maps.source.esSearch.loadErrorMessage": "找不到索引模式 {id}",
|
||||
"xpack.maps.source.esSearch.loadTooltipPropertiesErrorMsg": "找不到文档,_id:{docId}",
|
||||
"xpack.maps.source.esSearch.selectIndexPatternPlaceholder": "选择索引模式",
|
||||
"xpack.maps.source.esSearch.selectLabel": "选择地理字段",
|
||||
"xpack.maps.source.esSearch.sortFieldLabel": "字段",
|
||||
"xpack.maps.source.esSearch.sortFieldSelectPlaceholder": "选择排序字段",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue