[Maps] move joins from LayerDescriptor to VectorLayerDescriptor (#112427)

* [Maps] move joins from LayerDescriptor to VectorLayerDescriptor

* clean up ISource

* export isVectorLayer

* tslint

* tslint for ml plugin

* tslint apm plugin

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2021-09-22 12:47:35 -06:00 committed by GitHub
parent 11f1c44bde
commit 15846b27c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 321 additions and 258 deletions

View file

@ -15,6 +15,7 @@ import {
COLOR_MAP_TYPE, COLOR_MAP_TYPE,
FIELD_ORIGIN, FIELD_ORIGIN,
LABEL_BORDER_SIZES, LABEL_BORDER_SIZES,
LAYER_TYPE,
SOURCE_TYPES, SOURCE_TYPES,
STYLE_TYPE, STYLE_TYPE,
SYMBOLIZE_AS_TYPES, SYMBOLIZE_AS_TYPES,
@ -154,7 +155,7 @@ export function useLayerList() {
maxZoom: 24, maxZoom: 24,
alpha: 0.75, alpha: 0.75,
visible: true, visible: true,
type: 'VECTOR', type: LAYER_TYPE.VECTOR,
}; };
ES_TERM_SOURCE_REGION.whereQuery = getWhereQuery(serviceName!); ES_TERM_SOURCE_REGION.whereQuery = getWhereQuery(serviceName!);
@ -178,7 +179,7 @@ export function useLayerList() {
maxZoom: 24, maxZoom: 24,
alpha: 0.75, alpha: 0.75,
visible: true, visible: true,
type: 'VECTOR', type: LAYER_TYPE.VECTOR,
}; };
return [ return [

View file

@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { import {
FIELD_ORIGIN, FIELD_ORIGIN,
LAYER_TYPE,
SOURCE_TYPES, SOURCE_TYPES,
STYLE_TYPE, STYLE_TYPE,
COLOR_MAP_TYPE, COLOR_MAP_TYPE,
@ -85,7 +86,7 @@ export const getChoroplethTopValuesLayer = (
}, },
isTimeAware: true, isTimeAware: true,
}, },
type: 'VECTOR', type: LAYER_TYPE.VECTOR,
}; };
}; };

View file

@ -23,6 +23,7 @@ import {
KBN_IS_TILE_COMPLETE, KBN_IS_TILE_COMPLETE,
KBN_METADATA_FEATURE, KBN_METADATA_FEATURE,
KBN_VECTOR_SHAPE_TYPE_COUNTS, KBN_VECTOR_SHAPE_TYPE_COUNTS,
LAYER_TYPE,
} from '../constants'; } from '../constants';
export type Attribution = { export type Attribution = {
@ -56,7 +57,6 @@ export type LayerDescriptor = {
alpha?: number; alpha?: number;
attribution?: Attribution; attribution?: Attribution;
id: string; id: string;
joins?: JoinDescriptor[];
label?: string | null; label?: string | null;
areLabelsOnTop?: boolean; areLabelsOnTop?: boolean;
minZoom?: number; minZoom?: number;
@ -70,9 +70,12 @@ export type LayerDescriptor = {
}; };
export type VectorLayerDescriptor = LayerDescriptor & { export type VectorLayerDescriptor = LayerDescriptor & {
type: LAYER_TYPE.VECTOR | LAYER_TYPE.TILED_VECTOR | LAYER_TYPE.BLENDED_VECTOR;
joins?: JoinDescriptor[];
style: VectorStyleDescriptor; style: VectorStyleDescriptor;
}; };
export type HeatmapLayerDescriptor = LayerDescriptor & { export type HeatmapLayerDescriptor = LayerDescriptor & {
type: LAYER_TYPE.HEATMAP;
style: HeatmapStyleDescriptor; style: HeatmapStyleDescriptor;
}; };

View file

@ -12,6 +12,7 @@ export {
FIELD_ORIGIN, FIELD_ORIGIN,
INITIAL_LOCATION, INITIAL_LOCATION,
LABEL_BORDER_SIZES, LABEL_BORDER_SIZES,
LAYER_TYPE,
MAP_SAVED_OBJECT_TYPE, MAP_SAVED_OBJECT_TYPE,
SOURCE_TYPES, SOURCE_TYPES,
STYLE_TYPE, STYLE_TYPE,

View file

@ -6,8 +6,8 @@
*/ */
import { MapSavedObjectAttributes } from '../map_saved_object_type'; import { MapSavedObjectAttributes } from '../map_saved_object_type';
import { JoinDescriptor, LayerDescriptor } from '../descriptor_types'; import { JoinDescriptor, LayerDescriptor, VectorLayerDescriptor } from '../descriptor_types';
import { LAYER_TYPE, SOURCE_TYPES } from '../constants'; import { SOURCE_TYPES } from '../constants';
// enforce type property on joins. It's possible older saved-objects do not have this correctly filled in // enforce type property on joins. It's possible older saved-objects do not have this correctly filled in
// e.g. sample-data was missing the right.type field. // e.g. sample-data was missing the right.type field.
@ -24,14 +24,15 @@ export function addTypeToTermJoin({
const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON);
layerList.forEach((layer: LayerDescriptor) => { layerList.forEach((layer: LayerDescriptor) => {
if (layer.type !== LAYER_TYPE.VECTOR) { if (!('joins' in layer)) {
return; return;
} }
if (!layer.joins) { const vectorLayer = layer as VectorLayerDescriptor;
if (!vectorLayer.joins) {
return; return;
} }
layer.joins.forEach((join: JoinDescriptor) => { vectorLayer.joins.forEach((join: JoinDescriptor) => {
if (!join.right) { if (!join.right) {
return; return;
} }

View file

@ -9,7 +9,7 @@
import { SavedObjectReference } from '../../../../../src/core/types'; import { SavedObjectReference } from '../../../../../src/core/types';
import { MapSavedObjectAttributes } from '../map_saved_object_type'; import { MapSavedObjectAttributes } from '../map_saved_object_type';
import { LayerDescriptor } from '../descriptor_types'; import { LayerDescriptor, VectorLayerDescriptor } from '../descriptor_types';
interface IndexPatternReferenceDescriptor { interface IndexPatternReferenceDescriptor {
indexPatternId?: string; indexPatternId?: string;
@ -44,8 +44,10 @@ export function extractReferences({
sourceDescriptor.indexPatternRefName = refName; sourceDescriptor.indexPatternRefName = refName;
} }
if ('joins' in layer) {
// Extract index-pattern references from join // Extract index-pattern references from join
const joins = layer.joins ? layer.joins : []; const vectorLayer = layer as VectorLayerDescriptor;
const joins = vectorLayer.joins ? vectorLayer.joins : [];
joins.forEach((join, joinIndex) => { joins.forEach((join, joinIndex) => {
if ('indexPatternId' in join.right) { if ('indexPatternId' in join.right) {
const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; const sourceDescriptor = join.right as IndexPatternReferenceDescriptor;
@ -59,6 +61,7 @@ export function extractReferences({
sourceDescriptor.indexPatternRefName = refName; sourceDescriptor.indexPatternRefName = refName;
} }
}); });
}
}); });
return { return {
@ -99,8 +102,10 @@ export function injectReferences({
delete sourceDescriptor.indexPatternRefName; delete sourceDescriptor.indexPatternRefName;
} }
if ('joins' in layer) {
// Inject index-pattern references into join // Inject index-pattern references into join
const joins = layer.joins ? layer.joins : []; const vectorLayer = layer as VectorLayerDescriptor;
const joins = vectorLayer.joins ? vectorLayer.joins : [];
joins.forEach((join) => { joins.forEach((join) => {
if ('indexPatternRefName' in join.right) { if ('indexPatternRefName' in join.right) {
const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; const sourceDescriptor = join.right as IndexPatternReferenceDescriptor;
@ -109,6 +114,7 @@ export function injectReferences({
delete sourceDescriptor.indexPatternRefName; delete sourceDescriptor.indexPatternRefName;
} }
}); });
}
}); });
return { return {

View file

@ -33,7 +33,6 @@ import {
SizeDynamicOptions, SizeDynamicOptions,
DynamicStylePropertyOptions, DynamicStylePropertyOptions,
StylePropertyOptions, StylePropertyOptions,
LayerDescriptor,
Timeslice, Timeslice,
VectorLayerDescriptor, VectorLayerDescriptor,
VectorSourceRequestMeta, VectorSourceRequestMeta,
@ -179,7 +178,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
mapColors: string[] mapColors: string[]
): VectorLayerDescriptor { ): VectorLayerDescriptor {
const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); const layerDescriptor = VectorLayer.createDescriptor(options, mapColors);
layerDescriptor.type = BlendedVectorLayer.type; layerDescriptor.type = LAYER_TYPE.BLENDED_VECTOR;
return layerDescriptor; return layerDescriptor;
} }
@ -256,7 +255,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
return false; return false;
} }
async cloneDescriptor(): Promise<LayerDescriptor> { async cloneDescriptor(): Promise<VectorLayerDescriptor> {
const clonedDescriptor = await super.cloneDescriptor(); const clonedDescriptor = await super.cloneDescriptor();
// Use super getDisplayName instead of instance getDisplayName to avoid getting 'Clustered Clone of Clustered' // Use super getDisplayName instead of instance getDisplayName to avoid getting 'Clustered Clone of Clustered'

View file

@ -9,21 +9,6 @@
import { AbstractLayer } from './layer'; import { AbstractLayer } from './layer';
import { ISource } from '../sources/source'; import { ISource } from '../sources/source';
import {
AGG_TYPE,
FIELD_ORIGIN,
LAYER_STYLE_TYPE,
SOURCE_TYPES,
VECTOR_STYLES,
} from '../../../common/constants';
import { ESTermSourceDescriptor, VectorStyleDescriptor } from '../../../common/descriptor_types';
import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults';
jest.mock('uuid/v4', () => {
return function () {
return '12345';
};
});
class MockLayer extends AbstractLayer {} class MockLayer extends AbstractLayer {}
@ -36,111 +21,11 @@ class MockSource {
return {}; return {};
} }
getDisplayName() {
return 'mySource';
}
async supportsFitToBounds() { async supportsFitToBounds() {
return this._fitToBounds; return this._fitToBounds;
} }
} }
describe('cloneDescriptor', () => {
describe('with joins', () => {
const styleDescriptor = {
type: LAYER_STYLE_TYPE.VECTOR,
properties: {
...getDefaultDynamicProperties(),
},
} as VectorStyleDescriptor;
// @ts-expect-error
styleDescriptor.properties[VECTOR_STYLES.FILL_COLOR].options.field = {
name: '__kbnjoin__count__557d0f15',
origin: FIELD_ORIGIN.JOIN,
};
// @ts-expect-error
styleDescriptor.properties[VECTOR_STYLES.LINE_COLOR].options.field = {
name: 'bytes',
origin: FIELD_ORIGIN.SOURCE,
};
// @ts-expect-error
styleDescriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field = {
name: '__kbnjoin__count__6666666666',
origin: FIELD_ORIGIN.JOIN,
};
test('Should update data driven styling properties using join fields', async () => {
const layerDescriptor = AbstractLayer.createDescriptor({
style: styleDescriptor,
joins: [
{
leftField: 'iso2',
right: {
id: '557d0f15',
indexPatternId: 'myIndexPattern',
indexPatternTitle: 'logs-*',
metrics: [{ type: AGG_TYPE.COUNT }],
term: 'myTermField',
type: SOURCE_TYPES.ES_TERM_SOURCE,
applyGlobalQuery: true,
applyGlobalTime: true,
applyForceRefresh: true,
},
},
],
});
const layer = new MockLayer({
layerDescriptor,
source: new MockSource() as unknown as ISource,
});
const clonedDescriptor = await layer.cloneDescriptor();
const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties;
// Should update style field belonging to join
// @ts-expect-error
expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual(
'__kbnjoin__count__12345'
);
// Should not update style field belonging to source
// @ts-expect-error
expect(clonedStyleProps[VECTOR_STYLES.LINE_COLOR].options.field.name).toEqual('bytes');
// Should not update style feild belonging to different join
// @ts-expect-error
expect(clonedStyleProps[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field.name).toEqual(
'__kbnjoin__count__6666666666'
);
});
test('Should update data driven styling properties using join fields when metrics are not provided', async () => {
const layerDescriptor = AbstractLayer.createDescriptor({
style: styleDescriptor,
joins: [
{
leftField: 'iso2',
right: {
id: '557d0f15',
indexPatternId: 'myIndexPattern',
indexPatternTitle: 'logs-*',
term: 'myTermField',
type: 'joinSource',
} as unknown as ESTermSourceDescriptor,
},
],
});
const layer = new MockLayer({
layerDescriptor,
source: new MockSource() as unknown as ISource,
});
const clonedDescriptor = await layer.cloneDescriptor();
const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties;
// Should update style field belonging to join
// @ts-expect-error
expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual(
'__kbnjoin__count__12345'
);
});
});
});
describe('isFittable', () => { describe('isFittable', () => {
[ [
{ {

View file

@ -16,23 +16,16 @@ import uuid from 'uuid/v4';
import { FeatureCollection } from 'geojson'; import { FeatureCollection } from 'geojson';
import { DataRequest } from '../util/data_request'; import { DataRequest } from '../util/data_request';
import { import {
AGG_TYPE,
FIELD_ORIGIN,
LAYER_TYPE, LAYER_TYPE,
MAX_ZOOM, MAX_ZOOM,
MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER,
MIN_ZOOM, MIN_ZOOM,
SOURCE_BOUNDS_DATA_REQUEST_ID, SOURCE_BOUNDS_DATA_REQUEST_ID,
SOURCE_DATA_REQUEST_ID, SOURCE_DATA_REQUEST_ID,
SOURCE_TYPES,
STYLE_TYPE,
} from '../../../common/constants'; } from '../../../common/constants';
import { copyPersistentState } from '../../reducers/copy_persistent_state'; import { copyPersistentState } from '../../reducers/copy_persistent_state';
import { import {
AggDescriptor,
Attribution, Attribution,
ESTermSourceDescriptor,
JoinDescriptor,
LayerDescriptor, LayerDescriptor,
MapExtent, MapExtent,
StyleDescriptor, StyleDescriptor,
@ -42,7 +35,6 @@ import {
import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source';
import { DataRequestContext } from '../../actions'; import { DataRequestContext } from '../../actions';
import { IStyle } from '../styles/style'; import { IStyle } from '../styles/style';
import { getJoinAggKey } from '../../../common/get_agg_key';
import { LICENSED_FEATURES } from '../../licensed_features'; import { LICENSED_FEATURES } from '../../licensed_features';
import { IESSource } from '../sources/es_source'; import { IESSource } from '../sources/es_source';
@ -97,8 +89,6 @@ export interface ILayer {
isPreviewLayer: () => boolean; isPreviewLayer: () => boolean;
areLabelsOnTop: () => boolean; areLabelsOnTop: () => boolean;
supportsLabelsOnTop: () => boolean; supportsLabelsOnTop: () => boolean;
showJoinEditor(): boolean;
getJoinsDisabledReason(): string | null;
isFittable(): Promise<boolean>; isFittable(): Promise<boolean>;
isIncludeInFitToBounds(): boolean; isIncludeInFitToBounds(): boolean;
getLicensedFeatures(): Promise<LICENSED_FEATURES[]>; getLicensedFeatures(): Promise<LICENSED_FEATURES[]>;
@ -177,56 +167,6 @@ export class AbstractLayer implements ILayer {
const displayName = await this.getDisplayName(); const displayName = await this.getDisplayName();
clonedDescriptor.label = `Clone of ${displayName}`; clonedDescriptor.label = `Clone of ${displayName}`;
clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor();
if (clonedDescriptor.joins) {
clonedDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => {
if (joinDescriptor.right && joinDescriptor.right.type === SOURCE_TYPES.TABLE_SOURCE) {
throw new Error(
'Cannot clone table-source. Should only be used in MapEmbeddable, not in UX'
);
}
const termSourceDescriptor: ESTermSourceDescriptor =
joinDescriptor.right as ESTermSourceDescriptor;
// todo: must tie this to generic thing
const originalJoinId = joinDescriptor.right.id!;
// right.id is uuid used to track requests in inspector
joinDescriptor.right.id = uuid();
// Update all data driven styling properties using join fields
if (clonedDescriptor.style && 'properties' in clonedDescriptor.style) {
const metrics =
termSourceDescriptor.metrics && termSourceDescriptor.metrics.length
? termSourceDescriptor.metrics
: [{ type: AGG_TYPE.COUNT }];
metrics.forEach((metricsDescriptor: AggDescriptor) => {
const originalJoinKey = getJoinAggKey({
aggType: metricsDescriptor.type,
aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '',
rightSourceId: originalJoinId,
});
const newJoinKey = getJoinAggKey({
aggType: metricsDescriptor.type,
aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '',
rightSourceId: joinDescriptor.right.id!,
});
Object.keys(clonedDescriptor.style.properties).forEach((key) => {
const styleProp = clonedDescriptor.style.properties[key];
if (
styleProp.type === STYLE_TYPE.DYNAMIC &&
styleProp.options.field &&
styleProp.options.field.origin === FIELD_ORIGIN.JOIN &&
styleProp.options.field.name === originalJoinKey
) {
styleProp.options.field.name = newJoinKey;
}
});
});
}
});
}
return clonedDescriptor; return clonedDescriptor;
} }
@ -234,14 +174,6 @@ export class AbstractLayer implements ILayer {
return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`;
} }
showJoinEditor(): boolean {
return this.getSource().showJoinEditor();
}
getJoinsDisabledReason() {
return this.getSource().getJoinsDisabledReason();
}
isPreviewLayer(): boolean { isPreviewLayer(): boolean {
return !!this._descriptor.__isPreviewLayer; return !!this._descriptor.__isPreviewLayer;
} }

View file

@ -48,7 +48,7 @@ export class TiledVectorLayer extends VectorLayer {
mapColors?: string[] mapColors?: string[]
): VectorLayerDescriptor { ): VectorLayerDescriptor {
const layerDescriptor = super.createDescriptor(descriptor, mapColors); const layerDescriptor = super.createDescriptor(descriptor, mapColors);
layerDescriptor.type = TiledVectorLayer.type; layerDescriptor.type = LAYER_TYPE.TILED_VECTOR;
if (!layerDescriptor.style) { if (!layerDescriptor.style) {
const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []);

View file

@ -7,6 +7,7 @@
export { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; export { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils';
export { export {
isVectorLayer,
IVectorLayer, IVectorLayer,
VectorLayer, VectorLayer,
VectorLayerArguments, VectorLayerArguments,

View file

@ -0,0 +1,136 @@
/*
* 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.
*/
/* eslint-disable max-classes-per-file */
jest.mock('../../styles/vector/vector_style', () => ({
VectorStyle: class MockVectorStyle {},
}));
jest.mock('uuid/v4', () => {
return function () {
return '12345';
};
});
import {
AGG_TYPE,
FIELD_ORIGIN,
LAYER_STYLE_TYPE,
SOURCE_TYPES,
VECTOR_STYLES,
} from '../../../../common/constants';
import { ESTermSourceDescriptor, VectorStyleDescriptor } from '../../../../common/descriptor_types';
import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults';
import { IVectorSource } from '../../sources/vector_source';
import { VectorLayer } from './vector_layer';
class MockSource {
cloneDescriptor() {
return {};
}
getDisplayName() {
return 'mySource';
}
}
describe('cloneDescriptor', () => {
describe('with joins', () => {
const styleDescriptor = {
type: LAYER_STYLE_TYPE.VECTOR,
properties: {
...getDefaultDynamicProperties(),
},
} as VectorStyleDescriptor;
// @ts-expect-error
styleDescriptor.properties[VECTOR_STYLES.FILL_COLOR].options.field = {
name: '__kbnjoin__count__557d0f15',
origin: FIELD_ORIGIN.JOIN,
};
// @ts-expect-error
styleDescriptor.properties[VECTOR_STYLES.LINE_COLOR].options.field = {
name: 'bytes',
origin: FIELD_ORIGIN.SOURCE,
};
// @ts-expect-error
styleDescriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field = {
name: '__kbnjoin__count__6666666666',
origin: FIELD_ORIGIN.JOIN,
};
test('Should update data driven styling properties using join fields', async () => {
const layerDescriptor = VectorLayer.createDescriptor({
style: styleDescriptor,
joins: [
{
leftField: 'iso2',
right: {
id: '557d0f15',
indexPatternId: 'myIndexPattern',
indexPatternTitle: 'logs-*',
metrics: [{ type: AGG_TYPE.COUNT }],
term: 'myTermField',
type: SOURCE_TYPES.ES_TERM_SOURCE,
applyGlobalQuery: true,
applyGlobalTime: true,
applyForceRefresh: true,
},
},
],
});
const layer = new VectorLayer({
layerDescriptor,
source: new MockSource() as unknown as IVectorSource,
});
const clonedDescriptor = await layer.cloneDescriptor();
const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties;
// Should update style field belonging to join
// @ts-expect-error
expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual(
'__kbnjoin__count__12345'
);
// Should not update style field belonging to source
// @ts-expect-error
expect(clonedStyleProps[VECTOR_STYLES.LINE_COLOR].options.field.name).toEqual('bytes');
// Should not update style feild belonging to different join
// @ts-expect-error
expect(clonedStyleProps[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field.name).toEqual(
'__kbnjoin__count__6666666666'
);
});
test('Should update data driven styling properties using join fields when metrics are not provided', async () => {
const layerDescriptor = VectorLayer.createDescriptor({
style: styleDescriptor,
joins: [
{
leftField: 'iso2',
right: {
id: '557d0f15',
indexPatternId: 'myIndexPattern',
indexPatternTitle: 'logs-*',
term: 'myTermField',
type: 'joinSource',
} as unknown as ESTermSourceDescriptor,
},
],
});
const layer = new VectorLayer({
layerDescriptor,
source: new MockSource() as unknown as IVectorSource,
});
const clonedDescriptor = await layer.cloneDescriptor();
const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties;
// Should update style field belonging to join
// @ts-expect-error
expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual(
'__kbnjoin__count__12345'
);
});
});
});

View file

@ -6,6 +6,7 @@
*/ */
import React from 'react'; import React from 'react';
import uuid from 'uuid/v4';
import type { import type {
Map as MbMap, Map as MbMap,
AnyLayer as MbLayer, AnyLayer as MbLayer,
@ -19,6 +20,7 @@ import { i18n } from '@kbn/i18n';
import { AbstractLayer } from '../layer'; import { AbstractLayer } from '../layer';
import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style';
import { import {
AGG_TYPE,
FEATURE_ID_PROPERTY_NAME, FEATURE_ID_PROPERTY_NAME,
SOURCE_META_DATA_REQUEST_ID, SOURCE_META_DATA_REQUEST_ID,
SOURCE_FORMATTERS_DATA_REQUEST_ID, SOURCE_FORMATTERS_DATA_REQUEST_ID,
@ -29,8 +31,11 @@ import {
FIELD_ORIGIN, FIELD_ORIGIN,
KBN_TOO_MANY_FEATURES_IMAGE_ID, KBN_TOO_MANY_FEATURES_IMAGE_ID,
FieldFormatter, FieldFormatter,
SOURCE_TYPES,
STYLE_TYPE,
SUPPORTS_FEATURE_EDITING_REQUEST_ID, SUPPORTS_FEATURE_EDITING_REQUEST_ID,
KBN_IS_TILE_COMPLETE, KBN_IS_TILE_COMPLETE,
VECTOR_STYLES,
} from '../../../../common/constants'; } from '../../../../common/constants';
import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property';
import { DataRequestAbortError } from '../../util/data_request'; import { DataRequestAbortError } from '../../util/data_request';
@ -48,8 +53,11 @@ import {
TimesliceMaskConfig, TimesliceMaskConfig,
} from '../../util/mb_filter_expressions'; } from '../../util/mb_filter_expressions';
import { import {
AggDescriptor,
DynamicStylePropertyOptions, DynamicStylePropertyOptions,
DataFilters, DataFilters,
ESTermSourceDescriptor,
JoinDescriptor,
StyleMetaDescriptor, StyleMetaDescriptor,
Timeslice, Timeslice,
VectorLayerDescriptor, VectorLayerDescriptor,
@ -71,6 +79,11 @@ import { ITermJoinSource } from '../../sources/term_join_source';
import { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; import { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils';
import { JoinState, performInnerJoins } from './perform_inner_joins'; import { JoinState, performInnerJoins } from './perform_inner_joins';
import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { buildVectorRequestMeta } from '../build_vector_request_meta';
import { getJoinAggKey } from '../../../../common/get_agg_key';
export function isVectorLayer(layer: ILayer) {
return (layer as IVectorLayer).canShowTooltip !== undefined;
}
export interface VectorLayerArguments { export interface VectorLayerArguments {
source: IVectorSource; source: IVectorSource;
@ -83,11 +96,13 @@ export interface IVectorLayer extends ILayer {
getFields(): Promise<IField[]>; getFields(): Promise<IField[]>;
getStyleEditorFields(): Promise<IField[]>; getStyleEditorFields(): Promise<IField[]>;
getJoins(): InnerJoin[]; getJoins(): InnerJoin[];
getJoinsDisabledReason(): string | null;
getValidJoins(): InnerJoin[]; getValidJoins(): InnerJoin[];
getSource(): IVectorSource; getSource(): IVectorSource;
getFeatureById(id: string | number): Feature | null; getFeatureById(id: string | number): Feature | null;
getPropertiesForTooltip(properties: GeoJsonProperties): Promise<ITooltipProperty[]>; getPropertiesForTooltip(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
hasJoins(): boolean; hasJoins(): boolean;
showJoinEditor(): boolean;
canShowTooltip(): boolean; canShowTooltip(): boolean;
supportsFeatureEditing(): boolean; supportsFeatureEditing(): boolean;
getLeftJoinFields(): Promise<IField[]>; getLeftJoinFields(): Promise<IField[]>;
@ -113,8 +128,8 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
options: Partial<VectorLayerDescriptor>, options: Partial<VectorLayerDescriptor>,
mapColors?: string[] mapColors?: string[]
): VectorLayerDescriptor { ): VectorLayerDescriptor {
const layerDescriptor = super.createDescriptor(options); const layerDescriptor = super.createDescriptor(options) as VectorLayerDescriptor;
layerDescriptor.type = VectorLayer.type; layerDescriptor.type = LAYER_TYPE.VECTOR;
if (!options.style) { if (!options.style) {
const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []);
@ -125,7 +140,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
layerDescriptor.joins = []; layerDescriptor.joins = [];
} }
return layerDescriptor as VectorLayerDescriptor; return layerDescriptor;
} }
constructor({ constructor({
@ -147,6 +162,62 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
); );
} }
async cloneDescriptor(): Promise<VectorLayerDescriptor> {
const clonedDescriptor = (await super.cloneDescriptor()) as VectorLayerDescriptor;
if (clonedDescriptor.joins) {
clonedDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => {
if (joinDescriptor.right && joinDescriptor.right.type === SOURCE_TYPES.TABLE_SOURCE) {
throw new Error(
'Cannot clone table-source. Should only be used in MapEmbeddable, not in UX'
);
}
const termSourceDescriptor: ESTermSourceDescriptor =
joinDescriptor.right as ESTermSourceDescriptor;
// todo: must tie this to generic thing
const originalJoinId = joinDescriptor.right.id!;
// right.id is uuid used to track requests in inspector
joinDescriptor.right.id = uuid();
// Update all data driven styling properties using join fields
if (clonedDescriptor.style && 'properties' in clonedDescriptor.style) {
const metrics =
termSourceDescriptor.metrics && termSourceDescriptor.metrics.length
? termSourceDescriptor.metrics
: [{ type: AGG_TYPE.COUNT }];
metrics.forEach((metricsDescriptor: AggDescriptor) => {
const originalJoinKey = getJoinAggKey({
aggType: metricsDescriptor.type,
aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '',
rightSourceId: originalJoinId,
});
const newJoinKey = getJoinAggKey({
aggType: metricsDescriptor.type,
aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '',
rightSourceId: joinDescriptor.right.id!,
});
Object.keys(clonedDescriptor.style.properties).forEach((key) => {
const styleProp = clonedDescriptor.style.properties[key as VECTOR_STYLES];
if ('type' in styleProp && styleProp.type === STYLE_TYPE.DYNAMIC) {
const options = styleProp.options as DynamicStylePropertyOptions;
if (
options.field &&
options.field.origin === FIELD_ORIGIN.JOIN &&
options.field.name === originalJoinKey
) {
options.field.name = newJoinKey;
}
}
});
});
}
});
}
return clonedDescriptor;
}
getSource(): IVectorSource { getSource(): IVectorSource {
return super.getSource() as IVectorSource; return super.getSource() as IVectorSource;
} }
@ -176,6 +247,10 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
return this._joins.slice(); return this._joins.slice();
} }
getJoinsDisabledReason() {
return this.getSource().getJoinsDisabledReason();
}
getValidJoins() { getValidJoins() {
return this.getJoins().filter((join) => { return this.getJoins().filter((join) => {
return join.hasCompleteConfig(); return join.hasCompleteConfig();
@ -192,6 +267,10 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
return this.getValidJoins().length > 0; return this.getValidJoins().length > 0;
} }
showJoinEditor(): boolean {
return this.getSource().showJoinEditor();
}
isInitialDataLoadComplete() { isInitialDataLoadComplete() {
const sourceDataRequest = this.getSourceDataRequest(); const sourceDataRequest = this.getSourceDataRequest();
if (!sourceDataRequest || !sourceDataRequest.hasData()) { if (!sourceDataRequest || !sourceDataRequest.hasData()) {

View file

@ -243,6 +243,14 @@ export class MVTSingleLayerVectorSource
async getDefaultFields(): Promise<Record<string, Record<string, string>>> { async getDefaultFields(): Promise<Record<string, Record<string, string>>> {
return {}; return {};
} }
showJoinEditor(): boolean {
return false;
}
getJoinsDisabledReason(): string | null {
return null;
}
} }
registerSource({ registerSource({

View file

@ -53,8 +53,6 @@ export interface ISource {
isESSource(): boolean; isESSource(): boolean;
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> | null; renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> | null;
supportsFitToBounds(): Promise<boolean>; supportsFitToBounds(): Promise<boolean>;
showJoinEditor(): boolean;
getJoinsDisabledReason(): string | null;
cloneDescriptor(): AbstractSourceDescriptor; cloneDescriptor(): AbstractSourceDescriptor;
getFieldNames(): string[]; getFieldNames(): string[];
getApplyGlobalQuery(): boolean; getApplyGlobalQuery(): boolean;
@ -155,14 +153,6 @@ export class AbstractSource implements ISource {
return 0; return 0;
} }
showJoinEditor(): boolean {
return false;
}
getJoinsDisabledReason(): string | null {
return null;
}
isESSource(): boolean { isESSource(): boolean {
return false; return false;
} }

View file

@ -59,6 +59,8 @@ export interface IVectorSource extends ISource {
getFields(): Promise<IField[]>; getFields(): Promise<IField[]>;
getFieldByName(fieldName: string): IField | null; getFieldByName(fieldName: string): IField | null;
getLeftJoinFields(): Promise<IField[]>; getLeftJoinFields(): Promise<IField[]>;
showJoinEditor(): boolean;
getJoinsDisabledReason(): string | null;
/* /*
* Vector layer avoids unnecessarily re-fetching source data. * Vector layer avoids unnecessarily re-fetching source data.
@ -122,6 +124,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
return []; return [];
} }
getJoinsDisabledReason(): string | null {
return null;
}
async getGeoJsonWithMeta( async getGeoJsonWithMeta(
layerName: string, layerName: string,
searchFilters: VectorSourceRequestMeta, searchFilters: VectorSourceRequestMeta,

View file

@ -93,6 +93,7 @@ exports[`EditLayerPanel is rendered 1`] = `
<LayerSettings <LayerSettings
layer={ layer={
Object { Object {
"canShowTooltip": [Function],
"getDisplayName": [Function], "getDisplayName": [Function],
"getId": [Function], "getId": [Function],
"getImmutableSourceProperties": [Function], "getImmutableSourceProperties": [Function],
@ -113,6 +114,7 @@ exports[`EditLayerPanel is rendered 1`] = `
<JoinEditor <JoinEditor
layer={ layer={
Object { Object {
"canShowTooltip": [Function],
"getDisplayName": [Function], "getDisplayName": [Function],
"getId": [Function], "getId": [Function],
"getImmutableSourceProperties": [Function], "getImmutableSourceProperties": [Function],

View file

@ -64,6 +64,9 @@ const mockLayer = {
showJoinEditor: () => { showJoinEditor: () => {
return true; return true;
}, },
canShowTooltip: () => {
return true;
},
supportsElasticsearchFilters: () => { supportsElasticsearchFilters: () => {
return false; return false;
}, },

View file

@ -33,7 +33,7 @@ import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
import { LAYER_TYPE } from '../../../common/constants'; import { LAYER_TYPE } from '../../../common/constants';
import { getData, getCore } from '../../kibana_services'; import { getData, getCore } from '../../kibana_services';
import { ILayer } from '../../classes/layers/layer'; import { ILayer } from '../../classes/layers/layer';
import { IVectorLayer } from '../../classes/layers/vector_layer'; import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer';
import { ImmutableSourceProperty, OnSourceChangeArgs } from '../../classes/sources/source'; import { ImmutableSourceProperty, OnSourceChangeArgs } from '../../classes/sources/source';
import { IField } from '../../classes/fields/field'; import { IField } from '../../classes/fields/field';
@ -111,11 +111,12 @@ export class EditLayerPanel extends Component<Props, State> {
}; };
async _loadLeftJoinFields() { async _loadLeftJoinFields() {
if ( if (!this.props.selectedLayer || !isVectorLayer(this.props.selectedLayer)) {
!this.props.selectedLayer || return;
!this.props.selectedLayer.showJoinEditor() || }
(this.props.selectedLayer as IVectorLayer).getLeftJoinFields === undefined
) { const vectorLayer = this.props.selectedLayer as IVectorLayer;
if (!vectorLayer.showJoinEditor() || vectorLayer.getLeftJoinFields === undefined) {
return; return;
} }
@ -182,7 +183,11 @@ export class EditLayerPanel extends Component<Props, State> {
} }
_renderJoinSection() { _renderJoinSection() {
if (!this.props.selectedLayer || !this.props.selectedLayer.showJoinEditor()) { if (!this.props.selectedLayer || !isVectorLayer(this.props.selectedLayer)) {
return;
}
const vectorLayer = this.props.selectedLayer as IVectorLayer;
if (!vectorLayer.showJoinEditor()) {
return null; return null;
} }
@ -190,7 +195,7 @@ export class EditLayerPanel extends Component<Props, State> {
<Fragment> <Fragment>
<EuiPanel> <EuiPanel>
<JoinEditor <JoinEditor
layer={this.props.selectedLayer} layer={vectorLayer}
leftJoinFields={this.state.leftJoinFields} leftJoinFields={this.state.leftJoinFields}
layerDisplayName={this.state.displayName} layerDisplayName={this.state.displayName}
/> />

View file

@ -13,11 +13,18 @@ import { LAYER_TYPE } from '../../../common/constants';
import { getSelectedLayer } from '../../selectors/map_selectors'; import { getSelectedLayer } from '../../selectors/map_selectors';
import { updateSourceProp } from '../../actions'; import { updateSourceProp } from '../../actions';
import { MapStoreState } from '../../reducers/store'; import { MapStoreState } from '../../reducers/store';
import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer';
function mapStateToProps(state: MapStoreState) { function mapStateToProps(state: MapStoreState) {
const selectedLayer = getSelectedLayer(state); const selectedLayer = getSelectedLayer(state);
let key = 'none';
if (selectedLayer) {
key = isVectorLayer(selectedLayer)
? `${selectedLayer.getId()}${(selectedLayer as IVectorLayer).showJoinEditor()}`
: selectedLayer.getId();
}
return { return {
key: selectedLayer ? `${selectedLayer.getId()}${selectedLayer.showJoinEditor()}` : '', key,
selectedLayer, selectedLayer,
}; };
} }

View file

@ -6,7 +6,7 @@
*/ */
import React from 'react'; import React from 'react';
import { ILayer } from '../../../classes/layers/layer'; import { IVectorLayer } from '../../../classes/layers/vector_layer';
import { JoinEditor } from './join_editor'; import { JoinEditor } from './join_editor';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { JoinDescriptor } from '../../../../common/descriptor_types'; import { JoinDescriptor } from '../../../../common/descriptor_types';
@ -48,7 +48,7 @@ const defaultProps = {
test('Should render join editor', () => { test('Should render join editor', () => {
const component = shallow( const component = shallow(
<JoinEditor {...defaultProps} layer={new MockLayer(null) as unknown as ILayer} /> <JoinEditor {...defaultProps} layer={new MockLayer(null) as unknown as IVectorLayer} />
); );
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();
}); });
@ -57,7 +57,7 @@ test('Should render callout when joins are disabled', () => {
const component = shallow( const component = shallow(
<JoinEditor <JoinEditor
{...defaultProps} {...defaultProps}
layer={new MockLayer('Simulated disabled reason') as unknown as ILayer} layer={new MockLayer('Simulated disabled reason') as unknown as IVectorLayer}
/> />
); );
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();

View file

@ -20,7 +20,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { Join } from './resources/join'; import { Join } from './resources/join';
import { ILayer } from '../../../classes/layers/layer'; import { IVectorLayer } from '../../../classes/layers/vector_layer';
import { JoinDescriptor } from '../../../../common/descriptor_types'; import { JoinDescriptor } from '../../../../common/descriptor_types';
import { SOURCE_TYPES } from '../../../../common/constants'; import { SOURCE_TYPES } from '../../../../common/constants';
@ -31,10 +31,10 @@ export interface JoinField {
export interface Props { export interface Props {
joins: JoinDescriptor[]; joins: JoinDescriptor[];
layer: ILayer; layer: IVectorLayer;
layerDisplayName: string; layerDisplayName: string;
leftJoinFields: JoinField[]; leftJoinFields: JoinField[];
onChange: (layer: ILayer, joins: JoinDescriptor[]) => void; onChange: (layer: IVectorLayer, joins: JoinDescriptor[]) => void;
} }
export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }: Props) { export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }: Props) {

View file

@ -35,7 +35,7 @@ import { TooltipPopover } from './tooltip_popover';
import { FeatureGeometryFilterForm } from './features_tooltip'; import { FeatureGeometryFilterForm } from './features_tooltip';
import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions'; import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions';
import { ILayer } from '../../../classes/layers/layer'; import { ILayer } from '../../../classes/layers/layer';
import { IVectorLayer } from '../../../classes/layers/vector_layer'; import { IVectorLayer, isVectorLayer } from '../../../classes/layers/vector_layer';
import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property'; import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property';
function justifyAnchorLocation( function justifyAnchorLocation(
@ -58,10 +58,6 @@ function justifyAnchorLocation(
return popupAnchorLocation; return popupAnchorLocation;
} }
function isVectorLayer(layer: ILayer) {
return (layer as IVectorLayer).canShowTooltip !== undefined;
}
export interface Props { export interface Props {
addFilters: ((filters: Filter[], actionId: string) => Promise<void>) | null; addFilters: ((filters: Filter[], actionId: string) => Promise<void>) | null;
closeOnClickTooltip: (tooltipId: string) => void; closeOnClickTooltip: (tooltipId: string) => void;

View file

@ -10,6 +10,7 @@ import {
ESGeoGridSourceDescriptor, ESGeoGridSourceDescriptor,
ESSearchSourceDescriptor, ESSearchSourceDescriptor,
LayerDescriptor, LayerDescriptor,
VectorLayerDescriptor,
} from '../../common/descriptor_types'; } from '../../common/descriptor_types';
import { import {
GRID_RESOLUTION, GRID_RESOLUTION,
@ -265,8 +266,7 @@ export function getTermJoinsPerCluster(
): TELEMETRY_TERM_JOIN_COUNTS_PER_CLUSTER { ): TELEMETRY_TERM_JOIN_COUNTS_PER_CLUSTER {
return getCountsByCluster(layerLists, (layerDescriptor: LayerDescriptor) => { return getCountsByCluster(layerLists, (layerDescriptor: LayerDescriptor) => {
return layerDescriptor.type === LAYER_TYPE.VECTOR && return layerDescriptor.type === LAYER_TYPE.VECTOR &&
layerDescriptor.joins && (layerDescriptor as VectorLayerDescriptor)?.joins?.length
layerDescriptor.joins.length
? TELEMETRY_TERM_JOIN ? TELEMETRY_TERM_JOIN
: null; : null;
}); });

View file

@ -18,6 +18,7 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import { import {
FIELD_ORIGIN, FIELD_ORIGIN,
LAYER_TYPE,
SOURCE_TYPES, SOURCE_TYPES,
STYLE_TYPE, STYLE_TYPE,
COLOR_MAP_TYPE, COLOR_MAP_TYPE,
@ -125,7 +126,7 @@ export const getChoroplethAnomaliesLayer = (
isTimeAware: true, isTimeAware: true,
}, },
visible: false, visible: false,
type: 'VECTOR', type: LAYER_TYPE.VECTOR,
}; };
}; };