[Maps] fix data-mapping switch enabled for vector tiles (#116366)

* clean up IField API

* disable switch when using MVTs for es docs

* clean up interface comment style

* implement supportsFieldMetaFromEs and supportsFieldMetaFromLocalData in all Field classes

* fix dynamic_color_property test

* fix jest tests

* mock getRangeFieldMeta instead of passing in VectorLayerMock with MockStyle

* review feedback

* clean up supportsFieldMetaFromLocalData test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2021-10-29 12:47:58 -06:00 committed by GitHub
parent d284d65ad4
commit ee61368cff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 524 additions and 268 deletions

View file

@ -17,25 +17,48 @@ const defaultParams = {
origin: FIELD_ORIGIN.SOURCE,
};
describe('supportsFieldMeta', () => {
test('Non-counting aggregations should support field meta', () => {
describe('supportsFieldMetaFromEs', () => {
test('Non-counting aggregations should support field meta from ES', () => {
const avgMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.AVG });
expect(avgMetric.supportsFieldMeta()).toBe(true);
expect(avgMetric.supportsFieldMetaFromEs()).toBe(true);
const maxMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.MAX });
expect(maxMetric.supportsFieldMeta()).toBe(true);
expect(maxMetric.supportsFieldMetaFromEs()).toBe(true);
const minMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.MIN });
expect(minMetric.supportsFieldMeta()).toBe(true);
expect(minMetric.supportsFieldMetaFromEs()).toBe(true);
const termsMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.TERMS });
expect(termsMetric.supportsFieldMeta()).toBe(true);
expect(termsMetric.supportsFieldMetaFromEs()).toBe(true);
});
test('Counting aggregations should not support field meta', () => {
test('Counting aggregations should not support field meta from ES', () => {
const sumMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.SUM });
expect(sumMetric.supportsFieldMeta()).toBe(false);
expect(sumMetric.supportsFieldMetaFromEs()).toBe(false);
const uniqueCountMetric = new AggField({
...defaultParams,
aggType: AGG_TYPE.UNIQUE_COUNT,
});
expect(uniqueCountMetric.supportsFieldMeta()).toBe(false);
expect(uniqueCountMetric.supportsFieldMetaFromEs()).toBe(false);
});
});
describe('supportsFieldMetaFromLocalData', () => {
test('number metrics should support field meta from local', () => {
const avgMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.AVG });
expect(avgMetric.supportsFieldMetaFromLocalData()).toBe(true);
const maxMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.MAX });
expect(maxMetric.supportsFieldMetaFromLocalData()).toBe(true);
const minMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.MIN });
expect(minMetric.supportsFieldMetaFromLocalData()).toBe(true);
const sumMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.SUM });
expect(sumMetric.supportsFieldMetaFromLocalData()).toBe(true);
const uniqueCountMetric = new AggField({
...defaultParams,
aggType: AGG_TYPE.UNIQUE_COUNT,
});
expect(uniqueCountMetric.supportsFieldMetaFromLocalData()).toBe(true);
});
test('Non number metrics should not support field meta from local', () => {
const termMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.TERMS });
expect(termMetric.supportsFieldMetaFromLocalData()).toBe(false);
});
});

View file

@ -30,6 +30,16 @@ export class AggField extends CountAggField {
this._aggType = params.aggType;
}
supportsFieldMetaFromEs(): boolean {
// count and sum aggregations are not within field bounds so they do not support field meta.
return !isMetricCountable(this._getAggType());
}
supportsFieldMetaFromLocalData(): boolean {
// Elasticsearch vector tile search API returns meta tiles with numeric aggregation metrics.
return this._getDataTypeSynchronous() === 'number';
}
isValid(): boolean {
return !!this._esDocField;
}
@ -38,11 +48,6 @@ export class AggField extends CountAggField {
return this._source.isMvt() ? this.getName() + '.value' : this.getName();
}
supportsFieldMeta(): boolean {
// count and sum aggregations are not within field bounds so they do not support field meta.
return !isMetricCountable(this._getAggType());
}
canValueBeFormatted(): boolean {
return this._getAggType() !== AGG_TYPE.UNIQUE_COUNT;
}
@ -73,10 +78,14 @@ export class AggField extends CountAggField {
);
}
async getDataType(): Promise<string> {
_getDataTypeSynchronous(): string {
return this._getAggType() === AGG_TYPE.TERMS ? 'string' : 'number';
}
async getDataType(): Promise<string> {
return this._getDataTypeSynchronous();
}
getBucketCount(): number {
// terms aggregation increases the overall number of buckets per split bucket
return this._getAggType() === AGG_TYPE.TERMS ? TERMS_AGG_SHARD_SIZE : 0;

View file

@ -19,5 +19,4 @@ export interface CountAggFieldParams {
label?: string;
source: IESAggSource;
origin: FIELD_ORIGIN;
canReadFromGeoJson?: boolean;
}

View file

@ -18,9 +18,9 @@ const defaultParams = {
origin: FIELD_ORIGIN.SOURCE,
};
describe('supportsFieldMeta', () => {
describe('supportsFieldMetaFromEs', () => {
test('Counting aggregations should not support field meta', () => {
const countMetric = new CountAggField({ ...defaultParams });
expect(countMetric.supportsFieldMeta()).toBe(false);
expect(countMetric.supportsFieldMetaFromEs()).toBe(false);
});
});

View file

@ -18,13 +18,20 @@ export class CountAggField implements IESAggField {
protected readonly _source: IESAggSource;
private readonly _origin: FIELD_ORIGIN;
protected readonly _label?: string;
private readonly _canReadFromGeoJson: boolean;
constructor({ label, source, origin, canReadFromGeoJson = true }: CountAggFieldParams) {
constructor({ label, source, origin }: CountAggFieldParams) {
this._source = source;
this._origin = origin;
this._label = label;
this._canReadFromGeoJson = canReadFromGeoJson;
}
supportsFieldMetaFromEs(): boolean {
return false;
}
supportsFieldMetaFromLocalData(): boolean {
// Elasticsearch vector tile search API returns meta tiles for aggregation metrics
return true;
}
_getAggType(): AGG_TYPE {
@ -79,10 +86,6 @@ export class CountAggField implements IESAggField {
return null;
}
supportsFieldMeta(): boolean {
return false;
}
getBucketCount() {
return 0;
}
@ -103,14 +106,6 @@ export class CountAggField implements IESAggField {
return null;
}
supportsAutoDomain(): boolean {
return true;
}
canReadFromGeoJson(): boolean {
return this._canReadFromGeoJson;
}
isEqual(field: IESAggField) {
return field.getName() === this.getName();
}

View file

@ -18,8 +18,7 @@ import { PercentileAggField } from './percentile_agg_field';
export function esAggFieldsFactory(
aggDescriptor: AggDescriptor,
source: IESAggSource,
origin: FIELD_ORIGIN,
canReadFromGeoJson: boolean = true
origin: FIELD_ORIGIN
): IESAggField[] {
let aggField;
if (aggDescriptor.type === AGG_TYPE.COUNT) {
@ -27,7 +26,6 @@ export function esAggFieldsFactory(
label: aggDescriptor.label,
source,
origin,
canReadFromGeoJson,
});
} else if (aggDescriptor.type === AGG_TYPE.PERCENTILE) {
aggField = new PercentileAggField({
@ -42,7 +40,6 @@ export function esAggFieldsFactory(
: DEFAULT_PERCENTILE,
source,
origin,
canReadFromGeoJson,
});
} else {
aggField = new AggField({
@ -54,14 +51,13 @@ export function esAggFieldsFactory(
aggType: aggDescriptor.type,
source,
origin,
canReadFromGeoJson,
});
}
const aggFields: IESAggField[] = [aggField];
if ('field' in aggDescriptor && aggDescriptor.type === AGG_TYPE.TERMS) {
aggFields.push(new TopTermPercentageField(aggField, canReadFromGeoJson));
aggFields.push(new TopTermPercentageField(aggField));
}
return aggFields;

View file

@ -31,7 +31,12 @@ export class PercentileAggField extends AggField implements IESAggField {
this._percentile = params.percentile;
}
supportsFieldMeta(): boolean {
supportsFieldMetaFromEs(): boolean {
return true;
}
supportsFieldMetaFromLocalData(): boolean {
// Elasticsearch vector tile search API returns meta tiles for aggregation metrics
return true;
}

View file

@ -12,11 +12,18 @@ import { TOP_TERM_PERCENTAGE_SUFFIX, FIELD_ORIGIN } from '../../../../common/con
export class TopTermPercentageField implements IESAggField {
private readonly _topTermAggField: IESAggField;
private readonly _canReadFromGeoJson: boolean;
constructor(topTermAggField: IESAggField, canReadFromGeoJson: boolean = true) {
constructor(topTermAggField: IESAggField) {
this._topTermAggField = topTermAggField;
this._canReadFromGeoJson = canReadFromGeoJson;
}
supportsFieldMetaFromEs(): boolean {
return false;
}
supportsFieldMetaFromLocalData(): boolean {
// Elasticsearch vector tile search API does not support top term metric
return false;
}
getSource(): IVectorSource {
@ -64,15 +71,6 @@ export class TopTermPercentageField implements IESAggField {
getBucketCount(): number {
return 0;
}
supportsAutoDomain(): boolean {
return this._canReadFromGeoJson;
}
supportsFieldMeta(): boolean {
return false;
}
async getExtendedStatsFieldMetaRequest(): Promise<unknown | null> {
return null;
}
@ -89,10 +87,6 @@ export class TopTermPercentageField implements IESAggField {
return false;
}
canReadFromGeoJson(): boolean {
return this._canReadFromGeoJson;
}
isEqual(field: IESAggField) {
return field.getName() === this.getName();
}

View file

@ -26,6 +26,14 @@ export class EMSFileField extends AbstractField implements IField {
this._source = source;
}
supportsFieldMetaFromEs(): boolean {
return false;
}
supportsFieldMetaFromLocalData(): boolean {
return true;
}
getSource(): IVectorSource {
return this._source;
}

View file

@ -16,22 +16,27 @@ import { IVectorSource } from '../sources/vector_source';
export class ESDocField extends AbstractField implements IField {
private readonly _source: IESSource;
private readonly _canReadFromGeoJson: boolean;
constructor({
fieldName,
source,
origin,
canReadFromGeoJson = true,
}: {
fieldName: string;
source: IESSource;
origin: FIELD_ORIGIN;
canReadFromGeoJson?: boolean;
}) {
super({ fieldName, origin });
this._source = source;
this._canReadFromGeoJson = canReadFromGeoJson;
}
supportsFieldMetaFromEs(): boolean {
return true;
}
supportsFieldMetaFromLocalData(): boolean {
// Elasticsearch vector tile search API does not return meta tiles for documents
return !this.getSource().isMvt();
}
canValueBeFormatted(): boolean {
@ -73,14 +78,6 @@ export class ESDocField extends AbstractField implements IField {
: super.getLabel();
}
supportsFieldMeta(): boolean {
return true;
}
canReadFromGeoJson(): boolean {
return this._canReadFromGeoJson;
}
async getExtendedStatsFieldMetaRequest(): Promise<unknown | null> {
const indexPatternField = await this._getIndexPatternField();

View file

@ -22,19 +22,22 @@ export interface IField {
isValid(): boolean;
getExtendedStatsFieldMetaRequest(): Promise<unknown | null>;
getPercentilesFieldMetaRequest(percentiles: number[]): Promise<unknown | null>;
getCategoricalFieldMetaRequest(size: number): Promise<unknown>;
getCategoricalFieldMetaRequest(size: number): Promise<unknown | null>;
// Whether Maps-app can automatically determine the domain of the field-values
// if this is not the case (e.g. for .mvt tiled data),
// then styling properties that require the domain to be known cannot use this property.
supportsAutoDomain(): boolean;
/*
* IField.supportsFieldMetaFromLocalData returns boolean indicating whether field value domain
* can be determined from local data
*/
supportsFieldMetaFromLocalData(): boolean;
// Whether Maps-app can automatically determine the domain of the field-values
// _without_ having to retrieve the data as GeoJson
// e.g. for ES-sources, this would use the extended_stats API
supportsFieldMeta(): boolean;
/*
* IField.supportsFieldMetaFromEs returns boolean indicating whether field value domain
* can be determined from Elasticsearch.
* When true, getExtendedStatsFieldMetaRequest, getPercentilesFieldMetaRequest, and getCategoricalFieldMetaRequest
* can not return null
*/
supportsFieldMetaFromEs(): boolean;
canReadFromGeoJson(): boolean;
isEqual(field: IField): boolean;
}
@ -47,6 +50,14 @@ export class AbstractField implements IField {
this._origin = origin || FIELD_ORIGIN.SOURCE;
}
supportsFieldMetaFromEs(): boolean {
throw new Error('must implement AbstractField#supportsFieldMetaFromEs');
}
supportsFieldMetaFromLocalData(): boolean {
throw new Error('must implement AbstractField#supportsFieldMetaFromLocalData');
}
getName(): string {
return this._fieldName;
}
@ -64,7 +75,7 @@ export class AbstractField implements IField {
}
getSource(): IVectorSource {
throw new Error('must implement Field#getSource');
throw new Error('must implement AbstractField#getSource');
}
isValid(): boolean {
@ -88,10 +99,6 @@ export class AbstractField implements IField {
return this._origin;
}
supportsFieldMeta(): boolean {
return false;
}
async getExtendedStatsFieldMetaRequest(): Promise<unknown> {
return null;
}
@ -104,14 +111,6 @@ export class AbstractField implements IField {
return null;
}
supportsAutoDomain(): boolean {
return true;
}
canReadFromGeoJson(): boolean {
return true;
}
isEqual(field: IField) {
return this._origin === field.getOrigin() && this._fieldName === field.getName();
}

View file

@ -29,6 +29,14 @@ export class InlineField<T extends IVectorSource> extends AbstractField implemen
this._dataType = dataType;
}
supportsFieldMetaFromEs(): boolean {
return false;
}
supportsFieldMetaFromLocalData(): boolean {
return true;
}
getSource(): IVectorSource {
return this._source;
}

View file

@ -30,6 +30,14 @@ export class MVTField extends AbstractField implements IField {
this._type = type;
}
supportsFieldMetaFromEs(): boolean {
return false;
}
supportsFieldMetaFromLocalData(): boolean {
return false;
}
getMVTFieldDescriptor(): MVTFieldDescriptor {
return {
type: this._type,
@ -54,12 +62,4 @@ export class MVTField extends AbstractField implements IField {
async getLabel(): Promise<string> {
return this.getName();
}
supportsAutoDomain() {
return false;
}
canReadFromGeoJson(): boolean {
return false;
}
}

View file

@ -30,7 +30,6 @@ export interface IESAggSource extends IESSource {
export abstract class AbstractESAggSource extends AbstractESSource implements IESAggSource {
private readonly _metricFields: IESAggField[];
private readonly _canReadFromGeoJson: boolean;
static createDescriptor(
descriptor: Partial<AbstractESAggSourceDescriptor>
@ -44,23 +43,13 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE
};
}
constructor(
descriptor: AbstractESAggSourceDescriptor,
inspectorAdapters?: Adapters,
canReadFromGeoJson = true
) {
constructor(descriptor: AbstractESAggSourceDescriptor, inspectorAdapters?: Adapters) {
super(descriptor, inspectorAdapters);
this._metricFields = [];
this._canReadFromGeoJson = canReadFromGeoJson;
if (descriptor.metrics) {
descriptor.metrics.forEach((aggDescriptor: AggDescriptor) => {
this._metricFields.push(
...esAggFieldsFactory(
aggDescriptor,
this,
this.getOriginForField(),
this._canReadFromGeoJson
)
...esAggFieldsFactory(aggDescriptor, this, this.getOriginForField())
);
});
}
@ -89,12 +78,7 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE
const metrics = this._metricFields.filter((esAggField) => esAggField.isValid());
// Handle case where metrics is empty because older saved object state is empty array or there are no valid aggs.
return metrics.length === 0
? esAggFieldsFactory(
{ type: AGG_TYPE.COUNT },
this,
this.getOriginForField(),
this._canReadFromGeoJson
)
? esAggFieldsFactory({ type: AGG_TYPE.COUNT }, this, this.getOriginForField())
: metrics;
}

View file

@ -83,11 +83,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle
constructor(descriptor: Partial<ESGeoGridSourceDescriptor>, inspectorAdapters?: Adapters) {
const sourceDescriptor = ESGeoGridSource.createDescriptor(descriptor);
super(
sourceDescriptor,
inspectorAdapters,
descriptor.resolution !== GRID_RESOLUTION.SUPER_FINE
);
super(sourceDescriptor, inspectorAdapters);
this._descriptor = sourceDescriptor;
}

View file

@ -85,7 +85,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
constructor(descriptor: Partial<ESGeoLineSourceDescriptor>, inspectorAdapters?: Adapters) {
const sourceDescriptor = ESGeoLineSource.createDescriptor(descriptor);
super(sourceDescriptor, inspectorAdapters, true);
super(sourceDescriptor, inspectorAdapters);
this._descriptor = sourceDescriptor;
}
@ -140,7 +140,6 @@ export class ESGeoLineSource extends AbstractESAggSource {
fieldName: this._descriptor.splitField,
source: this,
origin: FIELD_ORIGIN.SOURCE,
canReadFromGeoJson: true,
});
}

View file

@ -152,20 +152,4 @@ describe('ESSearchSource', () => {
);
});
});
describe('getFields', () => {
it('default', () => {
const esSearchSource = new ESSearchSource(mockDescriptor);
const docField = esSearchSource.createField({ fieldName: 'prop1' });
expect(docField.canReadFromGeoJson()).toBe(true);
});
it('mvt', () => {
const esSearchSource = new ESSearchSource({
...mockDescriptor,
scalingType: SCALING_TYPES.MVT,
});
const docField = esSearchSource.createField({ fieldName: 'prop1' });
expect(docField.canReadFromGeoJson()).toBe(false);
});
});
});

View file

@ -154,7 +154,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
fieldName,
source: this,
origin: FIELD_ORIGIN.SOURCE,
canReadFromGeoJson: this._descriptor.scalingType !== SCALING_TYPES.MVT,
});
}

View file

@ -15,6 +15,7 @@ import { FieldMetaOptions } from '../../../../../../common/descriptor_types';
interface Props<DynamicOptions> {
fieldMetaOptions: FieldMetaOptions;
onChange: (updatedOptions: DynamicOptions) => void;
supportsFieldMetaFromLocalData: boolean;
}
export function CategoricalDataMappingPopover<DynamicOptions>(props: Props<DynamicOptions>) {
@ -38,6 +39,7 @@ export function CategoricalDataMappingPopover<DynamicOptions>(props: Props<Dynam
})}
checked={props.fieldMetaOptions.isEnabled}
onChange={onIsEnabledChange}
disabled={!props.supportsFieldMetaFromLocalData}
compressed
/>{' '}
<EuiToolTip

View file

@ -81,6 +81,7 @@ interface Props<DynamicOptions> {
onChange: (updatedOptions: DynamicOptions) => void;
dataMappingFunction: DATA_MAPPING_FUNCTION;
supportedDataMappingFunctions: DATA_MAPPING_FUNCTION[];
supportsFieldMetaFromLocalData: boolean;
}
export function OrdinalDataMappingPopover<DynamicOptions>(props: Props<DynamicOptions>) {
@ -167,6 +168,7 @@ export function OrdinalDataMappingPopover<DynamicOptions>(props: Props<DynamicOp
})}
checked={props.fieldMetaOptions.isEnabled}
onChange={onIsEnabledChange}
disabled={!props.supportsFieldMetaFromLocalData}
compressed
/>{' '}
<EuiToolTip

View file

@ -45,7 +45,7 @@ export function DynamicIconForm({
{...styleOptions}
styleProperty={styleProperty}
onChange={onIconMapChange}
isCustomOnly={!field.supportsAutoDomain()}
isCustomOnly={!field.supportsFieldMetaFromLocalData() && !field.supportsFieldMetaFromEs()}
/>
);
}

View file

@ -28,7 +28,11 @@ jest.mock('../../../../kibana_services', () => {
};
});
class MockField extends AbstractField {}
class MockField extends AbstractField {
supportsFieldMetaFromLocalData(): boolean {
return true;
}
}
function createLayerMock(numFields: number, supportedShapeTypes: VECTOR_SHAPE_TYPE[]) {
const fields: IField[] = [];

View file

@ -16,6 +16,7 @@ exports[`renderDataMappingPopover Should render OrdinalDataMappingPopover 1`] =
"PERCENTILES",
]
}
supportsFieldMetaFromLocalData={true}
/>
`;

View file

@ -18,6 +18,7 @@ import { Feature, Point } from 'geojson';
import { DynamicColorProperty } from './dynamic_color_property';
import {
COLOR_MAP_TYPE,
FIELD_ORIGIN,
RawValue,
DATA_MAPPING_FUNCTION,
VECTOR_STYLES,
@ -291,17 +292,27 @@ describe('supportsFieldMeta', () => {
expect(styleProp.supportsFieldMeta()).toEqual(true);
});
test('should not support fieldMeta when field does not support fieldMeta', () => {
const field = Object.create(mockField);
field.supportsFieldMeta = function () {
return false;
test('should not support fieldMeta when field does not support fieldMeta from ES', () => {
const field = {
supportsFieldMetaFromEs() {
return false;
},
} as unknown as IField;
const layer = {} as unknown as IVectorLayer;
const options = {
type: COLOR_MAP_TYPE.ORDINAL,
fieldMetaOptions: { isEnabled: true },
};
const dynamicStyleOptions = {
type: COLOR_MAP_TYPE.ORDINAL,
fieldMetaOptions,
};
const styleProp = makeProperty(dynamicStyleOptions, undefined, field);
const styleProp = new DynamicColorProperty(
options,
VECTOR_STYLES.LINE_COLOR,
field,
layer,
() => {
return (value: RawValue) => value + '_format';
}
);
expect(styleProp.supportsFieldMeta()).toEqual(false);
});
@ -382,12 +393,50 @@ describe('get mapbox color expression (via internal _getMbColor)', () => {
expect(colorProperty._getMbColor()).toBeNull();
});
test('should return mapbox expression for color ramp', async () => {
const dynamicStyleOptions = {
const field = {
getMbFieldName: () => {
return 'foobar';
},
getName: () => {
return 'foobar';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
supportsFieldMetaFromEs: () => {
return true;
},
getSource: () => {
return {
isMvt: () => {
return false;
},
};
},
} as unknown as IField;
const options = {
type: COLOR_MAP_TYPE.ORDINAL,
color: 'Blues',
fieldMetaOptions,
fieldMetaOptions: { isEnabled: true },
};
const colorProperty = makeProperty(dynamicStyleOptions);
const colorProperty = new DynamicColorProperty(
options,
VECTOR_STYLES.LINE_COLOR,
field,
{} as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
}
);
colorProperty.getRangeFieldMeta = () => {
return {
min: 0,
max: 100,
delta: 100,
};
};
expect(colorProperty._getMbColor()).toEqual([
'interpolate',
['linear'],
@ -445,17 +494,40 @@ describe('get mapbox color expression (via internal _getMbColor)', () => {
expect(colorProperty._getMbColor()).toBeNull();
});
test('should use `feature-state` by default', async () => {
const dynamicStyleOptions = {
test('should use `feature-state` for geojson source', async () => {
const field = {
getMbFieldName: () => {
return 'foobar';
},
getSource: () => {
return {
isMvt: () => {
return false;
},
};
},
} as unknown as IField;
const layer = {} as unknown as IVectorLayer;
const options = {
type: COLOR_MAP_TYPE.ORDINAL,
useCustomColorRamp: true,
customColorRamp: [
{ stop: 10, color: '#f7faff' },
{ stop: 100, color: '#072f6b' },
],
fieldMetaOptions,
fieldMetaOptions: { isEnabled: true },
};
const colorProperty = makeProperty(dynamicStyleOptions);
const colorProperty = new DynamicColorProperty(
options,
VECTOR_STYLES.LINE_COLOR,
field,
layer,
() => {
return (value: RawValue) => value + '_format';
}
);
expect(colorProperty._getMbColor()).toEqual([
'step',
[
@ -476,21 +548,40 @@ describe('get mapbox color expression (via internal _getMbColor)', () => {
]);
});
test('should use `get` when source cannot return raw geojson', async () => {
const field = Object.create(mockField);
field.canReadFromGeoJson = function () {
return false;
};
const dynamicStyleOptions = {
test('should use `get` for MVT source', async () => {
const field = {
getMbFieldName: () => {
return 'foobar';
},
getSource: () => {
return {
isMvt: () => {
return true;
},
};
},
} as unknown as IField;
const layer = {} as unknown as IVectorLayer;
const options = {
type: COLOR_MAP_TYPE.ORDINAL,
useCustomColorRamp: true,
customColorRamp: [
{ stop: 10, color: '#f7faff' },
{ stop: 100, color: '#072f6b' },
],
fieldMetaOptions,
fieldMetaOptions: { isEnabled: true },
};
const colorProperty = makeProperty(dynamicStyleOptions, undefined, field);
const colorProperty = new DynamicColorProperty(
options,
VECTOR_STYLES.LINE_COLOR,
field,
layer,
() => {
return (value: RawValue) => value + '_format';
}
);
expect(colorProperty._getMbColor()).toEqual([
'step',
[

View file

@ -101,7 +101,7 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
}
supportsFieldMeta() {
if (!this.isComplete() || !this._field || !this._field.supportsFieldMeta()) {
if (!this.isComplete() || !this._field || !this._field.supportsFieldMetaFromEs()) {
return false;
}

View file

@ -14,13 +14,10 @@ jest.mock('../components/vector_style_editor', () => ({
import React from 'react';
import { shallow } from 'enzyme';
// @ts-ignore
import { DynamicSizeProperty } from './dynamic_size_property';
import { RawValue, VECTOR_STYLES } from '../../../../../common/constants';
import { FIELD_ORIGIN, RawValue, VECTOR_STYLES } from '../../../../../common/constants';
import { IField } from '../../../fields/field';
import type { Map as MbMap } from '@kbn/mapbox-gl';
import { SizeDynamicOptions } from '../../../../../common/descriptor_types';
import { mockField, MockLayer, MockStyle } from './test_helpers/test_util';
import { IVectorLayer } from '../../../layers/vector_layer';
export class MockMbMap {
@ -38,31 +35,40 @@ export class MockMbMap {
}
}
const makeProperty = (
options: SizeDynamicOptions,
mockStyle: MockStyle,
field: IField = mockField
) => {
return new DynamicSizeProperty(
options,
VECTOR_STYLES.ICON_SIZE,
field,
new MockLayer(mockStyle) as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
},
false
);
};
const fieldMetaOptions = { isEnabled: true };
describe('renderLegendDetailRow', () => {
test('Should render as range', async () => {
const sizeProp = makeProperty(
{ minSize: 0, maxSize: 10, fieldMetaOptions },
new MockStyle({ min: 0, max: 100 })
const field = {
getLabel: async () => {
return 'foobar_label';
},
getName: () => {
return 'foodbar';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
supportsFieldMetaFromEs: () => {
return true;
},
} as unknown as IField;
const sizeProp = new DynamicSizeProperty(
{ minSize: 0, maxSize: 10, fieldMetaOptions: { isEnabled: true } },
VECTOR_STYLES.ICON_SIZE,
field,
{} as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
},
false
);
sizeProp.getRangeFieldMeta = () => {
return {
min: 0,
max: 100,
delta: 100,
};
};
const legendRow = sizeProp.renderLegendDetailRow();
const component = shallow(legendRow);
@ -76,10 +82,47 @@ describe('renderLegendDetailRow', () => {
describe('syncSize', () => {
test('Should sync with circle-radius prop', async () => {
const sizeProp = makeProperty(
{ minSize: 8, maxSize: 32, fieldMetaOptions },
new MockStyle({ min: 0, max: 100 })
const field = {
isValid: () => {
return true;
},
getName: () => {
return 'foodbar';
},
getMbFieldName: () => {
return 'foobar';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
getSource: () => {
return {
isMvt: () => {
return false;
},
};
},
supportsFieldMetaFromEs: () => {
return true;
},
} as unknown as IField;
const sizeProp = new DynamicSizeProperty(
{ minSize: 8, maxSize: 32, fieldMetaOptions: { isEnabled: true } },
VECTOR_STYLES.ICON_SIZE,
field,
{} as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
},
false
);
sizeProp.getRangeFieldMeta = () => {
return {
min: 0,
max: 100,
delta: 100,
};
};
const mockMbMap = new MockMbMap() as unknown as MbMap;
sizeProp.syncCircleRadiusWithMb('foobar', mockMbMap);
@ -112,10 +155,47 @@ describe('syncSize', () => {
});
test('Should truncate interpolate expression to max when no delta', async () => {
const sizeProp = makeProperty(
{ minSize: 8, maxSize: 32, fieldMetaOptions },
new MockStyle({ min: 100, max: 100 })
const field = {
isValid: () => {
return true;
},
getName: () => {
return 'foobar';
},
getMbFieldName: () => {
return 'foobar';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
getSource: () => {
return {
isMvt: () => {
return false;
},
};
},
supportsFieldMetaFromEs: () => {
return true;
},
} as unknown as IField;
const sizeProp = new DynamicSizeProperty(
{ minSize: 8, maxSize: 32, fieldMetaOptions: { isEnabled: true } },
VECTOR_STYLES.ICON_SIZE,
field,
{} as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
},
false
);
sizeProp.getRangeFieldMeta = () => {
return {
min: 100,
max: 100,
delta: 0,
};
};
const mockMbMap = new MockMbMap() as unknown as MbMap;
sizeProp.syncCircleRadiusWithMb('foobar', mockMbMap);

View file

@ -260,7 +260,7 @@ export class DynamicStyleProperty<T>
}
supportsFieldMeta() {
return this.isComplete() && !!this._field && this._field.supportsFieldMeta();
return this.isComplete() && !!this._field && this._field.supportsFieldMetaFromEs();
}
async getFieldMetaRequest() {
@ -287,7 +287,7 @@ export class DynamicStyleProperty<T>
}
supportsMbFeatureState() {
return !!this._field && this._field.canReadFromGeoJson();
return !!this._field && !this._field.getSource().isMvt();
}
getMbLookupFunction(): MB_LOOKUP_FUNCTION {
@ -466,13 +466,14 @@ export class DynamicStyleProperty<T>
}
renderDataMappingPopover(onChange: (updatedOptions: Partial<T>) => void) {
if (!this.supportsFieldMeta()) {
if (!this._field || !this.supportsFieldMeta()) {
return null;
}
return this.isCategorical() ? (
<CategoricalDataMappingPopover<T>
fieldMetaOptions={this.getFieldMetaOptions()}
onChange={onChange}
supportsFieldMetaFromLocalData={this._field.supportsFieldMetaFromLocalData()}
/>
) : (
<OrdinalDataMappingPopover<T>
@ -481,6 +482,7 @@ export class DynamicStyleProperty<T>
onChange={onChange}
dataMappingFunction={this.getDataMappingFunction()}
supportedDataMappingFunctions={this._getSupportedDataMappingFunctions()}
supportsFieldMetaFromLocalData={this._field.supportsFieldMetaFromLocalData()}
/>
);
}
@ -502,7 +504,7 @@ export class DynamicStyleProperty<T>
// They just re-use the original property-name
targetName = this._field.getName();
} else {
if (this._field.canReadFromGeoJson() && this._field.supportsAutoDomain()) {
if (!this._field.getSource().isMvt() && this._field.supportsFieldMetaFromLocalData()) {
// Geojson-sources can support rewrite
// e.g. field-formatters will create duplicate field
targetName = getComputedFieldName(this.getStyleName(), this._field.getName());

View file

@ -18,7 +18,7 @@ import { DynamicTextProperty } from './dynamic_text_property';
import { RawValue, VECTOR_STYLES } from '../../../../../common/constants';
import { IField } from '../../../fields/field';
import type { Map as MbMap } from '@kbn/mapbox-gl';
import { mockField, MockLayer, MockStyle } from './test_helpers/test_util';
import { MockLayer, MockStyle } from './test_helpers/test_util';
import { IVectorLayer } from '../../../layers/vector_layer';
export class MockMbMap {
@ -49,22 +49,37 @@ export class MockMbMap {
}
}
const makeProperty = (mockStyle: MockStyle, field: IField | null) => {
return new DynamicTextProperty(
{},
VECTOR_STYLES.LABEL_TEXT,
field,
new MockLayer(mockStyle) as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
}
);
};
describe('syncTextFieldWithMb', () => {
describe('with field', () => {
test('Should set', async () => {
const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), mockField);
test('Should set text-field', async () => {
const field = {
isValid: () => {
return true;
},
getName: () => {
return 'foobar';
},
getSource: () => {
return {
isMvt: () => {
return false;
},
};
},
supportsFieldMetaFromLocalData: () => {
return true;
},
} as unknown as IField;
const dynamicTextProperty = new DynamicTextProperty(
{},
VECTOR_STYLES.LABEL_TEXT,
field,
new MockLayer(new MockStyle({ min: 0, max: 100 })) as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
}
);
const mockMbMap = new MockMbMap() as unknown as MbMap;
dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap);
@ -77,8 +92,17 @@ describe('syncTextFieldWithMb', () => {
});
describe('without field', () => {
test('Should clear', async () => {
const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), null);
test('Should clear text-field', async () => {
const dynamicTextProperty = new DynamicTextProperty(
{},
VECTOR_STYLES.LABEL_TEXT,
null,
new MockLayer(new MockStyle({ min: 0, max: 100 })) as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
}
);
const mockMbMap = new MockMbMap([
'foobar',
['coalesce', ['get', '__kbn__dynamic__foobar__labelText'], ''],
@ -90,14 +114,22 @@ describe('syncTextFieldWithMb', () => {
expect(mockMbMap.getPaintPropertyCalls()).toEqual([['foobar', undefined]]);
});
test('Should not clear when already cleared', async () => {
test('Should not set or clear text-field', async () => {
// This verifies a weird edge-case in mapbox-gl, where setting the `text-field` layout-property to null causes tiles to be invalidated.
// This triggers a refetch of the tile during panning and zooming
// This affects vector-tile rendering in tiled_vector_layers with custom vector_styles
// It does _not_ affect EMS, since that does not have a code-path where a `text-field` need to be resynced.
// Do not remove this logic without verifying that mapbox-gl does not re-issue tile-requests for previously requested tiles
const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), null);
const dynamicTextProperty = new DynamicTextProperty(
{},
VECTOR_STYLES.LABEL_TEXT,
null,
new MockLayer(new MockStyle({ min: 0, max: 100 })) as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
}
);
const mockMbMap = new MockMbMap(undefined) as unknown as MbMap;
dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap);

View file

@ -19,22 +19,22 @@ import { IStyle } from '../../../style';
export class MockField extends AbstractField {
private readonly _dataType: string;
private readonly _supportsAutoDomain: boolean;
private readonly _supportsFieldMetaFromLocalData: boolean;
constructor({
fieldName,
origin = FIELD_ORIGIN.SOURCE,
dataType = 'string',
supportsAutoDomain = true,
supportsFieldMetaFromLocalData = true,
}: {
fieldName: string;
origin?: FIELD_ORIGIN;
dataType?: string;
supportsAutoDomain?: boolean;
supportsFieldMetaFromLocalData?: boolean;
}) {
super({ fieldName, origin });
this._dataType = dataType;
this._supportsAutoDomain = supportsAutoDomain;
this._supportsFieldMetaFromLocalData = supportsFieldMetaFromLocalData;
}
async getLabel(): Promise<string> {
@ -45,11 +45,11 @@ export class MockField extends AbstractField {
return this._dataType;
}
supportsAutoDomain(): boolean {
return this._supportsAutoDomain;
supportsFieldMetaFromLocalData(): boolean {
return this._supportsFieldMetaFromLocalData;
}
supportsFieldMeta(): boolean {
supportsFieldMetaFromEs(): boolean {
return true;
}
}

View file

@ -7,45 +7,76 @@
import { FIELD_ORIGIN, VECTOR_STYLES } from '../../../../common/constants';
import { createStyleFieldsHelper, StyleFieldsHelper } from './style_fields_helper';
import { AbstractField, IField } from '../../fields/field';
class MockField extends AbstractField {
private readonly _dataType: string;
private readonly _supportsAutoDomain: boolean;
constructor({ dataType, supportsAutoDomain }: { dataType: string; supportsAutoDomain: boolean }) {
super({ fieldName: 'foobar_' + dataType, origin: FIELD_ORIGIN.SOURCE });
this._dataType = dataType;
this._supportsAutoDomain = supportsAutoDomain;
}
async getDataType() {
return this._dataType;
}
supportsAutoDomain(): boolean {
return this._supportsAutoDomain;
}
}
import { IField } from '../../fields/field';
describe('StyleFieldHelper', () => {
describe('isFieldDataTypeCompatibleWithStyleType', () => {
async function createHelper(supportsAutoDomain: boolean): Promise<{
async function createHelper(supportsFieldMetaFromLocalData: boolean): Promise<{
styleFieldHelper: StyleFieldsHelper;
stringField: IField;
numberField: IField;
dateField: IField;
}> {
const stringField = new MockField({
dataType: 'string',
supportsAutoDomain,
});
const numberField = new MockField({
dataType: 'number',
supportsAutoDomain,
});
const dateField = new MockField({
dataType: 'date',
supportsAutoDomain,
});
const stringField = {
getDataType: async () => {
return 'string';
},
getLabel: async () => {
return 'foobar_string_label';
},
getName: () => {
return 'foobar_string';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
supportsFieldMetaFromLocalData: () => {
return supportsFieldMetaFromLocalData;
},
supportsFieldMetaFromEs: () => {
return false;
},
} as unknown as IField;
const numberField = {
getDataType: async () => {
return 'number';
},
getLabel: async () => {
return 'foobar_number_label';
},
getName: () => {
return 'foobar_number';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
supportsFieldMetaFromLocalData: () => {
return supportsFieldMetaFromLocalData;
},
supportsFieldMetaFromEs: () => {
return false;
},
} as unknown as IField;
const dateField = {
getDataType: async () => {
return 'date';
},
getLabel: async () => {
return 'foobar_date_label';
},
getName: () => {
return 'foobar_date';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
supportsFieldMetaFromLocalData: () => {
return supportsFieldMetaFromLocalData;
},
supportsFieldMetaFromEs: () => {
return false;
},
} as unknown as IField;
return {
styleFieldHelper: await createStyleFieldsHelper([stringField, numberField, dateField]),
stringField,

View file

@ -28,7 +28,7 @@ export async function createStyleFieldsHelper(fields: IField[]): Promise<StyleFi
name: field.getName(),
origin: field.getOrigin(),
type: await field.getDataType(),
supportsAutoDomain: field.supportsAutoDomain(),
supportsAutoDomain: field.supportsFieldMetaFromLocalData() || field.supportsFieldMetaFromEs(),
};
});
const styleFields = await Promise.all(promises);

View file

@ -107,11 +107,27 @@ describe('getDescriptorWithUpdatedStyleProps', () => {
const vectorStyle = new VectorStyle({ properties }, new MockSource());
const nextFields = [
new MockField({
fieldName: previousFieldName,
dataType: 'number',
supportsAutoDomain: false,
}),
{
getDataType: async () => {
return 'number';
},
getLabel: async () => {
return previousFieldName + '_label';
},
getName: () => {
return previousFieldName;
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
// ordinal field must support auto domain
supportsFieldMetaFromLocalData: () => {
return false;
},
supportsFieldMetaFromEs: () => {
return false;
},
},
];
const { hasChanges, nextStyleDescriptor } =
await vectorStyle.getDescriptorWithUpdatedStyleProps(nextFields, previousFields, mapColors);