mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[maps] distance spatial join (#156618)
Fixes https://github.com/elastic/kibana/issues/154605 PR adds new layer wizard for spatial join. Wizard provides an easy interface to create spatial join as well as advertising the capability in the main layer creation work flow. <img width="200" alt="Screen Shot 2023-05-04 at 12 16 45 PM" src="https://user-images.githubusercontent.com/373691/236293473-8a740171-0910-4574-8e38-0ba1ab38a5fd.png"> <img width="400" alt="Screen Shot 2023-05-04 at 12 17 07 PM" src="https://user-images.githubusercontent.com/373691/236293475-ad04cb1c-b49f-46aa-8ae6-2df62123b516.png"> PR renames `Terms joins` editor panel to `Joins` and updates panel to accommodate spatial joins. Displays UI for creating, editing and deleting spatial joins. <img width="200" alt="Screen Shot 2023-05-04 at 12 17 20 PM" src="https://user-images.githubusercontent.com/373691/236293486-49aa8063-0860-4aa7-af85-e47f899a3885.png"> <img width="400" alt="Screen Shot 2023-05-04 at 12 41 39 PM" src="https://user-images.githubusercontent.com/373691/236298721-e237b801-0539-4960-82e6-d992f5bd8bb4.png"> <img width="300" alt="Screen Shot 2023-05-04 at 12 17 25 PM" src="https://user-images.githubusercontent.com/373691/236293489-b18c7a0a-b339-42f0-870d-88785175c1f6.png"> <img width="300" alt="Screen Shot 2023-05-04 at 12 17 37 PM" src="https://user-images.githubusercontent.com/373691/236293492-f4ea3b9b-d28d-46d8-a243-c0e82cb5efda.png"> PR also updates inspector request registration name and description to provide less technical names that provide better meaning of what request is fetching and how everything fits together. I think this really helps understandability of join requests <img width="500" alt="Screen Shot 2023-05-04 at 12 22 56 PM" src="https://user-images.githubusercontent.com/373691/236294739-53d32f65-a5e5-4b6d-b41a-7f76fcd731b5.png"> #### Known issues Issues discovered by this PR that are in main and will be resolved separately. * When using spatial join wizard, if there are no matches to left source then layer gets stuck in loading state https://github.com/elastic/kibana/issues/156630 * Term join left field change not applied as expected https://github.com/elastic/kibana/issues/156631 #### Developer level changes LayerDescriptor * Changes joins from `JoinDescriptor` to `Partial<JoinDescriptor>`. This did not change the content, just updated the type to better reflect contents. JoinDescriptor * Changes right from `JoinSourceDescriptor` to `Partial<JoinSourceDescriptor>`. This did not change the content, just updated the type to better reflect contents. IVectorLayer interface changes * Remove getJoinsDisabledReason * Remove showJoinEditor IVectorSource interface changes * Replaced showJoinEditor with supportsJoins * Removed getJoinsDisabledReason Replaced GeoIndexPatternSelect prop `value` with `dataView`. 1) provides better symmetry since on change return DataView 2) First time GeoIndexPatternSelect need to use a pre-loaded data view. By passing in DataView, loading state can be more easily handled. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nick Peihl <nickpeihl@gmail.com>
This commit is contained in:
parent
5d96ef99d7
commit
596c7b3e70
102 changed files with 2311 additions and 1227 deletions
|
@ -9136,13 +9136,13 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:enableInspectEsQueries": {
|
||||
"observability:syntheticsThrottlingEnabled": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:syntheticsThrottlingEnabled": {
|
||||
"observability:enableInspectEsQueries": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
|
|
|
@ -267,6 +267,16 @@ export class InspectorService extends FtrService {
|
|||
return selectedOption[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens request by name. Use when inspector has multiple requests and you want to view a specific request
|
||||
*/
|
||||
public async openRequestByName(requestName: string): Promise<void> {
|
||||
await this.openInspectorRequestsView();
|
||||
this.log.debug(`Open Inspector request ${requestName}`);
|
||||
await this.testSubjects.click('inspectorRequestChooser');
|
||||
await this.testSubjects.click(`inspectorRequestChooser${requestName.replace(/\s+/, '_')}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns request name as the comma-separated string from combobox
|
||||
*/
|
||||
|
|
|
@ -69,13 +69,16 @@ export enum SOURCE_TYPES {
|
|||
ES_GEO_LINE = 'ES_GEO_LINE',
|
||||
ES_SEARCH = 'ES_SEARCH',
|
||||
ES_PEW_PEW = 'ES_PEW_PEW',
|
||||
ES_TERM_SOURCE = 'ES_TERM_SOURCE',
|
||||
ES_ML_ANOMALIES = 'ML_ANOMALIES',
|
||||
EMS_XYZ = 'EMS_XYZ', // identifies a custom TMS source. EMS-prefix in the name is a little unfortunate :(
|
||||
WMS = 'WMS',
|
||||
KIBANA_TILEMAP = 'KIBANA_TILEMAP',
|
||||
GEOJSON_FILE = 'GEOJSON_FILE',
|
||||
MVT_SINGLE_LAYER = 'MVT_SINGLE_LAYER',
|
||||
|
||||
// join sources
|
||||
ES_DISTANCE_SOURCE = 'ES_DISTANCE_SOURCE',
|
||||
ES_TERM_SOURCE = 'ES_TERM_SOURCE',
|
||||
TABLE_SOURCE = 'TABLE_SOURCE',
|
||||
}
|
||||
|
||||
|
@ -335,6 +338,7 @@ export enum WIZARD_ID {
|
|||
MVT_VECTOR = 'mvtVector',
|
||||
WMS_LAYER = 'wmsLayer',
|
||||
TMS_LAYER = 'tmsLayer',
|
||||
SPATIAL_JOIN = 'spatialJoin',
|
||||
}
|
||||
|
||||
export enum MASK_OPERATOR {
|
||||
|
|
|
@ -26,7 +26,7 @@ export type Attribution = {
|
|||
|
||||
export type JoinDescriptor = {
|
||||
leftField?: string;
|
||||
right: JoinSourceDescriptor;
|
||||
right: Partial<JoinSourceDescriptor>;
|
||||
};
|
||||
|
||||
export type TileMetaFeature = Feature & {
|
||||
|
@ -76,7 +76,7 @@ export type LayerDescriptor = {
|
|||
|
||||
export type VectorLayerDescriptor = LayerDescriptor & {
|
||||
type: LAYER_TYPE.GEOJSON_VECTOR | LAYER_TYPE.MVT_VECTOR | LAYER_TYPE.BLENDED_VECTOR;
|
||||
joins?: JoinDescriptor[];
|
||||
joins?: Array<Partial<JoinDescriptor>>;
|
||||
style: VectorStyleDescriptor;
|
||||
disableTooltips?: boolean;
|
||||
};
|
||||
|
|
|
@ -111,9 +111,18 @@ export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & {
|
|||
destGeoField: string;
|
||||
};
|
||||
|
||||
export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & {
|
||||
term: string; // term field name
|
||||
export type AbstractESJoinSourceDescriptor = AbstractESAggSourceDescriptor & {
|
||||
whereQuery?: Query;
|
||||
};
|
||||
|
||||
export type ESDistanceSourceDescriptor = AbstractESJoinSourceDescriptor & {
|
||||
distance: number; // km
|
||||
geoField: string;
|
||||
type: SOURCE_TYPES.ES_DISTANCE_SOURCE;
|
||||
};
|
||||
|
||||
export type ESTermSourceDescriptor = AbstractESJoinSourceDescriptor & {
|
||||
term: string; // term field name
|
||||
size?: number;
|
||||
type: SOURCE_TYPES.ES_TERM_SOURCE;
|
||||
};
|
||||
|
@ -183,4 +192,7 @@ export type TableSourceDescriptor = {
|
|||
term: string;
|
||||
};
|
||||
|
||||
export type JoinSourceDescriptor = ESTermSourceDescriptor | TableSourceDescriptor;
|
||||
export type JoinSourceDescriptor =
|
||||
| ESDistanceSourceDescriptor
|
||||
| ESTermSourceDescriptor
|
||||
| TableSourceDescriptor;
|
||||
|
|
|
@ -33,7 +33,7 @@ export function addTypeToTermJoin({ attributes }: { attributes: MapAttributes })
|
|||
if (!vectorLayer.joins) {
|
||||
return;
|
||||
}
|
||||
vectorLayer.joins.forEach((join: JoinDescriptor) => {
|
||||
vectorLayer.joins.forEach((join: Partial<JoinDescriptor>) => {
|
||||
if (!join.right) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -81,8 +81,8 @@ export function migrateJoinAggKey({ attributes }: { attributes: MapAttributes })
|
|||
return;
|
||||
}
|
||||
|
||||
const legacyJoinFields = new Map<string, JoinDescriptor>();
|
||||
vectorLayerDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => {
|
||||
const legacyJoinFields = new Map<string, Partial<JoinDescriptor>>();
|
||||
vectorLayerDescriptor.joins.forEach((joinDescriptor: Partial<JoinDescriptor>) => {
|
||||
_.get(joinDescriptor, 'right.metrics', []).forEach((aggDescriptor: AggDescriptor) => {
|
||||
const legacyAggKey = getLegacyAggKey({
|
||||
aggType: aggDescriptor.type,
|
||||
|
@ -104,13 +104,13 @@ export function migrateJoinAggKey({ attributes }: { attributes: MapAttributes })
|
|||
const style: any = vectorLayerDescriptor.style!.properties[key as VECTOR_STYLES];
|
||||
if (_.get(style, 'options.field.origin') === FIELD_ORIGIN.JOIN) {
|
||||
const joinDescriptor = legacyJoinFields.get(style.options.field.name);
|
||||
if (joinDescriptor) {
|
||||
if (joinDescriptor?.right?.id) {
|
||||
const { aggType, aggFieldName } = parseLegacyAggKey(style.options.field.name);
|
||||
// Update legacy join agg key to new join agg key
|
||||
style.options.field.name = getJoinAggKey({
|
||||
aggType,
|
||||
aggFieldName,
|
||||
rightSourceId: joinDescriptor.right.id!,
|
||||
rightSourceId: joinDescriptor.right.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ export function extractReferences({
|
|||
const joins = vectorLayer.joins ? vectorLayer.joins : [];
|
||||
joins.forEach((join, joinIndex) => {
|
||||
if (
|
||||
join.right &&
|
||||
'indexPatternId' in join.right &&
|
||||
!adhocDataViewIds.includes(
|
||||
(join.right as IndexPatternReferenceDescriptor).indexPatternId!
|
||||
|
@ -147,7 +148,7 @@ export function injectReferences({
|
|||
const vectorLayer = layer as VectorLayerDescriptor;
|
||||
const joins = vectorLayer.joins ? vectorLayer.joins : [];
|
||||
joins.forEach((join) => {
|
||||
if ('indexPatternRefName' in join.right) {
|
||||
if (join.right && 'indexPatternRefName' in join.right) {
|
||||
const sourceDescriptor = join.right as IndexPatternReferenceDescriptor;
|
||||
const reference = findReference(sourceDescriptor.indexPatternRefName!, references);
|
||||
sourceDescriptor.indexPatternId = reference.id;
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
ESGeoGridSourceDescriptor,
|
||||
ESSearchSourceDescriptor,
|
||||
LayerDescriptor,
|
||||
JoinDescriptor,
|
||||
VectorLayerDescriptor,
|
||||
} from '../descriptor_types';
|
||||
import type { MapAttributes } from '../content_management';
|
||||
|
@ -49,7 +50,12 @@ export class LayerStatsCollector {
|
|||
this._layerCount = layerList.length;
|
||||
layerList.forEach((layerDescriptor) => {
|
||||
this._updateCounts(getBasemapKey(layerDescriptor), this._basemapCounts);
|
||||
this._updateCounts(getJoinKey(layerDescriptor), this._joinCounts);
|
||||
const joins = (layerDescriptor as VectorLayerDescriptor)?.joins;
|
||||
if (joins && joins.length) {
|
||||
joins.forEach((joinDescriptor) => {
|
||||
this._updateCounts(getJoinKey(joinDescriptor), this._joinCounts);
|
||||
});
|
||||
}
|
||||
this._updateCounts(getLayerKey(layerDescriptor), this._layerCounts);
|
||||
this._updateCounts(getResolutionKey(layerDescriptor), this._resolutionCounts);
|
||||
this._updateCounts(getScalingKey(layerDescriptor), this._scalingCounts);
|
||||
|
@ -147,11 +153,16 @@ function getBasemapKey(layerDescriptor: LayerDescriptor): EMS_BASEMAP_KEYS | nul
|
|||
return null;
|
||||
}
|
||||
|
||||
function getJoinKey(layerDescriptor: LayerDescriptor): JOIN_KEYS | null {
|
||||
return layerDescriptor.type === LAYER_TYPE.GEOJSON_VECTOR &&
|
||||
(layerDescriptor as VectorLayerDescriptor)?.joins?.length
|
||||
? JOIN_KEYS.TERM
|
||||
: null;
|
||||
function getJoinKey(joinDescriptor: Partial<JoinDescriptor>): JOIN_KEYS | null {
|
||||
if (joinDescriptor?.right?.type === SOURCE_TYPES.ES_TERM_SOURCE) {
|
||||
return JOIN_KEYS.TERM;
|
||||
}
|
||||
|
||||
if (joinDescriptor?.right?.type === SOURCE_TYPES.ES_DISTANCE_SOURCE) {
|
||||
return JOIN_KEYS.DISTANCE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLayerKey(layerDescriptor: LayerDescriptor): LAYER_KEYS | null {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"title": "France Map",
|
||||
"description": "",
|
||||
"mapStateJSON": "{\"zoom\":3.43,\"center\":{\"lon\":-16.30411,\"lat\":42.88411},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}",
|
||||
"layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\"},\"id\":\"65xbw\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.25,\"visible\":true,\"joins\":[{\"leftField\":\"iso_3166_2\",\"right\":{\"id\":\"6a263f96-7a96-4f5a-a00e-c89178c1d017\"}}],\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#19c1e6\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"id\":\"240125db-e612-4001-b853-50107e55d984\",\"type\":\"ES_SEARCH\",\"scalingType\":\"LIMIT\",\"indexPatternId\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"id\":\"mdae9\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#1ce619\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"GEOJSON_VECTOR\"}]",
|
||||
"layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\"},\"id\":\"65xbw\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.25,\"visible\":true,\"joins\":[{\"leftField\":\"iso_3166_2\",\"right\":{\"id\":\"6a263f96-7a96-4f5a-a00e-c89178c1d017\",\"type\":\"ES_TERM_SOURCE\"}}],\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#19c1e6\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"id\":\"240125db-e612-4001-b853-50107e55d984\",\"type\":\"ES_SEARCH\",\"scalingType\":\"LIMIT\",\"indexPatternId\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"id\":\"mdae9\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#1ce619\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"GEOJSON_VECTOR\"}]",
|
||||
"uiStateJSON": "{}"
|
||||
},
|
||||
"references": [
|
||||
|
|
|
@ -13,6 +13,7 @@ export enum EMS_BASEMAP_KEYS {
|
|||
}
|
||||
|
||||
export enum JOIN_KEYS {
|
||||
DISTANCE = 'distance',
|
||||
TERM = 'term',
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ import { IField } from '../classes/fields/field';
|
|||
import type { IESSource } from '../classes/sources/es_source';
|
||||
import { getDrawMode, getOpenTOCDetails } from '../selectors/ui_selectors';
|
||||
import { isLayerGroup, LayerGroup } from '../classes/layers/layer_group';
|
||||
import { isSpatialJoin } from '../classes/joins/is_spatial_join';
|
||||
|
||||
export function trackCurrentLayerState(layerId: string) {
|
||||
return {
|
||||
|
@ -453,15 +454,16 @@ function updateSourcePropWithoutSync(
|
|||
});
|
||||
await dispatch(updateStyleProperties(layerId));
|
||||
} else if (value === SCALING_TYPES.MVT) {
|
||||
if (joins.length > 1) {
|
||||
// Maplibre feature-state join uses promoteId and there is a limit to one promoteId
|
||||
// Therefore, Vector tile scaling supports only one join
|
||||
dispatch({
|
||||
type: SET_JOINS,
|
||||
layerId,
|
||||
joins: [joins[0]],
|
||||
});
|
||||
}
|
||||
const filteredJoins = joins.filter((joinDescriptor) => {
|
||||
return !isSpatialJoin(joinDescriptor);
|
||||
});
|
||||
// Maplibre feature-state join uses promoteId and there is a limit to one promoteId
|
||||
// Therefore, Vector tile scaling supports only one join
|
||||
dispatch({
|
||||
type: SET_JOINS,
|
||||
layerId,
|
||||
joins: filteredJoins.length ? [filteredJoins[0]] : [],
|
||||
});
|
||||
// update style props regardless of updating joins
|
||||
// Allow style to clean-up data driven style properties with join fields that do not support feature-state.
|
||||
await dispatch(updateStyleProperties(layerId));
|
||||
|
@ -764,7 +766,7 @@ export function updateLayerStyleForSelectedLayer(styleDescriptor: StyleDescripto
|
|||
};
|
||||
}
|
||||
|
||||
export function setJoinsForLayer(layer: ILayer, joins: JoinDescriptor[]) {
|
||||
export function setJoinsForLayer(layer: ILayer, joins: Array<Partial<JoinDescriptor>>) {
|
||||
return async (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => {
|
||||
const previousFields = await (layer as IVectorLayer).getFields();
|
||||
dispatch({
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
SOURCE_TYPES,
|
||||
} from '../../../common/constants';
|
||||
import {
|
||||
ESDistanceSourceDescriptor,
|
||||
ESTermSourceDescriptor,
|
||||
JoinDescriptor,
|
||||
JoinSourceDescriptor,
|
||||
|
@ -24,7 +25,13 @@ import { IVectorSource } from '../sources/vector_source';
|
|||
import { IField } from '../fields/field';
|
||||
import { PropertiesMap } from '../../../common/elasticsearch_util';
|
||||
import { IJoinSource } from '../sources/join_sources';
|
||||
import { ESTermSource, TableSource } from '../sources/join_sources';
|
||||
import {
|
||||
ESDistanceSource,
|
||||
isSpatialSourceComplete,
|
||||
ESTermSource,
|
||||
isTermSourceComplete,
|
||||
TableSource,
|
||||
} from '../sources/join_sources';
|
||||
|
||||
export function createJoinSource(
|
||||
descriptor: Partial<JoinSourceDescriptor> | undefined
|
||||
|
@ -33,23 +40,25 @@ export function createJoinSource(
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
descriptor.type === SOURCE_TYPES.ES_TERM_SOURCE &&
|
||||
descriptor.indexPatternId !== undefined &&
|
||||
descriptor.term !== undefined
|
||||
) {
|
||||
if (descriptor.type === SOURCE_TYPES.ES_DISTANCE_SOURCE && isSpatialSourceComplete(descriptor)) {
|
||||
return new ESDistanceSource(descriptor as ESDistanceSourceDescriptor);
|
||||
}
|
||||
|
||||
if (descriptor.type === SOURCE_TYPES.ES_TERM_SOURCE && isTermSourceComplete(descriptor)) {
|
||||
return new ESTermSource(descriptor as ESTermSourceDescriptor);
|
||||
} else if (descriptor.type === SOURCE_TYPES.TABLE_SOURCE) {
|
||||
}
|
||||
|
||||
if (descriptor.type === SOURCE_TYPES.TABLE_SOURCE) {
|
||||
return new TableSource(descriptor as TableSourceDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
export class InnerJoin {
|
||||
private readonly _descriptor: JoinDescriptor;
|
||||
private readonly _descriptor: Partial<JoinDescriptor>;
|
||||
private readonly _rightSource?: IJoinSource;
|
||||
private readonly _leftField?: IField;
|
||||
|
||||
constructor(joinDescriptor: JoinDescriptor, leftSource: IVectorSource) {
|
||||
constructor(joinDescriptor: Partial<JoinDescriptor>, leftSource: IVectorSource) {
|
||||
this._descriptor = joinDescriptor;
|
||||
this._rightSource = createJoinSource(this._descriptor.right);
|
||||
this._leftField = joinDescriptor.leftField
|
||||
|
@ -133,7 +142,7 @@ export class InnerJoin {
|
|||
return this._rightSource;
|
||||
}
|
||||
|
||||
toDescriptor(): JoinDescriptor {
|
||||
toDescriptor(): Partial<JoinDescriptor> {
|
||||
return this._descriptor;
|
||||
}
|
||||
|
||||
|
|
13
x-pack/plugins/maps/public/classes/joins/is_spatial_join.ts
Normal file
13
x-pack/plugins/maps/public/classes/joins/is_spatial_join.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 type { JoinDescriptor } from '../../../common/descriptor_types';
|
||||
import { SOURCE_TYPES } from '../../../common/constants';
|
||||
|
||||
export function isSpatialJoin(joinDescriptor: Partial<JoinDescriptor>) {
|
||||
return joinDescriptor?.right?.type === SOURCE_TYPES.ES_DISTANCE_SOURCE;
|
||||
}
|
|
@ -232,14 +232,6 @@ export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLay
|
|||
: displayName;
|
||||
}
|
||||
|
||||
showJoinEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getJoinsDisabledReason() {
|
||||
return this._documentSource.getJoinsDisabledReason();
|
||||
}
|
||||
|
||||
getJoins() {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -266,7 +266,7 @@ export class GeoJsonVectorLayer extends AbstractVectorLayer {
|
|||
return;
|
||||
}
|
||||
|
||||
const joinStates = await this._syncJoins(syncContext, style);
|
||||
const joinStates = await this._syncJoins(syncContext, style, sourceResult.featureCollection);
|
||||
await performInnerJoins(
|
||||
sourceResult,
|
||||
joinStates,
|
||||
|
|
|
@ -115,7 +115,7 @@ export async function performInnerJoins(
|
|||
values: { leftFieldName },
|
||||
})
|
||||
: i18n.translate('xpack.maps.vectorLayer.joinError.noMatchesMsg', {
|
||||
defaultMessage: `Left field does not match right field. Left field: '{leftFieldName}' has values { leftFieldValues }. Right field: '{rightFieldName}' has values: { rightFieldValues }.`,
|
||||
defaultMessage: `Left field values do not match right field values. Left field: '{leftFieldName}' has values { leftFieldValues }. Right field: '{rightFieldName}' has values: { rightFieldValues }.`,
|
||||
values: {
|
||||
leftFieldName,
|
||||
leftFieldValues: prettyPrintArray(joinStatus.keys),
|
||||
|
|
|
@ -10,7 +10,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import type { FilterSpecification, Map as MbMap, LayerSpecification } from '@kbn/mapbox-gl';
|
||||
import type { KibanaExecutionContext } from '@kbn/core/public';
|
||||
import type { Query } from '@kbn/data-plugin/common';
|
||||
import { Feature, GeoJsonProperties, Geometry, Position } from 'geojson';
|
||||
import { Feature, FeatureCollection, GeoJsonProperties, Geometry, Position } from 'geojson';
|
||||
import _ from 'lodash';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -23,7 +23,6 @@ import {
|
|||
LAYER_TYPE,
|
||||
FIELD_ORIGIN,
|
||||
FieldFormatter,
|
||||
SOURCE_TYPES,
|
||||
STYLE_TYPE,
|
||||
VECTOR_STYLES,
|
||||
} from '../../../../common/constants';
|
||||
|
@ -38,11 +37,11 @@ import {
|
|||
TimesliceMaskConfig,
|
||||
} from '../../util/mb_filter_expressions';
|
||||
import {
|
||||
AbstractESJoinSourceDescriptor,
|
||||
AggDescriptor,
|
||||
CustomIcon,
|
||||
DynamicStylePropertyOptions,
|
||||
DataFilters,
|
||||
ESTermSourceDescriptor,
|
||||
JoinDescriptor,
|
||||
StyleMetaDescriptor,
|
||||
VectorLayerDescriptor,
|
||||
|
@ -92,7 +91,6 @@ export interface IVectorLayer extends ILayer {
|
|||
getFields(): Promise<IField[]>;
|
||||
getStyleEditorFields(): Promise<IField[]>;
|
||||
getJoins(): InnerJoin[];
|
||||
getJoinsDisabledReason(): string | null;
|
||||
getValidJoins(): InnerJoin[];
|
||||
getSource(): IVectorSource;
|
||||
getFeatureId(feature: Feature): string | number | undefined;
|
||||
|
@ -102,7 +100,6 @@ export interface IVectorLayer extends ILayer {
|
|||
executionContext: KibanaExecutionContext
|
||||
): Promise<ITooltipProperty[]>;
|
||||
hasJoins(): boolean;
|
||||
showJoinEditor(): boolean;
|
||||
canShowTooltip(): boolean;
|
||||
areTooltipsDisabled(): boolean;
|
||||
supportsFeatureEditing(): boolean;
|
||||
|
@ -179,27 +176,21 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
|
||||
const clonedDescriptor = clones[0] 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'
|
||||
);
|
||||
clonedDescriptor.joins.forEach((joinDescriptor: Partial<JoinDescriptor>) => {
|
||||
if (!joinDescriptor.right) {
|
||||
return;
|
||||
}
|
||||
const termSourceDescriptor: ESTermSourceDescriptor =
|
||||
joinDescriptor.right as ESTermSourceDescriptor;
|
||||
|
||||
// todo: must tie this to generic thing
|
||||
const originalJoinId = joinDescriptor.right.id!;
|
||||
const joinSourceDescriptor =
|
||||
joinDescriptor.right as Partial<AbstractESJoinSourceDescriptor>;
|
||||
const originalJoinId = joinSourceDescriptor.id ?? '';
|
||||
|
||||
// right.id is uuid used to track requests in inspector
|
||||
joinDescriptor.right.id = uuidv4();
|
||||
const clonedJoinId = uuidv4();
|
||||
joinDescriptor.right.id = clonedJoinId;
|
||||
|
||||
// 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 }];
|
||||
const metrics = joinSourceDescriptor.metrics ?? [{ type: AGG_TYPE.COUNT }];
|
||||
metrics.forEach((metricsDescriptor: AggDescriptor) => {
|
||||
const originalJoinKey = getJoinAggKey({
|
||||
aggType: metricsDescriptor.type,
|
||||
|
@ -209,7 +200,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
const newJoinKey = getJoinAggKey({
|
||||
aggType: metricsDescriptor.type,
|
||||
aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '',
|
||||
rightSourceId: joinDescriptor.right.id!,
|
||||
rightSourceId: clonedJoinId,
|
||||
});
|
||||
|
||||
Object.keys(clonedDescriptor.style.properties).forEach((key) => {
|
||||
|
@ -252,10 +243,6 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
return this._joins.slice();
|
||||
}
|
||||
|
||||
getJoinsDisabledReason() {
|
||||
return this.getSource().getJoinsDisabledReason();
|
||||
}
|
||||
|
||||
getValidJoins() {
|
||||
return this.getJoins().filter((join) => {
|
||||
return join.hasCompleteConfig();
|
||||
|
@ -272,10 +259,6 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
return this.getValidJoins().length > 0;
|
||||
}
|
||||
|
||||
showJoinEditor(): boolean {
|
||||
return this.getSource().showJoinEditor();
|
||||
}
|
||||
|
||||
isLayerLoading() {
|
||||
const isSourceLoading = super.isLayerLoading();
|
||||
if (isSourceLoading) {
|
||||
|
@ -553,6 +536,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
|
||||
async _syncJoin({
|
||||
join,
|
||||
featureCollection,
|
||||
startLoading,
|
||||
stopLoading,
|
||||
onLoadError,
|
||||
|
@ -561,7 +545,10 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
isForceRefresh,
|
||||
isFeatureEditorOpenForLayer,
|
||||
inspectorAdapters,
|
||||
}: { join: InnerJoin } & DataRequestContext): Promise<JoinState> {
|
||||
}: {
|
||||
join: InnerJoin;
|
||||
featureCollection?: FeatureCollection;
|
||||
} & DataRequestContext): Promise<JoinState> {
|
||||
const joinSource = join.getRightJoinSource();
|
||||
const sourceDataId = join.getSourceDataRequestId();
|
||||
const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`);
|
||||
|
@ -580,7 +567,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
source: joinSource,
|
||||
prevDataRequest,
|
||||
nextRequestMeta: joinRequestMeta,
|
||||
extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource).
|
||||
extentAware: false, // join-sources are spatially unaware. For spatial joins, spatial constraints are from vector source feature geometry and not map extent geometry
|
||||
getUpdateDueToTimeslice: () => {
|
||||
return true;
|
||||
},
|
||||
|
@ -602,7 +589,8 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
leftSourceName,
|
||||
join.getLeftField().getName(),
|
||||
registerCancelCallback.bind(null, requestToken),
|
||||
inspectorAdapters
|
||||
inspectorAdapters,
|
||||
featureCollection
|
||||
);
|
||||
stopLoading(sourceDataId, requestToken, propertiesMap);
|
||||
return {
|
||||
|
@ -618,11 +606,15 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
}
|
||||
}
|
||||
|
||||
async _syncJoins(syncContext: DataRequestContext, style: IVectorStyle) {
|
||||
async _syncJoins(
|
||||
syncContext: DataRequestContext,
|
||||
style: IVectorStyle,
|
||||
featureCollection?: FeatureCollection
|
||||
) {
|
||||
const joinSyncs = this.getValidJoins().map(async (join) => {
|
||||
await this._syncJoinStyleMeta(syncContext, join, style);
|
||||
await this._syncJoinFormatters(syncContext, join, style);
|
||||
return this._syncJoin({ join, ...syncContext });
|
||||
return this._syncJoin({ join, featureCollection, ...syncContext });
|
||||
});
|
||||
|
||||
return await Promise.all(joinSyncs);
|
||||
|
|
|
@ -94,8 +94,8 @@ exports[`should render elasticsearch UI when left source is BOUNDARIES_SOURCE.EL
|
|||
/>
|
||||
</EuiFormRow>
|
||||
<GeoIndexPatternSelect
|
||||
dataView={null}
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
|
|
|
@ -17,7 +17,7 @@ export const choroplethLayerWizardConfig: LayerWizard = {
|
|||
order: 10,
|
||||
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
|
||||
description: i18n.translate('xpack.maps.choropleth.desc', {
|
||||
defaultMessage: 'Shaded areas to compare statistics across boundaries',
|
||||
defaultMessage: 'Shade areas to compare statistics across boundaries',
|
||||
}),
|
||||
icon: ChoroplethLayerIcon,
|
||||
renderWizard: (renderWizardArguments: RenderWizardArguments) => {
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
createEmsChoroplethLayerDescriptor,
|
||||
createEsChoroplethLayerDescriptor,
|
||||
} from './create_choropleth_layer_descriptor';
|
||||
import { inputStrings } from '../../../../connected_components/input_strings';
|
||||
|
||||
export enum BOUNDARIES_SOURCE {
|
||||
ELASTICSEARCH = 'ELASTICSEARCH',
|
||||
|
@ -305,9 +306,7 @@ export class LayerTemplate extends Component<RenderWizardArguments, State> {
|
|||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.choropleth.joinFieldPlaceholder', {
|
||||
defaultMessage: 'Select field',
|
||||
})}
|
||||
placeholder={inputStrings.fieldSelectPlaceholder}
|
||||
value={this.state.leftElasticsearchJoinField}
|
||||
onChange={this._onLeftJoinFieldSelect}
|
||||
fields={this.state.leftJoinFields}
|
||||
|
@ -319,8 +318,7 @@ export class LayerTemplate extends Component<RenderWizardArguments, State> {
|
|||
return (
|
||||
<>
|
||||
<GeoIndexPatternSelect
|
||||
// @ts-expect-error - avoid wrong "Property 'id' does not exist on type 'never'." compile error
|
||||
value={this.state.leftIndexPattern ? this.state.leftIndexPattern!.id : ''}
|
||||
dataView={this.state.leftIndexPattern}
|
||||
onChange={this._onLeftIndexPatternChange}
|
||||
/>
|
||||
{geoFieldSelect}
|
||||
|
@ -405,9 +403,7 @@ export class LayerTemplate extends Component<RenderWizardArguments, State> {
|
|||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.choropleth.joinFieldPlaceholder', {
|
||||
defaultMessage: 'Select field',
|
||||
})}
|
||||
placeholder={inputStrings.fieldSelectPlaceholder}
|
||||
value={this.state.rightJoinField}
|
||||
onChange={this._onRightJoinFieldSelect}
|
||||
fields={this.state.rightTermsFields}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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, { FunctionComponent } from 'react';
|
||||
|
||||
export const SpatialJoinLayerIcon: FunctionComponent = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="49"
|
||||
height="25"
|
||||
fill="none"
|
||||
viewBox="0 0 49 25"
|
||||
className="mapLayersWizardIcon"
|
||||
>
|
||||
<path
|
||||
className="mapLayersWizardIcon__background"
|
||||
d="M32.206 15.364V1.588l-1.492.204c-.743.254-1.768.38-2.702.45a41.39 41.39 0 01-1.258.07l-.093.005c-.312.014-.57.025-.72.042a1.895 1.895 0 00-.558.167c-.156.07-.316.156-.47.238l-.057.031c-.356.19-.687.357-1.025.4l-2.262.474v5.864c0 .698-.677 2.578-.818 3.852l.34 1.807 11.114.17z"
|
||||
/>
|
||||
<path
|
||||
fill="#69707D"
|
||||
d="M22.36 3.558c-.178-.104-.416-.15-.664-.172a6.333 6.333 0 00-.885 0c-.657.04-1.452.144-2.264.261l-.703.103c-.583.087-1.16.172-1.692.238-.751.093-1.39.145-1.82.113-.593-.044-1.125.341-1.611.843-.453.467-.925 1.101-1.429 1.777l-.144.193c-1.132 1.517-2.452 3.218-4.22 4.102-.213.107-.346.312-.431.528a3.065 3.065 0 00-.173.78c-.064.586-.046 1.324.007 2.083.072 1.01.211 2.102.32 2.955.055.42.101.784.128 1.05.08.78-.128 1.77-.37 2.653-.088.319-.178.616-.258.882l-.086.285a9.67 9.67 0 00-.12.434 1.518 1.518 0 00-.053.334c0 .28.11.577.283.842a2.3 2.3 0 00.735.708l.067.04h.078c4.133-.069 11.445-.366 12.196-.45a.58.58 0 00.334-.17 1.41 1.41 0 00.224-.293c.134-.223.264-.524.388-.867.249-.69.495-1.6.72-2.52.162-.654.314-1.32.452-1.92l.16-.695c.088-.378.167-.707.235-.963.07-.267.123-.43.156-.494a1.57 1.57 0 00.1-.314c.03-.129.063-.291.096-.48.067-.38.14-.876.213-1.45.146-1.149.296-2.614.403-4.073.107-1.458.171-2.917.146-4.052a10.13 10.13 0 00-.112-1.438 2.469 2.469 0 00-.131-.495c-.053-.128-.135-.275-.276-.358z"
|
||||
/>
|
||||
<path
|
||||
className="mapLayersWizardIcon__background"
|
||||
d="M31.435 15.227c-.218.006-1.066-.01-1.445 0-.758.02-1.298-.026-2.353 0H22.15l-2.864 8.915c.2.067 1.938.445 2.242.447.62.005 1.478-.08 2.393-.197.88-.112 1.824-.257 2.679-.389l.112-.017c.888-.136 1.664-.254 2.166-.304.348-.035.996-.025 1.858-.007l.128.002c.827.017 1.818.037 2.868.023 2.199-.028 4.695-.203 6.5-.887 1.42-.538 2.1-.87 2.513-1.339.414-.469.558-.468.643-1.44.086-.972.086-2.063.086-2.063l-8.131-2.744h-3.907z"
|
||||
/>
|
||||
<path
|
||||
className="mapLayersWizardIcon__background"
|
||||
d="M32.206 1.588s-2.046 13.417-1.603 13.639c.787.393 2.66.63 3.785 1.636 1.23 1.1 1.171 2.5 2.318 3.239.834.538 6.73-.874 6.73-.874l.228-7.956s-.174-4.5-.89-5.216c-.716-.715-2.489-1.125-3-2.08-1.115-2.08-3.068-2.351-3.068-2.351l-4.5-.037z"
|
||||
/>
|
||||
<circle cx="14.761" cy="9.754" r="1.091" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="36.63" cy="10.706" r="1.091" className="mapLayersWizardIcon__backgroundDarker" />
|
||||
<circle cx="24.842" cy="19.181" r="1.091" className="mapLayersWizardIcon__backgroundDarker" />
|
||||
<circle cx="39.024" cy="15.363" r="1.091" className="mapLayersWizardIcon__backgroundDarker" />
|
||||
<circle cx="27.569" cy="6.091" r="1.091" className="mapLayersWizardIcon__backgroundDarker" />
|
||||
<circle cx="28.66" cy="11" r="1.091" className="mapLayersWizardIcon__backgroundDarker" />
|
||||
<circle cx="35.206" cy="4.454" r="1.091" className="mapLayersWizardIcon__backgroundDarker" />
|
||||
<circle cx="18.842" cy="6.091" r="1.091" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="10.115" cy="13.727" r="1.091" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="16.66" cy="14.818" r="1.091" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="11.206" cy="20.272" r="1.091" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="17.751" cy="20.818" r="1.091" className="mapLayersWizardIcon__highlight" />
|
||||
<circle cx="30.842" cy="20.818" r="1.091" className="mapLayersWizardIcon__backgroundDarker" />
|
||||
</svg>
|
||||
);
|
|
@ -28,6 +28,7 @@ import { ObservabilityLayerWizardConfig } from './solution_layers/observability'
|
|||
import { SecurityLayerWizardConfig } from './solution_layers/security';
|
||||
import { choroplethLayerWizardConfig } from './choropleth_layer_wizard';
|
||||
import { newVectorLayerWizardConfig } from './new_vector_layer_wizard';
|
||||
import { spatialJoinWizardConfig } from './spatial_join_wizard';
|
||||
|
||||
let registered = false;
|
||||
|
||||
|
@ -38,17 +39,25 @@ export function registerLayerWizards() {
|
|||
|
||||
registerLayerWizardInternal(uploadLayerWizardConfig);
|
||||
registerLayerWizardInternal(layerGroupWizardConfig);
|
||||
|
||||
registerLayerWizardInternal(esDocumentsLayerWizardConfig);
|
||||
registerLayerWizardInternal(choroplethLayerWizardConfig);
|
||||
|
||||
registerLayerWizardInternal(spatialJoinWizardConfig);
|
||||
registerLayerWizardInternal(point2PointLayerWizardConfig);
|
||||
|
||||
registerLayerWizardInternal(clustersLayerWizardConfig);
|
||||
registerLayerWizardInternal(heatmapLayerWizardConfig);
|
||||
|
||||
registerLayerWizardInternal(esTopHitsLayerWizardConfig);
|
||||
registerLayerWizardInternal(geoLineLayerWizardConfig);
|
||||
registerLayerWizardInternal(point2PointLayerWizardConfig);
|
||||
|
||||
registerLayerWizardInternal(emsBoundariesLayerWizardConfig);
|
||||
registerLayerWizardInternal(newVectorLayerWizardConfig);
|
||||
registerLayerWizardInternal(emsBaseMapLayerWizardConfig);
|
||||
|
||||
registerLayerWizardInternal(newVectorLayerWizardConfig);
|
||||
registerLayerWizardInternal(kibanaBasemapLayerWizardConfig);
|
||||
|
||||
registerLayerWizardInternal(tmsLayerWizardConfig);
|
||||
registerLayerWizardInternal(wmsLayerWizardConfig);
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { spatialJoinWizardConfig } from './spatial_join_wizard_config';
|
||||
export { RelationshipExpression } from './wizard_form';
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LAYER_WIZARD_CATEGORY, WIZARD_ID } from '../../../../../common/constants';
|
||||
import { LayerWizard, RenderWizardArguments } from '../layer_wizard_registry';
|
||||
import { WizardForm } from './wizard_form';
|
||||
import { SpatialJoinLayerIcon } from '../icons/spatial_join_layer_icon';
|
||||
|
||||
export const spatialJoinWizardConfig: LayerWizard = {
|
||||
id: WIZARD_ID.SPATIAL_JOIN,
|
||||
order: 10,
|
||||
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
|
||||
description: i18n.translate('xpack.maps.spatialJoinWizard.description', {
|
||||
defaultMessage: 'Group documents by geospatial relationships',
|
||||
}),
|
||||
icon: SpatialJoinLayerIcon,
|
||||
renderWizard: (renderWizardArguments: RenderWizardArguments) => {
|
||||
return <WizardForm {...renderWizardArguments} />;
|
||||
},
|
||||
title: i18n.translate('xpack.maps.spatialJoinWizard.title', {
|
||||
defaultMessage: 'Spatial join',
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
AGG_TYPE,
|
||||
FIELD_ORIGIN,
|
||||
SCALING_TYPES,
|
||||
SOURCE_TYPES,
|
||||
STYLE_TYPE,
|
||||
VECTOR_STYLES,
|
||||
} from '../../../../../../common/constants';
|
||||
import { getJoinAggKey } from '../../../../../../common/get_agg_key';
|
||||
import {
|
||||
CountAggDescriptor,
|
||||
JoinDescriptor,
|
||||
VectorStylePropertiesDescriptor,
|
||||
} from '../../../../../../common/descriptor_types';
|
||||
import { VectorStyle } from '../../../../styles/vector/vector_style';
|
||||
import { GeoJsonVectorLayer } from '../../../vector_layer';
|
||||
// @ts-ignore
|
||||
import { ESSearchSource } from '../../../../sources/es_search_source';
|
||||
import { getDefaultDynamicProperties } from '../../../../styles/vector/vector_style_defaults';
|
||||
|
||||
const defaultDynamicProperties = getDefaultDynamicProperties();
|
||||
|
||||
export function createDistanceJoinLayerDescriptor({
|
||||
distance,
|
||||
leftDataViewId,
|
||||
leftGeoField,
|
||||
rightDataViewId,
|
||||
rightGeoField,
|
||||
}: {
|
||||
distance: number;
|
||||
leftDataViewId: string;
|
||||
leftGeoField: string;
|
||||
rightDataViewId: string;
|
||||
rightGeoField: string;
|
||||
}) {
|
||||
const metricsDescriptor: CountAggDescriptor = { type: AGG_TYPE.COUNT };
|
||||
const joinId = uuidv4();
|
||||
const countJoinFieldName = getJoinAggKey({
|
||||
aggType: metricsDescriptor.type,
|
||||
rightSourceId: joinId,
|
||||
});
|
||||
|
||||
const styleProperties: Partial<VectorStylePropertiesDescriptor> = {
|
||||
[VECTOR_STYLES.LABEL_TEXT]: {
|
||||
type: STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options,
|
||||
field: {
|
||||
name: countJoinFieldName,
|
||||
origin: FIELD_ORIGIN.JOIN,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const joins = [
|
||||
{
|
||||
leftField: '_id',
|
||||
right: {
|
||||
type: SOURCE_TYPES.ES_DISTANCE_SOURCE,
|
||||
id: joinId,
|
||||
indexPatternId: rightDataViewId,
|
||||
metrics: [metricsDescriptor],
|
||||
distance,
|
||||
geoField: rightGeoField,
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
applyForceRefresh: true,
|
||||
},
|
||||
} as JoinDescriptor,
|
||||
];
|
||||
|
||||
return GeoJsonVectorLayer.createDescriptor({
|
||||
joins,
|
||||
sourceDescriptor: ESSearchSource.createDescriptor({
|
||||
indexPatternId: leftDataViewId,
|
||||
geoField: leftGeoField,
|
||||
scalingType: SCALING_TYPES.LIMIT,
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
applyForceRefresh: true,
|
||||
}),
|
||||
style: VectorStyle.createDescriptor(styleProperties),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFieldNumber,
|
||||
EuiFormRow,
|
||||
EuiPopoverFooter,
|
||||
} from '@elastic/eui';
|
||||
import { panelStrings } from '../../../../../connected_components/panel_strings';
|
||||
|
||||
export const KM_ABBREVIATION = i18n.translate(
|
||||
'xpack.maps.spatialJoin.wizardForm.kilometersAbbreviation',
|
||||
{
|
||||
defaultMessage: 'km',
|
||||
}
|
||||
);
|
||||
|
||||
interface Props {
|
||||
initialDistance: number;
|
||||
onClose: () => void;
|
||||
onDistanceChange: (distance: number) => void;
|
||||
}
|
||||
|
||||
function getDistanceAsNumber(distance: string | number): number {
|
||||
return typeof distance === 'string' ? parseFloat(distance as string) : distance;
|
||||
}
|
||||
|
||||
export function DistanceForm(props: Props) {
|
||||
const [distance, setDistance] = useState<number | string>(props.initialDistance);
|
||||
const distanceAsNumber = getDistanceAsNumber(distance);
|
||||
const isDistanceInvalid = isNaN(distanceAsNumber) || distanceAsNumber <= 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.spatialJoin.wizardForm.distanceLabel', {
|
||||
defaultMessage: 'Distance',
|
||||
})}
|
||||
isInvalid={isDistanceInvalid}
|
||||
error={
|
||||
isDistanceInvalid
|
||||
? [
|
||||
i18n.translate('xpack.maps.spatialJoin.wizardForm.invalidDistanceMessage', {
|
||||
defaultMessage: 'Value must be a number greater than 0',
|
||||
}),
|
||||
]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
append={KM_ABBREVIATION}
|
||||
aria-label={i18n.translate('xpack.maps.spatialJoin.wizardForm.distanceInputAriaLabel', {
|
||||
defaultMessage: 'distance input',
|
||||
})}
|
||||
isInvalid={isDistanceInvalid}
|
||||
min={0}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setDistance(e.target.value);
|
||||
}}
|
||||
value={distance}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiPopoverFooter paddingSize="s">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty onClick={props.onClose} size="s">
|
||||
{panelStrings.close}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={
|
||||
isDistanceInvalid || props.initialDistance.toString() === distance.toString()
|
||||
}
|
||||
onClick={() => {
|
||||
props.onDistanceChange(getDistanceAsNumber(distance));
|
||||
props.onClose();
|
||||
}}
|
||||
size="s"
|
||||
>
|
||||
{panelStrings.apply}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverFooter>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { RelationshipExpression } from './relationship_expression';
|
||||
export { WizardForm } from './wizard_form';
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import type { DataViewField, DataView } from '@kbn/data-plugin/common';
|
||||
import { GeoIndexPatternSelect } from '../../../../../components/geo_index_pattern_select';
|
||||
import { GeoFieldSelect } from '../../../../../components/geo_field_select';
|
||||
|
||||
interface Props {
|
||||
dataView?: DataView;
|
||||
geoField: string | undefined;
|
||||
geoFields: DataViewField[];
|
||||
onDataViewSelect: (dataView: DataView) => void;
|
||||
onGeoFieldSelect: (fieldName?: string) => void;
|
||||
}
|
||||
|
||||
export function LeftSourcePanel(props: Props) {
|
||||
const geoFieldSelect = props.geoFields.length ? (
|
||||
<GeoFieldSelect
|
||||
value={props.geoField ? props.geoField : ''}
|
||||
onChange={props.onGeoFieldSelect}
|
||||
geoFields={props.geoFields}
|
||||
isClearable={false}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
{i18n.translate('xpack.maps.spatialJoin.wizard.leftSourceTitle', {
|
||||
defaultMessage: 'Layer features source',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<GeoIndexPatternSelect
|
||||
dataView={props.dataView}
|
||||
onChange={props.onDataViewSelect}
|
||||
isGeoPointsOnly={true}
|
||||
/>
|
||||
|
||||
{geoFieldSelect}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiExpression, EuiPopover } from '@elastic/eui';
|
||||
import { KM_ABBREVIATION, DistanceForm } from './distance_form';
|
||||
|
||||
interface Props {
|
||||
distance: number;
|
||||
onDistanceChange: (distance: number) => void;
|
||||
}
|
||||
|
||||
export function RelationshipExpression(props: Props) {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
function closePopover() {
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="spatialJoinRelationship"
|
||||
button={
|
||||
<EuiExpression
|
||||
color="subdued"
|
||||
description={i18n.translate(
|
||||
'xpack.maps.spatialJoin.wizardForm.withinExpressionDescription',
|
||||
{
|
||||
defaultMessage: 'within',
|
||||
}
|
||||
)}
|
||||
value={i18n.translate('xpack.maps.spatialJoin.wizardForm.withinExpressionValue', {
|
||||
defaultMessage: '{distance} {units} of layer features',
|
||||
values: {
|
||||
distance: props.distance,
|
||||
units: KM_ABBREVIATION,
|
||||
},
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
uppercase={false}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="downCenter"
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<DistanceForm
|
||||
initialDistance={props.distance}
|
||||
onDistanceChange={props.onDistanceChange}
|
||||
onClose={closePopover}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import type { DataViewField, DataView } from '@kbn/data-plugin/common';
|
||||
import { GeoIndexPatternSelect } from '../../../../../components/geo_index_pattern_select';
|
||||
import { GeoFieldSelect } from '../../../../../components/geo_field_select';
|
||||
import { inputStrings } from '../../../../../connected_components/input_strings';
|
||||
import { RelationshipExpression } from './relationship_expression';
|
||||
|
||||
interface Props {
|
||||
dataView?: DataView;
|
||||
distance: number;
|
||||
geoField: string | undefined;
|
||||
geoFields: DataViewField[];
|
||||
onDataViewSelect: (dataView: DataView) => void;
|
||||
onDistanceChange: (distance: number) => void;
|
||||
onGeoFieldSelect: (fieldName?: string) => void;
|
||||
}
|
||||
|
||||
export function RightSourcePanel(props: Props) {
|
||||
const geoFieldSelect = props.dataView ? (
|
||||
<GeoFieldSelect
|
||||
value={props.geoField ? props.geoField : ''}
|
||||
onChange={props.onGeoFieldSelect}
|
||||
geoFields={props.geoFields}
|
||||
isClearable={false}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
{i18n.translate('xpack.maps.spatialJoin.wizardForm.rightSourceTitle', {
|
||||
defaultMessage: 'Join source',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow label={inputStrings.relationshipLabel}>
|
||||
<RelationshipExpression
|
||||
distance={props.distance}
|
||||
onDistanceChange={props.onDistanceChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<GeoIndexPatternSelect dataView={props.dataView} onChange={props.onDataViewSelect} />
|
||||
|
||||
{geoFieldSelect}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import type { DataViewField, DataView } from '@kbn/data-plugin/common';
|
||||
import { getGeoFields, getGeoPointFields } from '../../../../../index_pattern_util';
|
||||
import { RenderWizardArguments } from '../../layer_wizard_registry';
|
||||
import { LeftSourcePanel } from './left_source_panel';
|
||||
import { RightSourcePanel } from './right_source_panel';
|
||||
import { createDistanceJoinLayerDescriptor } from './create_spatial_join_layer_descriptor';
|
||||
import { DEFAULT_WITHIN_DISTANCE } from '../../../../sources/join_sources';
|
||||
|
||||
function isLeftConfigComplete(
|
||||
leftDataView: DataView | undefined,
|
||||
leftGeoField: string | undefined
|
||||
) {
|
||||
return leftDataView !== undefined && leftDataView.id && leftGeoField !== undefined;
|
||||
}
|
||||
|
||||
function isRightConfigComplete(
|
||||
rightDataView: DataView | undefined,
|
||||
rightGeoField: string | undefined
|
||||
) {
|
||||
return rightDataView !== undefined && rightDataView.id && rightGeoField !== undefined;
|
||||
}
|
||||
|
||||
export function WizardForm({ previewLayers }: RenderWizardArguments) {
|
||||
const [distance, setDistance] = useState<number>(DEFAULT_WITHIN_DISTANCE);
|
||||
const [leftDataView, setLeftDataView] = useState<DataView | undefined>();
|
||||
const [leftGeoFields, setLeftGeoFields] = useState<DataViewField[]>([]);
|
||||
const [leftGeoField, setLeftGeoField] = useState<string | undefined>();
|
||||
const [rightDataView, setRightDataView] = useState<DataView | undefined>();
|
||||
const [rightGeoFields, setRightGeoFields] = useState<DataViewField[]>([]);
|
||||
const [rightGeoField, setRightGeoField] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isLeftConfigComplete(leftDataView, leftGeoField) ||
|
||||
!isRightConfigComplete(rightDataView, rightGeoField)
|
||||
) {
|
||||
previewLayers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const layerDescriptor = createDistanceJoinLayerDescriptor({
|
||||
distance,
|
||||
leftDataViewId: leftDataView!.id!, // leftDataView.id verified in isLeftConfigComplete
|
||||
leftGeoField: leftGeoField!, // leftGeoField verified in isLeftConfigComplete
|
||||
rightDataViewId: rightDataView!.id!, // rightDataView.id verified in isRightConfigComplete
|
||||
rightGeoField: rightGeoField!, // rightGeoField verified in isRightConfigComplete
|
||||
});
|
||||
|
||||
previewLayers([layerDescriptor]);
|
||||
}, [distance, leftDataView, leftGeoField, rightDataView, rightGeoField, previewLayers]);
|
||||
|
||||
const rightSourcePanel = isLeftConfigComplete(leftDataView, leftGeoField) ? (
|
||||
<RightSourcePanel
|
||||
dataView={rightDataView}
|
||||
distance={distance}
|
||||
geoField={rightGeoField}
|
||||
geoFields={rightGeoFields}
|
||||
onDataViewSelect={(dataView: DataView) => {
|
||||
setRightDataView(dataView);
|
||||
const geoFields = getGeoFields(dataView.fields);
|
||||
setRightGeoFields(geoFields);
|
||||
setRightGeoField(geoFields.length ? geoFields[0].name : undefined);
|
||||
}}
|
||||
onDistanceChange={setDistance}
|
||||
onGeoFieldSelect={setRightGeoField}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftSourcePanel
|
||||
dataView={leftDataView}
|
||||
geoField={leftGeoField}
|
||||
geoFields={leftGeoFields}
|
||||
onDataViewSelect={(dataView: DataView) => {
|
||||
setLeftDataView(dataView);
|
||||
const geoFields = getGeoPointFields(dataView.fields);
|
||||
setLeftGeoFields(geoFields);
|
||||
setLeftGeoField(geoFields.length ? geoFields[0].name : undefined);
|
||||
}}
|
||||
onGeoFieldSelect={setLeftGeoField}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{rightSourcePanel}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -41,8 +41,7 @@ export const clustersLayerWizardConfig: LayerWizard = {
|
|||
order: 10,
|
||||
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
|
||||
description: i18n.translate('xpack.maps.source.esGridClustersDescription', {
|
||||
defaultMessage:
|
||||
'Group Elasticsearch documents into grids and hexagons and display metrics for each group',
|
||||
defaultMessage: 'Group documents into grids and hexagons',
|
||||
}),
|
||||
icon: ClustersLayerIcon,
|
||||
renderWizard: ({ previewLayers }: RenderWizardArguments) => {
|
||||
|
|
|
@ -118,7 +118,7 @@ export class CreateSourceEditor extends Component {
|
|||
return (
|
||||
<EuiPanel>
|
||||
<GeoIndexPatternSelect
|
||||
value={this.state.indexPattern ? this.state.indexPattern.id : ''}
|
||||
dataView={this.state.indexPattern}
|
||||
onChange={this.onIndexPatternSelect}
|
||||
/>
|
||||
{this._renderGeoSelect()}
|
||||
|
|
|
@ -42,6 +42,7 @@ describe('ESGeoGridSource', () => {
|
|||
get() {
|
||||
return {
|
||||
getIndexPattern: () => 'foo-*',
|
||||
getName: () => 'foo-*',
|
||||
fields: {
|
||||
getByName() {
|
||||
return {
|
||||
|
|
|
@ -206,7 +206,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
|
|||
}
|
||||
}
|
||||
|
||||
showJoinEditor(): boolean {
|
||||
supportsJoins(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -355,14 +355,26 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
|
|||
: this.getId();
|
||||
const esResponse: estypes.SearchResponse<unknown> = await this._runEsQuery({
|
||||
requestId,
|
||||
requestName: `${layerName} (${requestCount})`,
|
||||
requestName: i18n.translate('xpack.maps.source.esGrid.compositeInspector.requestName', {
|
||||
defaultMessage: '{layerName} {bucketsName} composite request ({requestCount})',
|
||||
values: {
|
||||
bucketsName: this.getBucketsName(),
|
||||
layerName,
|
||||
requestCount,
|
||||
},
|
||||
}),
|
||||
searchSource,
|
||||
registerCancelCallback,
|
||||
requestDescription: i18n.translate(
|
||||
'xpack.maps.source.esGrid.compositeInspectorDescription',
|
||||
{
|
||||
defaultMessage: 'Elasticsearch geo grid aggregation request: {requestId}',
|
||||
values: { requestId },
|
||||
defaultMessage:
|
||||
'Get {bucketsName} from data view: {dataViewName}, geospatial field: {geoFieldName}',
|
||||
values: {
|
||||
bucketsName: this.getBucketsName(),
|
||||
dataViewName: indexPattern.getName(),
|
||||
geoFieldName: this._descriptor.geoField,
|
||||
},
|
||||
}
|
||||
),
|
||||
searchSessionId,
|
||||
|
@ -438,11 +450,23 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
|
|||
|
||||
const esResponse = await this._runEsQuery({
|
||||
requestId: this.getId(),
|
||||
requestName: layerName,
|
||||
requestName: i18n.translate('xpack.maps.source.esGrid.inspector.requestName', {
|
||||
defaultMessage: '{layerName} {bucketsName} request',
|
||||
values: {
|
||||
bucketsName: this.getBucketsName(),
|
||||
layerName,
|
||||
},
|
||||
}),
|
||||
searchSource,
|
||||
registerCancelCallback,
|
||||
requestDescription: i18n.translate('xpack.maps.source.esGrid.inspectorDescription', {
|
||||
defaultMessage: 'Elasticsearch geo grid aggregation request',
|
||||
requestDescription: i18n.translate('xpack.maps.source.esGrid.inspector.requestDescription', {
|
||||
defaultMessage:
|
||||
'Get {bucketsName} from data view: {dataViewName}, geospatial field: {geoFieldName}',
|
||||
values: {
|
||||
bucketsName: this.getBucketsName(),
|
||||
dataViewName: indexPattern.getName(),
|
||||
geoFieldName: this._descriptor.geoField,
|
||||
},
|
||||
}),
|
||||
searchSessionId,
|
||||
executionContext: mergeExecutionContext(
|
||||
|
|
|
@ -26,7 +26,7 @@ export const heatmapLayerWizardConfig: LayerWizard = {
|
|||
order: 10,
|
||||
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
|
||||
description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', {
|
||||
defaultMessage: 'Geospatial data grouped in grids to show density',
|
||||
defaultMessage: 'Group documents in grids to show density',
|
||||
}),
|
||||
icon: HeatmapLayerIcon,
|
||||
renderWizard: ({ previewLayers }: RenderWizardArguments) => {
|
||||
|
|
|
@ -138,9 +138,7 @@ export class CreateSourceEditor extends Component<Props, State> {
|
|||
return (
|
||||
<EuiPanel>
|
||||
<GeoIndexPatternSelect
|
||||
value={
|
||||
this.state.indexPattern && this.state.indexPattern.id ? this.state.indexPattern.id : ''
|
||||
}
|
||||
dataView={this.state.indexPattern}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
isGeoPointsOnly={true}
|
||||
/>
|
||||
|
|
|
@ -164,7 +164,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
|
|||
return false;
|
||||
}
|
||||
|
||||
showJoinEditor() {
|
||||
supportsJoins() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -214,7 +214,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
|
|||
const entityResp = await this._runEsQuery({
|
||||
requestId: `${this.getId()}_entities`,
|
||||
requestName: i18n.translate('xpack.maps.source.esGeoLine.entityRequestName', {
|
||||
defaultMessage: '{layerName} entities',
|
||||
defaultMessage: '{layerName} entities request',
|
||||
values: {
|
||||
layerName,
|
||||
},
|
||||
|
@ -222,7 +222,12 @@ export class ESGeoLineSource extends AbstractESAggSource {
|
|||
searchSource: entitySearchSource,
|
||||
registerCancelCallback,
|
||||
requestDescription: i18n.translate('xpack.maps.source.esGeoLine.entityRequestDescription', {
|
||||
defaultMessage: 'Elasticsearch terms request to fetch entities within map buffer.',
|
||||
defaultMessage:
|
||||
'Get entities within map buffer from data view: {dataViewName}, entities: {splitFieldName}',
|
||||
values: {
|
||||
dataViewName: indexPattern.getName(),
|
||||
splitFieldName: this._descriptor.splitField,
|
||||
},
|
||||
}),
|
||||
searchSessionId: requestMeta.searchSessionId,
|
||||
executionContext: mergeExecutionContext(
|
||||
|
@ -289,7 +294,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
|
|||
const tracksResp = await this._runEsQuery({
|
||||
requestId: `${this.getId()}_tracks`,
|
||||
requestName: i18n.translate('xpack.maps.source.esGeoLine.trackRequestName', {
|
||||
defaultMessage: '{layerName} tracks',
|
||||
defaultMessage: '{layerName} tracks request',
|
||||
values: {
|
||||
layerName,
|
||||
},
|
||||
|
@ -298,7 +303,12 @@ export class ESGeoLineSource extends AbstractESAggSource {
|
|||
registerCancelCallback,
|
||||
requestDescription: i18n.translate('xpack.maps.source.esGeoLine.trackRequestDescription', {
|
||||
defaultMessage:
|
||||
'Elasticsearch geo_line request to fetch tracks for entities. Tracks are not filtered by map buffer.',
|
||||
'Get tracks for {numEntities} entities from data view: {dataViewName}, geospatial field: {geoFieldName}',
|
||||
values: {
|
||||
dataViewName: indexPattern.getName(),
|
||||
geoFieldName: this._descriptor.geoField,
|
||||
numEntities: entityBuckets.length,
|
||||
},
|
||||
}),
|
||||
searchSessionId: requestMeta.searchSessionId,
|
||||
executionContext: mergeExecutionContext(
|
||||
|
|
|
@ -88,7 +88,7 @@ export class ESPewPewSource extends AbstractESAggSource {
|
|||
return true;
|
||||
}
|
||||
|
||||
showJoinEditor() {
|
||||
supportsJoins() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -190,11 +190,20 @@ export class ESPewPewSource extends AbstractESAggSource {
|
|||
|
||||
const esResponse = await this._runEsQuery({
|
||||
requestId: this.getId(),
|
||||
requestName: layerName,
|
||||
requestName: i18n.translate('xpack.maps.pewPew.requestName', {
|
||||
defaultMessage: '{layerName} paths request',
|
||||
values: { layerName },
|
||||
}),
|
||||
searchSource,
|
||||
registerCancelCallback,
|
||||
requestDescription: i18n.translate('xpack.maps.source.pewPew.inspectorDescription', {
|
||||
defaultMessage: 'Source-destination connections request',
|
||||
defaultMessage:
|
||||
'Get paths from data view: {dataViewName}, source: {sourceFieldName}, destination: {destFieldName}',
|
||||
values: {
|
||||
dataViewName: indexPattern.getName(),
|
||||
destFieldName: this._descriptor.destGeoField,
|
||||
sourceFieldName: this._descriptor.sourceGeoField,
|
||||
},
|
||||
}),
|
||||
searchSessionId: requestMeta.searchSessionId,
|
||||
executionContext: mergeExecutionContext(
|
||||
|
|
|
@ -118,9 +118,7 @@ export class CreateSourceEditor extends Component<Props, State> {
|
|||
return (
|
||||
<EuiPanel>
|
||||
<GeoIndexPatternSelect
|
||||
value={
|
||||
this.state.indexPattern && this.state.indexPattern.id ? this.state.indexPattern.id : ''
|
||||
}
|
||||
dataView={this.state.indexPattern}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
/>
|
||||
|
||||
|
|
|
@ -174,29 +174,27 @@ describe('ESSearchSource', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getJoinsDisabledReason', () => {
|
||||
describe('supportsJoins', () => {
|
||||
it('limit', () => {
|
||||
const esSearchSource = new ESSearchSource({
|
||||
...mockDescriptor,
|
||||
scalingType: SCALING_TYPES.LIMIT,
|
||||
});
|
||||
expect(esSearchSource.getJoinsDisabledReason()).toBe(null);
|
||||
expect(esSearchSource.supportsJoins()).toBe(true);
|
||||
});
|
||||
it('blended layer', () => {
|
||||
const esSearchSource = new ESSearchSource({
|
||||
...mockDescriptor,
|
||||
scalingType: SCALING_TYPES.CLUSTERS,
|
||||
});
|
||||
expect(esSearchSource.getJoinsDisabledReason()).toBe(
|
||||
'Joins are not supported when scaling by clusters'
|
||||
);
|
||||
expect(esSearchSource.supportsJoins()).toBe(false);
|
||||
});
|
||||
it('mvt', () => {
|
||||
const esSearchSource = new ESSearchSource({
|
||||
...mockDescriptor,
|
||||
scalingType: SCALING_TYPES.MVT,
|
||||
});
|
||||
expect(esSearchSource.getJoinsDisabledReason()).toBe(null);
|
||||
expect(esSearchSource.supportsJoins()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -190,6 +190,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
|
|||
scalingType={this._descriptor.scalingType}
|
||||
filterByMapBounds={this.isFilterByMapBounds()}
|
||||
numberOfJoins={sourceEditorArgs.numberOfJoins}
|
||||
hasSpatialJoins={sourceEditorArgs.hasSpatialJoins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -350,10 +351,21 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
|
|||
|
||||
const resp = await this._runEsQuery({
|
||||
requestId: this.getId(),
|
||||
requestName: layerName,
|
||||
requestName: i18n.translate('xpack.maps.esSearchSource.topHits.requestName', {
|
||||
defaultMessage: '{layerName} top hits request',
|
||||
values: { layerName },
|
||||
}),
|
||||
searchSource,
|
||||
registerCancelCallback,
|
||||
requestDescription: 'Elasticsearch document top hits request',
|
||||
requestDescription: i18n.translate('xpack.maps.esSearchSource.topHits.requestDescription', {
|
||||
defaultMessage:
|
||||
'Get top hits from data view: {dataViewName}, entities: {entitiesFieldName}, geospatial field: {geoFieldName}',
|
||||
values: {
|
||||
dataViewName: indexPattern.getName(),
|
||||
entitiesFieldName: topHitsSplitFieldName,
|
||||
geoFieldName: this._descriptor.geoField,
|
||||
},
|
||||
}),
|
||||
searchSessionId: requestMeta.searchSessionId,
|
||||
executionContext: mergeExecutionContext(
|
||||
{ description: 'es_search_source:top_hits' },
|
||||
|
@ -437,10 +449,20 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
|
|||
|
||||
const resp = await this._runEsQuery({
|
||||
requestId: this.getId(),
|
||||
requestName: layerName,
|
||||
requestName: i18n.translate('xpack.maps.esSearchSource.requestName', {
|
||||
defaultMessage: '{layerName} documents request',
|
||||
values: { layerName },
|
||||
}),
|
||||
searchSource,
|
||||
registerCancelCallback,
|
||||
requestDescription: 'Elasticsearch document request',
|
||||
requestDescription: i18n.translate('xpack.maps.esSearchSource.requestDescription', {
|
||||
defaultMessage:
|
||||
'Get documents from data view: {dataViewName}, geospatial field: {geoFieldName}',
|
||||
values: {
|
||||
dataViewName: indexPattern.getName(),
|
||||
geoFieldName: this._descriptor.geoField,
|
||||
},
|
||||
}),
|
||||
searchSessionId: requestMeta.searchSessionId,
|
||||
executionContext: mergeExecutionContext(
|
||||
{ description: 'es_search_source:doc_search' },
|
||||
|
@ -792,16 +814,9 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
|
|||
};
|
||||
}
|
||||
|
||||
getJoinsDisabledReason(): string | null {
|
||||
let reason;
|
||||
if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) {
|
||||
reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', {
|
||||
defaultMessage: 'Joins are not supported when scaling by clusters',
|
||||
});
|
||||
} else {
|
||||
reason = null;
|
||||
}
|
||||
return reason;
|
||||
supportsJoins(): boolean {
|
||||
// can only join with features, not aggregated clusters
|
||||
return this._descriptor.scalingType !== SCALING_TYPES.CLUSTERS;
|
||||
}
|
||||
|
||||
async _getEditableIndex(): Promise<string> {
|
||||
|
|
|
@ -144,9 +144,7 @@ export class CreateSourceEditor extends Component<Props, State> {
|
|||
return (
|
||||
<EuiPanel>
|
||||
<GeoIndexPatternSelect
|
||||
value={
|
||||
this.state.indexPattern && this.state.indexPattern.id ? this.state.indexPattern.id : ''
|
||||
}
|
||||
dataView={this.state.indexPattern}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
/>
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ interface Props {
|
|||
sortOrder: SortDirection;
|
||||
scalingType: SCALING_TYPES;
|
||||
source: IESSource;
|
||||
hasSpatialJoins: boolean;
|
||||
numberOfJoins: number;
|
||||
getGeoField(): Promise<DataViewField>;
|
||||
filterByMapBounds: boolean;
|
||||
|
@ -217,6 +218,7 @@ export class UpdateSourceEditor extends Component<Props, State> {
|
|||
scalingType={this.props.scalingType}
|
||||
supportsClustering={this.state.supportsClustering}
|
||||
clusteringDisabledReason={this.state.clusteringDisabledReason}
|
||||
hasSpatialJoins={this.props.hasSpatialJoins}
|
||||
numberOfJoins={this.props.numberOfJoins}
|
||||
/>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { EuiButtonIcon, EuiLink, EuiPopover, EuiText } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiButtonIcon, EuiLink, EuiPopover, EuiPopoverTitle, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { getDocLinks } from '../../../../kibana_services';
|
||||
|
||||
|
@ -17,39 +17,43 @@ interface Props {
|
|||
mvtOptionLabel: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
export function ScalingDocumenationPopover(props: Props) {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
export class ScalingDocumenationPopover extends Component<Props, State> {
|
||||
state: State = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
return (
|
||||
<EuiPopover
|
||||
id="scalingHelpPopover"
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
iconType="documentation"
|
||||
aria-label="Scaling documentation"
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => {
|
||||
setIsPopoverOpen(false);
|
||||
}}
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage id="xpack.maps.scalingDocs.title" defaultMessage="Scaling" />
|
||||
</EuiPopoverTitle>
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
_renderContent() {
|
||||
return (
|
||||
<div>
|
||||
<EuiText style={{ maxWidth: '36em' }}>
|
||||
<EuiText size="s" style={{ maxWidth: '36em' }}>
|
||||
<dl>
|
||||
<dt>{this.props.mvtOptionLabel} (Default)</dt>
|
||||
<dt>{props.mvtOptionLabel} (Default)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.mvtDetails"
|
||||
defaultMessage="Vector tiles partition your map into tiles, with each tile displaying features from the first {maxResultWindow} documents. Results exceeding {maxResultWindow} are not displayed in a tile. A bounding box indicates the area where data is incomplete."
|
||||
values={{ maxResultWindow: this.props.maxResultWindow }}
|
||||
values={{ maxResultWindow: props.maxResultWindow }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
|
@ -60,64 +64,74 @@ export class ScalingDocumenationPopover extends Component<Props, State> {
|
|||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>{this.props.clustersOptionLabel}</dt>
|
||||
<dt>{props.clustersOptionLabel}</dt>
|
||||
<dd>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.clustersDetails"
|
||||
defaultMessage="Display clusters when results exceed {maxResultWindow} documents. Display documents when results are less then {maxResultWindow}."
|
||||
values={{ maxResultWindow: this.props.maxResultWindow }}
|
||||
values={{ maxResultWindow: props.maxResultWindow }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.clustersUseCase"
|
||||
defaultMessage="Use this option to display large data sets. Does not support term joins."
|
||||
defaultMessage="Use this option to display large data sets. "
|
||||
/>
|
||||
<i>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.doesNotSupportJoins"
|
||||
defaultMessage="Does not support joins."
|
||||
/>
|
||||
</i>
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>{this.props.limitOptionLabel}</dt>
|
||||
<dt>{props.limitOptionLabel}</dt>
|
||||
<dd>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitDetails"
|
||||
defaultMessage="Display features from the first {maxResultWindow} documents."
|
||||
values={{ maxResultWindow: this.props.maxResultWindow }}
|
||||
values={{ maxResultWindow: props.maxResultWindow }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCases"
|
||||
defaultMessage="Use this option when you can not use vector tiles for the following reasons:"
|
||||
/>
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.formatLabels"
|
||||
defaultMessage="Formatted labels"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.multipleJoins"
|
||||
defaultMessage="Multiple term joins"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.joinFieldsWithLayoutStyles"
|
||||
defaultMessage="Data driven styling from term join metrics with 'Label', 'Label size', icon 'Symbol size', and 'Symbol orientation' style properties"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.scriptedFields"
|
||||
defaultMessage="Data driven styling from scripted fields"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCases"
|
||||
defaultMessage="Use this option when you can not use vector tiles for the following reasons:"
|
||||
/>
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.formatLabels"
|
||||
defaultMessage="Formatted labels"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.multipleJoins"
|
||||
defaultMessage="Spatial joins"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.spatialJoins"
|
||||
defaultMessage="Multiple term joins"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.joinFieldsWithLayoutStyles"
|
||||
defaultMessage="Data driven styling from join metrics with 'Label', 'Label size', icon 'Symbol size', and 'Symbol orientation' style properties"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.scalingDocs.limitUseCase.scriptedFields"
|
||||
defaultMessage="Data driven styling from scripted fields"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
@ -126,7 +140,7 @@ export class ScalingDocumenationPopover extends Component<Props, State> {
|
|||
id="xpack.maps.scalingDocs.maxResultWindow"
|
||||
defaultMessage="{maxResultWindow} constraint provided by {link} index setting."
|
||||
values={{
|
||||
maxResultWindow: this.props.maxResultWindow,
|
||||
maxResultWindow: props.maxResultWindow,
|
||||
link: (
|
||||
<EuiLink
|
||||
href={getDocLinks().links.elasticsearch.dynamicIndexSettings}
|
||||
|
@ -141,28 +155,6 @@ export class ScalingDocumenationPopover extends Component<Props, State> {
|
|||
</p>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPopover
|
||||
id="scalingHelpPopover"
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
onClick={this._togglePopover}
|
||||
iconType="documentation"
|
||||
aria-label="Scaling documentation"
|
||||
/>
|
||||
}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
>
|
||||
{this._renderContent()}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ const defaultProps = {
|
|||
supportsClustering: true,
|
||||
termFields: [],
|
||||
numberOfJoins: 0,
|
||||
hasSpatialJoins: false,
|
||||
};
|
||||
|
||||
describe('scaling form', () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
import {
|
||||
EuiConfirmModal,
|
||||
EuiFormRow,
|
||||
|
@ -35,6 +35,7 @@ interface Props {
|
|||
scalingType: SCALING_TYPES;
|
||||
supportsClustering: boolean;
|
||||
clusteringDisabledReason?: string | null;
|
||||
hasSpatialJoins: boolean;
|
||||
numberOfJoins: number;
|
||||
}
|
||||
|
||||
|
@ -42,13 +43,13 @@ interface State {
|
|||
nextScalingType?: SCALING_TYPES;
|
||||
maxResultWindow: string;
|
||||
showModal: boolean;
|
||||
modalMsg: string | null;
|
||||
modalContent: ReactNode;
|
||||
}
|
||||
|
||||
export class ScalingForm extends Component<Props, State> {
|
||||
state: State = {
|
||||
maxResultWindow: DEFAULT_MAX_RESULT_WINDOW.toLocaleString(),
|
||||
modalMsg: null,
|
||||
modalContent: null,
|
||||
showModal: false,
|
||||
};
|
||||
_isMounted = false;
|
||||
|
@ -76,25 +77,42 @@ export class ScalingForm extends Component<Props, State> {
|
|||
|
||||
_onScalingTypeSelect = (optionId: SCALING_TYPES): void => {
|
||||
if (this.props.numberOfJoins > 0 && optionId === SCALING_TYPES.CLUSTERS) {
|
||||
this._openModal(
|
||||
optionId,
|
||||
this._openModal(optionId, [
|
||||
i18n.translate('xpack.maps.source.esSearch.clusterScalingJoinMsg', {
|
||||
defaultMessage: `Scaling with clusters does not support term joins. Switching to clusters will remove all term joins from your layer configuration.`,
|
||||
})
|
||||
);
|
||||
} else if (this.props.numberOfJoins > 1 && optionId === SCALING_TYPES.MVT) {
|
||||
this._openModal(
|
||||
optionId,
|
||||
i18n.translate('xpack.maps.source.esSearch.mvtScalingJoinMsg', {
|
||||
defaultMessage: `Vector tiles support one term join. Your layer has {numberOfJoins} term joins. Switching to vector tiles will keep the first term join and remove all other term joins from your layer configuration.`,
|
||||
values: {
|
||||
numberOfJoins: this.props.numberOfJoins,
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this._onScalingTypeChange(optionId);
|
||||
defaultMessage: `Scaling with clusters does not support joins. Switching to clusters will remove all joins from your layer configuration.`,
|
||||
}),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (optionId === SCALING_TYPES.MVT) {
|
||||
const messages: string[] = [];
|
||||
if (this.props.hasSpatialJoins) {
|
||||
messages.push(
|
||||
i18n.translate('xpack.maps.source.esSearch.mvtNoSpatialJoinMsg', {
|
||||
defaultMessage: `Vector tiles do not support spatial joins. Switching to vector tiles will remove all spatial joins from your layer configuration.`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.numberOfJoins > 1) {
|
||||
messages.push(
|
||||
i18n.translate('xpack.maps.source.esSearch.mvtScalingJoinMsg', {
|
||||
defaultMessage: `Vector tiles support one term join. Your layer has {numberOfJoins} joins. Switching to vector tiles will keep the first term join and remove all other joins from your layer configuration.`,
|
||||
values: {
|
||||
numberOfJoins: this.props.numberOfJoins,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (messages.length) {
|
||||
this._openModal(optionId, messages);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._onScalingTypeChange(optionId);
|
||||
};
|
||||
|
||||
_onScalingTypeChange = (optionId: SCALING_TYPES): void => {
|
||||
|
@ -114,9 +132,13 @@ export class ScalingForm extends Component<Props, State> {
|
|||
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
|
||||
};
|
||||
|
||||
_openModal = (optionId: SCALING_TYPES, modalMsg: string) => {
|
||||
_openModal = (optionId: SCALING_TYPES, messages: string[]) => {
|
||||
this.setState({
|
||||
modalMsg,
|
||||
modalContent: messages.length
|
||||
? messages.map((message, index) => {
|
||||
return <p key={index}>{message}</p>;
|
||||
})
|
||||
: null,
|
||||
nextScalingType: optionId,
|
||||
showModal: true,
|
||||
});
|
||||
|
@ -124,7 +146,7 @@ export class ScalingForm extends Component<Props, State> {
|
|||
|
||||
_closeModal = () => {
|
||||
this.setState({
|
||||
modalMsg: null,
|
||||
modalContent: null,
|
||||
nextScalingType: undefined,
|
||||
showModal: false,
|
||||
});
|
||||
|
@ -158,7 +180,11 @@ export class ScalingForm extends Component<Props, State> {
|
|||
}
|
||||
|
||||
_renderModal() {
|
||||
if (!this.state.showModal || !this.state.modalMsg || this.state.nextScalingType === undefined) {
|
||||
if (
|
||||
!this.state.showModal ||
|
||||
!this.state.modalContent ||
|
||||
this.state.nextScalingType === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -181,7 +207,7 @@ export class ScalingForm extends Component<Props, State> {
|
|||
buttonColor="danger"
|
||||
defaultFocusedButton="cancel"
|
||||
>
|
||||
<p>{this.state.modalMsg}</p>
|
||||
{this.state.modalContent}
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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 { FeatureCollection } from 'geojson';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { ISearchSource } from '@kbn/data-plugin/public';
|
||||
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
|
||||
import { AGG_TYPE, FIELD_ORIGIN, SOURCE_TYPES } from '../../../../../common/constants';
|
||||
import { getJoinAggKey } from '../../../../../common/get_agg_key';
|
||||
import { AbstractESAggSource } from '../../es_agg_source';
|
||||
import type { BucketProperties } from '../../../../../common/elasticsearch_util';
|
||||
import {
|
||||
ESDistanceSourceDescriptor,
|
||||
VectorSourceRequestMeta,
|
||||
} from '../../../../../common/descriptor_types';
|
||||
import { PropertiesMap } from '../../../../../common/elasticsearch_util';
|
||||
import { isValidStringConfig } from '../../../util/valid_string_config';
|
||||
import { IJoinSource } from '../types';
|
||||
import type { IESAggSource } from '../../es_agg_source';
|
||||
import { IField } from '../../../fields/field';
|
||||
import { mergeExecutionContext } from '../../execution_context_utils';
|
||||
import { processDistanceResponse } from './process_distance_response';
|
||||
import { isSpatialSourceComplete } from '../is_spatial_source_complete';
|
||||
|
||||
export const DEFAULT_WITHIN_DISTANCE = 5;
|
||||
|
||||
type ESDistanceSourceSyncMeta = Pick<ESDistanceSourceDescriptor, 'distance' | 'geoField'>;
|
||||
|
||||
export class ESDistanceSource extends AbstractESAggSource implements IJoinSource, IESAggSource {
|
||||
static type = SOURCE_TYPES.ES_DISTANCE_SOURCE;
|
||||
|
||||
static createDescriptor(
|
||||
descriptor: Partial<ESDistanceSourceDescriptor>
|
||||
): ESDistanceSourceDescriptor {
|
||||
const normalizedDescriptor = AbstractESAggSource.createDescriptor(descriptor);
|
||||
if (!isValidStringConfig(descriptor.geoField)) {
|
||||
throw new Error('Cannot create an ESDistanceSource without a geoField property');
|
||||
}
|
||||
return {
|
||||
...normalizedDescriptor,
|
||||
geoField: descriptor.geoField!,
|
||||
distance:
|
||||
typeof descriptor.distance === 'number' ? descriptor.distance : DEFAULT_WITHIN_DISTANCE,
|
||||
type: SOURCE_TYPES.ES_DISTANCE_SOURCE,
|
||||
};
|
||||
}
|
||||
|
||||
readonly _descriptor: ESDistanceSourceDescriptor;
|
||||
|
||||
constructor(descriptor: Partial<ESDistanceSourceDescriptor>) {
|
||||
const sourceDescriptor = ESDistanceSource.createDescriptor(descriptor);
|
||||
super(sourceDescriptor);
|
||||
this._descriptor = sourceDescriptor;
|
||||
}
|
||||
|
||||
hasCompleteConfig(): boolean {
|
||||
return isSpatialSourceComplete(this._descriptor);
|
||||
}
|
||||
|
||||
getOriginForField(): FIELD_ORIGIN {
|
||||
return FIELD_ORIGIN.JOIN;
|
||||
}
|
||||
|
||||
getWhereQuery(): Query | undefined {
|
||||
return this._descriptor.whereQuery;
|
||||
}
|
||||
|
||||
getAggKey(aggType: AGG_TYPE, fieldName?: string): string {
|
||||
return getJoinAggKey({
|
||||
aggType,
|
||||
aggFieldName: fieldName,
|
||||
rightSourceId: this._descriptor.id,
|
||||
});
|
||||
}
|
||||
|
||||
async getPropertiesMap(
|
||||
requestMeta: VectorSourceRequestMeta,
|
||||
leftSourceName: string,
|
||||
leftFieldName: string,
|
||||
registerCancelCallback: (callback: () => void) => void,
|
||||
inspectorAdapters: Adapters,
|
||||
featureCollection?: FeatureCollection
|
||||
): Promise<PropertiesMap> {
|
||||
if (featureCollection === undefined) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.maps.esDistanceSource.noFeatureCollectionMsg', {
|
||||
defaultMessage: `Unable to perform distance join, features not provided. To enable distance join, select 'Limit results' in 'Scaling'`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.hasCompleteConfig()) {
|
||||
return new Map<string, BucketProperties>();
|
||||
}
|
||||
|
||||
const distance = `${this._descriptor.distance}km`;
|
||||
let hasFilters = false;
|
||||
const filters: Record<string, unknown> = {};
|
||||
for (let i = 0; i < featureCollection.features.length; i++) {
|
||||
const feature = featureCollection.features[i];
|
||||
if (feature.geometry.type === 'Point' && feature?.properties?._id) {
|
||||
filters[feature.properties._id] = {
|
||||
geo_distance: {
|
||||
distance,
|
||||
[this._descriptor.geoField]: feature.geometry.coordinates,
|
||||
},
|
||||
};
|
||||
if (!hasFilters) {
|
||||
hasFilters = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasFilters) {
|
||||
return new Map<string, BucketProperties>();
|
||||
}
|
||||
|
||||
const indexPattern = await this.getIndexPattern();
|
||||
const searchSource: ISearchSource = await this.makeSearchSource(requestMeta, 0);
|
||||
searchSource.setField('trackTotalHits', false);
|
||||
searchSource.setField('aggs', {
|
||||
distance: {
|
||||
filters: {
|
||||
filters,
|
||||
},
|
||||
aggs: this.getValueAggsDsl(indexPattern),
|
||||
},
|
||||
});
|
||||
const rawEsData = await this._runEsQuery({
|
||||
requestId: this.getId(),
|
||||
requestName: i18n.translate('xpack.maps.distanceSource.requestName', {
|
||||
defaultMessage: '{leftSourceName} within distance join request',
|
||||
values: { leftSourceName },
|
||||
}),
|
||||
searchSource,
|
||||
registerCancelCallback,
|
||||
requestDescription: i18n.translate('xpack.maps.distanceSource.requestDescription', {
|
||||
defaultMessage:
|
||||
'Get metrics from data view: {dataViewName}, geospatial field: {geoFieldName}',
|
||||
values: {
|
||||
dataViewName: indexPattern.getName(),
|
||||
geoFieldName: this._descriptor.geoField,
|
||||
},
|
||||
}),
|
||||
searchSessionId: requestMeta.searchSessionId,
|
||||
executionContext: mergeExecutionContext(
|
||||
{ description: 'es_distance_source:distance_join_request' },
|
||||
requestMeta.executionContext
|
||||
),
|
||||
requestsAdapter: inspectorAdapters.requests,
|
||||
});
|
||||
|
||||
return processDistanceResponse(rawEsData, this.getAggKey(AGG_TYPE.COUNT));
|
||||
}
|
||||
|
||||
isFilterByMapBounds(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getFieldNames(): string[] {
|
||||
return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName());
|
||||
}
|
||||
|
||||
getSyncMeta(): ESDistanceSourceSyncMeta {
|
||||
return {
|
||||
distance: this._descriptor.distance,
|
||||
geoField: this._descriptor.geoField,
|
||||
};
|
||||
}
|
||||
|
||||
getRightFields(): IField[] {
|
||||
return this.getMetricFields();
|
||||
}
|
||||
}
|
|
@ -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 { DEFAULT_WITHIN_DISTANCE, ESDistanceSource } from './es_distance_source';
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { processDistanceResponse } from './process_distance_response';
|
||||
|
||||
test('should convert elasticsearch response into table', () => {
|
||||
const response = {
|
||||
aggregations: {
|
||||
distance: {
|
||||
buckets: {
|
||||
'06-Fv4cB_nxKbZ5eWyyP': {
|
||||
doc_count: 1,
|
||||
__kbnjoin__avg_of_bytes__673ff994: {
|
||||
value: 5794,
|
||||
},
|
||||
},
|
||||
'0a-Fv4cB_nxKbZ5eWyyP': {
|
||||
doc_count: 0,
|
||||
__kbnjoin__avg_of_bytes__673ff994: {
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
'1q-Fv4cB_nxKbZ5eWyyP': {
|
||||
doc_count: 2,
|
||||
__kbnjoin__avg_of_bytes__673ff994: {
|
||||
value: 5771,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const table = processDistanceResponse(response, '__kbnjoin__count__673ff994');
|
||||
|
||||
expect(table.size).toBe(2);
|
||||
|
||||
const bucketProperties = table.get('1q-Fv4cB_nxKbZ5eWyyP');
|
||||
expect(bucketProperties?.__kbnjoin__count__673ff994).toBe(2);
|
||||
expect(bucketProperties?.__kbnjoin__avg_of_bytes__673ff994).toBe(5771);
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { COUNT_PROP_NAME } from '../../../../../common/constants';
|
||||
import type { BucketProperties, PropertiesMap } from '../../../../../common/elasticsearch_util';
|
||||
import { extractPropertiesFromBucket } from '../../../../../common/elasticsearch_util';
|
||||
|
||||
const IGNORE_LIST = [COUNT_PROP_NAME];
|
||||
|
||||
export function processDistanceResponse(response: any, countPropertyName: string): PropertiesMap {
|
||||
const propertiesMap: PropertiesMap = new Map<string, BucketProperties>();
|
||||
const buckets: any = response?.aggregations?.distance?.buckets ?? {};
|
||||
for (const docId in buckets) {
|
||||
if (buckets.hasOwnProperty(docId)) {
|
||||
const bucket = buckets[docId];
|
||||
|
||||
// skip empty buckets
|
||||
if (bucket[COUNT_PROP_NAME] === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const properties = extractPropertiesFromBucket(bucket, IGNORE_LIST);
|
||||
// Manually set 'doc_count' so join name, like '__kbnjoin__count__673ff994', is used
|
||||
properties[countPropertyName] = bucket[COUNT_PROP_NAME];
|
||||
propertiesMap.set(docId, properties);
|
||||
}
|
||||
}
|
||||
return propertiesMap;
|
||||
}
|
|
@ -34,6 +34,7 @@ import { ITermJoinSource } from '../types';
|
|||
import type { IESAggSource } from '../../es_agg_source';
|
||||
import { IField } from '../../../fields/field';
|
||||
import { mergeExecutionContext } from '../../execution_context_utils';
|
||||
import { isTermSourceComplete } from './is_term_source_complete';
|
||||
|
||||
const TERMS_AGG_NAME = 'join';
|
||||
const TERMS_BUCKET_KEYS_TO_IGNORE = ['key', 'doc_count'];
|
||||
|
@ -83,7 +84,7 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource
|
|||
}
|
||||
|
||||
hasCompleteConfig(): boolean {
|
||||
return this._descriptor.indexPatternId !== undefined && this._descriptor.term !== undefined;
|
||||
return isTermSourceComplete(this._descriptor);
|
||||
}
|
||||
|
||||
getTermField(): ESDocField {
|
||||
|
@ -149,14 +150,17 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource
|
|||
|
||||
const rawEsData = await this._runEsQuery({
|
||||
requestId: this.getId(),
|
||||
requestName: `${indexPattern.getName()}.${this._termField.getName()}`,
|
||||
requestName: i18n.translate('xpack.maps.termSource.requestName', {
|
||||
defaultMessage: '{leftSourceName} term join request',
|
||||
values: { leftSourceName },
|
||||
}),
|
||||
searchSource,
|
||||
registerCancelCallback,
|
||||
requestDescription: i18n.translate('xpack.maps.source.esJoin.joinDescription', {
|
||||
defaultMessage: `Elasticsearch terms aggregation request, left source: {leftSource}, right source: {rightSource}`,
|
||||
requestDescription: i18n.translate('xpack.maps.termSource.requestDescription', {
|
||||
defaultMessage: 'Get metrics from data view: {dataViewName}, term field: {termFieldName}',
|
||||
values: {
|
||||
leftSource: `${leftSourceName}:${leftFieldName}`,
|
||||
rightSource: `${indexPattern.getName()}:${this._termField.getName()}`,
|
||||
dataViewName: indexPattern.getName(),
|
||||
termFieldName: this._termField.getName(),
|
||||
},
|
||||
}),
|
||||
searchSessionId: requestMeta.searchSessionId,
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './es_term_source';
|
||||
export { isTermSourceComplete } from './is_term_source_complete';
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { ESTermSourceDescriptor } from '../../../../../common/descriptor_types';
|
||||
|
||||
export function isTermSourceComplete(descriptor: Partial<ESTermSourceDescriptor>) {
|
||||
return descriptor.indexPatternId !== undefined && descriptor.term !== undefined;
|
||||
}
|
|
@ -8,5 +8,7 @@
|
|||
export type { IJoinSource, ITermJoinSource } from './types';
|
||||
|
||||
export { isTermJoinSource } from './types';
|
||||
export { ESTermSource } from './es_term_source';
|
||||
export { isSpatialSourceComplete } from './is_spatial_source_complete';
|
||||
export { DEFAULT_WITHIN_DISTANCE, ESDistanceSource } from './es_distance_source';
|
||||
export { ESTermSource, isTermSourceComplete } from './es_term_source';
|
||||
export { TableSource } from './table_source';
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { ESDistanceSourceDescriptor } from '../../../../common/descriptor_types';
|
||||
|
||||
export function isSpatialSourceComplete(descriptor: Partial<ESDistanceSourceDescriptor>) {
|
||||
return descriptor.indexPatternId !== undefined && descriptor.geoField !== undefined;
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GeoJsonProperties } from 'geojson';
|
||||
import { FeatureCollection, GeoJsonProperties } from 'geojson';
|
||||
import type { KibanaExecutionContext } from '@kbn/core/public';
|
||||
import { Query } from '@kbn/data-plugin/common/query';
|
||||
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
|
||||
|
@ -23,7 +23,8 @@ export interface IJoinSource extends ISource {
|
|||
leftSourceName: string,
|
||||
leftFieldName: string,
|
||||
registerCancelCallback: (callback: () => void) => void,
|
||||
inspectorAdapters: Adapters
|
||||
inspectorAdapters: Adapters,
|
||||
featureCollection?: FeatureCollection
|
||||
): Promise<PropertiesMap>;
|
||||
|
||||
/*
|
||||
|
|
|
@ -237,14 +237,10 @@ export class MVTSingleLayerVectorSource extends AbstractSource implements IMvtVe
|
|||
return {};
|
||||
}
|
||||
|
||||
showJoinEditor(): boolean {
|
||||
supportsJoins(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getJoinsDisabledReason(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getFeatureActions(args: GetFeatureActionsArgs): TooltipFeatureAction[] {
|
||||
// Its not possible to filter by geometry for vector tile sources since there is no way to get original geometry
|
||||
return [];
|
||||
|
|
|
@ -29,6 +29,7 @@ export type OnSourceChangeArgs = {
|
|||
|
||||
export type SourceEditorArgs = {
|
||||
currentLayerType: string;
|
||||
hasSpatialJoins: boolean;
|
||||
numberOfJoins: number;
|
||||
onChange: (...args: OnSourceChangeArgs[]) => Promise<void>;
|
||||
onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void;
|
||||
|
|
|
@ -106,8 +106,7 @@ export interface IVectorSource extends ISource {
|
|||
getFields(): Promise<IField[]>;
|
||||
getFieldByName(fieldName: string): IField | null;
|
||||
getLeftJoinFields(): Promise<IField[]>;
|
||||
showJoinEditor(): boolean;
|
||||
getJoinsDisabledReason(): string | null;
|
||||
supportsJoins(): boolean;
|
||||
|
||||
/*
|
||||
* Vector layer avoids unnecessarily re-fetching source data.
|
||||
|
@ -189,10 +188,6 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
|
|||
return [];
|
||||
}
|
||||
|
||||
getJoinsDisabledReason(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getGeoJsonWithMeta(
|
||||
layerName: string,
|
||||
requestMeta: VectorSourceRequestMeta,
|
||||
|
@ -227,7 +222,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
|
|||
return false;
|
||||
}
|
||||
|
||||
showJoinEditor() {
|
||||
supportsJoins() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,85 +5,19 @@ exports[`should render 1`] = `
|
|||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error="Data view does not contain any geospatial fields"
|
||||
error=""
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={true}
|
||||
isInvalid={false}
|
||||
label="Data view"
|
||||
labelType="label"
|
||||
>
|
||||
<MockIndexPatternSelect
|
||||
data-test-subj="mapGeoIndexPatternSelect"
|
||||
indexPatternId="indexPatternId"
|
||||
indexPatternId="weblogs"
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
isInvalid={true}
|
||||
onChange={[Function]}
|
||||
onNoIndexPatterns={[Function]}
|
||||
placeholder="Select data view"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`should render no index pattern warning when there are no matching index patterns 1`] = `
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title="Couldn't find any data views"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="You'll need to "
|
||||
id="xpack.maps.noIndexPattern.doThisPrefixDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
<EuiLink
|
||||
href="abc//app/management/kibana/dataViews"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Create a data view."
|
||||
id="xpack.maps.noIndexPattern.doThisLinkTextDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Don't have any data? "
|
||||
id="xpack.maps.noIndexPattern.hintDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
<EuiLink
|
||||
href="abc//app/home#/tutorial_directory/sampleData"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Get started with some sample data sets."
|
||||
id="xpack.maps.noIndexPattern.getStartedLinkText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error="Data view does not contain any geospatial fields"
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={true}
|
||||
label="Data view"
|
||||
labelType="label"
|
||||
>
|
||||
<MockIndexPatternSelect
|
||||
data-test-subj="mapGeoIndexPatternSelect"
|
||||
indexPatternId="indexPatternId"
|
||||
isClearable={false}
|
||||
isDisabled={true}
|
||||
isInvalid={true}
|
||||
isInvalid={false}
|
||||
onChange={[Function]}
|
||||
onNoIndexPatterns={[Function]}
|
||||
placeholder="Select data view"
|
||||
|
|
|
@ -9,15 +9,14 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { SingleFieldSelect } from './single_field_select';
|
||||
import { type Props as SingleFieldSelectProps, SingleFieldSelect } from './single_field_select';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
type Props = SingleFieldSelectProps & {
|
||||
geoFields: DataViewField[];
|
||||
onChange: (geoFieldName?: string) => void;
|
||||
}
|
||||
};
|
||||
|
||||
export function GeoFieldSelect(props: Props) {
|
||||
const { geoFields, ...rest } = props;
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.geofieldLabel', {
|
||||
|
@ -28,9 +27,8 @@ export function GeoFieldSelect(props: Props) {
|
|||
placeholder={i18n.translate('xpack.maps.source.selectLabel', {
|
||||
defaultMessage: 'Select geo field',
|
||||
})}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
fields={props.geoFields}
|
||||
{...rest}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -28,16 +28,23 @@ jest.mock('../kibana_services', () => {
|
|||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DataView } from '@kbn/data-plugin/common';
|
||||
import { GeoIndexPatternSelect } from './geo_index_pattern_select';
|
||||
|
||||
const defaultProps = {
|
||||
dataView: {
|
||||
id: 'weblogs',
|
||||
fields: [
|
||||
{
|
||||
type: 'geo_point',
|
||||
},
|
||||
],
|
||||
} as unknown as DataView,
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
test('should render', async () => {
|
||||
const component = shallow(<GeoIndexPatternSelect onChange={() => {}} value={'indexPatternId'} />);
|
||||
const component = shallow(<GeoIndexPatternSelect {...defaultProps} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render no index pattern warning when there are no matching index patterns', async () => {
|
||||
const component = shallow(<GeoIndexPatternSelect onChange={() => {}} value={'indexPatternId'} />);
|
||||
component.setState({ noIndexPatternsExist: true });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { EuiCallOut, EuiFormRow, EuiLink, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { indexPatterns } from '@kbn/data-plugin/public';
|
||||
import { DataView } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
getIndexPatternSelectComponent,
|
||||
|
@ -20,36 +22,32 @@ import { ES_GEO_FIELD_TYPE, ES_GEO_FIELD_TYPES } from '../../common/constants';
|
|||
|
||||
interface Props {
|
||||
onChange: (indexPattern: DataView) => void;
|
||||
value: string | null;
|
||||
dataView?: DataView | null;
|
||||
isGeoPointsOnly?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
doesIndexPatternHaveGeoField: boolean;
|
||||
noIndexPatternsExist: boolean;
|
||||
}
|
||||
export function GeoIndexPatternSelect(props: Props) {
|
||||
const [noDataViews, setNoDataViews] = useState(false);
|
||||
|
||||
export class GeoIndexPatternSelect extends Component<Props, State> {
|
||||
private _isMounted: boolean = false;
|
||||
const hasGeoFields = useMemo(() => {
|
||||
return props.dataView
|
||||
? props.dataView.fields.some((field) => {
|
||||
return !indexPatterns.isNestedField(field) && props?.isGeoPointsOnly
|
||||
? (ES_GEO_FIELD_TYPE.GEO_POINT as string) === field.type
|
||||
: ES_GEO_FIELD_TYPES.includes(field.type);
|
||||
})
|
||||
: false;
|
||||
}, [props.dataView, props?.isGeoPointsOnly]);
|
||||
|
||||
state = {
|
||||
doesIndexPatternHaveGeoField: false,
|
||||
noIndexPatternsExist: false,
|
||||
};
|
||||
const isMounted = useMountedState();
|
||||
const dataViewIdRef = useRef<string | undefined>();
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
_onIndexPatternSelect = async (indexPatternId?: string) => {
|
||||
async function _onIndexPatternSelect(indexPatternId?: string) {
|
||||
if (!indexPatternId || indexPatternId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataViewIdRef.current = indexPatternId;
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await getIndexPatternService().get(indexPatternId);
|
||||
|
@ -59,24 +57,13 @@ export class GeoIndexPatternSelect extends Component<Props, State> {
|
|||
|
||||
// method may be called again before 'get' returns
|
||||
// ignore response when fetched index pattern does not match active index pattern
|
||||
if (this._isMounted && indexPattern.id === indexPatternId) {
|
||||
this.setState({
|
||||
doesIndexPatternHaveGeoField: indexPattern.fields.some((field) => {
|
||||
return this.props?.isGeoPointsOnly
|
||||
? (ES_GEO_FIELD_TYPE.GEO_POINT as string) === field.type
|
||||
: ES_GEO_FIELD_TYPES.includes(field.type);
|
||||
}),
|
||||
});
|
||||
this.props.onChange(indexPattern);
|
||||
if (isMounted() && indexPattern.id === dataViewIdRef.current) {
|
||||
props.onChange(indexPattern);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_onNoIndexPatterns = () => {
|
||||
this.setState({ noIndexPatternsExist: true });
|
||||
};
|
||||
|
||||
_renderNoIndexPatternWarning() {
|
||||
if (!this.state.noIndexPatternsExist) {
|
||||
function _renderNoIndexPatternWarning() {
|
||||
if (!noDataViews) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -118,31 +105,31 @@ export class GeoIndexPatternSelect extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const IndexPatternSelect = getIndexPatternSelectComponent();
|
||||
const isIndexPatternInvalid = !!this.props.value && !this.state.doesIndexPatternHaveGeoField;
|
||||
const error = isIndexPatternInvalid
|
||||
? i18n.translate('xpack.maps.noGeoFieldInIndexPattern.message', {
|
||||
defaultMessage: 'Data view does not contain any geospatial fields',
|
||||
})
|
||||
: '';
|
||||
return (
|
||||
<>
|
||||
{this._renderNoIndexPatternWarning()}
|
||||
const IndexPatternSelect = getIndexPatternSelectComponent();
|
||||
const error = !hasGeoFields
|
||||
? i18n.translate('xpack.maps.noGeoFieldInIndexPattern.message', {
|
||||
defaultMessage: 'Data view does not contain any geospatial fields',
|
||||
})
|
||||
: '';
|
||||
|
||||
<EuiFormRow label={getDataViewLabel()} isInvalid={isIndexPatternInvalid} error={error}>
|
||||
<IndexPatternSelect
|
||||
isInvalid={isIndexPatternInvalid}
|
||||
isDisabled={this.state.noIndexPatternsExist}
|
||||
indexPatternId={this.props.value ? this.props.value : ''}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
placeholder={getDataViewSelectPlaceholder()}
|
||||
onNoIndexPatterns={this._onNoIndexPatterns}
|
||||
isClearable={false}
|
||||
data-test-subj="mapGeoIndexPatternSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{_renderNoIndexPatternWarning()}
|
||||
|
||||
<EuiFormRow label={getDataViewLabel()} isInvalid={!hasGeoFields} error={error}>
|
||||
<IndexPatternSelect
|
||||
isInvalid={!hasGeoFields}
|
||||
isDisabled={noDataViews}
|
||||
indexPatternId={props.dataView?.id ? props.dataView.id : ''}
|
||||
onChange={_onIndexPatternSelect}
|
||||
placeholder={getDataViewSelectPlaceholder()}
|
||||
onNoIndexPatterns={() => {
|
||||
setNoDataViews(true);
|
||||
}}
|
||||
isClearable={false}
|
||||
data-test-subj="mapGeoIndexPatternSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ 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';
|
||||
import { inputStrings } from '../../connected_components/input_strings';
|
||||
|
||||
function filterFieldsForAgg(fields: DataViewField[], aggType: AGG_TYPE) {
|
||||
if (!fields) {
|
||||
|
@ -141,9 +142,7 @@ export function MetricEditor({
|
|||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.metricsEditor.selectFieldPlaceholder', {
|
||||
defaultMessage: 'Select field',
|
||||
})}
|
||||
placeholder={inputStrings.fieldSelectPlaceholder}
|
||||
value={metric.field ? metric.field : null}
|
||||
onChange={onFieldChange}
|
||||
fields={filterFieldsForAgg(fields, metric.type)}
|
||||
|
|
|
@ -44,7 +44,7 @@ function fieldsToOptions(
|
|||
});
|
||||
}
|
||||
|
||||
type Props = Omit<
|
||||
export type Props = Omit<
|
||||
EuiComboBoxProps<DataViewField>,
|
||||
'isDisabled' | 'onChange' | 'options' | 'renderOption' | 'selectedOptions' | 'singleSelection'
|
||||
> & {
|
||||
|
|
|
@ -94,12 +94,12 @@ exports[`EditLayerPanel is rendered 1`] = `
|
|||
"getId": [Function],
|
||||
"getImmutableSourceProperties": [Function],
|
||||
"getLayerTypeIconName": [Function],
|
||||
"getSource": [Function],
|
||||
"getStyleForEditing": [Function],
|
||||
"getType": [Function],
|
||||
"hasErrors": [Function],
|
||||
"hasJoins": [Function],
|
||||
"renderSourceSettingsEditor": [Function],
|
||||
"showJoinEditor": [Function],
|
||||
"supportsElasticsearchFilters": [Function],
|
||||
"supportsFitToBounds": [Function],
|
||||
}
|
||||
|
@ -119,12 +119,12 @@ exports[`EditLayerPanel is rendered 1`] = `
|
|||
"getId": [Function],
|
||||
"getImmutableSourceProperties": [Function],
|
||||
"getLayerTypeIconName": [Function],
|
||||
"getSource": [Function],
|
||||
"getStyleForEditing": [Function],
|
||||
"getType": [Function],
|
||||
"hasErrors": [Function],
|
||||
"hasJoins": [Function],
|
||||
"renderSourceSettingsEditor": [Function],
|
||||
"showJoinEditor": [Function],
|
||||
"supportsElasticsearchFilters": [Function],
|
||||
"supportsFitToBounds": [Function],
|
||||
}
|
||||
|
|
|
@ -65,9 +65,6 @@ const mockLayer = {
|
|||
getImmutableSourceProperties: () => {
|
||||
return [{ label: 'source prop1', value: 'you get one chance to set me' }];
|
||||
},
|
||||
showJoinEditor: () => {
|
||||
return true;
|
||||
},
|
||||
canShowTooltip: () => {
|
||||
return true;
|
||||
},
|
||||
|
@ -95,6 +92,13 @@ const mockLayer = {
|
|||
getStyleForEditing: () => {
|
||||
return {};
|
||||
},
|
||||
getSource: () => {
|
||||
return {
|
||||
supportsJoins: () => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
} as unknown as ILayer;
|
||||
|
||||
const defaultProps = {
|
||||
|
|
|
@ -36,6 +36,7 @@ import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer';
|
|||
import { ImmutableSourceProperty, OnSourceChangeArgs } from '../../classes/sources/source';
|
||||
import { IField } from '../../classes/fields/field';
|
||||
import { isLayerGroup } from '../../classes/layers/layer_group';
|
||||
import { isSpatialJoin } from '../../classes/joins/is_spatial_join';
|
||||
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
|
||||
|
@ -112,7 +113,7 @@ export class EditLayerPanel extends Component<Props, State> {
|
|||
}
|
||||
|
||||
const vectorLayer = this.props.selectedLayer as IVectorLayer;
|
||||
if (!vectorLayer.showJoinEditor() || vectorLayer.getLeftJoinFields === undefined) {
|
||||
if (!vectorLayer.getSource().supportsJoins() || vectorLayer.getLeftJoinFields === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -184,7 +185,7 @@ export class EditLayerPanel extends Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
const vectorLayer = this.props.selectedLayer as IVectorLayer;
|
||||
if (!vectorLayer.showJoinEditor()) {
|
||||
if (!vectorLayer.getSource().supportsJoins()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -244,12 +245,12 @@ export class EditLayerPanel extends Component<Props, State> {
|
|||
}
|
||||
|
||||
const descriptor = this.props.selectedLayer.getDescriptor() as VectorLayerDescriptor;
|
||||
const numberOfJoins = descriptor.joins ? descriptor.joins.length : 0;
|
||||
return isLayerGroup(this.props.selectedLayer)
|
||||
? null
|
||||
: this.props.selectedLayer.renderSourceSettingsEditor({
|
||||
currentLayerType: this.props.selectedLayer.getType(),
|
||||
numberOfJoins,
|
||||
hasSpatialJoins: (descriptor.joins ?? []).some(isSpatialJoin),
|
||||
numberOfJoins: descriptor.joins ? descriptor.joins.length : 0,
|
||||
onChange: this._onSourceChange,
|
||||
onStyleDescriptorChange: this.props.updateStyleDescriptor,
|
||||
style: this.props.selectedLayer.getStyleForEditing(),
|
||||
|
|
|
@ -21,7 +21,7 @@ function mapStateToProps(state: MapStoreState) {
|
|||
let key = 'none';
|
||||
if (selectedLayer) {
|
||||
key = isVectorLayer(selectedLayer)
|
||||
? `${selectedLayer.getId()}${(selectedLayer as IVectorLayer).showJoinEditor()}`
|
||||
? `${selectedLayer.getId()}${(selectedLayer as IVectorLayer).getSource().supportsJoins()}`
|
||||
: selectedLayer.getId();
|
||||
}
|
||||
return {
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Should disable add button when layer source is MVT and there is one join 1`] = `
|
||||
<EuiToolTip
|
||||
content="Vector tiles support one term join. To add multiple joins, select 'Limit results' in 'Scaling'."
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
aria-label="Add join"
|
||||
iconType="plusInCircleFilled"
|
||||
isDisabled={true}
|
||||
onClick={[Function]}
|
||||
size="xs"
|
||||
>
|
||||
Add join
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
`;
|
||||
|
||||
exports[`Should enable add button when layer source is MVT and there is no join 1`] = `
|
||||
<EuiButtonEmpty
|
||||
aria-label="Add join"
|
||||
iconType="plusInCircleFilled"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="xs"
|
||||
>
|
||||
Add join
|
||||
</EuiButtonEmpty>
|
||||
`;
|
||||
|
||||
exports[`Should render add join button 1`] = `
|
||||
<EuiButtonEmpty
|
||||
aria-label="Add join"
|
||||
iconType="plusInCircleFilled"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="xs"
|
||||
>
|
||||
Add join
|
||||
</EuiButtonEmpty>
|
||||
`;
|
|
@ -1,28 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Should render callout when joins are disabled 1`] = `
|
||||
<div>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
defaultMessage="Term joins"
|
||||
id="xpack.maps.layerPanel.joinEditor.termJoinsTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
||||
<JoinDocumentationPopover />
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
>
|
||||
Simulated disabled reason
|
||||
</EuiCallOut>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Should render join editor 1`] = `
|
||||
<div>
|
||||
<EuiTitle
|
||||
|
@ -30,17 +7,14 @@ exports[`Should render join editor 1`] = `
|
|||
>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
defaultMessage="Term joins"
|
||||
id="xpack.maps.layerPanel.joinEditor.termJoinsTitle"
|
||||
defaultMessage="Joins"
|
||||
id="xpack.maps.layerPanel.joinEditor.title"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
||||
<JoinDocumentationPopover />
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<Join
|
||||
join={
|
||||
Object {
|
||||
|
@ -55,30 +29,36 @@ exports[`Should render join editor 1`] = `
|
|||
},
|
||||
],
|
||||
"term": "geo.src",
|
||||
"type": "ES_TERM_SOURCE",
|
||||
},
|
||||
}
|
||||
}
|
||||
layer={
|
||||
MockLayer {
|
||||
"_disableReason": null,
|
||||
}
|
||||
}
|
||||
key="673ff994-fc75-4c67-909b-69fcb0e1060e"
|
||||
leftFields={Array []}
|
||||
leftSourceName="myLeftJoinField"
|
||||
onChange={[Function]}
|
||||
onRemove={[Function]}
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiTextAlign
|
||||
textAlign="center"
|
||||
<EuiSkeletonText
|
||||
isLoading={false}
|
||||
lines={1}
|
||||
>
|
||||
<AddJoinButton
|
||||
addJoin={[Function]}
|
||||
isLayerSourceMvt={false}
|
||||
numJoins={1}
|
||||
/>
|
||||
</EuiTextAlign>
|
||||
<EuiTextAlign
|
||||
textAlign="center"
|
||||
>
|
||||
<AddJoinButton
|
||||
disabledReason=""
|
||||
isDisabled={true}
|
||||
label="Add spatial join"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<AddJoinButton
|
||||
disabledReason="Vector tiles can only support a single join."
|
||||
isDisabled={false}
|
||||
label="Add term join"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</EuiTextAlign>
|
||||
</EuiSkeletonText>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AddJoinButton } from './add_join_button';
|
||||
|
||||
test('Should render add join button', () => {
|
||||
const component = shallow(
|
||||
<AddJoinButton addJoin={() => {}} isLayerSourceMvt={false} numJoins={0} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should enable add button when layer source is MVT and there is no join', () => {
|
||||
const component = shallow(
|
||||
<AddJoinButton addJoin={() => {}} isLayerSourceMvt={true} numJoins={0} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should disable add button when layer source is MVT and there is one join', () => {
|
||||
const component = shallow(
|
||||
<AddJoinButton addJoin={() => {}} isLayerSourceMvt={true} numJoins={1} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -5,42 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
const BUTTON_LABEL = i18n.translate('xpack.maps.layerPanel.joinEditor.addJoinButton.label', {
|
||||
defaultMessage: 'Add join',
|
||||
});
|
||||
|
||||
export interface Props {
|
||||
addJoin: () => void;
|
||||
isLayerSourceMvt: boolean;
|
||||
numJoins: number;
|
||||
disabledReason: string;
|
||||
isDisabled: boolean;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function AddJoinButton({ addJoin, isLayerSourceMvt, numJoins }: Props) {
|
||||
const isDisabled = isLayerSourceMvt && numJoins >= 1;
|
||||
export function AddJoinButton(props: Props) {
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
onClick={addJoin}
|
||||
onClick={props.onClick}
|
||||
size="xs"
|
||||
iconType="plusInCircleFilled"
|
||||
aria-label={BUTTON_LABEL}
|
||||
isDisabled={isDisabled}
|
||||
aria-label={props.label}
|
||||
isDisabled={props.isDisabled}
|
||||
>
|
||||
{BUTTON_LABEL}
|
||||
{props.label}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return isDisabled ? (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.maps.layerPanel.joinEditor.addJoinButton.mvtSingleJoinMsg', {
|
||||
defaultMessage: `Vector tiles support one term join. To add multiple joins, select 'Limit results' in 'Scaling'.`,
|
||||
})}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
return props.isDisabled ? (
|
||||
<EuiToolTip content={props.disabledReason}>{button}</EuiToolTip>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
|
|
|
@ -23,7 +23,7 @@ function mapStateToProps(state: MapStoreState) {
|
|||
|
||||
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
|
||||
return {
|
||||
onChange: (layer: ILayer, joins: JoinDescriptor[]) => {
|
||||
onChange: (layer: ILayer, joins: Array<Partial<JoinDescriptor>>) => {
|
||||
dispatch(setJoinsForLayer(layer, joins));
|
||||
},
|
||||
};
|
||||
|
|
|
@ -10,18 +10,9 @@ import { IVectorLayer } from '../../../classes/layers/vector_layer';
|
|||
import { JoinEditor } from './join_editor';
|
||||
import { shallow } from 'enzyme';
|
||||
import { JoinDescriptor } from '../../../../common/descriptor_types';
|
||||
import { SOURCE_TYPES } from '../../../../common/constants';
|
||||
|
||||
class MockLayer {
|
||||
private readonly _disableReason: string | null;
|
||||
|
||||
constructor(disableReason: string | null) {
|
||||
this._disableReason = disableReason;
|
||||
}
|
||||
|
||||
getJoinsDisabledReason() {
|
||||
return this._disableReason;
|
||||
}
|
||||
|
||||
getSource() {
|
||||
return {
|
||||
isMvt: () => {
|
||||
|
@ -45,6 +36,7 @@ const defaultProps = {
|
|||
label: 'web logs count',
|
||||
},
|
||||
],
|
||||
type: SOURCE_TYPES.ES_TERM_SOURCE,
|
||||
},
|
||||
} as JoinDescriptor,
|
||||
],
|
||||
|
@ -55,17 +47,7 @@ const defaultProps = {
|
|||
|
||||
test('Should render join editor', () => {
|
||||
const component = shallow(
|
||||
<JoinEditor {...defaultProps} layer={new MockLayer(null) as unknown as IVectorLayer} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should render callout when joins are disabled', () => {
|
||||
const component = shallow(
|
||||
<JoinEditor
|
||||
{...defaultProps}
|
||||
layer={new MockLayer('Simulated disabled reason') as unknown as IVectorLayer}
|
||||
/>
|
||||
<JoinEditor {...defaultProps} layer={new MockLayer() as unknown as IVectorLayer} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -5,17 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { EuiTitle, EuiSpacer, EuiTextAlign, EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSkeletonText, EuiTextAlign, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Join } from './resources/join';
|
||||
import { JoinDocumentationPopover } from './resources/join_documentation_popover';
|
||||
import { IVectorLayer } from '../../../classes/layers/vector_layer';
|
||||
import { JoinDescriptor } from '../../../../common/descriptor_types';
|
||||
import { SOURCE_TYPES } from '../../../../common/constants';
|
||||
import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
|
||||
import { AddJoinButton } from './add_join_button';
|
||||
|
||||
export interface JoinField {
|
||||
|
@ -24,17 +23,78 @@ export interface JoinField {
|
|||
}
|
||||
|
||||
export interface Props {
|
||||
joins: JoinDescriptor[];
|
||||
joins: Array<Partial<JoinDescriptor>>;
|
||||
layer: IVectorLayer;
|
||||
layerDisplayName: string;
|
||||
leftJoinFields: JoinField[];
|
||||
onChange: (layer: IVectorLayer, joins: JoinDescriptor[]) => void;
|
||||
onChange: (layer: IVectorLayer, joins: Array<Partial<JoinDescriptor>>) => void;
|
||||
}
|
||||
|
||||
export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }: Props) {
|
||||
const [supportsSpatialJoin, setSupportsSpatialJoin] = useState(false);
|
||||
const [spatialJoinDisableReason, setSpatialJoinDisableReason] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
const source = layer.getSource();
|
||||
if (!source.isESSource()) {
|
||||
setSpatialJoinDisableReason(
|
||||
i18n.translate('xpack.maps.layerPanel.joinEditor.spatialJoin.disabled.esSourceOnly', {
|
||||
defaultMessage: 'Spatial joins are not supported for {sourceType}.',
|
||||
values: { sourceType: source.getType() },
|
||||
})
|
||||
);
|
||||
setSupportsSpatialJoin(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.isMvt()) {
|
||||
setSpatialJoinDisableReason(
|
||||
i18n.translate('xpack.maps.layerPanel.joinEditor.spatialJoin.disabled.geoJsonOnly', {
|
||||
defaultMessage: 'Spatial joins are not supported with vector tiles.',
|
||||
})
|
||||
);
|
||||
setSupportsSpatialJoin(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO remove isPointsOnly check once non-point spatial joins have been implemented
|
||||
setIsLoading(true);
|
||||
source
|
||||
.getSupportedShapeTypes()
|
||||
.then((supportedShapes) => {
|
||||
if (!ignore) {
|
||||
const isPointsOnly =
|
||||
supportedShapes.length === 1 && supportedShapes[0] === VECTOR_SHAPE_TYPE.POINT;
|
||||
if (!isPointsOnly) {
|
||||
setSpatialJoinDisableReason(
|
||||
i18n.translate('xpack.maps.layerPanel.joinEditor.spatialJoin.disabled.pointsOnly', {
|
||||
defaultMessage: 'Spatial joins are not supported with geo_shape geometry.',
|
||||
})
|
||||
);
|
||||
setSupportsSpatialJoin(isPointsOnly);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSpatialJoinDisableReason('');
|
||||
setSupportsSpatialJoin(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// keep spatial joins disabled when unable to verify if they are supported
|
||||
});
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [layer]);
|
||||
|
||||
const renderJoins = () => {
|
||||
return joins.map((joinDescriptor: JoinDescriptor, index: number) => {
|
||||
const handleOnChange = (updatedDescriptor: JoinDescriptor) => {
|
||||
return joins.map((joinDescriptor: Partial<JoinDescriptor>, index: number) => {
|
||||
const handleOnChange = (updatedDescriptor: Partial<JoinDescriptor>) => {
|
||||
onChange(layer, [...joins.slice(0, index), updatedDescriptor, ...joins.slice(index + 1)]);
|
||||
};
|
||||
|
||||
|
@ -42,74 +102,86 @@ export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDispla
|
|||
onChange(layer, [...joins.slice(0, index), ...joins.slice(index + 1)]);
|
||||
};
|
||||
|
||||
if (joinDescriptor.right.type === SOURCE_TYPES.TABLE_SOURCE) {
|
||||
throw new Error(
|
||||
'PEBKAC - Table sources cannot be edited in the UX and should only be used in MapEmbeddable'
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<EuiSpacer size="m" />
|
||||
<Join
|
||||
join={joinDescriptor}
|
||||
layer={layer}
|
||||
onChange={handleOnChange}
|
||||
onRemove={handleOnRemove}
|
||||
leftFields={leftJoinFields}
|
||||
leftSourceName={layerDisplayName}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Join
|
||||
key={joinDescriptor?.right?.id ?? index}
|
||||
join={joinDescriptor}
|
||||
onChange={handleOnChange}
|
||||
onRemove={handleOnRemove}
|
||||
leftFields={leftJoinFields}
|
||||
leftSourceName={layerDisplayName}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const addJoin = () => {
|
||||
const addJoin = (joinDescriptor: Partial<JoinDescriptor>) => {
|
||||
onChange(layer, [
|
||||
...joins,
|
||||
{
|
||||
...joinDescriptor,
|
||||
right: {
|
||||
id: uuidv4(),
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
...(joinDescriptor?.right ?? {}),
|
||||
},
|
||||
} as JoinDescriptor,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
function renderContent() {
|
||||
const disabledReason = layer.getJoinsDisabledReason();
|
||||
|
||||
return disabledReason ? (
|
||||
<EuiCallOut color="warning">{disabledReason}</EuiCallOut>
|
||||
) : (
|
||||
<Fragment>
|
||||
{renderJoins()}
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTextAlign textAlign="center">
|
||||
<AddJoinButton
|
||||
addJoin={addJoin}
|
||||
isLayerSourceMvt={layer.getSource().isMvt()}
|
||||
numJoins={joins.length}
|
||||
/>
|
||||
</EuiTextAlign>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.joinEditor.termJoinsTitle"
|
||||
defaultMessage="Term joins"
|
||||
/>{' '}
|
||||
<FormattedMessage id="xpack.maps.layerPanel.joinEditor.title" defaultMessage="Joins" />{' '}
|
||||
<JoinDocumentationPopover />
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
{renderContent()}
|
||||
{renderJoins()}
|
||||
|
||||
<EuiSkeletonText lines={1} isLoading={isLoading}>
|
||||
<EuiTextAlign textAlign="center">
|
||||
<AddJoinButton
|
||||
disabledReason={spatialJoinDisableReason}
|
||||
isDisabled={!supportsSpatialJoin}
|
||||
label={i18n.translate('xpack.maps.layerPanel.joinEditor.spatialJoin.addButtonLabel', {
|
||||
defaultMessage: 'Add spatial join',
|
||||
})}
|
||||
onClick={() => {
|
||||
addJoin({
|
||||
leftField: '_id',
|
||||
right: {
|
||||
type: SOURCE_TYPES.ES_DISTANCE_SOURCE,
|
||||
id: uuidv4(),
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
},
|
||||
} as Partial<JoinDescriptor>);
|
||||
}}
|
||||
/>
|
||||
<AddJoinButton
|
||||
disabledReason={i18n.translate(
|
||||
'xpack.maps.layerPanel.joinEditor.termJoin.mvtSingleJoinMsg',
|
||||
{
|
||||
defaultMessage: 'Vector tiles can only support a single join.',
|
||||
}
|
||||
)}
|
||||
isDisabled={layer.getSource().isMvt() && joins.length >= 1}
|
||||
label={i18n.translate('xpack.maps.layerPanel.joinEditor.termJoin.addButtonLabel', {
|
||||
defaultMessage: 'Add term join',
|
||||
})}
|
||||
onClick={() => {
|
||||
addJoin({
|
||||
right: {
|
||||
type: SOURCE_TYPES.ES_TERM_SOURCE,
|
||||
id: uuidv4(),
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
},
|
||||
} as Partial<JoinDescriptor>);
|
||||
}}
|
||||
/>
|
||||
</EuiTextAlign>
|
||||
</EuiSkeletonText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ exports[`Should render default props 1`] = `
|
|||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="m"
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
|
@ -29,7 +30,7 @@ exports[`Should render default props 1`] = `
|
|||
>
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Metrics"
|
||||
defaultMessage="Configure join metrics"
|
||||
id="xpack.maps.layerPanel.metricsExpression.metricsPopoverTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
@ -38,7 +39,7 @@ exports[`Should render default props 1`] = `
|
|||
className="mapJoinExpressionHelpText"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Configure the metrics for the right source. These values are added to the layer features."
|
||||
defaultMessage="Metrics are added to layer features and used for data driven styling and tooltip content."
|
||||
id="xpack.maps.layerPanel.metricsExpression.helpText"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
@ -79,6 +80,7 @@ exports[`Should render metrics expression for metrics 1`] = `
|
|||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="m"
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
|
@ -89,7 +91,7 @@ exports[`Should render metrics expression for metrics 1`] = `
|
|||
>
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Metrics"
|
||||
defaultMessage="Configure join metrics"
|
||||
id="xpack.maps.layerPanel.metricsExpression.metricsPopoverTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
@ -98,7 +100,7 @@ exports[`Should render metrics expression for metrics 1`] = `
|
|||
className="mapJoinExpressionHelpText"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Configure the metrics for the right source. These values are added to the layer features."
|
||||
defaultMessage="Metrics are added to layer features and used for data driven styling and tooltip content."
|
||||
id="xpack.maps.layerPanel.metricsExpression.helpText"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
background: tintOrShade($euiColorLightShade, 85%, 0);
|
||||
border-radius: $euiBorderRadius;
|
||||
padding: $euiSizeS $euiSizeXS;
|
||||
margin-bottom: $euiSizeM;
|
||||
|
||||
.mapJoinItem__inner {
|
||||
@include euiScrollBar;
|
||||
|
|
|
@ -7,31 +7,38 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui';
|
||||
import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiText, EuiTextColor } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataViewField, DataView, Query } from '@kbn/data-plugin/common';
|
||||
import { indexPatterns } from '@kbn/data-plugin/public';
|
||||
import { JoinExpression } from './join_expression';
|
||||
import { SpatialJoinExpression } from './spatial_join_expression';
|
||||
import { TermJoinExpression } from './term_join_expression';
|
||||
import { MetricsExpression } from './metrics_expression';
|
||||
import { WhereExpression } from './where_expression';
|
||||
import { GlobalFilterCheckbox } from '../../../../components/global_filter_checkbox';
|
||||
import { GlobalTimeCheckbox } from '../../../../components/global_time_checkbox';
|
||||
import {
|
||||
AbstractESJoinSourceDescriptor,
|
||||
AggDescriptor,
|
||||
ESDistanceSourceDescriptor,
|
||||
ESTermSourceDescriptor,
|
||||
JoinDescriptor,
|
||||
JoinSourceDescriptor,
|
||||
} from '../../../../../common/descriptor_types';
|
||||
import { ILayer } from '../../../../classes/layers/layer';
|
||||
|
||||
import { getIndexPatternService } from '../../../../kibana_services';
|
||||
import { getDataViewNotFoundMessage } from '../../../../../common/i18n_getters';
|
||||
import { AGG_TYPE, SOURCE_TYPES } from '../../../../../common/constants';
|
||||
import type { JoinField } from '../join_editor';
|
||||
import { isSpatialJoin } from '../../../../classes/joins/is_spatial_join';
|
||||
import {
|
||||
isSpatialSourceComplete,
|
||||
isTermSourceComplete,
|
||||
} from '../../../../classes/sources/join_sources';
|
||||
|
||||
interface Props {
|
||||
join: JoinDescriptor;
|
||||
layer: ILayer;
|
||||
onChange: (joinDescriptor: JoinDescriptor) => void;
|
||||
join: Partial<JoinDescriptor>;
|
||||
onChange: (joinDescriptor: Partial<JoinDescriptor>) => void;
|
||||
onRemove: () => void;
|
||||
leftFields: JoinField[];
|
||||
leftSourceName: string;
|
||||
|
@ -45,6 +52,7 @@ interface State {
|
|||
|
||||
export class Join extends Component<Props, State> {
|
||||
private _isMounted = false;
|
||||
private _nextIndexPatternId: string | undefined;
|
||||
|
||||
state: State = {
|
||||
rightFields: [],
|
||||
|
@ -66,6 +74,7 @@ export class Join extends Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
this._nextIndexPatternId = indexPatternId;
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await getIndexPatternService().get(indexPatternId);
|
||||
|
@ -78,7 +87,7 @@ export class Join extends Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!this._isMounted) {
|
||||
if (!this._isMounted || this._nextIndexPatternId !== indexPatternId) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -95,40 +104,25 @@ export class Join extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
_onRightSourceChange = (indexPatternId: string) => {
|
||||
this.setState({
|
||||
rightFields: [],
|
||||
loadError: undefined,
|
||||
});
|
||||
this._loadRightFields(indexPatternId);
|
||||
const { term, ...restOfRight } = this.props.join.right as ESTermSourceDescriptor;
|
||||
this.props.onChange({
|
||||
leftField: this.props.join.leftField,
|
||||
right: {
|
||||
...restOfRight,
|
||||
indexPatternId,
|
||||
type: SOURCE_TYPES.ES_TERM_SOURCE,
|
||||
} as ESTermSourceDescriptor,
|
||||
});
|
||||
};
|
||||
_onRightSourceDescriptorChange = (sourceDescriptor: Partial<JoinSourceDescriptor>) => {
|
||||
const indexPatternId = (sourceDescriptor as Partial<AbstractESJoinSourceDescriptor>)
|
||||
.indexPatternId;
|
||||
if (this.state.indexPattern?.id !== indexPatternId) {
|
||||
this.setState({
|
||||
indexPattern: undefined,
|
||||
rightFields: [],
|
||||
loadError: undefined,
|
||||
});
|
||||
if (indexPatternId) {
|
||||
this._loadRightFields(indexPatternId);
|
||||
}
|
||||
}
|
||||
|
||||
_onRightFieldChange = (term?: string) => {
|
||||
this.props.onChange({
|
||||
leftField: this.props.join.leftField,
|
||||
right: {
|
||||
...this.props.join.right,
|
||||
term,
|
||||
} as ESTermSourceDescriptor,
|
||||
});
|
||||
};
|
||||
|
||||
_onRightSizeChange = (size: number) => {
|
||||
this.props.onChange({
|
||||
leftField: this.props.join.leftField,
|
||||
right: {
|
||||
...this.props.join.right,
|
||||
size,
|
||||
} as ESTermSourceDescriptor,
|
||||
...sourceDescriptor,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -138,7 +132,7 @@ export class Join extends Component<Props, State> {
|
|||
right: {
|
||||
...this.props.join.right,
|
||||
metrics,
|
||||
} as ESTermSourceDescriptor,
|
||||
} as Partial<JoinSourceDescriptor>,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -148,7 +142,7 @@ export class Join extends Component<Props, State> {
|
|||
right: {
|
||||
...this.props.join.right,
|
||||
whereQuery,
|
||||
} as ESTermSourceDescriptor,
|
||||
} as Partial<JoinSourceDescriptor>,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -158,7 +152,7 @@ export class Join extends Component<Props, State> {
|
|||
right: {
|
||||
...this.props.join.right,
|
||||
applyGlobalQuery,
|
||||
} as ESTermSourceDescriptor,
|
||||
} as Partial<JoinSourceDescriptor>,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -168,20 +162,61 @@ export class Join extends Component<Props, State> {
|
|||
right: {
|
||||
...this.props.join.right,
|
||||
applyGlobalTime,
|
||||
} as ESTermSourceDescriptor,
|
||||
} as Partial<JoinSourceDescriptor>,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { join, onRemove, leftFields, leftSourceName } = this.props;
|
||||
const { rightFields, indexPattern } = this.state;
|
||||
const right = _.get(join, 'right', {}) as ESTermSourceDescriptor;
|
||||
const rightSourceName = indexPattern ? indexPattern.getName() : right.indexPatternId;
|
||||
const isJoinConfigComplete = join.leftField && right.indexPatternId && right.term;
|
||||
const right = (join?.right ?? {}) as Partial<AbstractESJoinSourceDescriptor>;
|
||||
|
||||
let isJoinConfigComplete = false;
|
||||
let joinExpression;
|
||||
if (right.type === SOURCE_TYPES.ES_TERM_SOURCE) {
|
||||
isJoinConfigComplete =
|
||||
join.leftField !== undefined &&
|
||||
isTermSourceComplete(right as Partial<ESTermSourceDescriptor>);
|
||||
joinExpression = (
|
||||
<TermJoinExpression
|
||||
leftSourceName={leftSourceName}
|
||||
leftValue={join.leftField}
|
||||
leftFields={leftFields}
|
||||
onLeftFieldChange={this._onLeftFieldChange}
|
||||
sourceDescriptor={right as Partial<ESTermSourceDescriptor>}
|
||||
onSourceDescriptorChange={this._onRightSourceDescriptorChange}
|
||||
rightFields={rightFields}
|
||||
/>
|
||||
);
|
||||
} else if (isSpatialJoin(this.props.join)) {
|
||||
isJoinConfigComplete =
|
||||
join.leftField !== undefined &&
|
||||
isSpatialSourceComplete(right as Partial<ESDistanceSourceDescriptor>);
|
||||
joinExpression = (
|
||||
<SpatialJoinExpression
|
||||
sourceDescriptor={right as Partial<ESDistanceSourceDescriptor>}
|
||||
onSourceDescriptorChange={this._onRightSourceDescriptorChange}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
joinExpression = (
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<EuiTextColor color="warning">
|
||||
{i18n.translate('xpack.maps.layerPanel.join.joinExpression.noEditorMsg', {
|
||||
defaultMessage: 'Unable to edit {type} join.',
|
||||
values: { type: right.type },
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
let metricsExpression;
|
||||
let globalFilterCheckbox;
|
||||
let globalTimeCheckbox;
|
||||
|
||||
if (isJoinConfigComplete) {
|
||||
metricsExpression = (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -234,22 +269,7 @@ export class Join extends Component<Props, State> {
|
|||
return (
|
||||
<div className="mapJoinItem">
|
||||
<EuiFlexGroup className="mapJoinItem__inner" responsive={false} wrap={true} gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<JoinExpression
|
||||
leftSourceName={leftSourceName}
|
||||
leftValue={join.leftField}
|
||||
leftFields={leftFields}
|
||||
onLeftFieldChange={this._onLeftFieldChange}
|
||||
rightSourceIndexPatternId={right.indexPatternId}
|
||||
rightSourceName={rightSourceName}
|
||||
onRightSourceChange={this._onRightSourceChange}
|
||||
rightValue={right.term}
|
||||
rightSize={right.size}
|
||||
rightFields={rightFields}
|
||||
onRightFieldChange={this._onRightFieldChange}
|
||||
onRightSizeChange={this._onRightSizeChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{joinExpression}</EuiFlexItem>
|
||||
|
||||
{metricsExpression}
|
||||
|
||||
|
|
|
@ -5,110 +5,91 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiButtonIcon, EuiLink, EuiPopover, EuiPopoverTitle, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { getDocLinks } from '../../../../kibana_services';
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
export function JoinDocumentationPopover() {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
export class JoinDocumentationPopover extends Component<{}, State> {
|
||||
state: State = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
_renderContent() {
|
||||
return (
|
||||
return (
|
||||
<EuiPopover
|
||||
id="joinHelpPopover"
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
iconType="documentation"
|
||||
aria-label="Join documentation"
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => {
|
||||
setIsPopoverOpen(false);
|
||||
}}
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage id="xpack.maps.layerPanel.joinEditor.title" defaultMessage="Joins" />
|
||||
</EuiPopoverTitle>
|
||||
<div>
|
||||
<EuiText style={{ maxWidth: '36em' }}>
|
||||
<EuiText size="s" style={{ maxWidth: '36em' }}>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.intro"
|
||||
defaultMessage="Term joins augment layers with properties for data driven styling. Term joins work as follows:"
|
||||
/>
|
||||
defaultMessage="Joins add metrics to layer features for data driven styling and tooltip content."
|
||||
/>{' '}
|
||||
<i>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.noMatches"
|
||||
defaultMessage="Layer features that do have a matches are not visible on the map."
|
||||
/>
|
||||
</i>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<dl>
|
||||
<dt>
|
||||
<FormattedMessage id="xpack.maps.joinDocs.termJoinTitle" defaultMessage="Term join" />
|
||||
</dt>
|
||||
<dd>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.termsJoinIntro"
|
||||
defaultMessage="A term join uses a shared key to combine layer features with metrics from an Elasticsearch terms aggregation."
|
||||
/>
|
||||
</p>
|
||||
<EuiLink
|
||||
href={getDocLinks().links.maps.termJoinsExample}
|
||||
target="_blank"
|
||||
external={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.linkLabel"
|
||||
defaultMessage="Term join example"
|
||||
/>
|
||||
</EuiLink>
|
||||
</dd>
|
||||
|
||||
<dt>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.sharedKey"
|
||||
defaultMessage="A shared key combines vector features, the left source, with the results of an Elasticsearch aggregation, the right source."
|
||||
id="xpack.maps.joinDocs.spatialJoinTitle"
|
||||
defaultMessage="Spatial join"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.termsAggregation"
|
||||
defaultMessage="The terms aggregation creates a bucket for each unique shared key."
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.metrics"
|
||||
defaultMessage="Metrics are calculated for all documents in a bucket."
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.join"
|
||||
defaultMessage="The join adds metrics for each terms aggregation bucket with the corresponding shared key."
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.noMatches"
|
||||
defaultMessage="Features that do have have a corresponding terms aggregation bucket are not visible on the map."
|
||||
/>
|
||||
</p>
|
||||
<EuiLink href={getDocLinks().links.maps.termJoinsExample} target="_blank" external={true}>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.linkLabel"
|
||||
defaultMessage="Term join example"
|
||||
/>
|
||||
</EuiLink>
|
||||
</dt>
|
||||
<dd>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.joinDocs.spatialJoinIntro"
|
||||
defaultMessage="A spatial join uses a geospatial relationship to combine layer features with metrics from an Elasticsearch geo query filters aggregation."
|
||||
/>
|
||||
</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPopover
|
||||
id="joinHelpPopover"
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
onClick={this._togglePopover}
|
||||
iconType="documentation"
|
||||
aria-label="Join documentation"
|
||||
/>
|
||||
}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.joinEditor.termJoinsTitle"
|
||||
defaultMessage="Term joins"
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
{this._renderContent()}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,297 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiExpression,
|
||||
EuiFormRow,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFormHelpText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { getDataViewSelectPlaceholder } from '../../../../../common/i18n_getters';
|
||||
import { DEFAULT_MAX_BUCKETS_LIMIT } from '../../../../../common/constants';
|
||||
import { SingleFieldSelect } from '../../../../components/single_field_select';
|
||||
import { ValidatedNumberInput } from '../../../../components/validated_number_input';
|
||||
|
||||
import { getTermsFields } from '../../../../index_pattern_util';
|
||||
import { getIndexPatternSelectComponent } from '../../../../kibana_services';
|
||||
import type { JoinField } from '../join_editor';
|
||||
|
||||
interface Props {
|
||||
// Left source props (static - can not change)
|
||||
leftSourceName?: string;
|
||||
|
||||
// Left field props
|
||||
leftValue?: string;
|
||||
leftFields: JoinField[];
|
||||
onLeftFieldChange: (leftField: string) => void;
|
||||
|
||||
// Right source props
|
||||
rightSourceIndexPatternId: string;
|
||||
rightSourceName: string;
|
||||
onRightSourceChange: (indexPatternId: string) => void;
|
||||
|
||||
// Right field props
|
||||
rightValue: string;
|
||||
rightSize?: number;
|
||||
rightFields: DataViewField[];
|
||||
onRightFieldChange: (term?: string) => void;
|
||||
onRightSizeChange: (size: number) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
export class JoinExpression extends Component<Props, State> {
|
||||
state: State = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
_onRightSourceChange = (indexPatternId?: string) => {
|
||||
if (!indexPatternId || indexPatternId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onRightSourceChange(indexPatternId);
|
||||
};
|
||||
|
||||
_onLeftFieldChange = (selectedFields: Array<EuiComboBoxOptionOption<JoinField>>) => {
|
||||
this.props.onLeftFieldChange(_.get(selectedFields, '[0].value.name', null));
|
||||
};
|
||||
|
||||
_onRightFieldChange = (term?: string) => {
|
||||
if (!term || term.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onRightFieldChange(term);
|
||||
};
|
||||
|
||||
_renderLeftFieldSelect() {
|
||||
const { leftValue, leftFields } = this.props;
|
||||
|
||||
if (!leftFields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = leftFields.map((field) => {
|
||||
return {
|
||||
value: field,
|
||||
label: field.label,
|
||||
};
|
||||
});
|
||||
|
||||
let leftFieldOption;
|
||||
if (leftValue) {
|
||||
leftFieldOption = options.find((option) => {
|
||||
const field = option.value;
|
||||
return field.name === leftValue;
|
||||
});
|
||||
}
|
||||
const selectedOptions = leftFieldOption ? [leftFieldOption] : [];
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.layerPanel.joinExpression.leftFieldLabel', {
|
||||
defaultMessage: 'Left field',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.maps.layerPanel.joinExpression.leftSourceLabelHelpText', {
|
||||
defaultMessage: 'Left source field that contains the shared key.',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
placeholder={getSelectFieldPlaceholder()}
|
||||
singleSelection={true}
|
||||
isClearable={false}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={this._onLeftFieldChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
_renderRightSourceSelect() {
|
||||
if (!this.props.leftValue) {
|
||||
return null;
|
||||
}
|
||||
const IndexPatternSelect = getIndexPatternSelectComponent();
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.layerPanel.joinExpression.rightSourceLabel', {
|
||||
defaultMessage: 'Right source',
|
||||
})}
|
||||
>
|
||||
<IndexPatternSelect
|
||||
placeholder={getDataViewSelectPlaceholder()}
|
||||
indexPatternId={this.props.rightSourceIndexPatternId}
|
||||
onChange={this._onRightSourceChange}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
_renderRightFieldSelect() {
|
||||
if (!this.props.rightFields || !this.props.leftValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.layerPanel.joinExpression.rightFieldLabel', {
|
||||
defaultMessage: 'Right field',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.maps.layerPanel.joinExpression.rightSourceLabelHelpText', {
|
||||
defaultMessage: 'Right source field that contains the shared key.',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={getSelectFieldPlaceholder()}
|
||||
value={this.props.rightValue}
|
||||
onChange={this._onRightFieldChange}
|
||||
fields={getTermsFields(this.props.rightFields)}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
_renderRightFieldSizeInput() {
|
||||
if (!this.props.rightValue || !this.props.leftValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ValidatedNumberInput
|
||||
initialValue={
|
||||
this.props.rightSize !== undefined ? this.props.rightSize : DEFAULT_MAX_BUCKETS_LIMIT
|
||||
}
|
||||
min={1}
|
||||
max={DEFAULT_MAX_BUCKETS_LIMIT}
|
||||
onChange={this.props.onRightSizeChange}
|
||||
label={i18n.translate('xpack.maps.layerPanel.joinExpression.rightSizeLabel', {
|
||||
defaultMessage: 'Right size',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.maps.layerPanel.joinExpression.rightSizeHelpText', {
|
||||
defaultMessage: 'Right field term limit.',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_getExpressionValue() {
|
||||
const { leftSourceName, leftValue, rightSourceName, rightValue, rightSize } = this.props;
|
||||
if (leftSourceName && leftValue && rightSourceName && rightValue) {
|
||||
return i18n.translate('xpack.maps.layerPanel.joinExpression.value', {
|
||||
defaultMessage:
|
||||
'{leftSourceName}:{leftValue} with {sizeFragment} {rightSourceName}:{rightValue}',
|
||||
values: {
|
||||
leftSourceName,
|
||||
leftValue,
|
||||
sizeFragment:
|
||||
rightSize !== undefined
|
||||
? i18n.translate('xpack.maps.layerPanel.joinExpression.sizeFragment', {
|
||||
defaultMessage: 'top {rightSize} terms from',
|
||||
values: { rightSize },
|
||||
})
|
||||
: '',
|
||||
rightSourceName,
|
||||
rightValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return i18n.translate('xpack.maps.layerPanel.joinExpression.selectPlaceholder', {
|
||||
defaultMessage: '-- select --',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { leftSourceName } = this.props;
|
||||
return (
|
||||
<EuiPopover
|
||||
id="joinPopover"
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
ownFocus
|
||||
initialFocus="body" /* avoid initialFocus on Combobox */
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiExpression
|
||||
onClick={this._togglePopover}
|
||||
description="Join"
|
||||
uppercase={false}
|
||||
value={this._getExpressionValue()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div style={{ width: 300 }}>
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.joinExpression.joinPopoverTitle"
|
||||
defaultMessage="Join"
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
<EuiFormHelpText className="mapJoinExpressionHelpText">
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.joinExpression.helpText"
|
||||
defaultMessage="Configure the shared key."
|
||||
/>
|
||||
</EuiFormHelpText>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.layerPanel.joinExpression.leftSourceLabel', {
|
||||
defaultMessage: 'Left source',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
selectedOptions={
|
||||
leftSourceName ? [{ value: leftSourceName, label: leftSourceName }] : []
|
||||
}
|
||||
isDisabled
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{this._renderLeftFieldSelect()}
|
||||
|
||||
{this._renderRightSourceSelect()}
|
||||
|
||||
{this._renderRightFieldSelect()}
|
||||
|
||||
{this._renderRightFieldSizeInput()}
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectFieldPlaceholder() {
|
||||
return i18n.translate('xpack.maps.layerPanel.joinExpression.selectFieldPlaceholder', {
|
||||
defaultMessage: 'Select field',
|
||||
});
|
||||
}
|
|
@ -116,18 +116,19 @@ export class MetricsExpression extends Component<Props, State> {
|
|||
value={metricExpressions.length > 0 ? metricExpressions.join(', ') : AGG_TYPE.COUNT}
|
||||
/>
|
||||
}
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<div style={{ width: 400 }}>
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.metricsExpression.metricsPopoverTitle"
|
||||
defaultMessage="Metrics"
|
||||
defaultMessage="Configure join metrics"
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
<EuiFormHelpText className="mapJoinExpressionHelpText">
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.metricsExpression.helpText"
|
||||
defaultMessage="Configure the metrics for the right source. These values are added to the layer features."
|
||||
defaultMessage="Metrics are added to layer features and used for data driven styling and tooltip content."
|
||||
/>
|
||||
</EuiFormHelpText>
|
||||
{this._renderMetricsEditor()}
|
||||
|
|
|
@ -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 { SpatialJoinExpression } from './spatial_join_expression';
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { EuiPopover, EuiExpression } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ESDistanceSourceDescriptor,
|
||||
JoinSourceDescriptor,
|
||||
} from '../../../../../../common/descriptor_types';
|
||||
import { SpatialJoinPopoverContent } from './spatial_join_popover_content';
|
||||
|
||||
interface Props {
|
||||
sourceDescriptor: Partial<ESDistanceSourceDescriptor>;
|
||||
onSourceDescriptorChange: (sourceDescriptor: Partial<JoinSourceDescriptor>) => void;
|
||||
}
|
||||
|
||||
export function SpatialJoinExpression(props: Props) {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const { geoField } = props.sourceDescriptor;
|
||||
const expressionValue =
|
||||
geoField !== undefined
|
||||
? i18n.translate('xpack.maps.spatialJoinExpression.value', {
|
||||
defaultMessage: 'features from {geoField}',
|
||||
values: { geoField },
|
||||
})
|
||||
: i18n.translate('xpack.maps.spatialJoinExpression.emptyValue', {
|
||||
defaultMessage: '-- configure spatial join --',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id={props.sourceDescriptor.id}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => {
|
||||
setIsPopoverOpen(false);
|
||||
}}
|
||||
ownFocus
|
||||
initialFocus="body" /* avoid initialFocus on Combobox */
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiExpression
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
description={i18n.translate('xpack.maps.spatialJoinExpression.description', {
|
||||
defaultMessage: 'Join with',
|
||||
})}
|
||||
uppercase={false}
|
||||
value={expressionValue}
|
||||
/>
|
||||
}
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<SpatialJoinPopoverContent
|
||||
sourceDescriptor={props.sourceDescriptor}
|
||||
onSourceDescriptorChange={props.onSourceDescriptorChange}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { EuiCallOut, EuiFormRow, EuiPopoverTitle, EuiSkeletonText, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView, DataViewField } from '@kbn/data-plugin/common';
|
||||
import type {
|
||||
ESDistanceSourceDescriptor,
|
||||
JoinSourceDescriptor,
|
||||
} from '../../../../../../common/descriptor_types';
|
||||
import { getIndexPatternService } from '../../../../../kibana_services';
|
||||
import { getGeoFields } from '../../../../../index_pattern_util';
|
||||
import { GeoIndexPatternSelect } from '../../../../../components/geo_index_pattern_select';
|
||||
import { GeoFieldSelect } from '../../../../../components/geo_field_select';
|
||||
import { inputStrings } from '../../../../input_strings';
|
||||
import { RelationshipExpression } from '../../../../../classes/layers/wizards/spatial_join_wizard';
|
||||
import { DEFAULT_WITHIN_DISTANCE } from '../../../../../classes/sources/join_sources';
|
||||
|
||||
interface Props {
|
||||
sourceDescriptor: Partial<ESDistanceSourceDescriptor>;
|
||||
onSourceDescriptorChange: (sourceDescriptor: Partial<JoinSourceDescriptor>) => void;
|
||||
}
|
||||
|
||||
export function SpatialJoinPopoverContent(props: Props) {
|
||||
const [rightDataView, setRightDataView] = useState<DataView | undefined>(undefined);
|
||||
const [rightGeoFields, setRightGeoFields] = useState<DataViewField[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [unableToLoadDataView, setUnableToLoadDataView] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.sourceDescriptor.indexPatternId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ignore = false;
|
||||
setIsLoading(true);
|
||||
getIndexPatternService()
|
||||
.get(props.sourceDescriptor.indexPatternId)
|
||||
.then((dataView) => {
|
||||
if (!ignore) {
|
||||
setIsLoading(false);
|
||||
setRightDataView(dataView);
|
||||
setRightGeoFields(getGeoFields(dataView.fields));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!ignore) {
|
||||
setIsLoading(false);
|
||||
setUnableToLoadDataView(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
// only run on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const dataViewCallout = unableToLoadDataView ? (
|
||||
<>
|
||||
<EuiCallOut color="warning">
|
||||
<p>
|
||||
{i18n.translate('xpack.maps.spatialJoinExpression.noDataViewTitle', {
|
||||
defaultMessage: 'Unable to load data view {dataViewId}.',
|
||||
values: { dataViewId: props.sourceDescriptor.indexPatternId },
|
||||
})}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
) : null;
|
||||
|
||||
const geoFieldSelect = rightDataView ? (
|
||||
<GeoFieldSelect
|
||||
value={props.sourceDescriptor.geoField ? props.sourceDescriptor.geoField : ''}
|
||||
onChange={(geoField?: string) => {
|
||||
if (!geoField) {
|
||||
return;
|
||||
}
|
||||
props.onSourceDescriptorChange({
|
||||
...props.sourceDescriptor,
|
||||
geoField,
|
||||
});
|
||||
}}
|
||||
geoFields={rightGeoFields}
|
||||
isClearable={false}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div style={{ width: 300 }}>
|
||||
<EuiPopoverTitle>
|
||||
{i18n.translate('xpack.maps.spatialJoinExpression.popoverTitle', {
|
||||
defaultMessage: 'Configure spatial join',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
|
||||
<EuiSkeletonText lines={3} isLoading={isLoading}>
|
||||
<EuiFormRow label={inputStrings.relationshipLabel}>
|
||||
<RelationshipExpression
|
||||
distance={
|
||||
typeof props.sourceDescriptor.distance === 'number'
|
||||
? props.sourceDescriptor.distance
|
||||
: DEFAULT_WITHIN_DISTANCE
|
||||
}
|
||||
onDistanceChange={(distance: number) => {
|
||||
props.onSourceDescriptorChange({
|
||||
...props.sourceDescriptor,
|
||||
distance,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{dataViewCallout}
|
||||
|
||||
<GeoIndexPatternSelect
|
||||
dataView={rightDataView}
|
||||
onChange={(dataView: DataView) => {
|
||||
setUnableToLoadDataView(false);
|
||||
setRightDataView(dataView);
|
||||
const geoFields = getGeoFields(dataView.fields);
|
||||
setRightGeoFields(geoFields);
|
||||
props.onSourceDescriptorChange({
|
||||
...props.sourceDescriptor,
|
||||
indexPatternId: dataView.id,
|
||||
geoField: geoFields.length ? geoFields[0].name : undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{geoFieldSelect}
|
||||
</EuiSkeletonText>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 { TermJoinExpression } from './term_join_expression';
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { EuiPopover, EuiExpression } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
ESTermSourceDescriptor,
|
||||
JoinSourceDescriptor,
|
||||
} from '../../../../../../common/descriptor_types';
|
||||
import type { JoinField } from '../../join_editor';
|
||||
import { TermJoinPopoverContent } from './term_join_popover_content';
|
||||
|
||||
interface Props {
|
||||
// Left source props (static - can not change)
|
||||
leftSourceName?: string;
|
||||
|
||||
// Left field props
|
||||
leftValue?: string;
|
||||
leftFields: JoinField[];
|
||||
onLeftFieldChange: (leftField: string) => void;
|
||||
|
||||
// Right source props
|
||||
sourceDescriptor: Partial<ESTermSourceDescriptor>;
|
||||
onSourceDescriptorChange: (sourceDescriptor: Partial<JoinSourceDescriptor>) => void;
|
||||
rightFields: DataViewField[];
|
||||
}
|
||||
|
||||
export function TermJoinExpression(props: Props) {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const { size, term } = props.sourceDescriptor;
|
||||
const expressionValue =
|
||||
term !== undefined
|
||||
? i18n.translate('xpack.maps.termJoinExpression.value', {
|
||||
defaultMessage: '{topTerms} terms from {term}',
|
||||
values: {
|
||||
topTerms:
|
||||
size !== undefined
|
||||
? i18n.translate('xpack.maps.termJoinExpression.topTerms', {
|
||||
defaultMessage: 'top {size}',
|
||||
values: { size },
|
||||
})
|
||||
: '',
|
||||
term,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.maps.termJoinExpression.placeholder', {
|
||||
defaultMessage: '-- configure term join --',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id={props.sourceDescriptor.id}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => {
|
||||
setIsPopoverOpen(false);
|
||||
}}
|
||||
ownFocus
|
||||
initialFocus="body" /* avoid initialFocus on Combobox */
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiExpression
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
description={i18n.translate('xpack.maps.termJoinExpression.description', {
|
||||
defaultMessage: 'Join with',
|
||||
})}
|
||||
uppercase={false}
|
||||
value={expressionValue}
|
||||
/>
|
||||
}
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<TermJoinPopoverContent
|
||||
leftSourceName={props.leftSourceName}
|
||||
leftValue={props.leftValue}
|
||||
leftFields={props.leftFields}
|
||||
onLeftFieldChange={props.onLeftFieldChange}
|
||||
sourceDescriptor={props.sourceDescriptor}
|
||||
onSourceDescriptorChange={props.onSourceDescriptorChange}
|
||||
rightFields={props.rightFields}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import {
|
||||
EuiPopoverTitle,
|
||||
EuiFormRow,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFormHelpText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { getDataViewSelectPlaceholder } from '../../../../../../common/i18n_getters';
|
||||
import { DEFAULT_MAX_BUCKETS_LIMIT } from '../../../../../../common/constants';
|
||||
import {
|
||||
ESTermSourceDescriptor,
|
||||
JoinSourceDescriptor,
|
||||
} from '../../../../../../common/descriptor_types';
|
||||
import { SingleFieldSelect } from '../../../../../components/single_field_select';
|
||||
import { ValidatedNumberInput } from '../../../../../components/validated_number_input';
|
||||
|
||||
import { getTermsFields } from '../../../../../index_pattern_util';
|
||||
import { getIndexPatternSelectComponent } from '../../../../../kibana_services';
|
||||
import type { JoinField } from '../../join_editor';
|
||||
import { inputStrings } from '../../../../input_strings';
|
||||
|
||||
interface Props {
|
||||
// Left source props (static - can not change)
|
||||
leftSourceName?: string;
|
||||
|
||||
// Left field props
|
||||
leftValue?: string;
|
||||
leftFields: JoinField[];
|
||||
onLeftFieldChange: (leftField: string) => void;
|
||||
|
||||
// Right source props
|
||||
sourceDescriptor: Partial<ESTermSourceDescriptor>;
|
||||
onSourceDescriptorChange: (sourceDescriptor: Partial<JoinSourceDescriptor>) => void;
|
||||
|
||||
// Right field props
|
||||
rightFields: DataViewField[];
|
||||
}
|
||||
|
||||
export function TermJoinPopoverContent(props: Props) {
|
||||
function onRightDataViewChange(indexPatternId?: string) {
|
||||
if (!indexPatternId || indexPatternId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { term, ...rest } = props.sourceDescriptor;
|
||||
props.onSourceDescriptorChange({
|
||||
...rest,
|
||||
indexPatternId,
|
||||
});
|
||||
}
|
||||
|
||||
function onLeftFieldChange(selectedFields: Array<EuiComboBoxOptionOption<JoinField>>) {
|
||||
const leftField = selectedFields?.[0]?.value?.name;
|
||||
if (leftField) {
|
||||
props.onLeftFieldChange(leftField);
|
||||
}
|
||||
}
|
||||
|
||||
function onRightFieldChange(term?: string) {
|
||||
if (!term || term.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onSourceDescriptorChange({
|
||||
...props.sourceDescriptor,
|
||||
term,
|
||||
});
|
||||
}
|
||||
|
||||
function renderLeftFieldSelect() {
|
||||
const { leftValue, leftFields } = props;
|
||||
|
||||
if (!leftFields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = leftFields.map((field) => {
|
||||
return {
|
||||
value: field,
|
||||
label: field.label,
|
||||
};
|
||||
});
|
||||
|
||||
let leftFieldOption;
|
||||
if (leftValue) {
|
||||
leftFieldOption = options.find((option) => {
|
||||
const field = option.value;
|
||||
return field.name === leftValue;
|
||||
});
|
||||
}
|
||||
const selectedOptions = leftFieldOption ? [leftFieldOption] : [];
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.termJoinExpression.leftFieldLabel', {
|
||||
defaultMessage: 'Left field',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.maps.termJoinExpression.leftSourceLabelHelpText', {
|
||||
defaultMessage: 'Left source field that contains the shared key.',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
placeholder={inputStrings.fieldSelectPlaceholder}
|
||||
singleSelection={true}
|
||||
isClearable={false}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onLeftFieldChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRightSourceSelect() {
|
||||
if (!props.leftValue) {
|
||||
return null;
|
||||
}
|
||||
const IndexPatternSelect = getIndexPatternSelectComponent();
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.termJoinExpression.rightSourceLabel', {
|
||||
defaultMessage: 'Right source',
|
||||
})}
|
||||
>
|
||||
<IndexPatternSelect
|
||||
placeholder={getDataViewSelectPlaceholder()}
|
||||
indexPatternId={props.sourceDescriptor.indexPatternId ?? ''}
|
||||
onChange={onRightDataViewChange}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRightFieldSelect() {
|
||||
if (!props.rightFields || !props.leftValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.termJoinExpression.rightFieldLabel', {
|
||||
defaultMessage: 'Right field',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.maps.termJoinExpression.rightSourceLabelHelpText', {
|
||||
defaultMessage: 'Right source field that contains the shared key.',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={inputStrings.fieldSelectPlaceholder}
|
||||
value={props.sourceDescriptor.term ?? null}
|
||||
onChange={onRightFieldChange}
|
||||
fields={getTermsFields(props.rightFields)}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRightFieldSizeInput() {
|
||||
if (!props.leftValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ValidatedNumberInput
|
||||
initialValue={
|
||||
props.sourceDescriptor.size !== undefined
|
||||
? props.sourceDescriptor.size
|
||||
: DEFAULT_MAX_BUCKETS_LIMIT
|
||||
}
|
||||
min={1}
|
||||
max={DEFAULT_MAX_BUCKETS_LIMIT}
|
||||
onChange={(size: number) => {
|
||||
props.onSourceDescriptorChange({
|
||||
...props.sourceDescriptor,
|
||||
size,
|
||||
});
|
||||
}}
|
||||
label={i18n.translate('xpack.maps.termJoinExpression.rightSizeLabel', {
|
||||
defaultMessage: 'Right size',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.maps.termJoinExpression.rightSizeHelpText', {
|
||||
defaultMessage: 'Right field term limit.',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { leftSourceName } = props;
|
||||
return (
|
||||
<div style={{ width: 300 }}>
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.termJoinExpression.popoverTitle"
|
||||
defaultMessage="Configure term join"
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
<EuiFormHelpText className="mapJoinExpressionHelpText">
|
||||
<FormattedMessage
|
||||
id="xpack.maps.termJoinExpression.helpText"
|
||||
defaultMessage="Configure the shared key that combines layer features, the left source, with the results of an Elasticsearch aggregation, the right source."
|
||||
/>
|
||||
</EuiFormHelpText>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.termJoinExpression.leftSourceLabel', {
|
||||
defaultMessage: 'Left source',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
selectedOptions={leftSourceName ? [{ value: leftSourceName, label: leftSourceName }] : []}
|
||||
isDisabled
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{renderLeftFieldSelect()}
|
||||
|
||||
{renderRightSourceSelect()}
|
||||
|
||||
{renderRightFieldSelect()}
|
||||
|
||||
{renderRightFieldSizeInput()}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -76,6 +76,7 @@ export class WhereExpression extends Component<Props, State> {
|
|||
data-test-subj="mapJoinWhereExpressionButton"
|
||||
/>
|
||||
}
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<div className="mapFilterEditor" data-test-subj="mapJoinWhereFilterEditor">
|
||||
<EuiFormHelpText className="mapJoinExpressionHelpText">
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const inputStrings = {
|
||||
fieldSelectPlaceholder: i18n.translate('xpack.maps.input.fieldSelectPlaceholder', {
|
||||
defaultMessage: 'Select field',
|
||||
}),
|
||||
relationshipLabel: i18n.translate('xpack.maps.input.relationshipLabel', {
|
||||
defaultMessage: 'Relationship',
|
||||
}),
|
||||
};
|
|
@ -374,6 +374,26 @@ export function registerMapsUsageCollector(usageCollection?: UsageCollectionSetu
|
|||
},
|
||||
},
|
||||
joins: {
|
||||
distance: {
|
||||
min: {
|
||||
type: 'long',
|
||||
_meta: { description: 'min number of distance joins per map' },
|
||||
},
|
||||
max: {
|
||||
type: 'long',
|
||||
_meta: { description: 'max number of distance joins per map' },
|
||||
},
|
||||
avg: {
|
||||
type: 'float',
|
||||
_meta: { description: 'avg number of distance joins per map' },
|
||||
},
|
||||
total: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description: 'total number of distance joins in cluster',
|
||||
},
|
||||
},
|
||||
},
|
||||
term: {
|
||||
min: {
|
||||
type: 'long',
|
||||
|
|
|
@ -177,8 +177,7 @@ export class AnomalySource implements IVectorSource {
|
|||
return false;
|
||||
}
|
||||
|
||||
showJoinEditor(): boolean {
|
||||
// Ignore, only show if joins are enabled for current configuration
|
||||
supportsJoins(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -6533,6 +6533,34 @@
|
|||
},
|
||||
"joins": {
|
||||
"properties": {
|
||||
"distance": {
|
||||
"properties": {
|
||||
"min": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "min number of distance joins per map"
|
||||
}
|
||||
},
|
||||
"max": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "max number of distance joins per map"
|
||||
}
|
||||
},
|
||||
"avg": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "avg number of distance joins per map"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "total number of distance joins in cluster"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"term": {
|
||||
"properties": {
|
||||
"min": {
|
||||
|
|
|
@ -20012,8 +20012,6 @@
|
|||
"xpack.maps.keydownScrollZoom.keydownToZoomInstructions": "Utilisez {key} + défilement pour zoomer sur la carte",
|
||||
"xpack.maps.labelPosition.iconSizeJoinFieldNotSupportMsg": "{labelPositionPropertyLabel} n'est pas pris en charge avec le champ de jointure {iconSizePropertyLabel} {iconSizeFieldName}. Définissez {iconSizePropertyLabel} sur le champ source pour l'activer.",
|
||||
"xpack.maps.layer.zoomFeedbackTooltip": "Le calque est visible entre les niveaux de zoom {minZoom} et {maxZoom}.",
|
||||
"xpack.maps.layerPanel.joinExpression.sizeFragment": "termes {rightSize} principaux de",
|
||||
"xpack.maps.layerPanel.joinExpression.value": "{leftSourceName}:{leftValue} avec {sizeFragment} {rightSourceName}:{rightValue}",
|
||||
"xpack.maps.layerPanel.metricsExpression.useMetricsDescription": "{metricsLength, plural, one {et utiliser l'indicateur} other {et utiliser les indicateurs}}",
|
||||
"xpack.maps.layers.newVectorLayerWizard.createIndexError": "Impossible de créer l'index avec le nom {message}",
|
||||
"xpack.maps.layers.newVectorLayerWizard.indexPermissionsError": "Vous devez disposer des privilèges \"create\" et \"create_index\" pour pouvoir créer et écrire des données sur \"{indexName}\".",
|
||||
|
@ -20040,11 +20038,9 @@
|
|||
"xpack.maps.source.esGeoLine.entityRequestName": "Entités {layerName}",
|
||||
"xpack.maps.source.esGeoLine.trackRequestName": "Pistes {layerName}",
|
||||
"xpack.maps.source.esGeoLineDisabledReason": "{title} requiert une licence Gold.",
|
||||
"xpack.maps.source.esGrid.compositeInspectorDescription": "Demande d'agrégation de grille géographique Elasticsearch : {requestId}",
|
||||
"xpack.maps.source.esGrid.compositePaginationErrorMessage": "{layerName} génère trop de requêtes. Réduisez \"Résolution de la grille\" et/ou réduisez le nombre d'indicateurs de premier terme.",
|
||||
"xpack.maps.source.esGrid.resolutionParamErrorMessage": "Paramètre de résolution de grille non reconnu : {resolution}",
|
||||
"xpack.maps.source.esJoin.countLabel": "Nombre de {indexPatternLabel}",
|
||||
"xpack.maps.source.esJoin.joinDescription": "Demande d'agrégation des termes Elasticsearch, source gauche : {leftSource}, source droite : {rightSource}",
|
||||
"xpack.maps.source.esSearch.clusterScalingLabel": "Afficher les clusters lorsque les résultats dépassent {maxResultWindow}",
|
||||
"xpack.maps.source.esSearch.convertToGeoJsonErrorMsg": "Impossible de convertir la réponse de la recherche en collection de fonctionnalités geoJson, erreur : {errorMsg}",
|
||||
"xpack.maps.source.esSearch.limitScalingLabel": "Limiter les résultats à {maxResultWindow}",
|
||||
|
@ -20093,7 +20089,6 @@
|
|||
"xpack.maps.choropleth.geofieldLabel": "Champ géospatial",
|
||||
"xpack.maps.choropleth.geofieldPlaceholder": "Sélectionner un champ géographique",
|
||||
"xpack.maps.choropleth.joinFieldLabel": "Champ de liaison",
|
||||
"xpack.maps.choropleth.joinFieldPlaceholder": "Sélectionner un champ",
|
||||
"xpack.maps.choropleth.statisticsLabel": "Source des statistiques",
|
||||
"xpack.maps.choropleth.title": "Choroplèthe",
|
||||
"xpack.maps.colorStops.otherCategoryColorPickerTooltip": "Lorsque le champ sélectionné comporte plus de termes que de couleurs dans la palette, le reste des termes est regroupé dans la catégorie \"Autres\". Sélectionnez une palette avec plus de couleurs pour augmenter le nombre de termes colorés dans votre carte.",
|
||||
|
@ -20196,12 +20191,8 @@
|
|||
"xpack.maps.inspector.vectorTileViewTitle": "Tuiles vectorielles",
|
||||
"xpack.maps.inspector.zoomLabel": "Effectuer un zoom",
|
||||
"xpack.maps.joinDocs.intro": "Les liaisons de terme ajoutent au calque les propriétés de style basées sur les données. Les liaisons de terme fonctionnent comme indiqué ci-dessous :",
|
||||
"xpack.maps.joinDocs.join": "La liaison ajoute des indicateurs pour chaque compartiment d'agrégation de termes avec la clé partagée correspondante.",
|
||||
"xpack.maps.joinDocs.linkLabel": "Exemple de liaison de terme",
|
||||
"xpack.maps.joinDocs.metrics": "Les indicateurs sont calculés pour tous les documents dans un compartiment.",
|
||||
"xpack.maps.joinDocs.noMatches": "Les fonctionnalités n'ayant pas de compartiment d'agrégation de termes correspondants ne sont pas visibles sur la carte.",
|
||||
"xpack.maps.joinDocs.sharedKey": "Une clé partagée combine les fonctionnalités vectorielles (source de gauche) avec les résultats d'une agrégation Elasticsearch (source de droite).",
|
||||
"xpack.maps.joinDocs.termsAggregation": "L'agrégation de termes crée un compartiment pour chaque clé partagée unique.",
|
||||
"xpack.maps.keydownScrollZoom.keydownClickAndDragZoomInstructions": "Utilisez Maj+clic et faites glisser pour zoomer sur la carte afin de l'adapter à une zone de délimitation.",
|
||||
"xpack.maps.kilometersAbbr": "km",
|
||||
"xpack.maps.layer.isUsingBoundsFilter": "Résultats affinés par zone de carte visible",
|
||||
|
@ -20239,21 +20230,6 @@
|
|||
"xpack.maps.layerPanel.join.applyGlobalTimeCheckboxLabel": "Appliquer une heure globale à la liaison",
|
||||
"xpack.maps.layerPanel.join.deleteJoinAriaLabel": "Supprimer la liaison",
|
||||
"xpack.maps.layerPanel.join.deleteJoinTitle": "Supprimer la liaison",
|
||||
"xpack.maps.layerPanel.joinEditor.addJoinButton.label": "Ajouter une liaison",
|
||||
"xpack.maps.layerPanel.joinEditor.addJoinButton.mvtSingleJoinMsg": "Les tuiles vectorielles prennent en charge une seule liaison de terme. Pour ajouter plusieurs liaisons, sélectionnez \"Limiter les résultats\" dans \"Montée en charge\".",
|
||||
"xpack.maps.layerPanel.joinEditor.termJoinsTitle": "Liaisons de terme",
|
||||
"xpack.maps.layerPanel.joinExpression.helpText": "Configurez la clé partagée.",
|
||||
"xpack.maps.layerPanel.joinExpression.joinPopoverTitle": "Liaison",
|
||||
"xpack.maps.layerPanel.joinExpression.leftFieldLabel": "Champ gauche",
|
||||
"xpack.maps.layerPanel.joinExpression.leftSourceLabel": "Source gauche",
|
||||
"xpack.maps.layerPanel.joinExpression.leftSourceLabelHelpText": "Champ de source gauche contenant la clé partagée.",
|
||||
"xpack.maps.layerPanel.joinExpression.rightFieldLabel": "Champ droit",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSizeHelpText": "Limite de terme du champ droit.",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSizeLabel": "Taille correcte",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSourceLabel": "Source droite",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSourceLabelHelpText": "Champ de source droite contenant la clé partagée.",
|
||||
"xpack.maps.layerPanel.joinExpression.selectFieldPlaceholder": "Sélectionner un champ",
|
||||
"xpack.maps.layerPanel.joinExpression.selectPlaceholder": "-- sélectionner --",
|
||||
"xpack.maps.layerPanel.layerSettingsTitle": "Paramètres du calque",
|
||||
"xpack.maps.layerPanel.metricsExpression.helpText": "Configurez les indicateurs pour la source droite. Ces valeurs sont ajoutées aux fonctionnalités du calque.",
|
||||
"xpack.maps.layerPanel.metricsExpression.joinMustBeSetErrorMessage": "La LIAISON doit être configurée",
|
||||
|
@ -20347,7 +20323,6 @@
|
|||
"xpack.maps.metricsEditor.deleteMetricButtonLabel": "Supprimer un indicateur",
|
||||
"xpack.maps.metricsEditor.selectFieldError": "Champ requis pour l'agrégation",
|
||||
"xpack.maps.metricsEditor.selectFieldLabel": "Champ",
|
||||
"xpack.maps.metricsEditor.selectFieldPlaceholder": "Sélectionner un champ",
|
||||
"xpack.maps.metricsEditor.selectPercentileLabel": "Centile",
|
||||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "Sélectionner une agrégation",
|
||||
"xpack.maps.mvtSource.addFieldLabel": "Ajouter",
|
||||
|
@ -20457,7 +20432,6 @@
|
|||
"xpack.maps.source.esGeoGrid.pointsDropdownOption": "Clusters",
|
||||
"xpack.maps.source.esGeoGrid.showAsLabel": "Afficher en tant que",
|
||||
"xpack.maps.source.esGeoGrid.showAsSelector": "Sélectionner la méthode d’affichage",
|
||||
"xpack.maps.source.esGeoLine.entityRequestDescription": "Requête de termes Elasticsearch pour récupérer des entités dans la mémoire tampon de la carte.",
|
||||
"xpack.maps.source.esGeoLine.geofieldLabel": "Champ géospatial",
|
||||
"xpack.maps.source.esGeoLine.geofieldPlaceholder": "Sélectionner un champ géographique",
|
||||
"xpack.maps.source.esGeoLine.geospatialFieldLabel": "Champ géospatial",
|
||||
|
@ -20467,13 +20441,11 @@
|
|||
"xpack.maps.source.esGeoLine.sortFieldPlaceholder": "Sélectionner le champ de tri",
|
||||
"xpack.maps.source.esGeoLine.splitFieldLabel": "Entité",
|
||||
"xpack.maps.source.esGeoLine.splitFieldPlaceholder": "Sélectionner un champ d'entité",
|
||||
"xpack.maps.source.esGeoLine.trackRequestDescription": "Requête geo_line Elasticsearch pour récupérer les pistes des entités. Les pistes ne sont pas filtrées par mémoire tampon de carte.",
|
||||
"xpack.maps.source.esGeoLine.trackSettingsLabel": "Pistes",
|
||||
"xpack.maps.source.esGeoLineDescription": "Créer des lignes à partir de points",
|
||||
"xpack.maps.source.esGeoLineTitle": "Pistes",
|
||||
"xpack.maps.source.esGrid.geospatialFieldLabel": "Champ de cluster",
|
||||
"xpack.maps.source.esGrid.highLabel": "élevé",
|
||||
"xpack.maps.source.esGrid.inspectorDescription": "Demande d'agrégation de grille géographique Elasticsearch",
|
||||
"xpack.maps.source.esGrid.lowLabel": "bas",
|
||||
"xpack.maps.source.esGrid.metricsLabel": "Indicateurs",
|
||||
"xpack.maps.source.esGrid.superFineHelpText": "La haute résolution utilise des tuiles vectorielles.",
|
||||
|
@ -20495,7 +20467,6 @@
|
|||
"xpack.maps.source.esSearch.geoFieldTypeLabel": "Type de champ géospatial",
|
||||
"xpack.maps.source.esSearch.indexOverOneLengthEditError": "Votre vue de données pointe vers plusieurs index. Un seul index est autorisé par vue de données.",
|
||||
"xpack.maps.source.esSearch.indexZeroLengthEditError": "Votre vue de données ne pointe vers aucun index.",
|
||||
"xpack.maps.source.esSearch.joinsDisabledReason": "Les liaisons ne sont pas prises en charge lors de la montée en charge par rapport aux clusters",
|
||||
"xpack.maps.source.esSearch.scalingModal.cancelBtnLabel": "Annuler",
|
||||
"xpack.maps.source.esSearch.scalingModal.confirmBtnLabel": "Accepter",
|
||||
"xpack.maps.source.esSearch.scalingModal.title": "Retirer les configurations non prises en charge ?",
|
||||
|
@ -20532,7 +20503,6 @@
|
|||
"xpack.maps.source.mvtVectorSourceWizard": "Service de données implémentant la spécification de tuiles vectorielles Mapbox",
|
||||
"xpack.maps.source.pewPew.destGeoFieldLabel": "Destination",
|
||||
"xpack.maps.source.pewPew.destGeoFieldPlaceholder": "Sélectionner le champ géographique de destination",
|
||||
"xpack.maps.source.pewPew.inspectorDescription": "Demande de connexions source-destination",
|
||||
"xpack.maps.source.pewPew.metricsLabel": "Indicateurs",
|
||||
"xpack.maps.source.pewPew.noSourceAndDestDetails": "La vue de données sélectionnée ne contient pas de champs source et de destination.",
|
||||
"xpack.maps.source.pewPew.sourceGeoFieldLabel": "Source",
|
||||
|
|
|
@ -20012,8 +20012,6 @@
|
|||
"xpack.maps.keydownScrollZoom.keydownToZoomInstructions": "マップをズームするには、{key}を押しながらスクロールします",
|
||||
"xpack.maps.labelPosition.iconSizeJoinFieldNotSupportMsg": "{labelPositionPropertyLabel}は、{iconSizePropertyLabel}結合フィールド{iconSizeFieldName}でサポートされていません。有効化するには、{iconSizePropertyLabel}をソースフィールドに設定します。",
|
||||
"xpack.maps.layer.zoomFeedbackTooltip": "レイヤーはズームレベル {minZoom} から {maxZoom} の間で表示されます。",
|
||||
"xpack.maps.layerPanel.joinExpression.sizeFragment": "上位の{rightSize}用語",
|
||||
"xpack.maps.layerPanel.joinExpression.value": "{leftSourceName}:{leftValue}と{sizeFragment} {rightSourceName}:{rightValue}",
|
||||
"xpack.maps.layerPanel.metricsExpression.useMetricsDescription": "{metricsLength, plural, other {およびメトリックを使用}}",
|
||||
"xpack.maps.layers.newVectorLayerWizard.createIndexError": "名前{message}のインデックスを作成できませんでした",
|
||||
"xpack.maps.layers.newVectorLayerWizard.indexPermissionsError": "データを作成し、\"{indexName}\"に書き込むには、「create」および「create_index」インデックス権限が必要です。",
|
||||
|
@ -20040,11 +20038,9 @@
|
|||
"xpack.maps.source.esGeoLine.entityRequestName": "{layerName}エンティティ",
|
||||
"xpack.maps.source.esGeoLine.trackRequestName": "{layerName}追跡",
|
||||
"xpack.maps.source.esGeoLineDisabledReason": "{title}には Gold ライセンスが必要です。",
|
||||
"xpack.maps.source.esGrid.compositeInspectorDescription": "Elasticsearch ジオグリッドアグリゲーションリクエスト:{requestId}",
|
||||
"xpack.maps.source.esGrid.compositePaginationErrorMessage": "{layerName}はリクエスト過多の原因になります。「グリッド解像度」を下げるか、またはトップ用語「メトリック」の数を減らしてください。",
|
||||
"xpack.maps.source.esGrid.resolutionParamErrorMessage": "グリッド解像度パラメーターが認識されません: {resolution}",
|
||||
"xpack.maps.source.esJoin.countLabel": "{indexPatternLabel}のカウント",
|
||||
"xpack.maps.source.esJoin.joinDescription": "Elasticsearch 用語アグリゲーションリクエスト、左ソース:{leftSource}、右ソース:{rightSource}",
|
||||
"xpack.maps.source.esSearch.clusterScalingLabel": "結果が{maxResultWindow}を超えたらクラスターを表示",
|
||||
"xpack.maps.source.esSearch.convertToGeoJsonErrorMsg": "検索への応答を geoJson 機能コレクションに変換できません。エラー: {errorMsg}",
|
||||
"xpack.maps.source.esSearch.limitScalingLabel": "結果を{maxResultWindow}に制限",
|
||||
|
@ -20093,7 +20089,6 @@
|
|||
"xpack.maps.choropleth.geofieldLabel": "地理空間フィールド",
|
||||
"xpack.maps.choropleth.geofieldPlaceholder": "ジオフィールドを選択",
|
||||
"xpack.maps.choropleth.joinFieldLabel": "フィールドを結合",
|
||||
"xpack.maps.choropleth.joinFieldPlaceholder": "フィールドを選択",
|
||||
"xpack.maps.choropleth.statisticsLabel": "統計ソース",
|
||||
"xpack.maps.choropleth.title": "階級区分図",
|
||||
"xpack.maps.colorStops.otherCategoryColorPickerTooltip": "選択したフィールドにパレットの色よりも多くの用語がある場合、残りの用語が[その他]カテゴリにグループ化されます。マップで色が付いた用語の数を増やすには、その他の色のパレットを選択します",
|
||||
|
@ -20196,12 +20191,8 @@
|
|||
"xpack.maps.inspector.vectorTileViewTitle": "ベクトルタイル",
|
||||
"xpack.maps.inspector.zoomLabel": "ズーム",
|
||||
"xpack.maps.joinDocs.intro": "用語結合強化レイヤーと、データに基づくスタイル設定のプロパティ。用語結合は次のように動作します。",
|
||||
"xpack.maps.joinDocs.join": "各用語アグリゲーションバケットのメトリックと対応する共有キーを追加します。",
|
||||
"xpack.maps.joinDocs.linkLabel": "用語結合の例",
|
||||
"xpack.maps.joinDocs.metrics": "バケットのすべてのドキュメントのメトリックが計算されます。",
|
||||
"xpack.maps.joinDocs.noMatches": "対応する用語アグリゲーションがない特徴量はマップに表示されません。",
|
||||
"xpack.maps.joinDocs.sharedKey": "共有キーはベクトル特徴量(左のソース)を、Elasticsearchアグリゲーションの結果(右のソース)と結合します。",
|
||||
"xpack.maps.joinDocs.termsAggregation": "用語アグリゲーションにより、各一意の共有キーのバケットが作成されます。",
|
||||
"xpack.maps.keydownScrollZoom.keydownClickAndDragZoomInstructions": "Shiftを押しながらドラッグすると、バウンディングボックス内に合うようにマップをズームします",
|
||||
"xpack.maps.kilometersAbbr": "km",
|
||||
"xpack.maps.layer.isUsingBoundsFilter": "表示されるマップ領域で絞り込まれた結果",
|
||||
|
@ -20239,21 +20230,6 @@
|
|||
"xpack.maps.layerPanel.join.applyGlobalTimeCheckboxLabel": "結合するグローバル時刻を適用",
|
||||
"xpack.maps.layerPanel.join.deleteJoinAriaLabel": "ジョブの削除",
|
||||
"xpack.maps.layerPanel.join.deleteJoinTitle": "ジョブの削除",
|
||||
"xpack.maps.layerPanel.joinEditor.addJoinButton.label": "結合を追加",
|
||||
"xpack.maps.layerPanel.joinEditor.addJoinButton.mvtSingleJoinMsg": "ベクトルタイルは1つの用語結合をサポートします。複数の結合を追加するには、[スケーリング]で[結果を制限]を選択します。",
|
||||
"xpack.maps.layerPanel.joinEditor.termJoinsTitle": "用語結合",
|
||||
"xpack.maps.layerPanel.joinExpression.helpText": "共有キーを構成します。",
|
||||
"xpack.maps.layerPanel.joinExpression.joinPopoverTitle": "結合",
|
||||
"xpack.maps.layerPanel.joinExpression.leftFieldLabel": "左のフィールド",
|
||||
"xpack.maps.layerPanel.joinExpression.leftSourceLabel": "左のソース",
|
||||
"xpack.maps.layerPanel.joinExpression.leftSourceLabelHelpText": "共有キーを含む左のソースフィールド。",
|
||||
"xpack.maps.layerPanel.joinExpression.rightFieldLabel": "右のフィールド",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSizeHelpText": "右フィールドの語句の制限。",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSizeLabel": "右サイズ",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSourceLabel": "右のソース",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSourceLabelHelpText": "共有キーを含む右のソースフィールド。",
|
||||
"xpack.maps.layerPanel.joinExpression.selectFieldPlaceholder": "フィールドを選択",
|
||||
"xpack.maps.layerPanel.joinExpression.selectPlaceholder": "-- 選択 --",
|
||||
"xpack.maps.layerPanel.layerSettingsTitle": "レイヤー設定",
|
||||
"xpack.maps.layerPanel.metricsExpression.helpText": "右のソースのメトリックを構成します。これらの値はレイヤー機能に追加されます。",
|
||||
"xpack.maps.layerPanel.metricsExpression.joinMustBeSetErrorMessage": "JOIN の設定が必要です",
|
||||
|
@ -20347,7 +20323,6 @@
|
|||
"xpack.maps.metricsEditor.deleteMetricButtonLabel": "メトリックを削除",
|
||||
"xpack.maps.metricsEditor.selectFieldError": "アグリゲーションにはフィールドが必要です",
|
||||
"xpack.maps.metricsEditor.selectFieldLabel": "フィールド",
|
||||
"xpack.maps.metricsEditor.selectFieldPlaceholder": "フィールドを選択",
|
||||
"xpack.maps.metricsEditor.selectPercentileLabel": "パーセンタイル",
|
||||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "集約を選択",
|
||||
"xpack.maps.mvtSource.addFieldLabel": "追加",
|
||||
|
@ -20457,7 +20432,6 @@
|
|||
"xpack.maps.source.esGeoGrid.pointsDropdownOption": "クラスター",
|
||||
"xpack.maps.source.esGeoGrid.showAsLabel": "表示形式",
|
||||
"xpack.maps.source.esGeoGrid.showAsSelector": "表示方法を選択",
|
||||
"xpack.maps.source.esGeoLine.entityRequestDescription": "Elasticsearch 用語はマップバッファー内のエンティティを取得するように要求します。",
|
||||
"xpack.maps.source.esGeoLine.geofieldLabel": "地理空間フィールド",
|
||||
"xpack.maps.source.esGeoLine.geofieldPlaceholder": "ジオフィールドを選択",
|
||||
"xpack.maps.source.esGeoLine.geospatialFieldLabel": "地理空間フィールド",
|
||||
|
@ -20467,13 +20441,11 @@
|
|||
"xpack.maps.source.esGeoLine.sortFieldPlaceholder": "ソートフィールドを選択",
|
||||
"xpack.maps.source.esGeoLine.splitFieldLabel": "エンティティ",
|
||||
"xpack.maps.source.esGeoLine.splitFieldPlaceholder": "エンティティフィールドを選択",
|
||||
"xpack.maps.source.esGeoLine.trackRequestDescription": "Elasticsearch geo_line はエンティティのトラックを取得するように要求します。トラックはマップバッファーでフィルタリングされていません。",
|
||||
"xpack.maps.source.esGeoLine.trackSettingsLabel": "追跡",
|
||||
"xpack.maps.source.esGeoLineDescription": "ポイントから線を作成",
|
||||
"xpack.maps.source.esGeoLineTitle": "追跡",
|
||||
"xpack.maps.source.esGrid.geospatialFieldLabel": "クラスターフィールド",
|
||||
"xpack.maps.source.esGrid.highLabel": "高",
|
||||
"xpack.maps.source.esGrid.inspectorDescription": "Elasticsearch ジオグリッド集約リクエスト",
|
||||
"xpack.maps.source.esGrid.lowLabel": "低",
|
||||
"xpack.maps.source.esGrid.metricsLabel": "メトリック",
|
||||
"xpack.maps.source.esGrid.superFineHelpText": "高解像度はベクトルタイルを使用します。",
|
||||
|
@ -20495,7 +20467,6 @@
|
|||
"xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空間フィールドタイプ",
|
||||
"xpack.maps.source.esSearch.indexOverOneLengthEditError": "データビューは複数のインデックスを参照しています。データビューごとに1つのインデックスのみが許可されています。",
|
||||
"xpack.maps.source.esSearch.indexZeroLengthEditError": "データビューはどのインデックスも参照していません。",
|
||||
"xpack.maps.source.esSearch.joinsDisabledReason": "クラスターでスケーリングするときに、結合はサポートされていません",
|
||||
"xpack.maps.source.esSearch.scalingModal.cancelBtnLabel": "キャンセル",
|
||||
"xpack.maps.source.esSearch.scalingModal.confirmBtnLabel": "承諾",
|
||||
"xpack.maps.source.esSearch.scalingModal.title": "サポートされていない構成を削除しますか?",
|
||||
|
@ -20532,7 +20503,6 @@
|
|||
"xpack.maps.source.mvtVectorSourceWizard": "Mapboxベクトルタイル仕様を実装するデータサービス",
|
||||
"xpack.maps.source.pewPew.destGeoFieldLabel": "送信先",
|
||||
"xpack.maps.source.pewPew.destGeoFieldPlaceholder": "デスティネーション地理情報フィールドを選択",
|
||||
"xpack.maps.source.pewPew.inspectorDescription": "ソースとデスティネーションの接続リクエスト",
|
||||
"xpack.maps.source.pewPew.metricsLabel": "メトリック",
|
||||
"xpack.maps.source.pewPew.noSourceAndDestDetails": "選択されたデータビューにはソースとデスティネーションのフィールドが含まれていません。",
|
||||
"xpack.maps.source.pewPew.sourceGeoFieldLabel": "送信元",
|
||||
|
|
|
@ -20014,8 +20014,6 @@
|
|||
"xpack.maps.keydownScrollZoom.keydownToZoomInstructions": "使用 {key} + 滚动以缩放地图",
|
||||
"xpack.maps.labelPosition.iconSizeJoinFieldNotSupportMsg": "{iconSizePropertyLabel} 联接字段 {iconSizeFieldName} 不支持 {labelPositionPropertyLabel}。将 {iconSizePropertyLabel} 设置为源字段以便启用。",
|
||||
"xpack.maps.layer.zoomFeedbackTooltip": "图层在缩放级别 {minZoom} 和 {maxZoom} 之间可见。",
|
||||
"xpack.maps.layerPanel.joinExpression.sizeFragment": "排名前 {rightSize} 的词 - 来自",
|
||||
"xpack.maps.layerPanel.joinExpression.value": "{leftSourceName}:{leftValue},{sizeFragment} {rightSourceName}:{rightValue}",
|
||||
"xpack.maps.layerPanel.metricsExpression.useMetricsDescription": "{metricsLength, plural, other {并使用指标}}",
|
||||
"xpack.maps.layers.newVectorLayerWizard.createIndexError": "无法使用名称 {message} 创建索引",
|
||||
"xpack.maps.layers.newVectorLayerWizard.indexPermissionsError": "必须具有“create”和“create_index”索引权限才能创建数据并将其写入到“{indexName}”。",
|
||||
|
@ -20042,7 +20040,6 @@
|
|||
"xpack.maps.source.esGeoLine.entityRequestName": "{layerName} 实体",
|
||||
"xpack.maps.source.esGeoLine.trackRequestName": "{layerName} 轨迹",
|
||||
"xpack.maps.source.esGeoLineDisabledReason": "{title} 需要黄金级许可证。",
|
||||
"xpack.maps.source.esGrid.compositeInspectorDescription": "Elasticsearch 地理网格聚合请求:{requestId}",
|
||||
"xpack.maps.source.esGrid.compositePaginationErrorMessage": "{layerName} 正导致过多的请求。降低“网格分辨率”和/或减少热门词“指标”的数量。",
|
||||
"xpack.maps.source.esGrid.resolutionParamErrorMessage": "无法识别网格分辨率参数:{resolution}",
|
||||
"xpack.maps.source.esJoin.countLabel": "{indexPatternLabel} 的计数",
|
||||
|
@ -20094,7 +20091,6 @@
|
|||
"xpack.maps.choropleth.geofieldLabel": "地理空间字段",
|
||||
"xpack.maps.choropleth.geofieldPlaceholder": "选择地理字段",
|
||||
"xpack.maps.choropleth.joinFieldLabel": "联接字段",
|
||||
"xpack.maps.choropleth.joinFieldPlaceholder": "选择字段",
|
||||
"xpack.maps.choropleth.statisticsLabel": "统计源",
|
||||
"xpack.maps.choropleth.title": "分级统计图",
|
||||
"xpack.maps.colorStops.otherCategoryColorPickerTooltip": "选定字段具有的字词数量多于调色板中的颜色数量时,其余字词将分组到“其他”类别下。选择具有多种颜色的调色板,以增加地图中彩色字词的数量",
|
||||
|
@ -20197,12 +20193,8 @@
|
|||
"xpack.maps.inspector.vectorTileViewTitle": "矢量磁贴",
|
||||
"xpack.maps.inspector.zoomLabel": "缩放",
|
||||
"xpack.maps.joinDocs.intro": "词联接会利用属性增强图层,以实现数据驱动的样式。词联接的工作方式如下:",
|
||||
"xpack.maps.joinDocs.join": "该联接会使用对应的共享密钥为每个词聚合存储桶添加指标。",
|
||||
"xpack.maps.joinDocs.linkLabel": "词联接示例",
|
||||
"xpack.maps.joinDocs.metrics": "将为存储桶中的所有文档计算指标。",
|
||||
"xpack.maps.joinDocs.noMatches": "没有对应词聚合存储桶的特征在地图中不可见。",
|
||||
"xpack.maps.joinDocs.sharedKey": "共享密钥会将矢量功能(左源)与 Elasticsearch 聚合的结果(右源)进行组合。",
|
||||
"xpack.maps.joinDocs.termsAggregation": "词聚合将为每个唯一的共享密钥创建存储桶。",
|
||||
"xpack.maps.keydownScrollZoom.keydownClickAndDragZoomInstructions": "使用 shift + 单击并拖动以缩放地图,使其适应边界框",
|
||||
"xpack.maps.kilometersAbbr": "km",
|
||||
"xpack.maps.layer.isUsingBoundsFilter": "可见地图区域缩减的结果",
|
||||
|
@ -20240,21 +20232,6 @@
|
|||
"xpack.maps.layerPanel.join.applyGlobalTimeCheckboxLabel": "应用全局时间到联接",
|
||||
"xpack.maps.layerPanel.join.deleteJoinAriaLabel": "删除联接",
|
||||
"xpack.maps.layerPanel.join.deleteJoinTitle": "删除联接",
|
||||
"xpack.maps.layerPanel.joinEditor.addJoinButton.label": "添加联接",
|
||||
"xpack.maps.layerPanel.joinEditor.addJoinButton.mvtSingleJoinMsg": "矢量磁贴支持一个词联接。要添加多个联接,请在“缩放”中选择“限制结果”。",
|
||||
"xpack.maps.layerPanel.joinEditor.termJoinsTitle": "词联接",
|
||||
"xpack.maps.layerPanel.joinExpression.helpText": "配置共享密钥。",
|
||||
"xpack.maps.layerPanel.joinExpression.joinPopoverTitle": "联接",
|
||||
"xpack.maps.layerPanel.joinExpression.leftFieldLabel": "左字段",
|
||||
"xpack.maps.layerPanel.joinExpression.leftSourceLabel": "左源",
|
||||
"xpack.maps.layerPanel.joinExpression.leftSourceLabelHelpText": "包含共享密钥的左源字段。",
|
||||
"xpack.maps.layerPanel.joinExpression.rightFieldLabel": "右字段",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSizeHelpText": "右字段词限制。",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSizeLabel": "右大小",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSourceLabel": "右源",
|
||||
"xpack.maps.layerPanel.joinExpression.rightSourceLabelHelpText": "包含共享密钥的右源字段。",
|
||||
"xpack.maps.layerPanel.joinExpression.selectFieldPlaceholder": "选择字段",
|
||||
"xpack.maps.layerPanel.joinExpression.selectPlaceholder": "-- 选择 --",
|
||||
"xpack.maps.layerPanel.layerSettingsTitle": "图层设置",
|
||||
"xpack.maps.layerPanel.metricsExpression.helpText": "配置右源的指标。这些值已添加到图层功能。",
|
||||
"xpack.maps.layerPanel.metricsExpression.joinMustBeSetErrorMessage": "必须设置联接",
|
||||
|
@ -20348,7 +20325,6 @@
|
|||
"xpack.maps.metricsEditor.deleteMetricButtonLabel": "删除指标",
|
||||
"xpack.maps.metricsEditor.selectFieldError": "聚合所需字段",
|
||||
"xpack.maps.metricsEditor.selectFieldLabel": "字段",
|
||||
"xpack.maps.metricsEditor.selectFieldPlaceholder": "选择字段",
|
||||
"xpack.maps.metricsEditor.selectPercentileLabel": "百分位数",
|
||||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "选择聚合",
|
||||
"xpack.maps.mvtSource.addFieldLabel": "添加",
|
||||
|
@ -20458,7 +20434,6 @@
|
|||
"xpack.maps.source.esGeoGrid.pointsDropdownOption": "集群",
|
||||
"xpack.maps.source.esGeoGrid.showAsLabel": "显示为",
|
||||
"xpack.maps.source.esGeoGrid.showAsSelector": "选择显示方法",
|
||||
"xpack.maps.source.esGeoLine.entityRequestDescription": "用于获取地图缓冲区内的实体的 Elasticsearch 词请求。",
|
||||
"xpack.maps.source.esGeoLine.geofieldLabel": "地理空间字段",
|
||||
"xpack.maps.source.esGeoLine.geofieldPlaceholder": "选择地理字段",
|
||||
"xpack.maps.source.esGeoLine.geospatialFieldLabel": "地理空间字段",
|
||||
|
@ -20468,13 +20443,11 @@
|
|||
"xpack.maps.source.esGeoLine.sortFieldPlaceholder": "选择排序字段",
|
||||
"xpack.maps.source.esGeoLine.splitFieldLabel": "实体",
|
||||
"xpack.maps.source.esGeoLine.splitFieldPlaceholder": "选择实体字段",
|
||||
"xpack.maps.source.esGeoLine.trackRequestDescription": "用于获取实体轨迹的 Elasticsearch geo_line 请求。轨迹不按地图缓冲区筛选。",
|
||||
"xpack.maps.source.esGeoLine.trackSettingsLabel": "轨迹",
|
||||
"xpack.maps.source.esGeoLineDescription": "从点创建线",
|
||||
"xpack.maps.source.esGeoLineTitle": "轨迹",
|
||||
"xpack.maps.source.esGrid.geospatialFieldLabel": "集群字段",
|
||||
"xpack.maps.source.esGrid.highLabel": "高",
|
||||
"xpack.maps.source.esGrid.inspectorDescription": "Elasticsearch 地理网格聚合请求",
|
||||
"xpack.maps.source.esGrid.lowLabel": "低",
|
||||
"xpack.maps.source.esGrid.metricsLabel": "指标",
|
||||
"xpack.maps.source.esGrid.superFineHelpText": "高分辨率使用矢量磁贴。",
|
||||
|
@ -20496,7 +20469,6 @@
|
|||
"xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空间字段类型",
|
||||
"xpack.maps.source.esSearch.indexOverOneLengthEditError": "您的数据视图指向多个索引。每个数据视图只允许一个索引。",
|
||||
"xpack.maps.source.esSearch.indexZeroLengthEditError": "您的数据视图未指向任何索引。",
|
||||
"xpack.maps.source.esSearch.joinsDisabledReason": "按集群缩放时不支持联接",
|
||||
"xpack.maps.source.esSearch.scalingModal.cancelBtnLabel": "取消",
|
||||
"xpack.maps.source.esSearch.scalingModal.confirmBtnLabel": "接受",
|
||||
"xpack.maps.source.esSearch.scalingModal.title": "是否移除不受支持的配置?",
|
||||
|
@ -20533,7 +20505,6 @@
|
|||
"xpack.maps.source.mvtVectorSourceWizard": "实施 Mapbox 矢量文件规范的数据服务",
|
||||
"xpack.maps.source.pewPew.destGeoFieldLabel": "目标",
|
||||
"xpack.maps.source.pewPew.destGeoFieldPlaceholder": "选择目标地理位置字段",
|
||||
"xpack.maps.source.pewPew.inspectorDescription": "源-目标连接请求",
|
||||
"xpack.maps.source.pewPew.metricsLabel": "指标",
|
||||
"xpack.maps.source.pewPew.noSourceAndDestDetails": "选定的数据视图不包含源和目标字段。",
|
||||
"xpack.maps.source.pewPew.sourceGeoFieldLabel": "源",
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(mapUsage).eql({
|
||||
mapsTotalCount: 27,
|
||||
basemaps: {},
|
||||
joins: { term: { min: 1, max: 1, total: 3, avg: 0.1111111111111111 } },
|
||||
joins: { term: { min: 1, max: 1, total: 4, avg: 0.14814814814814814 } },
|
||||
layerTypes: {
|
||||
es_docs: { min: 1, max: 2, total: 19, avg: 0.7037037037037037 },
|
||||
es_agg_grids: { min: 1, max: 1, total: 6, avg: 0.2222222222222222 },
|
||||
|
|
|
@ -77,7 +77,7 @@ export default function ({ getPageObjects, getService }) {
|
|||
await retry.try(async () => {
|
||||
const joinExampleRequestNames = await inspector.getRequestNames();
|
||||
expect(joinExampleRequestNames).to.equal(
|
||||
'geo_shapes*,meta_for_geo_shapes*.runtime_shape_name'
|
||||
'geo_shapes* documents request,geo_shapes* term join request'
|
||||
);
|
||||
});
|
||||
await inspector.close();
|
||||
|
@ -88,7 +88,7 @@ export default function ({ getPageObjects, getService }) {
|
|||
await inspector.close();
|
||||
|
||||
expect(singleExampleRequest).to.be(true);
|
||||
expect(selectedExampleRequest).to.equal('logstash-*');
|
||||
expect(selectedExampleRequest).to.equal('logstash-* grid request');
|
||||
});
|
||||
|
||||
it('should apply container state (time, query, filters) to embeddable when loaded', async () => {
|
||||
|
@ -120,7 +120,7 @@ export default function ({ getPageObjects, getService }) {
|
|||
|
||||
const { rawResponse: joinResponse } = await PageObjects.maps.getResponseFromDashboardPanel(
|
||||
'join example',
|
||||
'meta_for_geo_shapes*.runtime_shape_name'
|
||||
'geo_shapes* term join request'
|
||||
);
|
||||
expect(joinResponse.aggregations.join.buckets.length).to.equal(1);
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue