[Maps] Introduce fields (#50044) (#51379)

This introduces the `AbstractField` class and its implementations. Their use replace the ad-hoc object literals that were used to pass around field-level metadata in joins, metrics, styles, and tooltips.
This commit is contained in:
Thomas Neirynck 2019-11-22 16:23:57 -05:00 committed by GitHub
parent 4c14ca66ae
commit 820d61d977
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1124 additions and 723 deletions

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const EMS_CATALOGUE_PATH = 'ems/catalogue';
@ -114,3 +115,15 @@ export const METRIC_TYPE = {
SUM: 'sum',
UNIQUE_COUNT: 'cardinality',
};
export const COUNT_AGG_TYPE = METRIC_TYPE.COUNT;
export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', {
defaultMessage: 'count'
});
export const COUNT_PROP_NAME = 'doc_count';
export const STYLE_TYPE = {
'STATIC': 'STATIC',
'DYNAMIC': 'DYNAMIC'
};

View file

@ -732,9 +732,7 @@ export function clearMissingStyleProperties(layerId) {
return;
}
const dateFields = await targetLayer.getDateFields();
const numberFields = await targetLayer.getNumberFields();
const ordinalFields = [...dateFields, ...numberFields];
const ordinalFields = await targetLayer.getOrdinalFields();
const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved(ordinalFields);
if (hasChanges) {
dispatch(updateLayerStyle(layerId, nextStyleDescriptor));

View file

@ -41,6 +41,7 @@ exports[`TooltipSelector should render component 1`] = `
"type": "string",
},
Object {
"label": "foobar_label",
"name": "iso3",
"type": "string",
},
@ -50,7 +51,9 @@ exports[`TooltipSelector should render component 1`] = `
selectedFields={
Array [
Object {
"label": "foobar_label",
"name": "iso2",
"type": "foobar_type",
},
]
}

View file

@ -30,35 +30,109 @@ const reorder = (list, startIndex, endIndex) => {
return result;
};
const getProps = async field => {
return new Promise(async (resolve, reject) => {
try {
const label = await field.getLabel();
const type = await field.getDataType();
resolve({
label: label,
type: type,
name: field.getName()
});
} catch(e) {
reject(e);
}
});
};
export class TooltipSelector extends Component {
_getPropertyLabel = (propertyName) => {
if (!this.props.fields) {
return propertyName;
state = {
fieldProps: [],
selectedFieldProps: []
};
constructor() {
super();
this._isMounted = false;
this._previousFields = null;
this._previousSelectedTooltips = null;
}
componentDidMount() {
this._isMounted = true;
this._loadFieldProps();
this._loadTooltipFieldProps();
}
componentWillUnmount() {
this._isMounted = false;
}
componentDidUpdate() {
this._loadTooltipFieldProps();
this._loadFieldProps();
}
async _loadTooltipFieldProps() {
if (!this.props.tooltipFields || this.props.tooltipFields === this._previousSelectedTooltips) {
return;
}
const field = this.props.fields.find(field => {
this._previousSelectedTooltips = this.props.tooltipFields;
const selectedProps = this.props.tooltipFields.map(getProps);
const selectedFieldProps = await Promise.all(selectedProps);
if (this._isMounted) {
this.setState({ selectedFieldProps });
}
}
async _loadFieldProps() {
if (!this.props.fields || this.props.fields === this._previousFields) {
return;
}
this._previousFields = this.props.fields;
const props = this.props.fields.map(getProps);
const fieldProps = await Promise.all(props);
if (this._isMounted) {
this.setState({ fieldProps });
}
}
_getPropertyLabel = (propertyName) => {
if (!this.state.fieldProps.length) {
return propertyName;
}
const prop = this.state.fieldProps.find((field) => {
return field.name === propertyName;
});
return prop.label ? prop.label : propertyName;
}
return field && field.label
? field.label
: propertyName;
_getTooltipProperties() {
return this.props.tooltipFields.map(field => field.getName());
}
_onAdd = (properties) => {
if (!this.props.tooltipProperties) {
if (!this.props.tooltipFields) {
this.props.onChange([...properties]);
} else {
this.props.onChange([...this.props.tooltipProperties, ...properties]);
const existingProperties = this._getTooltipProperties();
this.props.onChange([...existingProperties, ...properties]);
}
}
_removeProperty = (index) => {
if (!this.props.tooltipProperties) {
if (!this.props.tooltipFields) {
this.props.onChange([]);
} else {
const tooltipProperties = [...this.props.tooltipProperties];
const tooltipProperties = this._getTooltipProperties();
tooltipProperties.splice(index, 1);
this.props.onChange(tooltipProperties);
}
@ -70,11 +144,11 @@ export class TooltipSelector extends Component {
return;
}
this.props.onChange(reorder(this.props.tooltipProperties, source.index, destination.index));
this.props.onChange(reorder(this._getTooltipProperties(), source.index, destination.index));
};
_renderProperties() {
if (!this.props.tooltipProperties) {
if (!this.state.selectedFieldProps.length) {
return null;
}
@ -82,12 +156,12 @@ export class TooltipSelector extends Component {
<EuiDragDropContext onDragEnd={this._onDragEnd}>
<EuiDroppable droppableId="mapLayerTOC" spacing="none">
{(provided, snapshot) => (
this.props.tooltipProperties.map((propertyName, idx) => (
this.state.selectedFieldProps.map((field, idx) => (
<EuiDraggable
spacing="none"
key={propertyName}
key={field.name}
index={idx}
draggableId={propertyName}
draggableId={field.name}
customDragHandle={true}
disableInteractiveElementBlocking // Allows button to be drag handle
>
@ -99,7 +173,7 @@ export class TooltipSelector extends Component {
})}
>
<EuiText className="mapTooltipSelector__propertyContent" size="s">
{this._getPropertyLabel(propertyName)}
{this._getPropertyLabel(field.name)}
</EuiText>
<div className="mapTooltipSelector__propertyIcons">
<EuiButtonIcon
@ -137,13 +211,6 @@ export class TooltipSelector extends Component {
}
render() {
const selectedFields = this.props.tooltipProperties
? this.props.tooltipProperties.map(propertyName => {
return { name: propertyName };
})
: [];
return (
<div>
<EuiTitle size="xxs">
@ -160,11 +227,12 @@ export class TooltipSelector extends Component {
<EuiTextAlign textAlign="center">
<AddTooltipFieldPopover
onAdd={this._onAdd}
fields={this.props.fields}
selectedFields={selectedFields}
fields={this.state.fieldProps}
selectedFields={this.state.selectedFieldProps}
/>
</EuiTextAlign>
</div>
);
}
}

View file

@ -9,33 +9,59 @@ import { shallow } from 'enzyme';
import { TooltipSelector } from './tooltip_selector';
class MockField {
constructor({ name, label, type }) {
this._name = name;
this._label = label;
this._type = type;
}
getName() {
return this._name;
}
async getLabel() {
return this._label || 'foobar_label';
}
async getDataType() {
return this._type || 'foobar_type';
}
}
const defaultProps = {
tooltipProperties: ['iso2'],
tooltipFields: [new MockField({ name: 'iso2' })],
onChange: (()=>{}),
fields: [
{
new MockField({
name: 'iso2',
label: 'ISO 3166-1 alpha-2 code',
type: 'string'
},
{
}),
new MockField({
name: 'iso3',
type: 'string'
},
})
]
};
describe('TooltipSelector', () => {
test('should render component', async () => {
const component = shallow(
<TooltipSelector
{...defaultProps}
/>
);
expect(component)
.toMatchSnapshot();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
});

View file

@ -111,7 +111,14 @@ export class Join extends Component {
async _loadLeftFields() {
let leftFields;
try {
leftFields = await this.props.layer.getLeftJoinFields();
const leftFieldsInstances = await this.props.layer.getLeftJoinFields();
const leftFieldPromises = leftFieldsInstances.map(async (field) => {
return {
name: field.getName(),
label: await field.getLabel()
};
});
leftFields = await Promise.all(leftFieldPromises);
} catch (error) {
leftFields = [];
}

View file

@ -155,6 +155,7 @@ export class FeatureProperties extends React.Component {
}
const rows = this.state.properties.map(tooltipProperty => {
const label = tooltipProperty.getPropertyName();
return (
<tr key={label}>

View file

@ -19,10 +19,10 @@ exports[`TOCEntry is rendered 1`] = `
Object {
"getDisplayName": [Function],
"getId": [Function],
"getLegendDetails": [Function],
"hasErrors": [Function],
"hasLegendDetails": [Function],
"isVisible": [Function],
"renderLegendDetails": [Function],
"showAtZoomLevel": [Function],
}
}
@ -87,10 +87,10 @@ exports[`TOCEntry props isReadOnly 1`] = `
Object {
"getDisplayName": [Function],
"getId": [Function],
"getLegendDetails": [Function],
"hasErrors": [Function],
"hasLegendDetails": [Function],
"isVisible": [Function],
"renderLegendDetails": [Function],
"showAtZoomLevel": [Function],
}
}
@ -137,10 +137,10 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is
Object {
"getDisplayName": [Function],
"getId": [Function],
"getLegendDetails": [Function],
"hasErrors": [Function],
"hasLegendDetails": [Function],
"isVisible": [Function],
"renderLegendDetails": [Function],
"showAtZoomLevel": [Function],
}
}

View file

@ -227,7 +227,7 @@ export class TOCEntry extends React.Component {
return null;
}
const tocDetails = this.props.layer.getLegendDetails();
const tocDetails = this.props.layer.renderLegendDetails();
if (!tocDetails) {
return null;
}

View file

@ -13,7 +13,7 @@ const LAYER_ID = '1';
const mockLayer = {
getId: () => { return LAYER_ID; },
getLegendDetails: () => { return (<div>TOC details mock</div>); },
renderLegendDetails: () => { return (<div>TOC details mock</div>); },
getDisplayName: () => { return 'layer 1'; },
isVisible: () => { return true; },
showAtZoomLevel: () => { return true; },

View file

@ -0,0 +1,26 @@
/*
* 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 { AbstractField } from './field';
import { TooltipProperty } from '../tooltips/tooltip_property';
export class EMSFileField extends AbstractField {
static type = 'EMS_FILE';
async getLabel() {
const emsFileLayer = await this._source.getEMSFileLayer();
const emsFields = emsFileLayer.getFieldsInLanguage();
// Map EMS field name to language specific label
const emsField = emsFields.find(field => field.name === this.getName());
return emsField ? emsField.description : this.getName();
}
async createTooltipProperty(value) {
const label = await this.getLabel();
return new TooltipProperty(this.getName(), label, value);
}
}

View file

@ -0,0 +1,72 @@
/*
* 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 { AbstractField } from './field';
import { COUNT_AGG_TYPE } from '../../../common/constants';
import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property';
export class ESAggMetricField extends AbstractField {
static type = 'ES_AGG';
constructor({ label, source, aggType, esDocField, origin }) {
super({ source, origin });
this._label = label;
this._aggType = aggType;
this._esDocField = esDocField;
}
getName() {
return this._source.formatMetricKey(this.getAggType(), this.getESDocFieldName());
}
async getLabel() {
return this._label ? await this._label : this._source.formatMetricLabel(this.getAggType(), this.getESDocFieldName());
}
getAggType() {
return this._aggType;
}
isValid() {
return (this.getAggType() === COUNT_AGG_TYPE) ? true : !!this._esDocField;
}
getESDocFieldName() {
return this._esDocField ? this._esDocField.getName() : '';
}
getRequestDescription() {
return this.getAggType() !== COUNT_AGG_TYPE ? `${this.getAggType()} ${this.getESDocFieldName()}` : COUNT_AGG_TYPE;
}
async createTooltipProperty(value) {
const indexPattern = await this._source.getIndexPattern();
return new ESAggMetricTooltipProperty(
this.getName(),
await this.getLabel(),
value,
indexPattern,
this
);
}
makeMetricAggConfig() {
const metricAggConfig = {
id: this.getName(),
enabled: true,
type: this.getAggType(),
schema: 'metric',
params: {}
};
if (this.getAggType() !== COUNT_AGG_TYPE) {
metricAggConfig.params = { field: this.getESDocFieldName() };
}
return metricAggConfig;
}
}

View file

@ -0,0 +1,30 @@
/*
* 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 { AbstractField } from './field';
import { ESTooltipProperty } from '../tooltips/es_tooltip_property';
export class ESDocField extends AbstractField {
static type = 'ES_DOC';
async _getField() {
const indexPattern = await this._source.getIndexPattern();
return indexPattern.fields.getByName(this._fieldName);
}
async createTooltipProperty(value) {
const indexPattern = await this._source.getIndexPattern();
return new ESTooltipProperty(this.getName(), this.getName(), value, indexPattern);
}
async getDataType() {
const field = await this._getField();
return field.type;
}
}

View file

@ -0,0 +1,45 @@
/*
* 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 { FIELD_ORIGIN } from '../../../common/constants';
export class AbstractField {
constructor({ fieldName, source, origin }) {
this._fieldName = fieldName;
this._source = source;
this._origin = origin || FIELD_ORIGIN.SOURCE;
}
getName() {
return this._fieldName;
}
getSource() {
return this._source;
}
isValid() {
return !!this._fieldName;
}
async getDataType() {
return 'string';
}
async getLabel() {
return this._fieldName;
}
async createTooltipProperty() {
throw new Error('must implement Field#createTooltipProperty');
}
getOrigin() {
return this._origin;
}
}

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 { AbstractField } from './field';
import { TooltipProperty } from '../tooltips/tooltip_property';
export class KibanaRegionField extends AbstractField {
static type = 'KIBANA_REGION';
async getLabel() {
const meta = await this._source.getVectorFileMeta();
const field = meta.fields.find(f => f.name === this._fieldName);
return field ? field.description : this._fieldName;
}
async createTooltipProperty(value) {
const label = await this.getLabel();
return new TooltipProperty(this.getName(), label, value);
}
}

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import { AbstractLayer } from './layer';
import { VectorLayer } from './vector_layer';
import { HeatmapStyle } from './styles/heatmap/heatmap_style';
@ -23,17 +22,19 @@ export class HeatmapLayer extends VectorLayer {
return heatmapLayerDescriptor;
}
constructor({ layerDescriptor, source, style }) {
super({ layerDescriptor, source, style });
if (!style) {
constructor({ layerDescriptor, source }) {
super({ layerDescriptor, source });
if (!layerDescriptor.style) {
const defaultStyle = HeatmapStyle.createDescriptor();
this._style = new HeatmapStyle(defaultStyle);
} else {
this._style = new HeatmapStyle(layerDescriptor.style);
}
}
_getPropKeyOfSelectedMetric() {
const metricfields = this._source.getMetricFields();
return metricfields[0].propertyKey;
return metricfields[0].getName();
}
_getHeatmapLayerId() {
@ -101,8 +102,8 @@ export class HeatmapLayer extends VectorLayer {
return true;
}
getLegendDetails() {
const label = _.get(this._source.getMetricFields(), '[0].propertyLabel', '');
return this._style.getLegendDetails(label);
renderLegendDetails() {
const metricFields = this._source.getMetricFields();
return this._style.renderLegendDetails(metricFields[0]);
}
}

View file

@ -6,13 +6,15 @@
import { ESTermSource } from '../sources/es_term_source';
import { VectorStyle } from '../styles/vector/vector_style';
import { getComputedFieldNamePrefix } from '../styles/vector/style_util';
export class InnerJoin {
constructor(joinDescriptor, inspectorAdapters) {
constructor(joinDescriptor, leftSource) {
this._descriptor = joinDescriptor;
const inspectorAdapters = leftSource.getInspectorAdapters();
this._rightSource = new ESTermSource(joinDescriptor.right, inspectorAdapters);
this._leftField = this._descriptor.leftField ? leftSource.createField({ fieldName: joinDescriptor.leftField }) : null;
}
destroy() {
@ -20,21 +22,15 @@ export class InnerJoin {
}
hasCompleteConfig() {
if (this._descriptor.leftField && this._rightSource) {
if (this._leftField && this._rightSource) {
return this._rightSource.hasCompleteConfig();
}
return false;
}
getRightMetricFields() {
return this._rightSource.getMetricFields();
}
getJoinFields() {
return this.getRightMetricFields().map(({ propertyKey: name, propertyLabel: label }) => {
return { label, name };
});
return this._rightSource.getMetricFields();
}
// Source request id must be static and unique because the re-fetch logic uses the id to locate the previous request.
@ -44,18 +40,19 @@ export class InnerJoin {
return `join_source_${this._rightSource.getId()}`;
}
getLeftFieldName() {
return this._descriptor.leftField;
getLeftField() {
return this._leftField;
}
joinPropertiesToFeature(feature, propertiesMap, rightMetricFields) {
joinPropertiesToFeature(feature, propertiesMap) {
const rightMetricFields = this._rightSource.getMetricFields();
// delete feature properties added by previous join
for (let j = 0; j < rightMetricFields.length; j++) {
const { propertyKey: metricPropertyKey } = rightMetricFields[j];
const metricPropertyKey = rightMetricFields[j].getName();
delete feature.properties[metricPropertyKey];
// delete all dynamic properties for metric field
const stylePropertyPrefix = VectorStyle.getComputedFieldNamePrefix(metricPropertyKey);
const stylePropertyPrefix = getComputedFieldNamePrefix(metricPropertyKey);
Object.keys(feature.properties).forEach(featurePropertyKey => {
if (featurePropertyKey.length >= stylePropertyPrefix.length &&
featurePropertyKey.substring(0, stylePropertyPrefix.length) === stylePropertyPrefix) {
@ -64,7 +61,7 @@ export class InnerJoin {
});
}
const joinKey = feature.properties[this._descriptor.leftField];
const joinKey = feature.properties[this._leftField.getName()];
const coercedKey = typeof joinKey === 'undefined' || joinKey === null ? null : joinKey.toString();
if (propertiesMap && coercedKey !== null && propertiesMap.has(coercedKey)) {
Object.assign(feature.properties, propertiesMap.get(coercedKey));

View file

@ -23,12 +23,25 @@ const rightSource = {
indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247',
indexPatternTitle: 'kibana_sample_data_logs',
term: 'geo.dest',
metrics: [{ type: 'count' }]
};
const mockSource = {
getInspectorAdapters() {
},
createField({ fieldName: name }) {
return {
getName() {
return name;
}
};
}
};
const leftJoin = new InnerJoin({
leftField: 'iso2',
right: rightSource
});
}, mockSource);
const COUNT_PROPERTY_NAME = '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.dest';
describe('joinPropertiesToFeature', () => {
@ -76,7 +89,7 @@ describe('joinPropertiesToFeature', () => {
const leftJoin = new InnerJoin({
leftField: 'zipcode',
right: rightSource
});
}, mockSource);
const feature = {
properties: {
@ -118,7 +131,7 @@ describe('joinPropertiesToFeature', () => {
const leftJoin = new InnerJoin({
leftField: 'code',
right: rightSource
});
}, mockSource);
const feature = {
properties: {

View file

@ -24,10 +24,9 @@ const NO_SOURCE_UPDATE_REQUIRED = false;
export class AbstractLayer {
constructor({ layerDescriptor, source, style }) {
constructor({ layerDescriptor, source }) {
this._descriptor = AbstractLayer.createDescriptor(layerDescriptor);
this._source = source;
this._style = style;
if (this._descriptor.__dataRequests) {
this._dataRequests = this._descriptor.__dataRequests.map(dataRequest => new DataRequest(dataRequest));
} else {
@ -196,7 +195,7 @@ export class AbstractLayer {
return false;
}
getLegendDetails() {
renderLegendDetails() {
return null;
}
@ -395,6 +394,10 @@ export class AbstractLayer {
return [];
}
async getOrdinalFields() {
return [];
}
syncVisibilityWithMb(mbMap, mbLayerId) {
mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
}

View file

@ -7,13 +7,13 @@
import { AbstractVectorSource } from '../vector_source';
import { VECTOR_SHAPE_TYPES } from '../vector_feature_types';
import React from 'react';
import { EMS_FILE, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
import { EMS_FILE, FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants';
import { getEMSClient } from '../../../meta';
import { EMSFileCreateSourceEditor } from './create_source_editor';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { UpdateSourceEditor } from './update_source_editor';
import { TooltipProperty } from '../../tooltips/tooltip_property';
import { EMSFileField } from '../../fields/ems_file_field';
export class EMSFileSource extends AbstractVectorSource {
@ -45,19 +45,29 @@ export class EMSFileSource extends AbstractVectorSource {
constructor(descriptor, inspectorAdapters) {
super(EMSFileSource.createDescriptor(descriptor), inspectorAdapters);
this._tooltipFields = this._descriptor.tooltipProperties.map(propertyKey => this.createField({ fieldName: propertyKey }));
}
createField({ fieldName }) {
return new EMSFileField({
fieldName,
source: this,
origin: FIELD_ORIGIN.SOURCE
});
}
renderSourceSettingsEditor({ onChange }) {
return (
<UpdateSourceEditor
onChange={onChange}
tooltipProperties={this._descriptor.tooltipProperties}
tooltipFields={this._tooltipFields}
layerId={this._descriptor.id}
source={this}
/>
);
}
async _getEMSFileLayer() {
async getEMSFileLayer() {
const emsClient = getEMSClient();
const emsFileLayers = await emsClient.getFileLayers();
const emsFileLayer = emsFileLayers.find((fileLayer => fileLayer.getId() === this._descriptor.id));
@ -73,7 +83,7 @@ export class EMSFileSource extends AbstractVectorSource {
}
async getGeoJsonWithMeta() {
const emsFileLayer = await this._getEMSFileLayer();
const emsFileLayer = await this.getEMSFileLayer();
const featureCollection = await AbstractVectorSource.getGeoJson({
format: emsFileLayer.getDefaultFormatType(),
featureCollectionPath: 'data',
@ -98,7 +108,7 @@ export class EMSFileSource extends AbstractVectorSource {
async getImmutableProperties() {
let emsLink;
try {
const emsFileLayer = await this._getEMSFileLayer();
const emsFileLayer = await this.getEMSFileLayer();
emsLink = emsFileLayer.getEMSHotLink();
} catch(error) {
// ignore error if EMS layer id could not be found
@ -121,7 +131,7 @@ export class EMSFileSource extends AbstractVectorSource {
async getDisplayName() {
try {
const emsFileLayer = await this._getEMSFileLayer();
const emsFileLayer = await this.getEMSFileLayer();
return emsFileLayer.getDisplayName();
} catch (error) {
return this._descriptor.id;
@ -129,36 +139,28 @@ export class EMSFileSource extends AbstractVectorSource {
}
async getAttributions() {
const emsFileLayer = await this._getEMSFileLayer();
const emsFileLayer = await this.getEMSFileLayer();
return emsFileLayer.getAttributions();
}
async getLeftJoinFields() {
const emsFileLayer = await this._getEMSFileLayer();
const emsFileLayer = await this.getEMSFileLayer();
const fields = emsFileLayer.getFieldsInLanguage();
return fields.map(f => {
return { name: f.name, label: f.description };
});
return fields.map(f => this.createField({ fieldName: f.name }));
}
canFormatFeatureProperties() {
return this._descriptor.tooltipProperties.length;
return this._tooltipFields.length > 0;
}
async filterAndFormatPropertiesToHtml(properties) {
const emsFileLayer = await this._getEMSFileLayer();
const emsFields = emsFileLayer.getFieldsInLanguage();
return this._descriptor.tooltipProperties.map(propertyName => {
// Map EMS field name to language specific label
const emsField = emsFields.find(field => {
return field.name === propertyName;
});
const label = emsField ? emsField.description : propertyName;
return new TooltipProperty(propertyName, label, properties[propertyName]);
const tooltipProperties = this._tooltipFields.map(field => {
const value = properties[field.getName()];
return field.createTooltipProperty(value);
});
return Promise.all(tooltipProperties);
}
async getSupportedShapeTypes() {

View file

@ -13,7 +13,7 @@ function makeEMSFileSource(tooltipProperties) {
const emsFileSource = new EMSFileSource({
tooltipProperties: tooltipProperties
});
emsFileSource._getEMSFileLayer = () => {
emsFileSource.getEMSFileLayer = () => {
return {
getFieldsInLanguage() {
return [{

View file

@ -13,7 +13,8 @@ export class UpdateSourceEditor extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired
tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired,
source: PropTypes.object
};
state = {
@ -36,16 +37,12 @@ export class UpdateSourceEditor extends Component {
const emsFiles = await emsClient.getFileLayers();
const emsFile = emsFiles.find((emsFile => emsFile.getId() === this.props.layerId));
const emsFields = emsFile.getFieldsInLanguage();
fields = emsFields.map(field => {
return {
name: field.name,
label: field.description
};
});
fields = emsFields.map(field => this.props.source.createField({ fieldName: field.name }));
} catch(e) {
//swallow this error. when a matching EMS-config cannot be found, the source already will have thrown errors during the data request. This will propagate to the vector-layer and be displayed in the UX
fields = [];
}
if (this._isMounted) {
this.setState({ fields: fields });
}
@ -56,9 +53,10 @@ export class UpdateSourceEditor extends Component {
};
render() {
return (
<TooltipSelector
tooltipProperties={this.props.tooltipProperties}
tooltipFields={this.props.tooltipFields}
onChange={this._onTooltipPropertiesSelect}
fields={this.state.fields}
/>

View file

@ -5,12 +5,10 @@
*/
import { AbstractESSource } from './es_source';
import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property';
import { METRIC_TYPE } from '../../../common/constants';
import _ from 'lodash';
import { ESAggMetricField } from '../fields/es_agg_field';
import { ESDocField } from '../fields/es_doc_field';
import { METRIC_TYPE, COUNT_AGG_TYPE, COUNT_PROP_LABEL, COUNT_PROP_NAME, FIELD_ORIGIN } from '../../../common/constants';
const COUNT_PROP_LABEL = 'count';
const COUNT_PROP_NAME = 'doc_count';
const AGG_DELIMITER = '_of_';
export class AbstractESAggSource extends AbstractESSource {
@ -34,109 +32,106 @@ export class AbstractESAggSource extends AbstractESSource {
]
};
_formatMetricKey(metric) {
const aggType = metric.type;
const fieldName = metric.field;
return aggType !== METRIC_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME;
constructor(descriptor, inspectorAdapters) {
super(descriptor, inspectorAdapters);
this._metricFields = this._descriptor.metrics ? this._descriptor.metrics.map(metric => {
const esDocField = metric.field ? new ESDocField({ fieldName: metric.field, source: this }) : null;
return new ESAggMetricField({
label: metric.label,
esDocField: esDocField,
aggType: metric.type,
source: this,
origin: this.getOriginForField()
});
}) : [];
}
_formatMetricLabel(metric) {
const aggType = metric.type;
const fieldName = metric.field;
return aggType !== METRIC_TYPE.COUNT ? `${aggType} of ${fieldName}` : COUNT_PROP_LABEL;
}
createField({ fieldName, label }) {
_getValidMetrics() {
const metrics = _.get(this._descriptor, 'metrics', []).filter(({ type, field }) => {
if (type === METRIC_TYPE.COUNT) {
return true;
//if there is a corresponding field with a custom label, use that one.
if (!label) {
const matchField = this._metricFields.find(field => field.getName() === fieldName);
if (matchField) {
label = matchField.getLabel();
}
}
if (field) {
return true;
}
return false;
if (fieldName === COUNT_PROP_NAME) {
return new ESAggMetricField({
aggType: COUNT_AGG_TYPE,
label: label,
source: this,
origin: this.getOriginForField()
});
}
//this only works because aggType is a fixed set and does not include the `_of_` string
const [aggType, docField] = fieldName.split(AGG_DELIMITER);
const esDocField = new ESDocField({ fieldName: docField, source: this });
return new ESAggMetricField({
label: label,
esDocField,
aggType,
source: this,
origin: this.getOriginForField()
});
}
getMetricFieldForName(fieldName) {
return this.getMetricFields().find(metricField => {
return metricField.getName() === fieldName;
});
}
getOriginForField() {
return FIELD_ORIGIN.SOURCE;
}
getMetricFields() {
const metrics = this._metricFields.filter(esAggField => esAggField.isValid());
if (metrics.length === 0) {
metrics.push({ type: METRIC_TYPE.COUNT });
metrics.push(new ESAggMetricField({
aggType: COUNT_AGG_TYPE,
source: this,
origin: this.getOriginForField()
}));
}
return metrics;
}
getMetricFields() {
return this._getValidMetrics().map(metric => {
const metricKey = this._formatMetricKey(metric);
const metricLabel = metric.label ? metric.label : this._formatMetricLabel(metric);
const metricCopy = { ...metric };
delete metricCopy.label;
return {
...metricCopy,
propertyKey: metricKey,
propertyLabel: metricLabel
};
});
formatMetricKey(aggType, fieldName) {
return aggType !== COUNT_AGG_TYPE ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME;
}
async getNumberFields() {
return this.getMetricFields().map(({ propertyKey: name, propertyLabel: label }) => {
return { label, name };
});
}
getFieldNames() {
return this.getMetricFields().map(({ propertyKey }) => {
return propertyKey;
});
formatMetricLabel(aggType, fieldName) {
return aggType !== COUNT_AGG_TYPE ? `${aggType} of ${fieldName}` : COUNT_PROP_LABEL;
}
createMetricAggConfigs() {
return this.getMetricFields().map(metric => {
const metricAggConfig = {
id: metric.propertyKey,
enabled: true,
type: metric.type,
schema: 'metric',
params: {}
};
if (metric.type !== METRIC_TYPE.COUNT) {
metricAggConfig.params = { field: metric.field };
}
return metricAggConfig;
});
return this.getMetricFields().map(esAggMetric => esAggMetric.makeMetricAggConfig());
}
async getNumberFields() {
return this.getMetricFields();
}
async filterAndFormatPropertiesToHtmlForMetricFields(properties) {
let indexPattern;
try {
indexPattern = await this._getIndexPattern();
} catch(error) {
console.warn(`Unable to find Index pattern ${this._descriptor.indexPatternId}, values are not formatted`);
return properties;
}
const metricFields = this.getMetricFields();
const tooltipProperties = [];
const tooltipPropertiesPromises = [];
metricFields.forEach((metricField) => {
let value;
for (const key in properties) {
if (properties.hasOwnProperty(key) && metricField.propertyKey === key) {
if (properties.hasOwnProperty(key) && metricField.getName() === key) {
value = properties[key];
break;
}
}
const tooltipProperty = new ESAggMetricTooltipProperty(
metricField.propertyKey,
metricField.propertyLabel,
value,
indexPattern,
metricField
);
tooltipProperties.push(tooltipProperty);
const tooltipPromise = metricField.createTooltipProperty(value);
tooltipPropertiesPromises.push(tooltipPromise);
});
return tooltipProperties;
return await Promise.all(tooltipPropertiesPromises);
}
}

View file

@ -20,13 +20,12 @@ import { RENDER_AS } from './render_as';
import { CreateSourceEditor } from './create_source_editor';
import { UpdateSourceEditor } from './update_source_editor';
import { GRID_RESOLUTION } from '../../grid_resolution';
import { SOURCE_DATA_ID_ORIGIN, ES_GEO_GRID } from '../../../../common/constants';
import { SOURCE_DATA_ID_ORIGIN, ES_GEO_GRID, COUNT_PROP_LABEL, COUNT_PROP_NAME } from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { AbstractESAggSource } from '../es_agg_source';
import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
const COUNT_PROP_LABEL = 'count';
const COUNT_PROP_NAME = 'doc_count';
const MAX_GEOTILE_LEVEL = 29;
const aggSchemas = new Schemas([
@ -93,7 +92,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
async getImmutableProperties() {
let indexPatternTitle = this._descriptor.indexPatternId;
try {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
indexPatternTitle = indexPattern.title;
} catch (error) {
// ignore error, title will just default to id
@ -124,6 +123,10 @@ export class ESGeoGridSource extends AbstractESAggSource {
];
}
getFieldNames() {
return this.getMetricFields().map((esAggMetricField => esAggMetricField.getName()));
}
isGeoGridPrecisionAware() {
return true;
}
@ -163,7 +166,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
}
async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const searchSource = await this._makeSearchSource(searchFilters, 0);
const aggConfigs = new AggConfigs(indexPattern, this._makeAggConfigs(searchFilters.geogridPrecision), aggSchemas.all);
searchSource.setField('aggs', aggConfigs.toDsl());
@ -225,7 +228,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
});
descriptor.style = VectorStyle.createDescriptor({
[vectorStyles.FILL_COLOR]: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
type: DynamicStyleProperty.type,
options: {
field: {
label: COUNT_PROP_LABEL,
@ -236,7 +239,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
}
},
[vectorStyles.ICON_SIZE]: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
type: DynamicStyleProperty.type,
options: {
field: {
label: COUNT_PROP_LABEL,

View file

@ -14,15 +14,14 @@ import { UpdateSourceEditor } from './update_source_editor';
import { VectorStyle } from '../../styles/vector/vector_style';
import { vectorStyles } from '../../styles/vector/vector_style_defaults';
import { i18n } from '@kbn/i18n';
import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW } from '../../../../common/constants';
import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW, COUNT_PROP_NAME, COUNT_PROP_LABEL } from '../../../../common/constants';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { convertToLines } from './convert_to_lines';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { AggConfigs } from 'ui/agg_types';
import { AbstractESAggSource } from '../es_agg_source';
import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
const COUNT_PROP_LABEL = 'count';
const COUNT_PROP_NAME = 'doc_count';
const MAX_GEOTILE_LEVEL = 29;
const aggSchemas = new Schemas([AbstractESAggSource.METRIC_SCHEMA_CONFIG]);
@ -92,7 +91,7 @@ export class ESPewPewSource extends AbstractESAggSource {
async getImmutableProperties() {
let indexPatternTitle = this._descriptor.indexPatternId;
try {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
indexPatternTitle = indexPattern.title;
} catch (error) {
// ignore error, title will just default to id
@ -126,7 +125,7 @@ export class ESPewPewSource extends AbstractESAggSource {
createDefaultLayer(options) {
const styleDescriptor = VectorStyle.createDescriptor({
[vectorStyles.LINE_COLOR]: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
type: DynamicStyleProperty.type,
options: {
field: {
label: COUNT_PROP_LABEL,
@ -137,7 +136,7 @@ export class ESPewPewSource extends AbstractESAggSource {
}
},
[vectorStyles.LINE_WIDTH]: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
type: DynamicStyleProperty.type,
options: {
field: {
label: COUNT_PROP_LABEL,
@ -167,7 +166,7 @@ export class ESPewPewSource extends AbstractESAggSource {
}
async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const metricAggConfigs = this.createMetricAggConfigs();
const aggConfigs = new AggConfigs(indexPattern, metricAggConfigs, aggSchemas.all);
@ -223,7 +222,7 @@ export class ESPewPewSource extends AbstractESAggSource {
}
async _getGeoField() {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const geoField = indexPattern.fields.getByName(this._descriptor.destGeoField);
if (!geoField) {
throw new Error(i18n.translate('xpack.maps.source.esSource.noGeoFieldErrorMessage', {

View file

@ -13,7 +13,7 @@ exports[`should enable sort order select when sort field provided 1`] = `
<TooltipSelector
fields={null}
onChange={[Function]}
tooltipProperties={Array []}
tooltipFields={Array []}
/>
</EuiFormRow>
<EuiFormRow
@ -115,7 +115,7 @@ exports[`should render top hits form when useTopHits is true 1`] = `
<TooltipSelector
fields={null}
onChange={[Function]}
tooltipProperties={Array []}
tooltipFields={Array []}
/>
</EuiFormRow>
<EuiFormRow
@ -256,7 +256,7 @@ exports[`should render update source editor 1`] = `
<TooltipSelector
fields={null}
onChange={[Function]}
tooltipProperties={Array []}
tooltipFields={Array []}
/>
</EuiFormRow>
<EuiFormRow

View file

@ -22,10 +22,10 @@ import {
} from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { ESTooltipProperty } from '../../tooltips/es_tooltip_property';
import { getSourceFields } from '../../../index_pattern_util';
import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
import { ESDocField } from '../../fields/es_doc_field';
export class ESSearchSource extends AbstractESSource {
@ -68,15 +68,25 @@ export class ESSearchSource extends AbstractESSource {
topHitsSplitField: descriptor.topHitsSplitField,
topHitsSize: _.get(descriptor, 'topHitsSize', 1),
}, inspectorAdapters);
this._tooltipFields = this._descriptor.tooltipProperties.map((property) => this.createField({ fieldName: property }));
}
createField({ fieldName }) {
return new ESDocField({
fieldName,
source: this
});
}
renderSourceSettingsEditor({ onChange }) {
return (
<UpdateSourceEditor
source={this}
indexPatternId={this._descriptor.indexPatternId}
onChange={onChange}
filterByMapBounds={this._descriptor.filterByMapBounds}
tooltipProperties={this._descriptor.tooltipProperties}
tooltipFields={this._tooltipFields}
sortField={this._descriptor.sortField}
sortOrder={this._descriptor.sortOrder}
useTopHits={this._descriptor.useTopHits}
@ -89,9 +99,9 @@ export class ESSearchSource extends AbstractESSource {
async getNumberFields() {
try {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
return indexPattern.fields.getByType('number').map(field => {
return { name: field.name, label: field.name };
return this.createField({ fieldName: field.name });
});
} catch (error) {
return [];
@ -100,19 +110,15 @@ export class ESSearchSource extends AbstractESSource {
async getDateFields() {
try {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
return indexPattern.fields.getByType('date').map(field => {
return { name: field.name, label: field.name };
return this.createField({ fieldName: field.name });
});
} catch (error) {
return [];
}
}
getMetricFields() {
return [];
}
getFieldNames() {
return [this._descriptor.geoField];
}
@ -121,7 +127,7 @@ export class ESSearchSource extends AbstractESSource {
let indexPatternTitle = this._descriptor.indexPatternId;
let geoFieldType = '';
try {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
indexPatternTitle = indexPattern.title;
const geoField = await this._getGeoField();
geoFieldType = geoField.type;
@ -171,7 +177,7 @@ export class ESSearchSource extends AbstractESSource {
}
async _excludeDateFields(fieldNames) {
const dateFieldNames = _.map(await this.getDateFields(), 'name');
const dateFieldNames = (await this.getDateFields()).map(field => field.getName());
return fieldNames.filter(field => {
return !dateFieldNames.includes(field);
});
@ -179,7 +185,7 @@ export class ESSearchSource extends AbstractESSource {
// Returns docvalue_fields array for the union of indexPattern's dateFields and request's field names.
async _getDateDocvalueFields(searchFields) {
const dateFieldNames = _.map(await this.getDateFields(), 'name');
const dateFieldNames = (await this.getDateFields()).map(field => field.getName());
return searchFields
.filter(fieldName => {
return dateFieldNames.includes(fieldName);
@ -198,7 +204,7 @@ export class ESSearchSource extends AbstractESSource {
topHitsSize,
} = this._descriptor;
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const geoField = await this._getGeoField();
const scriptFields = {};
@ -329,7 +335,7 @@ export class ESSearchSource extends AbstractESSource {
? await this._getTopHits(layerName, searchFilters, registerCancelCallback)
: await this._getSearchHits(layerName, searchFilters, registerCancelCallback);
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const unusedMetaFields = indexPattern.metaFields.filter(metaField => {
return !['_id', '_index'].includes(metaField);
});
@ -362,11 +368,11 @@ export class ESSearchSource extends AbstractESSource {
}
canFormatFeatureProperties() {
return this._descriptor.tooltipProperties.length > 0;
return this._tooltipFields.length > 0;
}
async _loadTooltipProperties(docId, index, indexPattern) {
if (this._descriptor.tooltipProperties.length === 0) {
if (this._tooltipFields.length === 0) {
return {};
}
@ -378,7 +384,7 @@ export class ESSearchSource extends AbstractESSource {
query: `_id:"${docId}" and _index:${index}`
};
searchSource.setField('query', query);
searchSource.setField('fields', this._descriptor.tooltipProperties);
searchSource.setField('fields', this._getTooltipPropertyNames());
const resp = await searchSource.fetch();
@ -394,7 +400,7 @@ export class ESSearchSource extends AbstractESSource {
const properties = indexPattern.flattenHit(hit);
indexPattern.metaFields.forEach(metaField => {
if (!this._descriptor.tooltipProperties.includes(metaField)) {
if (!this._getTooltipPropertyNames().includes(metaField)) {
delete properties[metaField];
}
});
@ -402,12 +408,13 @@ export class ESSearchSource extends AbstractESSource {
}
async filterAndFormatPropertiesToHtml(properties) {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const propertyValues = await this._loadTooltipProperties(properties._id, properties._index, indexPattern);
return this._descriptor.tooltipProperties.map(propertyName => {
return new ESTooltipProperty(propertyName, propertyName, propertyValues[propertyName], indexPattern);
const tooltipProperties = this._tooltipFields.map(field => {
const value = propertyValues[field.getName()];
return field.createTooltipProperty(value);
});
return Promise.all(tooltipProperties);
}
isFilterByMapBounds() {
@ -415,12 +422,9 @@ export class ESSearchSource extends AbstractESSource {
}
async getLeftJoinFields() {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
// Left fields are retrieved from _source.
return getSourceFields(indexPattern.fields)
.map(field => {
return { name: field.name, label: field.name };
});
return getSourceFields(indexPattern.fields).map(field => this.createField({ fieldName: field.name }));
}
async getSupportedShapeTypes() {
@ -507,7 +511,6 @@ export class ESSearchSource extends AbstractESSource {
async getPreIndexedShape(properties) {
const geoField = await this._getGeoField();
return {
index: properties._index, // Can not use index pattern title because it may reference many indices
id: properties._id,

View file

@ -22,22 +22,24 @@ import { i18n } from '@kbn/i18n';
import { getTermsFields, getSourceFields } from '../../../index_pattern_util';
import { ValidatedRange } from '../../../components/validated_range';
import { SORT_ORDER } from '../../../../common/constants';
import { ESDocField } from '../../fields/es_doc_field';
export class UpdateSourceEditor extends Component {
static propTypes = {
indexPatternId: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
filterByMapBounds: PropTypes.bool.isRequired,
tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired,
tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired,
sortField: PropTypes.string,
sortOrder: PropTypes.string.isRequired,
useTopHits: PropTypes.bool.isRequired,
topHitsSplitField: PropTypes.string,
topHitsSize: PropTypes.number.isRequired,
source: PropTypes.object
};
state = {
tooltipFields: null,
sourceFields: null,
termFields: null,
sortFields: null,
};
@ -73,10 +75,19 @@ export class UpdateSourceEditor extends Component {
return;
}
//todo move this all to the source
const rawTooltipFields = getSourceFields(indexPattern.fields);
const sourceFields = rawTooltipFields.map(field => {
return new ESDocField({
fieldName: field.name,
source: this.props.source
});
});
this.setState({
tooltipFields: getSourceFields(indexPattern.fields),
termFields: getTermsFields(indexPattern.fields),
sortFields: indexPattern.fields.filter(field => field.sortable),
sourceFields: sourceFields,
termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields
sortFields: indexPattern.fields.filter(field => field.sortable), //todo change sort fields to use fields
});
}
_onTooltipPropertiesChange = propertyNames => {
@ -173,9 +184,9 @@ export class UpdateSourceEditor extends Component {
<Fragment>
<EuiFormRow>
<TooltipSelector
tooltipProperties={this.props.tooltipProperties}
tooltipFields={this.props.tooltipFields}
onChange={this._onTooltipPropertiesChange}
fields={this.state.tooltipFields}
fields={this.state.sourceFields}
/>
</EuiFormRow>

View file

@ -15,7 +15,7 @@ const defaultProps = {
indexPatternId: 'indexPattern1',
onChange: () => {},
filterByMapBounds: true,
tooltipProperties: [],
tooltipFields: [],
sortOrder: 'DESC',
useTopHits: false,
topHitsSplitField: 'trackId',

View file

@ -69,6 +69,10 @@ export class AbstractESSource extends AbstractVectorSource {
return clonedDescriptor;
}
getMetricFields() {
return [];
}
async _runEsQuery(requestName, searchSource, registerCancelCallback, requestDescription) {
const abortController = new AbortController();
registerCancelCallback(() => abortController.abort());
@ -95,7 +99,7 @@ export class AbstractESSource extends AbstractVectorSource {
}
async _makeSearchSource(searchFilters, limit, initialSearchContext) {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const isTimeAware = await this.isTimeAware();
const applyGlobalQuery = _.get(searchFilters, 'applyGlobalQuery', true);
const globalFilters = applyGlobalQuery ? searchFilters.filters : [];
@ -130,7 +134,7 @@ export class AbstractESSource extends AbstractVectorSource {
const searchSource = await this._makeSearchSource({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }, 0);
const geoField = await this._getGeoField();
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const geoBoundsAgg = [{
type: 'geo_bounds',
@ -171,7 +175,7 @@ export class AbstractESSource extends AbstractVectorSource {
async isTimeAware() {
try {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const timeField = indexPattern.timeFieldName;
return !!timeField;
} catch (error) {
@ -179,7 +183,7 @@ export class AbstractESSource extends AbstractVectorSource {
}
}
async _getIndexPattern() {
async getIndexPattern() {
if (this.indexPattern) {
return this.indexPattern;
}
@ -208,7 +212,7 @@ export class AbstractESSource extends AbstractVectorSource {
async _getGeoField() {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const geoField = indexPattern.fields.getByName(this._descriptor.geoField);
if (!geoField) {
throw new Error(i18n.translate('xpack.maps.source.esSource.noGeoFieldErrorMessage', {
@ -221,7 +225,7 @@ export class AbstractESSource extends AbstractVectorSource {
async getDisplayName() {
try {
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
return indexPattern.title;
} catch (error) {
// Unable to load index pattern, just return id as display name
@ -238,25 +242,27 @@ export class AbstractESSource extends AbstractVectorSource {
}
async getFieldFormatter(fieldName) {
const metricField = this.getMetricFields().find(({ propertyKey }) => {
return propertyKey === fieldName;
});
const metricField = this.getMetricFields().find(field => field.getName() === fieldName);
// Do not use field formatters for counting metrics
if (metricField && metricField.type === METRIC_TYPE.COUNT || metricField.type === METRIC_TYPE.UNIQUE_COUNT) {
if (metricField && (metricField.type === METRIC_TYPE.COUNT || metricField.type === METRIC_TYPE.UNIQUE_COUNT)) {
return null;
}
// fieldName could be an aggregation so it needs to be unpacked to expose raw field.
const realFieldName = metricField ? metricField.getESDocFieldName() : fieldName;
if (!realFieldName) {
return null;
}
let indexPattern;
try {
indexPattern = await this._getIndexPattern();
indexPattern = await this.getIndexPattern();
} catch(error) {
return null;
}
const realFieldName = metricField
? metricField.field
: fieldName;
const fieldFromIndexPattern = indexPattern.fields.getByName(realFieldName);
if (!fieldFromIndexPattern) {
return null;
@ -264,4 +270,5 @@ export class AbstractESSource extends AbstractVectorSource {
return fieldFromIndexPattern.format.getConverterFor('text');
}
}

View file

@ -9,12 +9,15 @@ import _ from 'lodash';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { AggConfigs } from 'ui/agg_types';
import { i18n } from '@kbn/i18n';
import { ESTooltipProperty } from '../tooltips/es_tooltip_property';
import { ES_SIZE_LIMIT, METRIC_TYPE } from '../../../common/constants';
import { ES_SIZE_LIMIT, FIELD_ORIGIN, METRIC_TYPE } from '../../../common/constants';
import { ESDocField } from '../fields/es_doc_field';
import { AbstractESAggSource } from './es_agg_source';
const TERMS_AGG_NAME = 'join';
const FIELD_NAME_PREFIX = '__kbnjoin__';
const GROUP_BY_DELIMITER = '_groupby_';
const aggSchemas = new Schemas([
AbstractESAggSource.METRIC_SCHEMA_CONFIG,
{
@ -48,6 +51,10 @@ export class ESTermSource extends AbstractESAggSource {
static type = 'ES_TERM_SOURCE';
constructor(descriptor, inspectorAdapters) {
super(descriptor, inspectorAdapters);
this._termField = new ESDocField({ fieldName: descriptor.term, source: this, origin: this.getOriginForField() });
}
static renderEditor({}) {
//no need to localize. this editor is never rendered.
@ -62,22 +69,26 @@ export class ESTermSource extends AbstractESAggSource {
return [this._descriptor.indexPatternId];
}
getTerm() {
return this._descriptor.term;
getTermField() {
return this._termField;
}
getOriginForField() {
return FIELD_ORIGIN.JOIN;
}
getWhereQuery() {
return this._descriptor.whereQuery;
}
_formatMetricKey(metric) {
const metricKey = metric.type !== METRIC_TYPE.COUNT ? `${metric.type}_of_${metric.field}` : metric.type;
return `__kbnjoin__${metricKey}_groupby_${this._descriptor.indexPatternTitle}.${this._descriptor.term}`;
formatMetricKey(aggType, fieldName) {
const metricKey = aggType !== METRIC_TYPE.COUNT ? `${aggType}_of_${fieldName}` : aggType;
return `${FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${this._descriptor.indexPatternTitle}.${this._termField.getName()}`;
}
_formatMetricLabel(metric) {
const metricLabel = metric.type !== METRIC_TYPE.COUNT ? `${metric.type} ${metric.field}` : 'count';
return `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._descriptor.term}`;
formatMetricLabel(type, fieldName) {
const metricLabel = type !== METRIC_TYPE.COUNT ? `${type} ${fieldName}` : 'count';
return `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._termField.getName()}`;
}
async getPropertiesMap(searchFilters, leftSourceName, leftFieldName, registerCancelCallback) {
@ -86,13 +97,13 @@ export class ESTermSource extends AbstractESAggSource {
return [];
}
const indexPattern = await this._getIndexPattern();
const indexPattern = await this.getIndexPattern();
const searchSource = await this._makeSearchSource(searchFilters, 0);
const configStates = this._makeAggConfigs();
const aggConfigs = new AggConfigs(indexPattern, configStates, aggSchemas.all);
searchSource.setField('aggs', aggConfigs.toDsl());
const requestName = `${this._descriptor.indexPatternTitle}.${this._descriptor.term}`;
const requestName = `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`;
const requestDesc = this._getRequestDescription(leftSourceName, leftFieldName);
const rawEsData = await this._runEsQuery(requestName, searchSource, registerCancelCallback, requestDesc);
@ -117,15 +128,13 @@ export class ESTermSource extends AbstractESAggSource {
}
_getRequestDescription(leftSourceName, leftFieldName) {
const metrics = this._getValidMetrics().map(metric => {
return metric.type !== METRIC_TYPE.COUNT ? `${metric.type} ${metric.field}` : 'count';
});
const metrics = this.getMetricFields().map(esAggMetric => esAggMetric.getRequestDescription());
const joinStatement = [];
joinStatement.push(i18n.translate('xpack.maps.source.esJoin.joinLeftDescription', {
defaultMessage: `Join {leftSourceName}:{leftFieldName} with`,
values: { leftSourceName, leftFieldName }
}));
joinStatement.push(`${this._descriptor.indexPatternTitle}:${this._descriptor.term}`);
joinStatement.push(`${this._descriptor.indexPatternTitle}:${this._termField.getName()}`);
joinStatement.push(i18n.translate('xpack.maps.source.esJoin.joinMetricsDescription', {
defaultMessage: `for metrics {metrics}`,
values: { metrics: metrics.join(',') }
@ -148,7 +157,7 @@ export class ESTermSource extends AbstractESAggSource {
type: 'terms',
schema: 'segment',
params: {
field: this._descriptor.term,
field: this._termField.getName(),
size: ES_SIZE_LIMIT
}
}
@ -164,15 +173,7 @@ export class ESTermSource extends AbstractESAggSource {
return await this.filterAndFormatPropertiesToHtmlForMetricFields(properties);
}
async createESTooltipProperty(propertyName, rawValue) {
try {
const indexPattern = await this._getIndexPattern();
if (!indexPattern) {
return null;
}
return new ESTooltipProperty(propertyName, propertyName, rawValue, indexPattern);
} catch (e) {
return null;
}
getFieldNames() {
return this.getMetricFields().map(esAggMetricField => esAggMetricField.getName());
}
}

View file

@ -36,21 +36,21 @@ const metricExamples = [
describe('getMetricFields', () => {
it('should add default "count" metric when no metrics are provided', () => {
it('should add default "count" metric when no metrics are provided', async () => {
const source = new ESTermSource({
indexPatternTitle: indexPatternTitle,
term: termFieldName,
});
const metrics = source.getMetricFields();
expect(metrics.length).toBe(1);
expect(metrics[0]).toEqual({
type: 'count',
propertyKey: '__kbnjoin__count_groupby_myIndex.myTermField',
propertyLabel: 'count of myIndex:myTermField',
});
expect(metrics[0].getAggType()).toEqual('count');
expect(metrics[0].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField');
expect(await metrics[0].getLabel()).toEqual('count of myIndex:myTermField');
});
it('should remove incomplete metric configurations', () => {
it('should remove incomplete metric configurations', async () => {
const source = new ESTermSource({
indexPatternTitle: indexPatternTitle,
term: termFieldName,
@ -58,17 +58,16 @@ describe('getMetricFields', () => {
});
const metrics = source.getMetricFields();
expect(metrics.length).toBe(2);
expect(metrics[0]).toEqual({
type: 'sum',
field: sumFieldName,
propertyKey: '__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField',
propertyLabel: 'my custom label',
});
expect(metrics[1]).toEqual({
type: 'count',
propertyKey: '__kbnjoin__count_groupby_myIndex.myTermField',
propertyLabel: 'count of myIndex:myTermField',
});
expect(metrics[0].getAggType()).toEqual('sum');
expect(metrics[0].getESDocFieldName()).toEqual(sumFieldName);
expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField');
expect(await metrics[0].getLabel()).toEqual('my custom label');
expect(metrics[1].getAggType()).toEqual('count');
expect(metrics[1].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField');
expect(await metrics[1].getLabel()).toEqual('count of myIndex:myTermField');
});
});

View file

@ -10,7 +10,8 @@ import { CreateSourceEditor } from './create_source_editor';
import { getKibanaRegionList } from '../../../meta';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
import { FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants';
import { KibanaRegionField } from '../../fields/kibana_region_field';
export class KibanaRegionmapSource extends AbstractVectorSource {
@ -45,11 +46,20 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
);
};
createField({ fieldName }) {
return new KibanaRegionField({
fieldName,
source: this,
origin: FIELD_ORIGIN.SOURCE
});
}
async getImmutableProperties() {
return [
{
label: getDataSourceLabel(),
value: KibanaRegionmapSource.title },
value: KibanaRegionmapSource.title
},
{
label: i18n.translate('xpack.maps.source.kbnRegionMap.vectorLayerLabel', {
defaultMessage: 'Vector layer'
@ -59,7 +69,7 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
];
}
async _getVectorFileMeta() {
async getVectorFileMeta() {
const regionList = getKibanaRegionList();
const meta = regionList.find(source => source.name === this._descriptor.name);
if (!meta) {
@ -75,7 +85,7 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
}
async getGeoJsonWithMeta() {
const vectorFileMeta = await this._getVectorFileMeta();
const vectorFileMeta = await this.getVectorFileMeta();
const featureCollection = await AbstractVectorSource.getGeoJson({
format: vectorFileMeta.format.type,
featureCollectionPath: vectorFileMeta.meta.feature_collection_path,
@ -90,10 +100,8 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
}
async getLeftJoinFields() {
const vectorFileMeta = await this._getVectorFileMeta();
return vectorFileMeta.fields.map(f => {
return { name: f.name, label: f.description };
});
const vectorFileMeta = await this.getVectorFileMeta();
return vectorFileMeta.fields.map(f => this.createField({ fieldName: f.name }));
}
async getDisplayName() {

View file

@ -48,6 +48,10 @@ export class AbstractVectorSource extends AbstractSource {
}));
}
createField() {
throw new Error(`Should implemement ${this.constructor.type} ${this}`);
}
_createDefaultLayerDescriptor(options, mapColors) {
return VectorLayer.createDescriptor(
{
@ -57,6 +61,10 @@ export class AbstractVectorSource extends AbstractSource {
mapColors);
}
_getTooltipPropertyNames() {
return this._tooltipFields.map(field => field.getName());
}
createDefaultLayer(options, mapColors) {
const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors);
const style = new VectorStyle(layerDescriptor.style, this);
@ -131,4 +139,5 @@ export class AbstractVectorSource extends AbstractSource {
getSourceTooltipContent(/* sourceDataRequest */) {
return { tooltipContent: null, areResultsTrimmed: false };
}
}

View file

@ -15,26 +15,54 @@ import {
HEATMAP_COLOR_RAMP_LABEL
} from '../heatmap_constants';
export function HeatmapLegend({ colorRampName, label }) {
const header = colorRampName === DEFAULT_HEATMAP_COLOR_RAMP_NAME
? <ColorGradient colorRamp={DEFAULT_RGB_HEATMAP_COLOR_RAMP}/>
: <ColorGradient colorRampName={colorRampName}/>;
export class HeatmapLegend extends React.Component {
return (
<StyleLegendRow
header={header}
minLabel={
i18n.translate('xpack.maps.heatmapLegend.coldLabel', {
defaultMessage: 'cold'
})
}
maxLabel={
i18n.translate('xpack.maps.heatmapLegend.hotLabel', {
defaultMessage: 'hot'
})
}
propertyLabel={HEATMAP_COLOR_RAMP_LABEL}
fieldLabel={label}
/>
);
constructor() {
super();
this.state = { label: '' };
}
componentDidUpdate() {
this._loadLabel();
}
componentDidMount() {
this._isMounted = true;
this._loadLabel();
}
componentWillUnmount() {
this._isMounted = false;
}
async _loadLabel() {
const label = await this.props.field.getLabel();
if (this._isMounted && this.state.label !== label) {
this.setState({ label });
}
}
render() {
const colorRampName = this.props.colorRampName;
const header = colorRampName === DEFAULT_HEATMAP_COLOR_RAMP_NAME
? <ColorGradient colorRamp={DEFAULT_RGB_HEATMAP_COLOR_RAMP}/>
: <ColorGradient colorRampName={colorRampName}/>;
return (
<StyleLegendRow
header={header}
minLabel={
i18n.translate('xpack.maps.heatmapLegend.coldLabel', {
defaultMessage: 'cold'
})
}
maxLabel={
i18n.translate('xpack.maps.heatmapLegend.hotLabel', {
defaultMessage: 'hot'
})
}
propertyLabel={HEATMAP_COLOR_RAMP_LABEL}
fieldLabel={this.state.label}
/>
);
}
}

View file

@ -50,11 +50,11 @@ export class HeatmapStyle extends AbstractStyle {
);
}
getLegendDetails(label) {
renderLegendDetails(field) {
return (
<HeatmapLegend
colorRampName={this._descriptor.colorRampName}
label={label}
field={field}
/>
);
}

View file

@ -5,71 +5,13 @@
*/
import _ from 'lodash';
import React, { Component, Fragment } from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { styleOptionShapes, rangeShape } from '../style_option_shapes';
import { VectorStyle } from '../../vector_style';
import { ColorGradient } from '../../../components/color_gradient';
import { CircleIcon } from './circle_icon';
import { rangeShape } from '../style_option_shapes';
import { getVectorStyleLabel } from '../get_vector_style_label';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { StyleLegendRow } from '../../../components/style_legend_row';
function getLineWidthIcons() {
const defaultStyle = {
stroke: 'grey',
fill: 'none',
width: '12px',
};
return [
<CircleIcon style={{ ...defaultStyle, strokeWidth: '1px' }}/>,
<CircleIcon style={{ ...defaultStyle, strokeWidth: '2px' }}/>,
<CircleIcon style={{ ...defaultStyle, strokeWidth: '3px' }}/>,
];
}
function getSymbolSizeIcons() {
const defaultStyle = {
stroke: 'grey',
strokeWidth: 'none',
fill: 'grey',
};
return [
<CircleIcon style={{ ...defaultStyle, width: '4px' }}/>,
<CircleIcon style={{ ...defaultStyle, width: '8px' }}/>,
<CircleIcon style={{ ...defaultStyle, width: '12px' }}/>,
];
}
function renderHeaderWithIcons(icons) {
return (
<EuiFlexGroup gutterSize="s" justifyContent="spaceBetween" alignItems="center">
{
icons.map((icon, index) => {
const isLast = index === icons.length - 1;
let spacer;
if (!isLast) {
spacer = (
<EuiFlexItem>
<EuiHorizontalRule margin="xs" />
</EuiFlexItem>
);
}
return (
<Fragment key={index}>
<EuiFlexItem grow={false}>
{icon}
</EuiFlexItem>
{spacer}
</Fragment>
);
})
}
</EuiFlexGroup>
);
}
const EMPTY_VALUE = '';
export class StylePropertyLegendRow extends Component {
@ -97,19 +39,25 @@ export class StylePropertyLegendRow extends Component {
}
async _loadFieldFormatter() {
this._fieldValueFormatter = await this.props.getFieldFormatter(this.props.options.field);
if (this.props.style.isDynamic() && this.props.style.isComplete() && this.props.style.getField().getSource()) {
const field = this.props.style.getField();
const source = field.getSource();
this._fieldValueFormatter = await source.getFieldFormatter(field.getName());
} else {
this._fieldValueFormatter = null;
}
if (this._isMounted) {
this.setState({ hasLoadedFieldFormatter: true });
}
}
_loadLabel = async () => {
if (this._isStatic()) {
if (this._excludeFromHeader()) {
return;
}
// have to load label and then check for changes since field name stays constant while label may change
const label = await this.props.getFieldLabel(this.props.options.field.name);
const label = await this.props.style.getField().getLabel();
if (this._prevLabel === label) {
return;
}
@ -120,9 +68,8 @@ export class StylePropertyLegendRow extends Component {
}
}
_isStatic() {
return this.props.type === VectorStyle.STYLE_TYPE.STATIC ||
!this.props.options.field || !this.props.options.field.name;
_excludeFromHeader() {
return !this.props.style.isDynamic() || !this.props.style.isComplete() || !this.props.style.getField().getName();
}
_formatValue = value => {
@ -134,26 +81,19 @@ export class StylePropertyLegendRow extends Component {
}
render() {
const { name, options, range } = this.props;
if (this._isStatic()) {
const { range, style } = this.props;
if (this._excludeFromHeader()) {
return null;
}
let header;
if (options.color) {
header = <ColorGradient colorRampName={options.color}/>;
} else if (name === 'lineWidth') {
header = renderHeaderWithIcons(getLineWidthIcons());
} else if (name === 'iconSize') {
header = renderHeaderWithIcons(getSymbolSizeIcons());
}
const header = style.renderHeader();
return (
<StyleLegendRow
header={header}
minLabel={this._formatValue(_.get(range, 'min', EMPTY_VALUE))}
maxLabel={this._formatValue(_.get(range, 'max', EMPTY_VALUE))}
propertyLabel={getVectorStyleLabel(name)}
propertyLabel={getVectorStyleLabel(style.getStyleName())}
fieldLabel={this.state.label}
/>
);
@ -161,10 +101,6 @@ export class StylePropertyLegendRow extends Component {
}
StylePropertyLegendRow.propTypes = {
name: PropTypes.string.isRequired,
type: PropTypes.string,
options: PropTypes.oneOfType(styleOptionShapes).isRequired,
range: rangeShape,
getFieldLabel: PropTypes.func.isRequired,
getFieldFormatter: PropTypes.func.isRequired,
style: PropTypes.object
};

View file

@ -7,34 +7,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { styleOptionShapes, rangeShape } from '../style_option_shapes';
import { rangeShape } from '../style_option_shapes';
import { StylePropertyLegendRow } from './style_property_legend_row';
export function VectorStyleLegend({ getFieldLabel, getFieldFormatter, styleProperties }) {
export function VectorStyleLegend({ styleProperties }) {
return styleProperties.map(styleProperty => {
return (
<StylePropertyLegendRow
key={styleProperty.name}
name={styleProperty.name}
type={styleProperty.type}
options={styleProperty.options}
style={styleProperty.style}
key={styleProperty.style.getStyleName()}
range={styleProperty.range}
getFieldLabel={getFieldLabel}
getFieldFormatter={getFieldFormatter}
/>
);
});
}
const stylePropertyShape = PropTypes.shape({
name: PropTypes.string.isRequired,
type: PropTypes.string,
options: PropTypes.oneOfType(styleOptionShapes).isRequired,
range: rangeShape,
style: PropTypes.object
});
VectorStyleLegend.propTypes = {
styleProperties: PropTypes.arrayOf(stylePropertyShape).isRequired,
getFieldLabel: PropTypes.func.isRequired,
getFieldFormatter: PropTypes.func.isRequired,
styleProperties: PropTypes.arrayOf(stylePropertyShape).isRequired
};

View file

@ -36,15 +36,6 @@ export const dynamicSizeShape = PropTypes.shape({
field: fieldShape,
});
export const styleOptionShapes = [
staticColorShape,
dynamicColorShape,
staticOrientationShape,
dynamicOrientationShape,
staticSizeShape,
dynamicSizeShape
];
export const rangeShape = PropTypes.shape({
min: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,

View file

@ -52,20 +52,27 @@ export class VectorStyleEditor extends Component {
async _loadOrdinalFields() {
const getFieldMeta = async (field) => {
return {
label: await field.getLabel(),
name: field.getName(),
origin: field.getOrigin()
};
};
const dateFields = await this.props.layer.getDateFields();
if (!this._isMounted) {
return;
}
if (!_.isEqual(dateFields, this.state.dateFields)) {
this.setState({ dateFields });
const dateFieldPromises = dateFields.map(getFieldMeta);
const dateFieldsArray = await Promise.all(dateFieldPromises);
if (this._isMounted && !_.isEqual(dateFieldsArray, this.state.dateFields)) {
this.setState({ dateFields: dateFieldsArray });
}
const numberFields = await this.props.layer.getNumberFields();
if (!this._isMounted) {
return;
}
if (!_.isEqual(numberFields, this.state.numberFields)) {
this.setState({ numberFields });
const numberFieldPromises = numberFields.map(getFieldMeta);
const numberFieldsArray = await Promise.all(numberFieldPromises);
if (this._isMounted && !_.isEqual(numberFieldsArray, this.state.numberFields)) {
this.setState({ numberFields: numberFieldsArray });
}
}

View file

@ -9,11 +9,12 @@ import { DynamicStyleProperty } from './dynamic_style_property';
import _ from 'lodash';
import { getComputedFieldName } from '../style_util';
import { getColorRampStops } from '../../color_utils';
import { ColorGradient } from '../../components/color_gradient';
import React from 'react';
export class DynamicColorProperty extends DynamicStyleProperty {
syncCircleColorWithMb(mbLayerId, mbMap, alpha) {
const color = this._getMbColor();
mbMap.setPaintProperty(mbLayerId, 'circle-color', color);
@ -48,6 +49,18 @@ export class DynamicColorProperty extends DynamicStyleProperty {
mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha);
}
isCustomColorRamp() {
return !!this._options.customColorRamp;
}
supportsFeatureState() {
return true;
}
isScaled() {
return !this.isCustomColorRamp();
}
_getMbColor() {
const isDynamicConfigComplete = _.has(this._options, 'field.name') && _.has(this._options, 'color');
if (!isDynamicConfigComplete) {
@ -98,6 +111,14 @@ export class DynamicColorProperty extends DynamicStyleProperty {
return getColorRampStops(this._options.color);
}
renderHeader() {
if (this._options.color) {
return (<ColorGradient colorRampName={this._options.color}/>);
} else {
return null;
}
}
}

View file

@ -22,6 +22,14 @@ export class DynamicOrientationProperty extends DynamicStyleProperty {
}
}
supportsFeatureState() {
return false;
}
isScaled() {
return false;
}
}

View file

@ -10,6 +10,34 @@ import { getComputedFieldName } from '../style_util';
import { HALF_LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE, SMALL_MAKI_ICON_SIZE } from '../symbol_utils';
import { vectorStyles } from '../vector_style_defaults';
import _ from 'lodash';
import { CircleIcon } from '../components/legend/circle_icon';
import React, { Fragment } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
function getLineWidthIcons() {
const defaultStyle = {
stroke: 'grey',
fill: 'none',
width: '12px',
};
return [
<CircleIcon style={{ ...defaultStyle, strokeWidth: '1px' }}/>,
<CircleIcon style={{ ...defaultStyle, strokeWidth: '2px' }}/>,
<CircleIcon style={{ ...defaultStyle, strokeWidth: '3px' }}/>,
];
}
function getSymbolSizeIcons() {
const defaultStyle = {
stroke: 'grey',
fill: 'grey',
};
return [
<CircleIcon style={{ ...defaultStyle, width: '4px' }}/>,
<CircleIcon style={{ ...defaultStyle, width: '8px' }}/>,
<CircleIcon style={{ ...defaultStyle, width: '12px' }}/>,
];
}
export class DynamicSizeProperty extends DynamicStyleProperty {
@ -79,6 +107,43 @@ export class DynamicSizeProperty extends DynamicStyleProperty {
}
_isSizeDynamicConfigComplete() {
return this._options.field && this._options.field.name && _.has(this._options, 'minSize') && _.has(this._options, 'maxSize');
return this._field && this._field.isValid() && _.has(this._options, 'minSize') && _.has(this._options, 'maxSize');
}
renderHeader() {
let icons;
if (this.getStyleName() === vectorStyles.LINE_WIDTH) {
icons = getLineWidthIcons();
} else if (this.getStyleName() === vectorStyles.ICON_SIZE) {
icons = getSymbolSizeIcons();
} else {
return null;
}
return (
<EuiFlexGroup gutterSize="s" justifyContent="spaceBetween" alignItems="center">
{
icons.map((icon, index) => {
const isLast = index === icons.length - 1;
let spacer;
if (!isLast) {
spacer = (
<EuiFlexItem>
<EuiHorizontalRule margin="xs" />
</EuiFlexItem>
);
}
return (
<Fragment key={index}>
<EuiFlexItem grow={false}>
{icon}
</EuiFlexItem>
{spacer}
</Fragment>
);
})
}
</EuiFlexGroup>
);
}
}

View file

@ -6,7 +6,37 @@
import { AbstractStyleProperty } from './style_property';
import { STYLE_TYPE } from '../../../../../common/constants';
export class DynamicStyleProperty extends AbstractStyleProperty {
static type = 'DYNAMIC';
static type = STYLE_TYPE.DYNAMIC;
constructor(options, styleName, field) {
super(options, styleName);
this._field = field;
}
getField() {
return this._field;
}
isDynamic() {
return true;
}
isComplete() {
return !!this._field;
}
getFieldOrigin() {
return this._field.getOrigin();
}
supportsFeatureState() {
return true;
}
isScaled() {
return true;
}
}

View file

@ -6,8 +6,8 @@
import { AbstractStyleProperty } from './style_property';
import { STYLE_TYPE } from '../../../../../common/constants';
export class StaticStyleProperty extends AbstractStyleProperty {
static type = 'STATIC';
static type = STYLE_TYPE.STATIC;
}

View file

@ -10,4 +10,30 @@ export class AbstractStyleProperty {
this._options = options;
this._styleName = styleName;
}
isDynamic() {
return false;
}
/**
* Is the style fully defined and usable? (e.g. for rendering, in legend UX, ...)
* Why? during editing, partially-completed descriptors may be added to the layer-descriptor
* e.g. dynamic-fields can have an incomplete state when the field is not yet selected from the drop-down
* @returns {boolean}
*/
isComplete() {
return true;
}
getStyleName() {
return this._styleName;
}
getOptions() {
return this._options || {};
}
renderHeader() {
return null;
}
}

View file

@ -6,17 +6,16 @@
import _ from 'lodash';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { VectorStyleEditor } from './components/vector_style_editor';
import { getDefaultProperties, vectorStyles } from './vector_style_defaults';
import { AbstractStyle } from '../abstract_style';
import { SOURCE_DATA_ID_ORIGIN, GEO_JSON_TYPE } from '../../../../common/constants';
import { GEO_JSON_TYPE, FIELD_ORIGIN, STYLE_TYPE } from '../../../../common/constants';
import { VectorIcon } from './components/legend/vector_icon';
import { VectorStyleLegend } from './components/legend/vector_style_legend';
import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from './vector_constants';
import { getMakiSymbolAnchor } from './symbol_utils';
import { getComputedFieldName, getComputedFieldNamePrefix } from './style_util';
import { getComputedFieldName } from './style_util';
import { StaticStyleProperty } from './properties/static_style_property';
import { DynamicStyleProperty } from './properties/dynamic_style_property';
import { DynamicSizeProperty } from './properties/dynamic_size_property';
@ -33,27 +32,7 @@ const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON];
export class VectorStyle extends AbstractStyle {
static type = 'VECTOR';
static STYLE_TYPE = { 'DYNAMIC': DynamicStyleProperty.type, 'STATIC': StaticStyleProperty.type };
static getComputedFieldName = getComputedFieldName;
static getComputedFieldNamePrefix = getComputedFieldNamePrefix;
constructor(descriptor = {}, source) {
super();
this._source = source;
this._descriptor = {
...descriptor,
...VectorStyle.createDescriptor(descriptor.properties),
};
this._lineColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.LINE_COLOR], vectorStyles.LINE_COLOR);
this._fillColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.FILL_COLOR], vectorStyles.FILL_COLOR);
this._lineWidthStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.LINE_WIDTH], vectorStyles.LINE_WIDTH);
this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.ICON_SIZE], vectorStyles.ICON_SIZE);
// eslint-disable-next-line max-len
this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[vectorStyles.ICON_ORIENTATION], vectorStyles.ICON_ORIENTATION);
}
static STYLE_TYPE = STYLE_TYPE;
static createDescriptor(properties = {}) {
return {
type: VectorStyle.type,
@ -65,16 +44,36 @@ export class VectorStyle extends AbstractStyle {
return getDefaultProperties(mapColors);
}
static getDisplayName() {
return i18n.translate('xpack.maps.style.vector.displayNameLabel', {
defaultMessage: 'Vector style'
});
constructor(descriptor = {}, source, layer) {
super();
this._source = source;
this._layer = layer;
this._descriptor = {
...descriptor,
...VectorStyle.createDescriptor(descriptor.properties),
};
this._lineColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.LINE_COLOR], vectorStyles.LINE_COLOR);
this._fillColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.FILL_COLOR], vectorStyles.FILL_COLOR);
this._lineWidthStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.LINE_WIDTH], vectorStyles.LINE_WIDTH);
this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.ICON_SIZE], vectorStyles.ICON_SIZE);
// eslint-disable-next-line max-len
this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[vectorStyles.ICON_ORIENTATION], vectorStyles.ICON_ORIENTATION);
}
static description = '';
_getAllStyleProperties() {
return [
this._lineColorStyleProperty,
this._fillColorStyleProperty,
this._lineWidthStyleProperty,
this._iconSizeStyleProperty,
this._iconOrientationProperty
];
}
renderEditor({ layer, onStyleDescriptorChange }) {
const styleProperties = { ...this.getProperties() };
const styleProperties = { ...this.getRawProperties() };
const handlePropertyChange = (propertyName, settings) => {
styleProperties[propertyName] = settings;//override single property, but preserve the rest
const vectorStyleDescriptor = VectorStyle.createDescriptor(styleProperties);
@ -104,39 +103,45 @@ export class VectorStyle extends AbstractStyle {
* can then use to update store state via dispatch.
*/
getDescriptorWithMissingStylePropsRemoved(nextOrdinalFields) {
const originalProperties = this.getProperties();
const updatedProperties = {};
Object.keys(originalProperties).forEach(propertyName => {
if (!this._isPropertyDynamic(propertyName)) {
return;
}
const fieldName = _.get(originalProperties[propertyName], 'options.field.name');
const originalProperties = this.getRawProperties();
const updatedProperties = {};
const dynamicProperties = Object.keys(originalProperties).filter(key => {
const { type, options } = originalProperties[key] || {};
return type === STYLE_TYPE.DYNAMIC && options.field && options.field.name;
});
dynamicProperties.forEach(key => {
const dynamicProperty = originalProperties[key];
const fieldName = dynamicProperty && dynamicProperty.options.field && dynamicProperty.options.field.name;
if (!fieldName) {
return;
}
const matchingOrdinalField = nextOrdinalFields.find(oridinalField => {
return fieldName === oridinalField.name;
const matchingOrdinalField = nextOrdinalFields.find(ordinalField => {
return fieldName === ordinalField.getName();
});
if (matchingOrdinalField) {
return;
}
updatedProperties[propertyName] = {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
updatedProperties[key] = {
type: DynamicStyleProperty.type,
options: {
...originalProperties[propertyName].options
...originalProperties[key].options
}
};
delete updatedProperties[propertyName].options.field;
delete updatedProperties[key].options.field;
});
if (Object.keys(updatedProperties).length === 0) {
return {
hasChanges: false,
nextStyleDescriptor: { ...this._descriptor },
nextStyleDescriptor: { ...this._descriptor }
};
}
@ -156,9 +161,9 @@ export class VectorStyle extends AbstractStyle {
}
const scaledFields = this.getDynamicPropertiesArray()
.map(({ options }) => {
.map(styleProperty => {
return {
name: options.field.name,
name: styleProperty.getField().getName(),
min: Infinity,
max: -Infinity
};
@ -219,45 +224,22 @@ export class VectorStyle extends AbstractStyle {
}
getSourceFieldNames() {
const properties = this.getProperties();
const fieldNames = [];
Object.keys(properties).forEach(propertyName => {
if (!this._isPropertyDynamic(propertyName)) {
return;
}
const field = _.get(properties[propertyName], 'options.field', {});
if (field.origin === SOURCE_DATA_ID_ORIGIN && field.name) {
fieldNames.push(field.name);
this.getDynamicPropertiesArray().forEach(styleProperty => {
if (styleProperty.getFieldOrigin() === FIELD_ORIGIN.SOURCE) {
fieldNames.push(styleProperty.getField().getName());
}
});
return fieldNames;
}
getProperties() {
getRawProperties() {
return this._descriptor.properties || {};
}
getDynamicPropertiesArray() {
const styles = this.getProperties();
return Object.keys(styles)
.map(styleName => {
const { type, options } = styles[styleName];
return {
styleName,
type,
options
};
})
.filter(({ styleName }) => {
return this._isPropertyDynamic(styleName);
});
}
_isPropertyDynamic(propertyName) {
const { type, options } = _.get(this._descriptor, ['properties', propertyName], {});
return type === VectorStyle.STYLE_TYPE.DYNAMIC && options.field && options.field.name;
const styleProperties = this._getAllStyleProperties();
return styleProperties.filter(styleProperty => (styleProperty.isDynamic() && styleProperty.isComplete()));
}
_checkIfOnlyFeatureType = async (featureType) => {
@ -288,16 +270,12 @@ export class VectorStyle extends AbstractStyle {
return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.LINE);
}
_getIsPolygonsOnly = async () => {
return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.POLYGON);
}
_getFieldRange = (fieldName) => {
return _.get(this._descriptor, ['__styleMeta', fieldName]);
}
getIcon = () => {
const styles = this.getProperties();
const styles = this.getRawProperties();
const symbolId = this.arePointsSymbolizedAsCircles()
? undefined
: this._descriptor.properties.symbol.options.symbolId;
@ -305,65 +283,54 @@ export class VectorStyle extends AbstractStyle {
<VectorIcon
loadIsPointsOnly={this._getIsPointsOnly}
loadIsLinesOnly={this._getIsLinesOnly}
fillColor={styles.fillColor}
lineColor={styles.lineColor}
fillColor={styles[vectorStyles.FILL_COLOR]}
lineColor={styles[vectorStyles.LINE_COLOR]}
symbolId={symbolId}
/>
);
}
getLegendDetails(getFieldLabel, getFieldFormatter) {
const styles = this.getProperties();
const styleProperties = Object.keys(styles).map(styleName => {
const { type, options } = styles[styleName];
renderLegendDetails() {
const styles = this._getAllStyleProperties();
const styleProperties = styles.map((style) => {
return {
name: styleName,
type,
options,
range: options && options.field && options.field.name ? this._getFieldRange(options.field.name) : null,
// eslint-disable-next-line max-len
range: (style.isDynamic() && style.isComplete() && style.getField().getName()) ? this._getFieldRange(style.getField().getName()) : null,
style: style
};
});
return (
<VectorStyleLegend
styleProperties={styleProperties}
getFieldLabel={getFieldLabel}
getFieldFormatter={getFieldFormatter}
/>
);
}
_getStyleFields() {
return this.getDynamicPropertiesArray()
.map(({ styleName, options }) => {
const name = options.field.name;
.map(styleProperty => {
// "feature-state" data expressions are not supported with layout properties.
// To work around this limitation, some styling values must fall back to geojson property values.
let supportsFeatureState;
let isScaled;
if (styleName === 'iconSize'
if (styleProperty.getStyleName() === vectorStyles.ICON_SIZE
&& this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON) {
supportsFeatureState = false;
isScaled = true;
} else if (styleName === 'iconOrientation') {
supportsFeatureState = false;
isScaled = false;
} else if ((styleName === vectorStyles.FILL_COLOR || styleName === vectorStyles.LINE_COLOR)
&& options.useCustomColorRamp) {
supportsFeatureState = true;
isScaled = false;
} else {
supportsFeatureState = true;
isScaled = true;
supportsFeatureState = styleProperty.supportsFeatureState();
isScaled = styleProperty.isScaled();
}
const field = styleProperty.getField();
return {
supportsFeatureState,
isScaled,
name,
range: this._getFieldRange(name),
computedName: VectorStyle.getComputedFieldName(styleName, name),
name: field.getName(),
range: this._getFieldRange(field.getName()),
computedName: getComputedFieldName(styleProperty.getStyleName(), field.getName()),
};
});
}
@ -472,13 +439,46 @@ export class VectorStyle extends AbstractStyle {
}
arePointsSymbolizedAsCircles() {
return this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_CIRCLE;
}
_makeField(fieldDescriptor) {
if (!fieldDescriptor || !fieldDescriptor.name) {
return null;
}
//fieldDescriptor.label is ignored. This is essentially cruft duplicating label-info from the metric-selection
//Ignore this custom label
if (fieldDescriptor.origin === FIELD_ORIGIN.SOURCE) {
return this._source.createField({
fieldName: fieldDescriptor.name
});
} else if (fieldDescriptor.origin === FIELD_ORIGIN.JOIN) {
let matchingField = null;
const joins = this._layer.getValidJoins();
joins.find(join => {
const aggSource = join.getRightJoinSource();
matchingField = aggSource.getMetricFieldForName(fieldDescriptor.name);
return !!matchingField;
});
return matchingField;
} else {
throw new Error(`Unknown origin-type ${fieldDescriptor.origin}`);
}
}
_makeSizeProperty(descriptor, styleName) {
if (!descriptor || !descriptor.options) {
return new StaticSizeProperty({ size: 0 }, styleName);
} else if (descriptor.type === StaticStyleProperty.type) {
return new StaticSizeProperty(descriptor.options, styleName);
} else if (descriptor.type === DynamicStyleProperty.type) {
return new DynamicSizeProperty(descriptor.options, styleName);
const field = this._makeField(descriptor.options.field);
return new DynamicSizeProperty(descriptor.options, styleName, field);
} else {
throw new Error(`${descriptor} not implemented`);
}
@ -490,7 +490,8 @@ export class VectorStyle extends AbstractStyle {
} else if (descriptor.type === StaticStyleProperty.type) {
return new StaticColorProperty(descriptor.options, styleName);
} else if (descriptor.type === DynamicStyleProperty.type) {
return new DynamicColorProperty(descriptor.options, styleName);
const field = this._makeField(descriptor.options.field);
return new DynamicColorProperty(descriptor.options, styleName, field);
} else {
throw new Error(`${descriptor} not implemented`);
}
@ -502,7 +503,8 @@ export class VectorStyle extends AbstractStyle {
} else if (descriptor.type === StaticStyleProperty.type) {
return new StaticOrientationProperty(descriptor.options, styleName);
} else if (descriptor.type === DynamicStyleProperty.type) {
return new DynamicOrientationProperty(descriptor.options, styleName);
const field = this._makeField(descriptor.options.field);
return new DynamicOrientationProperty(descriptor.options, styleName, field);
} else {
throw new Error(`${descriptor} not implemented`);
}

View file

@ -7,6 +7,35 @@
import { VectorStyle } from './vector_style';
import { DataRequest } from '../../util/data_request';
import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
import { FIELD_ORIGIN } from '../../../../common/constants';
class MockField {
constructor({ fieldName }) {
this._fieldName = fieldName;
}
getName() {
return this._fieldName;
}
isValid() {
return !!this._fieldName;
}
}
class MockSource {
constructor({ supportedShapeTypes } = {}) {
this._supportedShapeTypes = supportedShapeTypes || Object.values(VECTOR_SHAPE_TYPES);
}
getSupportedShapeTypes() {
return this._supportedShapeTypes;
}
createField({ fieldName }) {
return new MockField({ fieldName });
}
}
describe('getDescriptorWithMissingStylePropsRemoved', () => {
const fieldName = 'doIStillExist';
@ -17,29 +46,32 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => {
},
lineColor: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {}
options: {
'field': {
'name': fieldName,
'origin': FIELD_ORIGIN.SOURCE
}
}
},
iconSize: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
color: 'a color',
field: { name: fieldName }
field: { name: fieldName, origin: FIELD_ORIGIN.SOURCE }
}
}
};
it('Should return no changes when next oridinal fields contain existing style property fields', () => {
const vectorStyle = new VectorStyle({ properties });
const vectorStyle = new VectorStyle({ properties }, new MockSource());
const nextOridinalFields = [
{ name: fieldName }
];
const nextOridinalFields = [new MockField({ fieldName })];
const { hasChanges } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextOridinalFields);
expect(hasChanges).toBe(false);
});
it('Should clear missing fields when next oridinal fields do not contain existing style property fields', () => {
const vectorStyle = new VectorStyle({ properties });
const vectorStyle = new VectorStyle({ properties }, new MockSource());
const nextOridinalFields = [];
const { hasChanges, nextStyleDescriptor } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextOridinalFields);
@ -83,12 +115,6 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => {
describe('pluckStyleMetaFromSourceDataRequest', () => {
const sourceMock = {
getSupportedShapeTypes: () => {
return Object.values(VECTOR_SHAPE_TYPES);
}
};
describe('has features', () => {
it('Should identify when feature collection only contains points', async () => {
const sourceDataRequest = new DataRequest({
@ -110,7 +136,7 @@ describe('pluckStyleMetaFromSourceDataRequest', () => {
],
}
});
const vectorStyle = new VectorStyle({}, sourceMock);
const vectorStyle = new VectorStyle({}, new MockSource());
const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest);
expect(featuresMeta.hasFeatureType).toEqual({
@ -140,7 +166,7 @@ describe('pluckStyleMetaFromSourceDataRequest', () => {
],
}
});
const vectorStyle = new VectorStyle({}, sourceMock);
const vectorStyle = new VectorStyle({}, new MockSource());
const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest);
expect(featuresMeta.hasFeatureType).toEqual({
@ -183,12 +209,13 @@ describe('pluckStyleMetaFromSourceDataRequest', () => {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
field: {
origin: FIELD_ORIGIN.SOURCE,
name: 'myDynamicFieldWithNoValues'
}
}
}
}
}, sourceMock);
}, new MockSource());
const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest);
expect(featuresMeta.hasFeatureType).toEqual({
@ -205,12 +232,13 @@ describe('pluckStyleMetaFromSourceDataRequest', () => {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
field: {
origin: FIELD_ORIGIN.SOURCE,
name: 'myDynamicField'
}
}
}
}
}, sourceMock);
}, new MockSource());
const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest);
expect(featuresMeta.myDynamicField).toEqual({
@ -226,32 +254,24 @@ describe('pluckStyleMetaFromSourceDataRequest', () => {
describe('checkIfOnlyFeatureType', () => {
describe('source supports single feature type', () => {
const sourceMock = {
getSupportedShapeTypes: () => {
return [VECTOR_SHAPE_TYPES.POINT];
}
};
it('isPointsOnly should be true when source feature type only supports points', async () => {
const vectorStyle = new VectorStyle({}, sourceMock);
const vectorStyle = new VectorStyle({}, new MockSource({
supportedShapeTypes: [VECTOR_SHAPE_TYPES.POINT]
}));
const isPointsOnly = await vectorStyle._getIsPointsOnly();
expect(isPointsOnly).toBe(true);
});
it('isLineOnly should be false when source feature type only supports points', async () => {
const vectorStyle = new VectorStyle({}, sourceMock);
const vectorStyle = new VectorStyle({}, new MockSource({
supportedShapeTypes: [VECTOR_SHAPE_TYPES.POINT]
}));
const isLineOnly = await vectorStyle._getIsLinesOnly();
expect(isLineOnly).toBe(false);
});
});
describe('source supports multiple feature types', () => {
const sourceMock = {
getSupportedShapeTypes: () => {
return Object.values(VECTOR_SHAPE_TYPES);
}
};
it('isPointsOnly should be true when data contains just points', async () => {
const vectorStyle = new VectorStyle({
__styleMeta: {
@ -261,7 +281,9 @@ describe('checkIfOnlyFeatureType', () => {
POLYGON: false
}
}
}, sourceMock);
}, new MockSource({
supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES)
}));
const isPointsOnly = await vectorStyle._getIsPointsOnly();
expect(isPointsOnly).toBe(true);
});
@ -275,7 +297,9 @@ describe('checkIfOnlyFeatureType', () => {
POLYGON: false
}
}
}, sourceMock);
}, new MockSource({
supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES)
}));
const isPointsOnly = await vectorStyle._getIsPointsOnly();
expect(isPointsOnly).toBe(false);
});
@ -289,7 +313,9 @@ describe('checkIfOnlyFeatureType', () => {
POLYGON: true
}
}
}, sourceMock);
}, new MockSource({
supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES)
}));
const isPointsOnly = await vectorStyle._getIsPointsOnly();
expect(isPointsOnly).toBe(false);
});

View file

@ -12,10 +12,6 @@ export class TileLayer extends AbstractLayer {
static type = LAYER_TYPE.TILE;
constructor({ layerDescriptor, source, style }) {
super({ layerDescriptor, source, style });
}
static createDescriptor(options) {
const tileLayerDescriptor = super.createDescriptor(options);
tileLayerDescriptor.type = TileLayer.type;

View file

@ -14,6 +14,7 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty {
super(propertyKey, propertyName, rawValue, indexPattern);
this._metricField = metricField;
}
isFilterable() {
return false;
}
@ -22,10 +23,10 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty {
if (typeof this._rawValue === 'undefined') {
return '-';
}
if (this._metricField.type === METRIC_TYPE.COUNT || this._metricField.type === METRIC_TYPE.UNIQUE_COUNT) {
if (this._metricField.getAggType() === METRIC_TYPE.COUNT || this._metricField.getAggType() === METRIC_TYPE.UNIQUE_COUNT) {
return this._rawValue;
}
const indexPatternField = this._indexPattern.fields.getByName(this._metricField.field);
const indexPatternField = this._indexPattern.fields.getByName(this._metricField.getESDocFieldName());
if (!indexPatternField) {
return this._rawValue;
}

View file

@ -38,12 +38,14 @@ export class JoinTooltipProperty extends TooltipProperty {
for (let i = 0; i < this._leftInnerJoins.length; i++) {
const rightSource = this._leftInnerJoins[i].getRightJoinSource();
const esTooltipProperty = await rightSource.createESTooltipProperty(
rightSource.getTerm(),
this._tooltipProperty.getRawValue()
);
if (esTooltipProperty) {
esFilters.push(...(await esTooltipProperty.getESFilters()));
const termField = rightSource.getTermField();
try {
const esTooltipProperty = await termField.createTooltipProperty(this._tooltipProperty.getRawValue());
if (esTooltipProperty) {
esFilters.push(...(await esTooltipProperty.getESFilters()));
}
} catch(e) {
console.error('Cannot create joined filter', e);
}
}

View file

@ -15,8 +15,7 @@ import {
SOURCE_DATA_ID_ORIGIN,
FEATURE_VISIBLE_PROPERTY_NAME,
EMPTY_FEATURE_COLLECTION,
LAYER_TYPE,
FIELD_ORIGIN,
LAYER_TYPE
} from '../../common/constants';
import _ from 'lodash';
import { JoinTooltipProperty } from './tooltips/join_tooltip_property';
@ -91,9 +90,11 @@ export class VectorLayer extends AbstractLayer {
this._joins = [];
if (options.layerDescriptor.joins) {
options.layerDescriptor.joins.forEach((joinDescriptor) => {
this._joins.push(new InnerJoin(joinDescriptor, this._source.getInspectorAdapters()));
const join = new InnerJoin(joinDescriptor, this._source);
this._joins.push(join);
});
}
this._style = new VectorStyle(this._descriptor.style, this._source, this);
}
destroy() {
@ -181,26 +182,8 @@ export class VectorLayer extends AbstractLayer {
return this._style.getDynamicPropertiesArray().length > 0;
}
getLegendDetails() {
const getFieldLabel = async fieldName => {
const ordinalFields = await this._getOrdinalFields();
const field = ordinalFields.find(({ name }) => {
return name === fieldName;
});
return field ? field.label : fieldName;
};
const getFieldFormatter = async field => {
const source = this._getFieldSource(field);
if (!source) {
return null;
}
return await source.getFieldFormatter(field.name);
};
return this._style.getLegendDetails(getFieldLabel, getFieldFormatter);
renderLegendDetails() {
return this._style.renderLegendDetails();
}
_getBoundsBasedOnData() {
@ -241,46 +224,24 @@ export class VectorLayer extends AbstractLayer {
return this._source.getDisplayName();
}
async getDateFields() {
const timeFields = await this._source.getDateFields();
return timeFields.map(({ label, name }) => {
return {
label,
name,
origin: SOURCE_DATA_ID_ORIGIN
};
});
return await this._source.getDateFields();
}
async getNumberFields() {
const numberFields = await this._source.getNumberFields();
const numberFieldOptions = numberFields.map(({ label, name }) => {
return {
label,
name,
origin: FIELD_ORIGIN.SOURCE
};
});
const numberFieldOptions = await this._source.getNumberFields();
const joinFields = [];
this.getValidJoins().forEach(join => {
const fields = join.getJoinFields().map(joinField => {
return {
...joinField,
origin: FIELD_ORIGIN.JOIN,
};
});
const fields = join.getJoinFields();
joinFields.push(...fields);
});
return [...numberFieldOptions, ...joinFields];
}
async _getOrdinalFields() {
async getOrdinalFields() {
return [
... await this.getDateFields(),
... await this.getNumberFields()
...await this.getDateFields(),
...await this.getNumberFields()
];
}
@ -391,7 +352,7 @@ export class VectorLayer extends AbstractLayer {
const joinSource = join.getRightJoinSource();
const sourceDataId = join.getSourceId();
const requestToken = Symbol(`layer-join-refresh:${ this.getId()} - ${sourceDataId}`);
const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`);
const searchFilters = {
...dataFilters,
@ -418,7 +379,7 @@ export class VectorLayer extends AbstractLayer {
} = await joinSource.getPropertiesMap(
searchFilters,
leftSourceName,
join.getLeftFieldName(),
join.getLeftField().getName(),
registerCancelCallback.bind(null, requestToken));
stopLoading(sourceDataId, requestToken, propertiesMap);
return {
@ -450,9 +411,7 @@ export class VectorLayer extends AbstractLayer {
const fieldNames = [
...this._source.getFieldNames(),
...this._style.getSourceFieldNames(),
...this.getValidJoins().map(join => {
return join.getLeftFieldName();
})
...this.getValidJoins().map(join => join.getLeftField().getName())
];
return {
@ -484,9 +443,8 @@ export class VectorLayer extends AbstractLayer {
let isFeatureVisible = true;
for (let j = 0; j < joinStates.length; j++) {
const joinState = joinStates[j];
const InnerJoin = joinState.join;
const rightMetricFields = InnerJoin.getRightMetricFields();
const canJoinOnCurrent = InnerJoin.joinPropertiesToFeature(feature, joinState.propertiesMap, rightMetricFields);
const innerJoin = joinState.join;
const canJoinOnCurrent = innerJoin.joinPropertiesToFeature(feature, joinState.propertiesMap);
isFeatureVisible = isFeatureVisible && canJoinOnCurrent;
}
@ -506,7 +464,7 @@ export class VectorLayer extends AbstractLayer {
startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters
}) {
const requestToken = Symbol(`layer-source-refresh:${ this.getId()} - source`);
const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`);
const searchFilters = this._getSearchFilters(dataFilters);
const canSkip = await this._canSkipSourceUpdate(this._source, SOURCE_DATA_ID_ORIGIN, searchFilters);
if (canSkip) {
@ -543,7 +501,7 @@ export class VectorLayer extends AbstractLayer {
_assignIdsToFeatures(featureCollection) {
//wrt https://github.com/elastic/kibana/issues/39317
// In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana.
//In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana.
//This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id.
//This is a work-around to avoid hitting such a worst-case
//This was tested as a suitable work-around for mapbox-gl 0.54
@ -770,7 +728,7 @@ export class VectorLayer extends AbstractLayer {
const tooltipProperty = tooltipsFromSource[i];
const matchingJoins = [];
for (let j = 0; j < this._joins.length; j++) {
if (this._joins[j].getLeftFieldName() === tooltipProperty.getPropertyKey()) {
if (this._joins[j].getLeftField().getName() === tooltipProperty.getPropertyKey()) {
matchingJoins.push(this._joins[j]);
}
}
@ -806,28 +764,4 @@ export class VectorLayer extends AbstractLayer {
return feature.properties[FEATURE_ID_PROPERTY_NAME] === id;
});
}
_getFieldSource(field) {
if (!field) {
return null;
}
if (field.origin === FIELD_ORIGIN.SOURCE) {
return this._source;
}
const join = this.getValidJoins().find(join => {
const matchingField = join.getJoinFields().find(joinField => {
return joinField.name === field.name;
});
return !!matchingField;
});
if (!join) {
return null;
}
return join.getRightJoinSource();
}
}

View file

@ -25,10 +25,6 @@ export class VectorTileLayer extends TileLayer {
static type = LAYER_TYPE.VECTOR_TILE;
constructor({ layerDescriptor, source, style }) {
super({ layerDescriptor, source, style });
}
static createDescriptor(options) {
const tileLayerDescriptor = super.createDescriptor(options);
tileLayerDescriptor.type = VectorTileLayer.type;

View file

@ -11,24 +11,22 @@ import { VectorTileLayer } from '../layers/vector_tile_layer';
import { VectorLayer } from '../layers/vector_layer';
import { HeatmapLayer } from '../layers/heatmap_layer';
import { ALL_SOURCES } from '../layers/sources/all_sources';
import { VectorStyle } from '../layers/styles/vector/vector_style';
import { HeatmapStyle } from '../layers/styles/heatmap/heatmap_style';
import { timefilter } from 'ui/timefilter';
import { getInspectorAdapters } from '../reducers/non_serializable_instances';
import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util';
function createLayerInstance(layerDescriptor, inspectorAdapters) {
const source = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters);
const style = createStyleInstance(layerDescriptor.style, source);
switch (layerDescriptor.type) {
case TileLayer.type:
return new TileLayer({ layerDescriptor, source, style });
return new TileLayer({ layerDescriptor, source });
case VectorLayer.type:
return new VectorLayer({ layerDescriptor, source, style });
return new VectorLayer({ layerDescriptor, source });
case VectorTileLayer.type:
return new VectorTileLayer({ layerDescriptor, source, style });
return new VectorTileLayer({ layerDescriptor, source });
case HeatmapLayer.type:
return new HeatmapLayer({ layerDescriptor, source, style });
return new HeatmapLayer({ layerDescriptor, source });
default:
throw new Error(`Unrecognized layerType ${layerDescriptor.type}`);
}
@ -44,25 +42,6 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) {
return new Source(sourceDescriptor, inspectorAdapters);
}
function createStyleInstance(styleDescriptor, source) {
if (!styleDescriptor || !styleDescriptor.type) {
return null;
}
switch (styleDescriptor.type) {
case 'TILE'://backfill for old tilestyles.
return null;
case VectorStyle.type:
return new VectorStyle(styleDescriptor, source);
case HeatmapStyle.type:
return new HeatmapStyle(styleDescriptor);
default:
throw new Error(`Unrecognized styleType ${styleDescriptor.type}`);
}
}
export const getTooltipState = ({ map }) => {
return map.tooltipState;
};

View file

@ -6623,7 +6623,6 @@
"xpack.maps.source.wmsTitle": "ウェブマップサービス",
"xpack.maps.style.heatmap.displayNameLabel": "ヒートマップスタイル",
"xpack.maps.style.heatmap.resolutionStyleErrorMessage": "解像度パラメーターが認識されません: {resolution}",
"xpack.maps.style.vector.displayNameLabel": "ベクタースタイル",
"xpack.maps.styles.staticDynamic.dynamicDescription": "プロパティ値で特徴をシンボル化します。",
"xpack.maps.styles.staticDynamic.staticDescription": "静的スタイルプロパティで特徴をシンボル化します。",
"xpack.maps.styles.vector.borderColorLabel": "境界線の色",

View file

@ -6564,7 +6564,6 @@
"xpack.maps.source.wmsTitle": "Web 地图服务",
"xpack.maps.style.heatmap.displayNameLabel": "热图样式",
"xpack.maps.style.heatmap.resolutionStyleErrorMessage": "无法识别分辨率参数:{resolution}",
"xpack.maps.style.vector.displayNameLabel": "矢量样式",
"xpack.maps.styles.staticDynamic.dynamicDescription": "使用属性值代表功能。",
"xpack.maps.styles.staticDynamic.staticDescription": "使用静态样式属性代表功能。",
"xpack.maps.styles.vector.borderColorLabel": "边框颜色",