mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[maps] metrics mask (#154983)
Fixes https://github.com/elastic/kibana/issues/55520 PR adds ability to set mask configuration for metric fields to hide features client side that are outside the range of the mask. <img width="600" alt="Screen Shot 2023-04-19 at 3 45 15 PM" src="https://user-images.githubusercontent.com/373691/233207269-854695dd-ea51-45cd-b092-3360092846bb.png"> <img width="600" alt="Screen Shot 2023-04-19 at 3 44 45 PM" src="https://user-images.githubusercontent.com/373691/233207329-e0c7f20c-e789-446c-8994-87bcdf8e4bbe.png"> For vector layer, I could not use easier solution of filter expression since filter expressions do not support feature-state. Instead, implementation sets opacity paint property to 0. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ef64acf405
commit
a108b8c9c6
51 changed files with 1022 additions and 140 deletions
|
@ -188,10 +188,6 @@ export const GEOCENTROID_AGG_NAME = 'gridCentroid';
|
|||
export const TOP_TERM_PERCENTAGE_SUFFIX = '__percentage';
|
||||
export const DEFAULT_PERCENTILE = 50;
|
||||
|
||||
export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', {
|
||||
defaultMessage: 'count',
|
||||
});
|
||||
|
||||
export const COUNT_PROP_NAME = 'doc_count';
|
||||
|
||||
export enum STYLE_TYPE {
|
||||
|
@ -341,6 +337,11 @@ export enum WIZARD_ID {
|
|||
TMS_LAYER = 'tmsLayer',
|
||||
}
|
||||
|
||||
export enum MASK_OPERATOR {
|
||||
ABOVE = 'ABOVE',
|
||||
BELOW = 'BELOW',
|
||||
}
|
||||
|
||||
// Maplibre does not provide any feedback when rendering is complete.
|
||||
// Workaround is hard-coded timeout period.
|
||||
export const RENDER_TIMEOUT = 1000;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SortDirection } from '@kbn/data-plugin/common/search';
|
|||
import {
|
||||
AGG_TYPE,
|
||||
GRID_RESOLUTION,
|
||||
MASK_OPERATOR,
|
||||
RENDER_AS,
|
||||
SCALING_TYPES,
|
||||
MVT_FIELD_TYPE,
|
||||
|
@ -49,6 +50,10 @@ export type AbstractESSourceDescriptor = AbstractSourceDescriptor & {
|
|||
type AbstractAggDescriptor = {
|
||||
type: AGG_TYPE;
|
||||
label?: string;
|
||||
mask?: {
|
||||
operator: MASK_OPERATOR;
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type CountAggDescriptor = AbstractAggDescriptor & {
|
||||
|
|
|
@ -9,14 +9,17 @@ import { DataView } from '@kbn/data-plugin/common';
|
|||
import { IField } from '../field';
|
||||
import { IESAggSource } from '../../sources/es_agg_source';
|
||||
import { FIELD_ORIGIN } from '../../../../common/constants';
|
||||
import { AggDescriptor } from '../../../../common/descriptor_types';
|
||||
|
||||
export interface IESAggField extends IField {
|
||||
getValueAggDsl(indexPattern: DataView): unknown | null;
|
||||
getBucketCount(): number;
|
||||
getMask(): AggDescriptor['mask'] | undefined;
|
||||
}
|
||||
|
||||
export interface CountAggFieldParams {
|
||||
label?: string;
|
||||
source: IESAggSource;
|
||||
origin: FIELD_ORIGIN;
|
||||
mask?: AggDescriptor['mask'];
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { DataView } from '@kbn/data-plugin/common';
|
|||
import { IESAggSource } from '../../sources/es_agg_source';
|
||||
import { IVectorSource } from '../../sources/vector_source';
|
||||
import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants';
|
||||
import { TileMetaFeature } from '../../../../common/descriptor_types';
|
||||
import { AggDescriptor, TileMetaFeature } from '../../../../common/descriptor_types';
|
||||
import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property';
|
||||
import { ESAggTooltipProperty } from '../../tooltips/es_agg_tooltip_property';
|
||||
import { IESAggField, CountAggFieldParams } from './agg_field_types';
|
||||
|
@ -25,11 +25,13 @@ export class CountAggField implements IESAggField {
|
|||
protected readonly _source: IESAggSource;
|
||||
private readonly _origin: FIELD_ORIGIN;
|
||||
protected readonly _label?: string;
|
||||
protected readonly _mask?: AggDescriptor['mask'];
|
||||
|
||||
constructor({ label, source, origin }: CountAggFieldParams) {
|
||||
constructor({ label, source, origin, mask }: CountAggFieldParams) {
|
||||
this._source = source;
|
||||
this._origin = origin;
|
||||
this._label = label;
|
||||
this._mask = mask;
|
||||
}
|
||||
|
||||
supportsFieldMetaFromEs(): boolean {
|
||||
|
@ -131,4 +133,8 @@ export class CountAggField implements IESAggField {
|
|||
pluckRangeFromTileMetaFeature(metaFeature: TileMetaFeature) {
|
||||
return getAggRange(metaFeature, '_count');
|
||||
}
|
||||
|
||||
getMask() {
|
||||
return this._mask;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export function esAggFieldsFactory(
|
|||
label: aggDescriptor.label,
|
||||
source,
|
||||
origin,
|
||||
mask: aggDescriptor.mask,
|
||||
});
|
||||
} else if (aggDescriptor.type === AGG_TYPE.PERCENTILE) {
|
||||
aggField = new PercentileAggField({
|
||||
|
@ -40,6 +41,7 @@ export function esAggFieldsFactory(
|
|||
: DEFAULT_PERCENTILE,
|
||||
source,
|
||||
origin,
|
||||
mask: aggDescriptor.mask,
|
||||
});
|
||||
} else {
|
||||
aggField = new AggField({
|
||||
|
@ -51,6 +53,7 @@ export function esAggFieldsFactory(
|
|||
aggType: aggDescriptor.type,
|
||||
source,
|
||||
origin,
|
||||
mask: aggDescriptor.mask,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -105,4 +105,8 @@ export class TopTermPercentageField implements IESAggField {
|
|||
pluckRangeFromTileMetaFeature(metaFeature: TileMetaFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getMask() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Map as MbMap, VectorTileSource } from '@kbn/mapbox-gl';
|
||||
import type { FilterSpecification, Map as MbMap, VectorTileSource } from '@kbn/mapbox-gl';
|
||||
import { AbstractLayer } from '../layer';
|
||||
import { HeatmapStyle } from '../../styles/heatmap/heatmap_style';
|
||||
import { LAYER_TYPE } from '../../../../common/constants';
|
||||
|
@ -21,6 +21,7 @@ import { DataRequestContext } from '../../../actions';
|
|||
import { buildVectorRequestMeta } from '../build_vector_request_meta';
|
||||
import { IMvtVectorSource } from '../../sources/vector_source';
|
||||
import { getAggsMeta } from '../../util/tile_meta_feature_utils';
|
||||
import { Mask } from '../vector_layer/mask';
|
||||
|
||||
export class HeatmapLayer extends AbstractLayer {
|
||||
private readonly _style: HeatmapStyle;
|
||||
|
@ -186,6 +187,19 @@ export class HeatmapLayer extends AbstractLayer {
|
|||
|
||||
this.syncVisibilityWithMb(mbMap, heatmapLayerId);
|
||||
mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha());
|
||||
|
||||
// heatmap can implement mask with filter expression because
|
||||
// feature-state support is not needed since heatmap layers do not support joins
|
||||
const maskDescriptor = metricField.getMask();
|
||||
if (maskDescriptor) {
|
||||
const mask = new Mask({
|
||||
esAggField: metricField,
|
||||
isGeometrySourceMvt: true,
|
||||
...maskDescriptor,
|
||||
});
|
||||
mbMap.setFilter(heatmapLayerId, mask.getMatchUnmaskedExpression() as FilterSpecification);
|
||||
}
|
||||
|
||||
mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom());
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style
|
|||
import { IDynamicStyleProperty } from '../../../styles/vector/properties/dynamic_style_property';
|
||||
import { IStyleProperty } from '../../../styles/vector/properties/style_property';
|
||||
import {
|
||||
COUNT_PROP_LABEL,
|
||||
COUNT_PROP_NAME,
|
||||
GRID_RESOLUTION,
|
||||
LAYER_TYPE,
|
||||
|
@ -67,7 +66,6 @@ function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle
|
|||
clusterSourceDescriptor.metrics = [
|
||||
{
|
||||
type: AGG_TYPE.COUNT,
|
||||
label: COUNT_PROP_LABEL,
|
||||
},
|
||||
...documentStyle.getDynamicPropertiesArray().map((dynamicProperty) => {
|
||||
return {
|
||||
|
@ -267,9 +265,9 @@ export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLay
|
|||
return [clonedDescriptor];
|
||||
}
|
||||
|
||||
getSource(): IVectorSource {
|
||||
getSource = () => {
|
||||
return this._isClustered ? this._clusterSource : this._documentSource;
|
||||
}
|
||||
};
|
||||
|
||||
getSourceForEditing() {
|
||||
// Layer is based on this._documentSource
|
||||
|
|
193
x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts
Normal file
193
x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts
Normal file
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MapGeoJSONFeature } from '@kbn/mapbox-gl';
|
||||
import type { IESAggSource } from '../../sources/es_agg_source';
|
||||
import type { IESAggField } from '../../fields/agg';
|
||||
import { FIELD_ORIGIN, MASK_OPERATOR, MB_LOOKUP_FUNCTION } from '../../../../common/constants';
|
||||
|
||||
export const BELOW = i18n.translate('xpack.maps.mask.belowLabel', {
|
||||
defaultMessage: 'below',
|
||||
});
|
||||
|
||||
export const ABOVE = i18n.translate('xpack.maps.mask.aboveLabel', {
|
||||
defaultMessage: 'above',
|
||||
});
|
||||
|
||||
export const BUCKETS = i18n.translate('xpack.maps.mask.genericBucketsName', {
|
||||
defaultMessage: 'buckets',
|
||||
});
|
||||
|
||||
const FEATURES = i18n.translate('xpack.maps.mask.genericFeaturesName', {
|
||||
defaultMessage: 'features',
|
||||
});
|
||||
|
||||
const VALUE = i18n.translate('xpack.maps.mask.genericAggLabel', {
|
||||
defaultMessage: 'value',
|
||||
});
|
||||
|
||||
const WHEN = i18n.translate('xpack.maps.mask.when', {
|
||||
defaultMessage: 'when',
|
||||
});
|
||||
|
||||
const WHEN_JOIN_METRIC = i18n.translate('xpack.maps.mask.whenJoinMetric', {
|
||||
defaultMessage: '{whenLabel} join metric',
|
||||
values: {
|
||||
whenLabel: WHEN,
|
||||
},
|
||||
});
|
||||
|
||||
function getOperatorLabel(operator: MASK_OPERATOR): string {
|
||||
if (operator === MASK_OPERATOR.BELOW) {
|
||||
return BELOW;
|
||||
}
|
||||
|
||||
if (operator === MASK_OPERATOR.ABOVE) {
|
||||
return ABOVE;
|
||||
}
|
||||
|
||||
return operator as string;
|
||||
}
|
||||
|
||||
export function getMaskI18nValue(operator: MASK_OPERATOR, value: number): string {
|
||||
return `${getOperatorLabel(operator)} ${value}`;
|
||||
}
|
||||
|
||||
export function getMaskI18nLabel({
|
||||
bucketsName,
|
||||
isJoin,
|
||||
}: {
|
||||
bucketsName?: string;
|
||||
isJoin: boolean;
|
||||
}): string {
|
||||
return i18n.translate('xpack.maps.mask.maskLabel', {
|
||||
defaultMessage: 'Hide {hideNoun}',
|
||||
values: {
|
||||
hideNoun: isJoin ? FEATURES : bucketsName ? bucketsName : BUCKETS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getMaskI18nDescription({
|
||||
aggLabel,
|
||||
bucketsName,
|
||||
isJoin,
|
||||
}: {
|
||||
aggLabel?: string;
|
||||
bucketsName?: string;
|
||||
isJoin: boolean;
|
||||
}): string {
|
||||
return i18n.translate('xpack.maps.mask.maskDescription', {
|
||||
defaultMessage: '{maskAdverb} {aggLabel} is ',
|
||||
values: {
|
||||
aggLabel: aggLabel ? aggLabel : VALUE,
|
||||
maskAdverb: isJoin ? WHEN_JOIN_METRIC : WHEN,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export class Mask {
|
||||
private readonly _esAggField: IESAggField;
|
||||
private readonly _isGeometrySourceMvt: boolean;
|
||||
private readonly _operator: MASK_OPERATOR;
|
||||
private readonly _value: number;
|
||||
|
||||
constructor({
|
||||
esAggField,
|
||||
isGeometrySourceMvt,
|
||||
operator,
|
||||
value,
|
||||
}: {
|
||||
esAggField: IESAggField;
|
||||
isGeometrySourceMvt: boolean;
|
||||
operator: MASK_OPERATOR;
|
||||
value: number;
|
||||
}) {
|
||||
this._esAggField = esAggField;
|
||||
this._isGeometrySourceMvt = isGeometrySourceMvt;
|
||||
this._operator = operator;
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
private _isFeatureState() {
|
||||
if (this._esAggField.getOrigin() === FIELD_ORIGIN.SOURCE) {
|
||||
// source fields are stored in properties
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._isGeometrySourceMvt) {
|
||||
// For geojson sources, join fields are stored in properties
|
||||
return false;
|
||||
}
|
||||
|
||||
// For vector tile sources, it is not possible to add join fields to properties
|
||||
// so join fields are stored in feature state
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns maplibre expression that matches masked features
|
||||
*/
|
||||
getMatchMaskedExpression() {
|
||||
const comparisionOperator = this._operator === MASK_OPERATOR.BELOW ? '<' : '>';
|
||||
const lookup = this._isFeatureState()
|
||||
? MB_LOOKUP_FUNCTION.FEATURE_STATE
|
||||
: MB_LOOKUP_FUNCTION.GET;
|
||||
return [comparisionOperator, [lookup, this._esAggField.getMbFieldName()], this._value];
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns maplibre expression that matches unmasked features
|
||||
*/
|
||||
getMatchUnmaskedExpression() {
|
||||
const comparisionOperator = this._operator === MASK_OPERATOR.BELOW ? '>=' : '<=';
|
||||
const lookup = this._isFeatureState()
|
||||
? MB_LOOKUP_FUNCTION.FEATURE_STATE
|
||||
: MB_LOOKUP_FUNCTION.GET;
|
||||
return [comparisionOperator, [lookup, this._esAggField.getMbFieldName()], this._value];
|
||||
}
|
||||
|
||||
getEsAggField() {
|
||||
return this._esAggField;
|
||||
}
|
||||
|
||||
getFieldOriginListLabel() {
|
||||
const source = this._esAggField.getSource();
|
||||
const isJoin = this._esAggField.getOrigin() === FIELD_ORIGIN.JOIN;
|
||||
const maskLabel = getMaskI18nLabel({
|
||||
bucketsName:
|
||||
'getBucketsName' in (source as IESAggSource)
|
||||
? (source as IESAggSource).getBucketsName()
|
||||
: undefined,
|
||||
isJoin,
|
||||
});
|
||||
const adverb = isJoin ? WHEN_JOIN_METRIC : WHEN;
|
||||
|
||||
return `${maskLabel} ${adverb}`;
|
||||
}
|
||||
|
||||
getOperator() {
|
||||
return this._operator;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
isFeatureMasked(feature: MapGeoJSONFeature) {
|
||||
const featureValue = this._isFeatureState()
|
||||
? feature?.state[this._esAggField.getMbFieldName()]
|
||||
: feature?.properties[this._esAggField.getMbFieldName()];
|
||||
if (typeof featureValue !== 'number') {
|
||||
return false;
|
||||
}
|
||||
return this._operator === MASK_OPERATOR.BELOW
|
||||
? featureValue < this._value
|
||||
: featureValue > this._value;
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from '../../../../../common/descriptor_types';
|
||||
import { LAYER_TYPE, SOURCE_TYPES } from '../../../../../common/constants';
|
||||
import { MvtVectorLayer } from './mvt_vector_layer';
|
||||
import { ITermJoinSource } from '../../../sources/term_join_source';
|
||||
|
||||
const defaultConfig = {
|
||||
urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf',
|
||||
|
@ -176,6 +177,9 @@ describe('isLayerLoading', () => {
|
|||
getSourceDataRequestId: () => {
|
||||
return 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2';
|
||||
},
|
||||
getRightJoinSource: () => {
|
||||
return {} as unknown as ITermJoinSource;
|
||||
},
|
||||
} as unknown as InnerJoin,
|
||||
],
|
||||
layerDescriptor: {
|
||||
|
@ -212,6 +216,9 @@ describe('isLayerLoading', () => {
|
|||
getSourceDataRequestId: () => {
|
||||
return 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2';
|
||||
},
|
||||
getRightJoinSource: () => {
|
||||
return {} as unknown as ITermJoinSource;
|
||||
},
|
||||
} as unknown as InnerJoin,
|
||||
],
|
||||
layerDescriptor: {
|
||||
|
|
|
@ -58,12 +58,14 @@ import { ITooltipProperty } from '../../tooltips/tooltip_property';
|
|||
import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
|
||||
import { IESSource } from '../../sources/es_source';
|
||||
import { ITermJoinSource } from '../../sources/term_join_source';
|
||||
import type { IESAggSource } from '../../sources/es_agg_source';
|
||||
import { buildVectorRequestMeta } from '../build_vector_request_meta';
|
||||
import { getJoinAggKey } from '../../../../common/get_agg_key';
|
||||
import { syncBoundsData } from './bounds_data';
|
||||
import { JoinState } from './types';
|
||||
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
|
||||
import { PropertiesMap } from '../../../../common/elasticsearch_util';
|
||||
import { Mask } from './mask';
|
||||
|
||||
const SUPPORTS_FEATURE_EDITING_REQUEST_ID = 'SUPPORTS_FEATURE_EDITING_REQUEST_ID';
|
||||
|
||||
|
@ -106,6 +108,7 @@ export interface IVectorLayer extends ILayer {
|
|||
getLeftJoinFields(): Promise<IField[]>;
|
||||
addFeature(geometry: Geometry | Position[]): Promise<void>;
|
||||
deleteFeature(featureId: string): Promise<void>;
|
||||
getMasks(): Mask[];
|
||||
}
|
||||
|
||||
export const noResultsIcon = <EuiIcon size="m" color="subdued" type="minusInCircle" />;
|
||||
|
@ -120,6 +123,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
protected readonly _style: VectorStyle;
|
||||
private readonly _joins: InnerJoin[];
|
||||
protected readonly _descriptor: VectorLayerDescriptor;
|
||||
private readonly _masks: Mask[];
|
||||
|
||||
static createDescriptor(
|
||||
options: Partial<VectorLayerDescriptor>,
|
||||
|
@ -163,6 +167,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
customIcons,
|
||||
chartsPaletteServiceGetColor
|
||||
);
|
||||
this._masks = this._createMasks();
|
||||
}
|
||||
|
||||
async cloneDescriptor(): Promise<VectorLayerDescriptor[]> {
|
||||
|
@ -692,6 +697,69 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
_createMasks() {
|
||||
const masks: Mask[] = [];
|
||||
const source = this.getSource();
|
||||
if ('getMetricFields' in (source as IESAggSource)) {
|
||||
const metricFields = (source as IESAggSource).getMetricFields();
|
||||
metricFields.forEach((metricField) => {
|
||||
const maskDescriptor = metricField.getMask();
|
||||
if (maskDescriptor) {
|
||||
masks.push(
|
||||
new Mask({
|
||||
esAggField: metricField,
|
||||
isGeometrySourceMvt: source.isMvt(),
|
||||
...maskDescriptor,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.getValidJoins().forEach((join) => {
|
||||
const rightSource = join.getRightJoinSource();
|
||||
if ('getMetricFields' in (rightSource as unknown as IESAggSource)) {
|
||||
const metricFields = (rightSource as unknown as IESAggSource).getMetricFields();
|
||||
metricFields.forEach((metricField) => {
|
||||
const maskDescriptor = metricField.getMask();
|
||||
if (maskDescriptor) {
|
||||
masks.push(
|
||||
new Mask({
|
||||
esAggField: metricField,
|
||||
isGeometrySourceMvt: source.isMvt(),
|
||||
...maskDescriptor,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return masks;
|
||||
}
|
||||
|
||||
getMasks() {
|
||||
return this._masks;
|
||||
}
|
||||
|
||||
// feature-state is not supported in filter expressions
|
||||
// https://github.com/mapbox/mapbox-gl-js/issues/8487
|
||||
// therefore, masking must be accomplished via setting opacity paint property (hack)
|
||||
_getAlphaExpression() {
|
||||
const maskCaseExpressions: unknown[] = [];
|
||||
this.getMasks().forEach((mask) => {
|
||||
// case expressions require 2 parts
|
||||
// 1) condition expression
|
||||
maskCaseExpressions.push(mask.getMatchMaskedExpression());
|
||||
// 2) output. 0 opacity styling "hides" feature
|
||||
maskCaseExpressions.push(0);
|
||||
});
|
||||
|
||||
return maskCaseExpressions.length
|
||||
? ['case', ...maskCaseExpressions, this.getAlpha()]
|
||||
: this.getAlpha();
|
||||
}
|
||||
|
||||
_setMbPointsProperties(
|
||||
mbMap: MbMap,
|
||||
mvtSourceLayer?: string,
|
||||
|
@ -759,13 +827,13 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
|
||||
if (this.getCurrentStyle().arePointsSymbolizedAsCircles()) {
|
||||
this.getCurrentStyle().setMBPaintPropertiesForPoints({
|
||||
alpha: this.getAlpha(),
|
||||
alpha: this._getAlphaExpression(),
|
||||
mbMap,
|
||||
pointLayerId: markerLayerId,
|
||||
});
|
||||
} else {
|
||||
this.getCurrentStyle().setMBSymbolPropertiesForPoints({
|
||||
alpha: this.getAlpha(),
|
||||
alpha: this._getAlphaExpression(),
|
||||
mbMap,
|
||||
symbolLayerId: markerLayerId,
|
||||
});
|
||||
|
@ -811,7 +879,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
}
|
||||
|
||||
this.getCurrentStyle().setMBPaintProperties({
|
||||
alpha: this.getAlpha(),
|
||||
alpha: this._getAlphaExpression(),
|
||||
mbMap,
|
||||
fillLayerId,
|
||||
lineLayerId,
|
||||
|
@ -865,7 +933,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
}
|
||||
|
||||
this.getCurrentStyle().setMBPropertiesForLabelText({
|
||||
alpha: this.getAlpha(),
|
||||
alpha: this._getAlphaExpression(),
|
||||
mbMap,
|
||||
textLayerId: labelLayerId,
|
||||
});
|
||||
|
|
|
@ -11,11 +11,13 @@ import { DataView } from '@kbn/data-plugin/common';
|
|||
import type { IESAggSource } from './types';
|
||||
import { AbstractESSource } from '../es_source';
|
||||
import { esAggFieldsFactory, IESAggField } from '../../fields/agg';
|
||||
import { AGG_TYPE, COUNT_PROP_LABEL, FIELD_ORIGIN } from '../../../../common/constants';
|
||||
import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants';
|
||||
import { getSourceAggKey } from '../../../../common/get_agg_key';
|
||||
import { AbstractESAggSourceDescriptor, AggDescriptor } from '../../../../common/descriptor_types';
|
||||
import { IField } from '../../fields/field';
|
||||
import { ITooltipProperty } from '../../tooltips/tooltip_property';
|
||||
import { getAggDisplayName } from './get_agg_display_name';
|
||||
import { BUCKETS } from '../../layers/vector_layer/mask';
|
||||
|
||||
export const DEFAULT_METRIC = { type: AGG_TYPE.COUNT };
|
||||
|
||||
|
@ -46,6 +48,10 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE
|
|||
}
|
||||
}
|
||||
|
||||
getBucketsName() {
|
||||
return BUCKETS;
|
||||
}
|
||||
|
||||
getFieldByName(fieldName: string): IField | null {
|
||||
return this.getMetricFieldForName(fieldName);
|
||||
}
|
||||
|
@ -83,14 +89,14 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE
|
|||
async getAggLabel(aggType: AGG_TYPE, fieldLabel: string): Promise<string> {
|
||||
switch (aggType) {
|
||||
case AGG_TYPE.COUNT:
|
||||
return COUNT_PROP_LABEL;
|
||||
return getAggDisplayName(aggType);
|
||||
case AGG_TYPE.TERMS:
|
||||
return i18n.translate('xpack.maps.source.esAggSource.topTermLabel', {
|
||||
defaultMessage: `Top {fieldLabel}`,
|
||||
defaultMessage: `top {fieldLabel}`,
|
||||
values: { fieldLabel },
|
||||
});
|
||||
default:
|
||||
return `${aggType} ${fieldLabel}`;
|
||||
return `${getAggDisplayName(aggType)} ${fieldLabel}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AGG_TYPE } from '../../../../common/constants';
|
||||
|
||||
export function getAggDisplayName(aggType: AGG_TYPE): string {
|
||||
switch (aggType) {
|
||||
case AGG_TYPE.AVG:
|
||||
return i18n.translate('xpack.maps.aggType.averageLabel', {
|
||||
defaultMessage: 'average',
|
||||
});
|
||||
case AGG_TYPE.COUNT:
|
||||
return i18n.translate('xpack.maps.aggType.countLabel', {
|
||||
defaultMessage: 'count',
|
||||
});
|
||||
case AGG_TYPE.MAX:
|
||||
return i18n.translate('xpack.maps.aggType.maximumLabel', {
|
||||
defaultMessage: 'max',
|
||||
});
|
||||
case AGG_TYPE.MIN:
|
||||
return i18n.translate('xpack.maps.aggType.minimumLabel', {
|
||||
defaultMessage: 'min',
|
||||
});
|
||||
case AGG_TYPE.PERCENTILE:
|
||||
return i18n.translate('xpack.maps.aggType.percentileLabel', {
|
||||
defaultMessage: 'percentile',
|
||||
});
|
||||
case AGG_TYPE.SUM:
|
||||
return i18n.translate('xpack.maps.aggType.sumLabel', {
|
||||
defaultMessage: 'sum',
|
||||
});
|
||||
case AGG_TYPE.TERMS:
|
||||
return i18n.translate('xpack.maps.aggType.topTermLabel', {
|
||||
defaultMessage: 'top term',
|
||||
});
|
||||
case AGG_TYPE.UNIQUE_COUNT:
|
||||
return i18n.translate('xpack.maps.aggType.cardinalityTermLabel', {
|
||||
defaultMessage: 'unique count',
|
||||
});
|
||||
default:
|
||||
return aggType;
|
||||
}
|
||||
}
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export type { IESAggSource } from './types';
|
||||
export { AbstractESAggSource, DEFAULT_METRIC } from './es_agg_source';
|
||||
export { getAggDisplayName } from './get_agg_display_name';
|
||||
|
|
|
@ -13,6 +13,12 @@ import { IESAggField } from '../../fields/agg';
|
|||
export interface IESAggSource extends IESSource {
|
||||
getAggKey(aggType: AGG_TYPE, fieldName: string): string;
|
||||
getAggLabel(aggType: AGG_TYPE, fieldLabel: string): Promise<string>;
|
||||
|
||||
/*
|
||||
* Returns human readable name describing buckets, like "clusters" or "grids"
|
||||
*/
|
||||
getBucketsName(): string;
|
||||
|
||||
getMetricFields(): IESAggField[];
|
||||
getMetricFieldForName(fieldName: string): IESAggField | null;
|
||||
getValueAggsDsl(indexPattern: DataView): { [key: string]: unknown };
|
||||
|
|
|
@ -19,7 +19,9 @@ exports[`source editor geo_grid_source should not allow editing multiple metrics
|
|||
/>
|
||||
<MetricsEditor
|
||||
allowMultipleMetrics={false}
|
||||
bucketsName="clusters"
|
||||
fields={Array []}
|
||||
isJoin={false}
|
||||
key="12345"
|
||||
metrics={Array []}
|
||||
metricsFilter={[Function]}
|
||||
|
@ -77,7 +79,9 @@ exports[`source editor geo_grid_source should render editor 1`] = `
|
|||
/>
|
||||
<MetricsEditor
|
||||
allowMultipleMetrics={true}
|
||||
bucketsName="clusters"
|
||||
fields={Array []}
|
||||
isJoin={false}
|
||||
key="12345"
|
||||
metrics={Array []}
|
||||
metricsFilter={[Function]}
|
||||
|
|
|
@ -98,6 +98,24 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
|
|||
this._descriptor = sourceDescriptor;
|
||||
}
|
||||
|
||||
getBucketsName() {
|
||||
if (this._descriptor.requestType === RENDER_AS.HEX) {
|
||||
return i18n.translate('xpack.maps.source.esGeoGrid.hex.bucketsName', {
|
||||
defaultMessage: 'hexagons',
|
||||
});
|
||||
}
|
||||
|
||||
if (this._descriptor.requestType === RENDER_AS.GRID) {
|
||||
return i18n.translate('xpack.maps.source.esGeoGrid.grid.bucketsName', {
|
||||
defaultMessage: 'grid',
|
||||
});
|
||||
}
|
||||
|
||||
return i18n.translate('xpack.maps.source.esGeoGrid.cluster.bucketsName', {
|
||||
defaultMessage: 'clusters',
|
||||
});
|
||||
}
|
||||
|
||||
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> {
|
||||
async function onChange(...sourceChanges: OnSourceChangeArgs[]) {
|
||||
sourceEditorArgs.onChange(...sourceChanges);
|
||||
|
@ -129,6 +147,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
|
|||
}
|
||||
return (
|
||||
<UpdateSourceEditor
|
||||
bucketsName={this.getBucketsName()}
|
||||
currentLayerType={sourceEditorArgs.currentLayerType}
|
||||
geoFieldName={this.getGeoFieldName()}
|
||||
indexPatternId={this.getIndexPatternId()}
|
||||
|
|
|
@ -16,6 +16,7 @@ jest.mock('uuid', () => ({
|
|||
}));
|
||||
|
||||
const defaultProps = {
|
||||
bucketsName: 'clusters',
|
||||
currentLayerType: LAYER_TYPE.GEOJSON_VECTOR,
|
||||
geoFieldName: 'myLocation',
|
||||
indexPatternId: 'foobar',
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment, Component } from 'react';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiPanel, EuiSpacer, EuiComboBoxOptionOption, EuiTitle } from '@elastic/eui';
|
||||
|
@ -25,6 +24,7 @@ import { clustersTitle, heatmapTitle } from './es_geo_grid_source';
|
|||
import { isMvt } from './is_mvt';
|
||||
|
||||
interface Props {
|
||||
bucketsName: string;
|
||||
currentLayerType?: string;
|
||||
geoFieldName: string;
|
||||
indexPatternId: string;
|
||||
|
@ -148,6 +148,8 @@ export class UpdateSourceEditor extends Component<Props, State> {
|
|||
<MetricsEditor
|
||||
key={this.state.metricsEditorKey}
|
||||
allowMultipleMetrics={this.props.currentLayerType !== LAYER_TYPE.HEATMAP}
|
||||
bucketsName={this.props.bucketsName}
|
||||
isJoin={false}
|
||||
metricsFilter={this._getMetricsFilter()}
|
||||
fields={this.state.fields}
|
||||
metrics={this.props.metrics}
|
||||
|
|
|
@ -89,9 +89,16 @@ export class ESGeoLineSource extends AbstractESAggSource {
|
|||
this._descriptor = sourceDescriptor;
|
||||
}
|
||||
|
||||
getBucketsName() {
|
||||
return i18n.translate('xpack.maps.source.esGeoLine.bucketsName', {
|
||||
defaultMessage: 'tracks',
|
||||
});
|
||||
}
|
||||
|
||||
renderSourceSettingsEditor({ onChange }: SourceEditorArgs) {
|
||||
return (
|
||||
<UpdateSourceEditor
|
||||
bucketsName={this.getBucketsName()}
|
||||
indexPatternId={this.getIndexPatternId()}
|
||||
onChange={onChange}
|
||||
metrics={this._descriptor.metrics}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment, Component } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import type { DataViewField, DataView } from '@kbn/data-plugin/common';
|
||||
|
@ -14,10 +13,11 @@ import { indexPatterns } from '@kbn/data-plugin/public';
|
|||
import { MetricsEditor } from '../../../components/metrics_editor';
|
||||
import { getIndexPatternService } from '../../../kibana_services';
|
||||
import { GeoLineForm } from './geo_line_form';
|
||||
import { AggDescriptor } from '../../../../common/descriptor_types';
|
||||
import { OnSourceChangeArgs } from '../source';
|
||||
import type { AggDescriptor } from '../../../../common/descriptor_types';
|
||||
import type { OnSourceChangeArgs } from '../source';
|
||||
|
||||
interface Props {
|
||||
bucketsName: string;
|
||||
indexPatternId: string;
|
||||
splitField: string;
|
||||
sortField: string;
|
||||
|
@ -96,6 +96,8 @@ export class UpdateSourceEditor extends Component<Props, State> {
|
|||
<EuiSpacer size="m" />
|
||||
<MetricsEditor
|
||||
allowMultipleMetrics={true}
|
||||
bucketsName={this.props.bucketsName}
|
||||
isJoin={false}
|
||||
fields={this.state.fields}
|
||||
metrics={this.props.metrics}
|
||||
onChange={this._onMetricsChange}
|
||||
|
|
|
@ -67,13 +67,19 @@ export class ESPewPewSource extends AbstractESAggSource {
|
|||
this._descriptor = descriptor;
|
||||
}
|
||||
|
||||
getBucketsName() {
|
||||
return i18n.translate('xpack.maps.source.pewPew.bucketsName', {
|
||||
defaultMessage: 'paths',
|
||||
});
|
||||
}
|
||||
|
||||
renderSourceSettingsEditor({ onChange }: SourceEditorArgs) {
|
||||
return (
|
||||
<UpdateSourceEditor
|
||||
bucketsName={this.getBucketsName()}
|
||||
indexPatternId={this.getIndexPatternId()}
|
||||
onChange={onChange}
|
||||
metrics={this._descriptor.metrics}
|
||||
applyGlobalQuery={this._descriptor.applyGlobalQuery}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,17 +6,31 @@
|
|||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters';
|
||||
import { MetricsEditor } from '../../../components/metrics_editor';
|
||||
import { getIndexPatternService } from '../../../kibana_services';
|
||||
import type { DataViewField } from '@kbn/data-plugin/common';
|
||||
import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { indexPatterns } from '@kbn/data-plugin/public';
|
||||
import { MetricsEditor } from '../../../components/metrics_editor';
|
||||
import { getIndexPatternService } from '../../../kibana_services';
|
||||
import type { AggDescriptor } from '../../../../common/descriptor_types';
|
||||
import type { OnSourceChangeArgs } from '../source';
|
||||
|
||||
interface Props {
|
||||
bucketsName: string;
|
||||
indexPatternId: string;
|
||||
metrics: AggDescriptor[];
|
||||
onChange: (...args: OnSourceChangeArgs[]) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
fields: DataViewField[];
|
||||
}
|
||||
|
||||
export class UpdateSourceEditor extends Component<Props, State> {
|
||||
private _isMounted: boolean = false;
|
||||
|
||||
export class UpdateSourceEditor extends Component {
|
||||
state = {
|
||||
fields: null,
|
||||
fields: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -33,11 +47,6 @@ export class UpdateSourceEditor extends Component {
|
|||
try {
|
||||
indexPattern = await getIndexPatternService().get(this.props.indexPatternId);
|
||||
} catch (err) {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
loadError: getDataViewNotFoundMessage(this.props.indexPatternId),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -50,7 +59,7 @@ export class UpdateSourceEditor extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onMetricsChange = (metrics) => {
|
||||
_onMetricsChange = (metrics: AggDescriptor[]) => {
|
||||
this.props.onChange({ propName: 'metrics', value: metrics });
|
||||
};
|
||||
|
||||
|
@ -69,6 +78,8 @@ export class UpdateSourceEditor extends Component {
|
|||
<EuiSpacer size="m" />
|
||||
<MetricsEditor
|
||||
allowMultipleMetrics={true}
|
||||
bucketsName={this.props.bucketsName}
|
||||
isJoin={false}
|
||||
fields={this.state.fields}
|
||||
metrics={this.props.metrics}
|
||||
onChange={this._onMetricsChange}
|
|
@ -37,7 +37,7 @@ describe('getMetricFields', () => {
|
|||
});
|
||||
const metrics = source.getMetricFields();
|
||||
expect(metrics[0].getName()).toEqual('__kbnjoin__count__1234');
|
||||
expect(await metrics[0].getLabel()).toEqual('Count of foobar');
|
||||
expect(await metrics[0].getLabel()).toEqual('count of foobar');
|
||||
});
|
||||
|
||||
it('should override name and label of sum metric', async () => {
|
||||
|
@ -51,7 +51,7 @@ describe('getMetricFields', () => {
|
|||
expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed__1234');
|
||||
expect(await metrics[0].getLabel()).toEqual('my custom label');
|
||||
expect(metrics[1].getName()).toEqual('__kbnjoin__count__1234');
|
||||
expect(await metrics[1].getLabel()).toEqual('Count of foobar');
|
||||
expect(await metrics[1].getLabel()).toEqual('count of foobar');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
import { PropertiesMap } from '../../../../common/elasticsearch_util';
|
||||
import { isValidStringConfig } from '../../util/valid_string_config';
|
||||
import { ITermJoinSource } from '../term_join_source';
|
||||
import type { IESAggSource } from '../es_agg_source';
|
||||
import { IField } from '../../fields/field';
|
||||
import { mergeExecutionContext } from '../execution_context_utils';
|
||||
|
||||
|
@ -52,7 +53,7 @@ export function extractPropertiesMap(rawEsData: any, countPropertyName: string):
|
|||
return propertiesMap;
|
||||
}
|
||||
|
||||
export class ESTermSource extends AbstractESAggSource implements ITermJoinSource {
|
||||
export class ESTermSource extends AbstractESAggSource implements ITermJoinSource, IESAggSource {
|
||||
static type = SOURCE_TYPES.ES_TERM_SOURCE;
|
||||
|
||||
static createDescriptor(descriptor: Partial<ESTermSourceDescriptor>): ESTermSourceDescriptor {
|
||||
|
@ -115,7 +116,7 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource
|
|||
}
|
||||
return aggType === AGG_TYPE.COUNT
|
||||
? i18n.translate('xpack.maps.source.esJoin.countLabel', {
|
||||
defaultMessage: `Count of {indexPatternLabel}`,
|
||||
defaultMessage: `count of {indexPatternLabel}`,
|
||||
values: { indexPatternLabel },
|
||||
})
|
||||
: super.getAggLabel(aggType, fieldLabel);
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorGradient } from './color_gradient';
|
||||
import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row';
|
||||
import { HEATMAP_COLOR_RAMP_LABEL } from '../heatmap_constants';
|
||||
import { IField } from '../../../../fields/field';
|
||||
import type { IField } from '../../../../fields/field';
|
||||
import type { IESAggField } from '../../../../fields/agg';
|
||||
import { MaskLegend } from '../../../vector/components/legend/mask_legend';
|
||||
|
||||
interface Props {
|
||||
colorRampName: string;
|
||||
|
@ -47,7 +49,7 @@ export class HeatmapLegend extends Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
const metricLegend = (
|
||||
<RangedStyleLegendRow
|
||||
header={<ColorGradient colorPaletteId={this.props.colorRampName} />}
|
||||
minLabel={i18n.translate('xpack.maps.heatmapLegend.coldLabel', {
|
||||
|
@ -61,5 +63,28 @@ export class HeatmapLegend extends Component<Props, State> {
|
|||
invert={false}
|
||||
/>
|
||||
);
|
||||
|
||||
let maskLegend: ReactNode | undefined;
|
||||
if ('getMask' in (this.props.field as IESAggField)) {
|
||||
const mask = (this.props.field as IESAggField).getMask();
|
||||
if (mask) {
|
||||
maskLegend = (
|
||||
<MaskLegend
|
||||
esAggField={this.props.field as IESAggField}
|
||||
operator={mask.operator}
|
||||
value={mask.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return maskLegend ? (
|
||||
<>
|
||||
{maskLegend}
|
||||
{metricLegend}
|
||||
</>
|
||||
) : (
|
||||
metricLegend
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { FIELD_ORIGIN, MASK_OPERATOR } from '../../../../../../common/constants';
|
||||
import type { IESAggField } from '../../../../fields/agg';
|
||||
import type { IESAggSource } from '../../../../sources/es_agg_source';
|
||||
import {
|
||||
getMaskI18nDescription,
|
||||
getMaskI18nLabel,
|
||||
getMaskI18nValue,
|
||||
} from '../../../../layers/vector_layer/mask';
|
||||
|
||||
interface Props {
|
||||
esAggField: IESAggField;
|
||||
onlyShowLabelAndValue?: boolean;
|
||||
operator: MASK_OPERATOR;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
aggLabel?: string;
|
||||
}
|
||||
|
||||
export class MaskLegend extends Component<Props, State> {
|
||||
private _isMounted = false;
|
||||
|
||||
state: State = {};
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._loadAggLabel();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._loadAggLabel();
|
||||
}
|
||||
|
||||
_loadAggLabel = async () => {
|
||||
const aggLabel = await this.props.esAggField.getLabel();
|
||||
if (this._isMounted && aggLabel !== this.state.aggLabel) {
|
||||
this.setState({ aggLabel });
|
||||
}
|
||||
};
|
||||
|
||||
_getBucketsName() {
|
||||
const source = this.props.esAggField.getSource();
|
||||
return 'getBucketsName' in (source as IESAggSource)
|
||||
? (source as IESAggSource).getBucketsName()
|
||||
: undefined;
|
||||
}
|
||||
|
||||
_getPrefix() {
|
||||
if (this.props.onlyShowLabelAndValue) {
|
||||
return i18n.translate('xpack.maps.maskLegend.is', {
|
||||
defaultMessage: '{aggLabel} is',
|
||||
values: {
|
||||
aggLabel: this.state.aggLabel,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isJoin = this.props.esAggField.getOrigin() === FIELD_ORIGIN.JOIN;
|
||||
const maskLabel = getMaskI18nLabel({
|
||||
bucketsName: this._getBucketsName(),
|
||||
isJoin,
|
||||
});
|
||||
const maskDescription = getMaskI18nDescription({
|
||||
aggLabel: this.state.aggLabel,
|
||||
isJoin,
|
||||
});
|
||||
return `${maskLabel} ${maskDescription}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiText size="xs" textAlign="left" color="subdued">
|
||||
<small>
|
||||
{`${this._getPrefix()} `}
|
||||
<strong>{getMaskI18nValue(this.props.operator, this.props.value)}</strong>
|
||||
</small>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,17 +6,30 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { FIELD_ORIGIN } from '../../../../../../common/constants';
|
||||
import { Mask } from '../../../../layers/vector_layer/mask';
|
||||
import { IStyleProperty } from '../../properties/style_property';
|
||||
import { MaskLegend } from './mask_legend';
|
||||
|
||||
interface Props {
|
||||
isLinesOnly: boolean;
|
||||
isPointsOnly: boolean;
|
||||
masks: Mask[];
|
||||
styles: Array<IStyleProperty<any>>;
|
||||
symbolId?: string;
|
||||
svg?: string;
|
||||
}
|
||||
|
||||
export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId, svg }: Props) {
|
||||
export function VectorStyleLegend({
|
||||
isLinesOnly,
|
||||
isPointsOnly,
|
||||
masks,
|
||||
styles,
|
||||
symbolId,
|
||||
svg,
|
||||
}: Props) {
|
||||
const legendRows = [];
|
||||
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
|
@ -34,5 +47,55 @@ export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId,
|
|||
);
|
||||
}
|
||||
|
||||
return <>{legendRows}</>;
|
||||
function renderMasksByFieldOrigin(fieldOrigin: FIELD_ORIGIN) {
|
||||
const masksByFieldOrigin = masks.filter(
|
||||
(mask) => mask.getEsAggField().getOrigin() === fieldOrigin
|
||||
);
|
||||
if (masksByFieldOrigin.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (masksByFieldOrigin.length === 1) {
|
||||
const mask = masksByFieldOrigin[0];
|
||||
return (
|
||||
<MaskLegend
|
||||
key={mask.getEsAggField().getMbFieldName()}
|
||||
esAggField={mask.getEsAggField()}
|
||||
operator={mask.getOperator()}
|
||||
value={mask.getValue()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiText size="xs" textAlign="left" color="subdued">
|
||||
<small>{masksByFieldOrigin[0].getFieldOriginListLabel()}</small>
|
||||
</EuiText>
|
||||
<ul>
|
||||
{masksByFieldOrigin.map((mask) => (
|
||||
<li
|
||||
key={mask.getEsAggField().getMbFieldName()}
|
||||
style={{ marginLeft: euiThemeVars.euiSizeS }}
|
||||
>
|
||||
<MaskLegend
|
||||
esAggField={mask.getEsAggField()}
|
||||
onlyShowLabelAndValue={true}
|
||||
operator={mask.getOperator()}
|
||||
value={mask.getValue()}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderMasksByFieldOrigin(FIELD_ORIGIN.SOURCE)}
|
||||
{renderMasksByFieldOrigin(FIELD_ORIGIN.JOIN)}
|
||||
{legendRows}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
|
|||
this._chartsPaletteServiceGetColor = chartsPaletteServiceGetColor;
|
||||
}
|
||||
|
||||
syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
const color = this._getMbColor();
|
||||
mbMap.setPaintProperty(mbLayerId, 'circle-color', color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'circle-opacity', alpha);
|
||||
|
@ -67,25 +67,25 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
|
|||
mbMap.setPaintProperty(mbLayerId, 'icon-halo-color', color);
|
||||
}
|
||||
|
||||
syncCircleStrokeWithMb(pointLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncCircleStrokeWithMb(pointLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
const color = this._getMbColor();
|
||||
mbMap.setPaintProperty(pointLayerId, 'circle-stroke-color', color);
|
||||
mbMap.setPaintProperty(pointLayerId, 'circle-stroke-opacity', alpha);
|
||||
}
|
||||
|
||||
syncFillColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncFillColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
const color = this._getMbColor();
|
||||
mbMap.setPaintProperty(mbLayerId, 'fill-color', color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'fill-opacity', alpha);
|
||||
}
|
||||
|
||||
syncLineColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncLineColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
const color = this._getMbColor();
|
||||
mbMap.setPaintProperty(mbLayerId, 'line-color', color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha);
|
||||
}
|
||||
|
||||
syncLabelColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncLabelColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
const color = this._getMbColor();
|
||||
mbMap.setPaintProperty(mbLayerId, 'text-color', color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'text-opacity', alpha);
|
||||
|
|
|
@ -10,12 +10,12 @@ import { StaticStyleProperty } from './static_style_property';
|
|||
import { ColorStaticOptions } from '../../../../../common/descriptor_types';
|
||||
|
||||
export class StaticColorProperty extends StaticStyleProperty<ColorStaticOptions> {
|
||||
syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
mbMap.setPaintProperty(mbLayerId, 'circle-color', this._options.color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'circle-opacity', alpha);
|
||||
}
|
||||
|
||||
syncFillColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncFillColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
mbMap.setPaintProperty(mbLayerId, 'fill-color', this._options.color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'fill-opacity', alpha);
|
||||
}
|
||||
|
@ -28,17 +28,17 @@ export class StaticColorProperty extends StaticStyleProperty<ColorStaticOptions>
|
|||
mbMap.setPaintProperty(mbLayerId, 'icon-halo-color', this._options.color);
|
||||
}
|
||||
|
||||
syncLineColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncLineColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
mbMap.setPaintProperty(mbLayerId, 'line-color', this._options.color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha);
|
||||
}
|
||||
|
||||
syncCircleStrokeWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncCircleStrokeWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-color', this._options.color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-opacity', alpha);
|
||||
}
|
||||
|
||||
syncLabelColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
|
||||
syncLabelColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) {
|
||||
mbMap.setPaintProperty(mbLayerId, 'text-color', this._options.color);
|
||||
mbMap.setPaintProperty(mbLayerId, 'text-opacity', alpha);
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ export interface IVectorStyle extends IStyle {
|
|||
fillLayerId,
|
||||
lineLayerId,
|
||||
}: {
|
||||
alpha: number;
|
||||
alpha: unknown;
|
||||
mbMap: MbMap;
|
||||
fillLayerId: string;
|
||||
lineLayerId: string;
|
||||
|
@ -140,7 +140,7 @@ export interface IVectorStyle extends IStyle {
|
|||
mbMap,
|
||||
pointLayerId,
|
||||
}: {
|
||||
alpha: number;
|
||||
alpha: unknown;
|
||||
mbMap: MbMap;
|
||||
pointLayerId: string;
|
||||
}) => void;
|
||||
|
@ -149,7 +149,7 @@ export interface IVectorStyle extends IStyle {
|
|||
mbMap,
|
||||
textLayerId,
|
||||
}: {
|
||||
alpha: number;
|
||||
alpha: unknown;
|
||||
mbMap: MbMap;
|
||||
textLayerId: string;
|
||||
}) => void;
|
||||
|
@ -158,7 +158,7 @@ export interface IVectorStyle extends IStyle {
|
|||
symbolLayerId,
|
||||
alpha,
|
||||
}: {
|
||||
alpha: number;
|
||||
alpha: unknown;
|
||||
mbMap: MbMap;
|
||||
symbolLayerId: string;
|
||||
}) => void;
|
||||
|
@ -730,6 +730,7 @@ export class VectorStyle implements IVectorStyle {
|
|||
|
||||
return (
|
||||
<VectorStyleLegend
|
||||
masks={this._layer.getMasks()}
|
||||
styles={this._getLegendDetailStyleProperties()}
|
||||
isPointsOnly={this.getIsPointsOnly()}
|
||||
isLinesOnly={this._getIsLinesOnly()}
|
||||
|
@ -800,7 +801,7 @@ export class VectorStyle implements IVectorStyle {
|
|||
fillLayerId,
|
||||
lineLayerId,
|
||||
}: {
|
||||
alpha: number;
|
||||
alpha: unknown;
|
||||
mbMap: MbMap;
|
||||
fillLayerId: string;
|
||||
lineLayerId: string;
|
||||
|
@ -815,7 +816,7 @@ export class VectorStyle implements IVectorStyle {
|
|||
mbMap,
|
||||
pointLayerId,
|
||||
}: {
|
||||
alpha: number;
|
||||
alpha: unknown;
|
||||
mbMap: MbMap;
|
||||
pointLayerId: string;
|
||||
}) {
|
||||
|
@ -832,7 +833,7 @@ export class VectorStyle implements IVectorStyle {
|
|||
mbMap,
|
||||
textLayerId,
|
||||
}: {
|
||||
alpha: number;
|
||||
alpha: unknown;
|
||||
mbMap: MbMap;
|
||||
textLayerId: string;
|
||||
}) {
|
||||
|
@ -850,7 +851,7 @@ export class VectorStyle implements IVectorStyle {
|
|||
symbolLayerId,
|
||||
alpha,
|
||||
}: {
|
||||
alpha: number;
|
||||
alpha: unknown;
|
||||
mbMap: MbMap;
|
||||
symbolLayerId: string;
|
||||
}) {
|
||||
|
|
|
@ -11,6 +11,7 @@ exports[`should add default count metric when metrics is empty array 1`] = `
|
|||
>
|
||||
<MetricEditor
|
||||
fields={Array []}
|
||||
isJoin={false}
|
||||
metric={
|
||||
Object {
|
||||
"type": "count",
|
||||
|
@ -54,6 +55,7 @@ exports[`should render metrics editor 1`] = `
|
|||
>
|
||||
<MetricEditor
|
||||
fields={Array []}
|
||||
isJoin={false}
|
||||
metric={
|
||||
Object {
|
||||
"field": "myField",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { MaskExpression } from './mask_expression';
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, Component } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFieldNumber,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiFormRow,
|
||||
EuiPopoverFooter,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { MASK_OPERATOR } from '../../../../common/constants';
|
||||
import { AggDescriptor } from '../../../../common/descriptor_types';
|
||||
import { panelStrings } from '../../../connected_components/panel_strings';
|
||||
import { ABOVE, BELOW } from '../../../classes/layers/vector_layer/mask';
|
||||
|
||||
const operatorOptions = [
|
||||
{
|
||||
value: MASK_OPERATOR.BELOW,
|
||||
text: BELOW,
|
||||
},
|
||||
{
|
||||
value: MASK_OPERATOR.ABOVE,
|
||||
text: ABOVE,
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
metric: AggDescriptor;
|
||||
onChange: (metric: AggDescriptor) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
operator: MASK_OPERATOR;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
export class MaskEditor extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
operator:
|
||||
this.props.metric.mask !== undefined
|
||||
? this.props.metric.mask.operator
|
||||
: MASK_OPERATOR.BELOW,
|
||||
value: this.props.metric.mask !== undefined ? this.props.metric.mask.value : '',
|
||||
};
|
||||
}
|
||||
|
||||
_onSet = () => {
|
||||
if (this._isValueInValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange({
|
||||
...this.props.metric,
|
||||
mask: {
|
||||
operator: this.state.operator,
|
||||
value: this.state.value as number,
|
||||
},
|
||||
});
|
||||
this.props.onClose();
|
||||
};
|
||||
|
||||
_onClear = () => {
|
||||
const newMetric = {
|
||||
...this.props.metric,
|
||||
};
|
||||
delete newMetric.mask;
|
||||
this.props.onChange(newMetric);
|
||||
this.props.onClose();
|
||||
};
|
||||
|
||||
_onOperatorChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.setState({
|
||||
operator: e.target.value as MASK_OPERATOR,
|
||||
});
|
||||
};
|
||||
|
||||
_onValueChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = parseFloat(evt.target.value);
|
||||
this.setState({
|
||||
value: isNaN(sanitizedValue) ? evt.target.value : sanitizedValue,
|
||||
});
|
||||
};
|
||||
|
||||
_hasChanges() {
|
||||
return (
|
||||
this.props.metric.mask === undefined ||
|
||||
this.props.metric.mask.operator !== this.state.operator ||
|
||||
this.props.metric.mask.value !== this.state.value
|
||||
);
|
||||
}
|
||||
|
||||
_isValueInValid() {
|
||||
return typeof this.state.value === 'string';
|
||||
}
|
||||
|
||||
_renderForm() {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow>
|
||||
<EuiSelect
|
||||
id="maskOperatorSelect"
|
||||
options={operatorOptions}
|
||||
value={this.state.operator}
|
||||
onChange={this._onOperatorChange}
|
||||
aria-label={i18n.translate('xpack.maps.maskEditor.operatorSelectLabel', {
|
||||
defaultMessage: 'Mask operator select',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow>
|
||||
<EuiFieldNumber
|
||||
value={this.state.value}
|
||||
onChange={this._onValueChange}
|
||||
aria-label={i18n.translate('xpack.maps.maskEditor.valueInputLabel', {
|
||||
defaultMessage: 'Mask value input',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
_renderFooter() {
|
||||
return (
|
||||
<EuiPopoverFooter paddingSize="s">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty onClick={this.props.onClose} size="s">
|
||||
{panelStrings.close}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
isDisabled={this.props.metric.mask === undefined}
|
||||
onClick={this._onClear}
|
||||
size="s"
|
||||
>
|
||||
{panelStrings.clear}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={this._isValueInValid() || !this._hasChanges()}
|
||||
onClick={this._onSet}
|
||||
size="s"
|
||||
>
|
||||
{panelStrings.apply}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverFooter>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this._renderForm()}
|
||||
<EuiSpacer size="xs" />
|
||||
{this._renderFooter()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { EuiExpression, EuiPopover } from '@elastic/eui';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { AGG_TYPE } from '../../../../common/constants';
|
||||
import { AggDescriptor, FieldedAggDescriptor } from '../../../../common/descriptor_types';
|
||||
import { MaskEditor } from './mask_editor';
|
||||
import { getAggDisplayName } from '../../../classes/sources/es_agg_source';
|
||||
import {
|
||||
getMaskI18nDescription,
|
||||
getMaskI18nValue,
|
||||
} from '../../../classes/layers/vector_layer/mask';
|
||||
|
||||
interface Props {
|
||||
fields: DataViewField[];
|
||||
isJoin: boolean;
|
||||
metric: AggDescriptor;
|
||||
onChange: (metric: AggDescriptor) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
export class MaskExpression extends Component<Props, State> {
|
||||
state: State = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
_getMaskExpressionValue() {
|
||||
return this.props.metric.mask === undefined
|
||||
? '...'
|
||||
: getMaskI18nValue(this.props.metric.mask.operator, this.props.metric.mask.value);
|
||||
}
|
||||
|
||||
_getAggLabel() {
|
||||
const aggDisplayName = getAggDisplayName(this.props.metric.type);
|
||||
if (this.props.metric.type === AGG_TYPE.COUNT || this.props.metric.field === undefined) {
|
||||
return aggDisplayName;
|
||||
}
|
||||
|
||||
const targetField = this.props.fields.find(
|
||||
(field) => field.name === (this.props.metric as FieldedAggDescriptor).field
|
||||
);
|
||||
const fieldDisplayName = targetField?.displayName
|
||||
? targetField?.displayName
|
||||
: this.props.metric.field;
|
||||
return `${aggDisplayName} ${fieldDisplayName}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
// masks only supported for numerical metrics
|
||||
if (this.props.metric.type === AGG_TYPE.TERMS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="mask"
|
||||
button={
|
||||
<EuiExpression
|
||||
color="subdued"
|
||||
description={getMaskI18nDescription({
|
||||
aggLabel: this._getAggLabel(),
|
||||
isJoin: this.props.isJoin,
|
||||
})}
|
||||
value={this._getMaskExpressionValue()}
|
||||
onClick={this._togglePopover}
|
||||
uppercase={false}
|
||||
/>
|
||||
}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="downCenter"
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<MaskEditor
|
||||
metric={this.props.metric}
|
||||
onChange={this.props.onChange}
|
||||
onClose={this._closePopover}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,8 @@ import { AggDescriptor } from '../../../common/descriptor_types';
|
|||
import { AGG_TYPE, DEFAULT_PERCENTILE } from '../../../common/constants';
|
||||
import { getTermsFields } from '../../index_pattern_util';
|
||||
import { ValidatedNumberInput } from '../validated_number_input';
|
||||
import { getMaskI18nLabel } from '../../classes/layers/vector_layer/mask';
|
||||
import { MaskExpression } from './mask_expression';
|
||||
|
||||
function filterFieldsForAgg(fields: DataViewField[], aggType: AGG_TYPE) {
|
||||
if (!fields) {
|
||||
|
@ -43,6 +45,8 @@ function filterFieldsForAgg(fields: DataViewField[], aggType: AGG_TYPE) {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
bucketsName?: string;
|
||||
isJoin: boolean;
|
||||
metric: AggDescriptor;
|
||||
fields: DataViewField[];
|
||||
onChange: (metric: AggDescriptor) => void;
|
||||
|
@ -52,7 +56,9 @@ interface Props {
|
|||
}
|
||||
|
||||
export function MetricEditor({
|
||||
bucketsName,
|
||||
fields,
|
||||
isJoin,
|
||||
metricsFilter,
|
||||
metric,
|
||||
onChange,
|
||||
|
@ -64,6 +70,8 @@ export function MetricEditor({
|
|||
return;
|
||||
}
|
||||
|
||||
// Intentionally not adding mask.
|
||||
// Changing aggregation likely changes value range so keeping old mask does not seem relevent
|
||||
const descriptor = {
|
||||
type: metricAggregationType,
|
||||
label: metric.label,
|
||||
|
@ -93,6 +101,8 @@ export function MetricEditor({
|
|||
if (!fieldName || metric.type === AGG_TYPE.COUNT) {
|
||||
return;
|
||||
}
|
||||
// Intentionally not adding mask.
|
||||
// Changing field likely changes value range so keeping old mask does not seem relevent
|
||||
onChange({
|
||||
label: metric.label,
|
||||
type: metric.type,
|
||||
|
@ -223,6 +233,11 @@ export function MetricEditor({
|
|||
{fieldSelect}
|
||||
{percentileSelect}
|
||||
{labelInput}
|
||||
|
||||
<EuiFormRow label={getMaskI18nLabel({ bucketsName, isJoin })} display="columnCompressed">
|
||||
<MaskExpression fields={fields} isJoin={isJoin} metric={metric} onChange={onChange} />
|
||||
</EuiFormRow>
|
||||
|
||||
{removeButton}
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -9,54 +9,39 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui';
|
||||
import { AGG_TYPE } from '../../../common/constants';
|
||||
import { getAggDisplayName } from '../../classes/sources/es_agg_source';
|
||||
|
||||
const AGG_OPTIONS = [
|
||||
{
|
||||
label: i18n.translate('xpack.maps.metricSelect.averageDropDownOptionLabel', {
|
||||
defaultMessage: 'Average',
|
||||
}),
|
||||
label: getAggDisplayName(AGG_TYPE.AVG),
|
||||
value: AGG_TYPE.AVG,
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.maps.metricSelect.countDropDownOptionLabel', {
|
||||
defaultMessage: 'Count',
|
||||
}),
|
||||
label: getAggDisplayName(AGG_TYPE.COUNT),
|
||||
value: AGG_TYPE.COUNT,
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.maps.metricSelect.maxDropDownOptionLabel', {
|
||||
defaultMessage: 'Max',
|
||||
}),
|
||||
label: getAggDisplayName(AGG_TYPE.MAX),
|
||||
value: AGG_TYPE.MAX,
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.maps.metricSelect.minDropDownOptionLabel', {
|
||||
defaultMessage: 'Min',
|
||||
}),
|
||||
label: getAggDisplayName(AGG_TYPE.MIN),
|
||||
value: AGG_TYPE.MIN,
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.maps.metricSelect.percentileDropDownOptionLabel', {
|
||||
defaultMessage: 'Percentile',
|
||||
}),
|
||||
label: getAggDisplayName(AGG_TYPE.PERCENTILE),
|
||||
value: AGG_TYPE.PERCENTILE,
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.maps.metricSelect.sumDropDownOptionLabel', {
|
||||
defaultMessage: 'Sum',
|
||||
}),
|
||||
label: getAggDisplayName(AGG_TYPE.SUM),
|
||||
value: AGG_TYPE.SUM,
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.maps.metricSelect.termsDropDownOptionLabel', {
|
||||
defaultMessage: 'Top term',
|
||||
}),
|
||||
label: getAggDisplayName(AGG_TYPE.TERMS),
|
||||
value: AGG_TYPE.TERMS,
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.maps.metricSelect.cardinalityDropDownOptionLabel', {
|
||||
defaultMessage: 'Unique count',
|
||||
}),
|
||||
label: getAggDisplayName(AGG_TYPE.UNIQUE_COUNT),
|
||||
value: AGG_TYPE.UNIQUE_COUNT,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -20,6 +20,7 @@ const defaultProps = {
|
|||
fields: [],
|
||||
onChange: () => {},
|
||||
allowMultipleMetrics: true,
|
||||
isJoin: false,
|
||||
};
|
||||
|
||||
test('should render metrics editor', () => {
|
||||
|
|
|
@ -22,6 +22,8 @@ export function isMetricValid(aggDescriptor: AggDescriptor) {
|
|||
|
||||
interface Props {
|
||||
allowMultipleMetrics: boolean;
|
||||
bucketsName?: string;
|
||||
isJoin: boolean;
|
||||
metrics: AggDescriptor[];
|
||||
fields: DataViewField[];
|
||||
onChange: (metrics: AggDescriptor[]) => void;
|
||||
|
@ -81,6 +83,8 @@ export class MetricsEditor extends Component<Props, State> {
|
|||
return (
|
||||
<div key={index} className="mapMetricEditorPanel__metricEditor">
|
||||
<MetricEditor
|
||||
bucketsName={this.props.bucketsName}
|
||||
isJoin={this.props.isJoin}
|
||||
onChange={onMetricChange}
|
||||
metric={metric}
|
||||
fields={this.props.fields}
|
||||
|
|
|
@ -46,6 +46,7 @@ exports[`Should render default props 1`] = `
|
|||
<MetricsEditor
|
||||
allowMultipleMetrics={true}
|
||||
fields={Array []}
|
||||
isJoin={true}
|
||||
metrics={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -105,6 +106,7 @@ exports[`Should render metrics expression for metrics 1`] = `
|
|||
<MetricsEditor
|
||||
allowMultipleMetrics={true}
|
||||
fields={Array []}
|
||||
isJoin={true}
|
||||
metrics={
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -66,6 +66,7 @@ export class MetricsExpression extends Component<Props, State> {
|
|||
metrics={this.props.metrics}
|
||||
onChange={this.props.onChange}
|
||||
allowMultipleMetrics={true}
|
||||
isJoin={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -76,11 +76,7 @@ exports[`Should render edit form row when attribution not provided 1`] = `
|
|||
onClick={[Function]}
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Clear"
|
||||
id="xpack.maps.attribution.clearBtnLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
Clear
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
import React from 'react';
|
||||
import { EuiButtonEmpty, EuiLink, EuiPanel } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Attribution } from '../../../../common/descriptor_types';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
import { AttributionPopover } from './attribution_popover';
|
||||
import { panelStrings } from '../../panel_strings';
|
||||
|
||||
interface Props {
|
||||
layer: ILayer;
|
||||
|
@ -65,9 +65,7 @@ export function AttributionFormRow(props: Props) {
|
|||
defaultMessage: 'Edit attribution',
|
||||
}
|
||||
)}
|
||||
popoverButtonLabel={i18n.translate('xpack.maps.attribution.editBtnLabel', {
|
||||
defaultMessage: 'Edit',
|
||||
})}
|
||||
popoverButtonLabel={panelStrings.edit}
|
||||
label={layerDescriptor.attribution.label}
|
||||
url={layerDescriptor.attribution.url}
|
||||
/>
|
||||
|
@ -83,10 +81,7 @@ export function AttributionFormRow(props: Props) {
|
|||
defaultMessage: 'Clear attribution',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.attribution.clearBtnLabel"
|
||||
defaultMessage="Clear"
|
||||
/>
|
||||
{panelStrings.clear}
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Attribution } from '../../../../common/descriptor_types';
|
||||
import { panelStrings } from '../../panel_strings';
|
||||
|
||||
interface Props {
|
||||
onChange: (attribution: Attribution) => void;
|
||||
|
@ -128,7 +129,7 @@ export class AttributionPopover extends Component<Props, State> {
|
|||
onClick={this._onApply}
|
||||
size="s"
|
||||
>
|
||||
<FormattedMessage id="xpack.maps.attribution.applyBtnLabel" defaultMessage="Apply" />
|
||||
{panelStrings.apply}
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiPopoverFooter>
|
||||
|
|
|
@ -35,7 +35,7 @@ export function StyleSettings({ layer, updateStyleDescriptor, updateCustomIcons
|
|||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.styleSettingsTitle"
|
||||
defaultMessage="Layer Style"
|
||||
defaultMessage="Layer style"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
|
|
@ -70,6 +70,9 @@ const mockLayer = {
|
|||
},
|
||||
} as unknown as IVectorSource;
|
||||
},
|
||||
getMasks: () => {
|
||||
return [];
|
||||
},
|
||||
} as unknown as IVectorLayer;
|
||||
|
||||
const mockMbMapHandlers: { [key: string]: (event?: MapMouseEvent) => void } = {};
|
||||
|
|
|
@ -184,6 +184,15 @@ export class TooltipControl extends Component<Props, {}> {
|
|||
continue;
|
||||
}
|
||||
|
||||
// masking must use paint property "opacity" to hide features in order to support feature state
|
||||
// therefore, there is no way to remove masked features with queryRenderedFeatures
|
||||
// masked features must be removed via manual filtering
|
||||
const masks = layer.getMasks();
|
||||
const maskHiddingFeature = masks.find((mask) => mask.isFeatureMasked(mbFeature));
|
||||
if (maskHiddingFeature) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const featureId = layer.getFeatureId(mbFeature);
|
||||
if (featureId === undefined) {
|
||||
continue;
|
||||
|
|
|
@ -8,12 +8,21 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const panelStrings = {
|
||||
apply: i18n.translate('xpack.maps.panel.applyLabel', {
|
||||
defaultMessage: 'Apply',
|
||||
}),
|
||||
clear: i18n.translate('xpack.maps.panel.clearLabel', {
|
||||
defaultMessage: 'Clear',
|
||||
}),
|
||||
close: i18n.translate('xpack.maps.panel.closeLabel', {
|
||||
defaultMessage: 'Close',
|
||||
}),
|
||||
discardChanges: i18n.translate('xpack.maps.panel.discardChangesLabel', {
|
||||
defaultMessage: 'Discard changes',
|
||||
}),
|
||||
edit: i18n.translate('xpack.maps.panel.editLabel', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
keepChanges: i18n.translate('xpack.maps.panel.keepChangesLabel', {
|
||||
defaultMessage: 'Keep changes',
|
||||
}),
|
||||
|
|
|
@ -20315,15 +20315,11 @@
|
|||
"xpack.maps.addLayerPanel.addLayer": "Ajouter un calque",
|
||||
"xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "Changer de calque",
|
||||
"xpack.maps.addLayerPanel.footer.cancelButtonLabel": "Annuler",
|
||||
"xpack.maps.aggs.defaultCountLabel": "compte",
|
||||
"xpack.maps.attribution.addBtnAriaLabel": "Ajouter une attribution",
|
||||
"xpack.maps.attribution.addBtnLabel": "Ajouter une attribution",
|
||||
"xpack.maps.attribution.applyBtnLabel": "Appliquer",
|
||||
"xpack.maps.attribution.attributionFormLabel": "Attribution",
|
||||
"xpack.maps.attribution.clearBtnAriaLabel": "Effacer l'attribution",
|
||||
"xpack.maps.attribution.clearBtnLabel": "Effacer",
|
||||
"xpack.maps.attribution.editBtnAriaLabel": "Modifier l'attribution",
|
||||
"xpack.maps.attribution.editBtnLabel": "Modifier",
|
||||
"xpack.maps.attribution.labelFieldLabel": "Étiquette",
|
||||
"xpack.maps.attribution.urlLabel": "Lien",
|
||||
"xpack.maps.badge.readOnly.text": "Lecture seule",
|
||||
|
@ -20595,15 +20591,7 @@
|
|||
"xpack.maps.metricsEditor.selectFieldLabel": "Champ",
|
||||
"xpack.maps.metricsEditor.selectFieldPlaceholder": "Sélectionner un champ",
|
||||
"xpack.maps.metricsEditor.selectPercentileLabel": "Centile",
|
||||
"xpack.maps.metricSelect.averageDropDownOptionLabel": "Moyenne",
|
||||
"xpack.maps.metricSelect.cardinalityDropDownOptionLabel": "Compte unique",
|
||||
"xpack.maps.metricSelect.countDropDownOptionLabel": "Décompte",
|
||||
"xpack.maps.metricSelect.maxDropDownOptionLabel": "Max.",
|
||||
"xpack.maps.metricSelect.minDropDownOptionLabel": "Min.",
|
||||
"xpack.maps.metricSelect.percentileDropDownOptionLabel": "Centile",
|
||||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "Sélectionner une agrégation",
|
||||
"xpack.maps.metricSelect.sumDropDownOptionLabel": "Somme",
|
||||
"xpack.maps.metricSelect.termsDropDownOptionLabel": "Premier terme",
|
||||
"xpack.maps.mvtSource.addFieldLabel": "Ajouter",
|
||||
"xpack.maps.mvtSource.fieldPlaceholderText": "Nom du champ",
|
||||
"xpack.maps.mvtSource.numberFieldLabel": "numéro",
|
||||
|
|
|
@ -20315,15 +20315,11 @@
|
|||
"xpack.maps.addLayerPanel.addLayer": "レイヤーを追加",
|
||||
"xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "レイヤーを変更",
|
||||
"xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル",
|
||||
"xpack.maps.aggs.defaultCountLabel": "カウント",
|
||||
"xpack.maps.attribution.addBtnAriaLabel": "属性を追加",
|
||||
"xpack.maps.attribution.addBtnLabel": "属性を追加",
|
||||
"xpack.maps.attribution.applyBtnLabel": "適用",
|
||||
"xpack.maps.attribution.attributionFormLabel": "属性",
|
||||
"xpack.maps.attribution.clearBtnAriaLabel": "属性を消去",
|
||||
"xpack.maps.attribution.clearBtnLabel": "クリア",
|
||||
"xpack.maps.attribution.editBtnAriaLabel": "属性を編集",
|
||||
"xpack.maps.attribution.editBtnLabel": "編集",
|
||||
"xpack.maps.attribution.labelFieldLabel": "ラベル",
|
||||
"xpack.maps.attribution.urlLabel": "リンク",
|
||||
"xpack.maps.badge.readOnly.text": "読み取り専用",
|
||||
|
@ -20595,15 +20591,7 @@
|
|||
"xpack.maps.metricsEditor.selectFieldLabel": "フィールド",
|
||||
"xpack.maps.metricsEditor.selectFieldPlaceholder": "フィールドを選択",
|
||||
"xpack.maps.metricsEditor.selectPercentileLabel": "パーセンタイル",
|
||||
"xpack.maps.metricSelect.averageDropDownOptionLabel": "平均",
|
||||
"xpack.maps.metricSelect.cardinalityDropDownOptionLabel": "ユニークカウント",
|
||||
"xpack.maps.metricSelect.countDropDownOptionLabel": "カウント",
|
||||
"xpack.maps.metricSelect.maxDropDownOptionLabel": "最高",
|
||||
"xpack.maps.metricSelect.minDropDownOptionLabel": "最低",
|
||||
"xpack.maps.metricSelect.percentileDropDownOptionLabel": "パーセンタイル",
|
||||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "集約を選択",
|
||||
"xpack.maps.metricSelect.sumDropDownOptionLabel": "合計",
|
||||
"xpack.maps.metricSelect.termsDropDownOptionLabel": "トップ用語",
|
||||
"xpack.maps.mvtSource.addFieldLabel": "追加",
|
||||
"xpack.maps.mvtSource.fieldPlaceholderText": "フィールド名",
|
||||
"xpack.maps.mvtSource.numberFieldLabel": "数字",
|
||||
|
@ -23391,7 +23379,7 @@
|
|||
"xpack.ml.ruleEditor.scopeSection.noPermissionToViewFilterListsTitle": "フィルターリストを表示するパーミッションがありません",
|
||||
"xpack.ml.ruleEditor.scopeSection.scopeTitle": "範囲",
|
||||
"xpack.ml.ruleEditor.selectRuleAction.createRuleLinkText": "ルールを作成",
|
||||
"xpack.ml.ruleEditor.selectRuleAction.orText": "OR ",
|
||||
"xpack.ml.ruleEditor.selectRuleAction.orText": "OR ",
|
||||
"xpack.ml.ruleEditor.typicalAppliesTypeText": "通常",
|
||||
"xpack.ml.sampleDataLinkLabel": "ML ジョブ",
|
||||
"xpack.ml.selectDataViewLabel": "データビューを選択",
|
||||
|
|
|
@ -20315,15 +20315,11 @@
|
|||
"xpack.maps.addLayerPanel.addLayer": "添加图层",
|
||||
"xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "更改图层",
|
||||
"xpack.maps.addLayerPanel.footer.cancelButtonLabel": "取消",
|
||||
"xpack.maps.aggs.defaultCountLabel": "计数",
|
||||
"xpack.maps.attribution.addBtnAriaLabel": "添加归因",
|
||||
"xpack.maps.attribution.addBtnLabel": "添加归因",
|
||||
"xpack.maps.attribution.applyBtnLabel": "应用",
|
||||
"xpack.maps.attribution.attributionFormLabel": "归因",
|
||||
"xpack.maps.attribution.clearBtnAriaLabel": "清除归因",
|
||||
"xpack.maps.attribution.clearBtnLabel": "清除",
|
||||
"xpack.maps.attribution.editBtnAriaLabel": "编辑归因",
|
||||
"xpack.maps.attribution.editBtnLabel": "编辑",
|
||||
"xpack.maps.attribution.labelFieldLabel": "标签",
|
||||
"xpack.maps.attribution.urlLabel": "链接",
|
||||
"xpack.maps.badge.readOnly.text": "只读",
|
||||
|
@ -20595,15 +20591,7 @@
|
|||
"xpack.maps.metricsEditor.selectFieldLabel": "字段",
|
||||
"xpack.maps.metricsEditor.selectFieldPlaceholder": "选择字段",
|
||||
"xpack.maps.metricsEditor.selectPercentileLabel": "百分位数",
|
||||
"xpack.maps.metricSelect.averageDropDownOptionLabel": "平均值",
|
||||
"xpack.maps.metricSelect.cardinalityDropDownOptionLabel": "唯一计数",
|
||||
"xpack.maps.metricSelect.countDropDownOptionLabel": "计数",
|
||||
"xpack.maps.metricSelect.maxDropDownOptionLabel": "最大值",
|
||||
"xpack.maps.metricSelect.minDropDownOptionLabel": "最小值",
|
||||
"xpack.maps.metricSelect.percentileDropDownOptionLabel": "百分位数",
|
||||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "选择聚合",
|
||||
"xpack.maps.metricSelect.sumDropDownOptionLabel": "求和",
|
||||
"xpack.maps.metricSelect.termsDropDownOptionLabel": "热门词",
|
||||
"xpack.maps.mvtSource.addFieldLabel": "添加",
|
||||
"xpack.maps.mvtSource.fieldPlaceholderText": "字段名称",
|
||||
"xpack.maps.mvtSource.numberFieldLabel": "数字",
|
||||
|
@ -23403,7 +23391,7 @@
|
|||
"xpack.ml.ruleEditor.scopeSection.noPermissionToViewFilterListsTitle": "您无权查看筛选列表",
|
||||
"xpack.ml.ruleEditor.scopeSection.scopeTitle": "范围",
|
||||
"xpack.ml.ruleEditor.selectRuleAction.createRuleLinkText": "创建规则",
|
||||
"xpack.ml.ruleEditor.selectRuleAction.orText": "或 ",
|
||||
"xpack.ml.ruleEditor.selectRuleAction.orText": "或 ",
|
||||
"xpack.ml.ruleEditor.typicalAppliesTypeText": "典型",
|
||||
"xpack.ml.sampleDataLinkLabel": "ML 作业",
|
||||
"xpack.ml.selectDataViewLabel": "选择数据视图",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue