[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:
Nathan Reese 2020-06-29 15:12:09 -06:00 committed by GitHub
parent 7db95a1691
commit 917598141f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1343 additions and 446 deletions

View file

@ -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>
`;

View file

@ -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',
}),
};

View file

@ -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,
});
}

View file

@ -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';

View file

@ -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();
});

View file

@ -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()}
</>
);
}
}

View file

@ -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);

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>

View file

@ -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>
);
}
}

View file

@ -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) => {

View file

@ -25,6 +25,7 @@ const defaultProps = {
scalingType: SCALING_TYPES.LIMIT,
supportsClustering: true,
termFields: [],
topHitsSplitField: null,
topHitsSize: 1,
};

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View 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>
`;

View 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>
);
}
}

View file

@ -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();

View file

@ -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();
});

View file

@ -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>
</>
);
}
}

View file

@ -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>
);
}

View file

@ -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;
};

View file

@ -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>
);
}

View file

@ -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": "ソートフィールドを選択",

View file

@ -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": "选择排序字段",