[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:
Nathan Reese 2023-05-17 06:20:04 -06:00 committed by GitHub
parent 5d96ef99d7
commit 596c7b3e70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 2311 additions and 1227 deletions

View file

@ -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."

View file

@ -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
*/

View file

@ -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 {

View file

@ -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;
};

View file

@ -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;

View file

@ -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;
}

View file

@ -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,
});
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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": [

View file

@ -13,6 +13,7 @@ export enum EMS_BASEMAP_KEYS {
}
export enum JOIN_KEYS {
DISTANCE = 'distance',
TERM = 'term',
}

View file

@ -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({

View file

@ -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;
}

View 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;
}

View file

@ -232,14 +232,6 @@ export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLay
: displayName;
}
showJoinEditor() {
return true;
}
getJoinsDisabledReason() {
return this._documentSource.getJoinsDisabledReason();
}
getJoins() {
return [];
}

View file

@ -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,

View file

@ -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),

View file

@ -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);

View file

@ -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

View file

@ -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) => {

View file

@ -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}

View file

@ -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>
);

View file

@ -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);

View file

@ -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';

View file

@ -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',
}),
};

View file

@ -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),
});
}

View file

@ -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>
</>
);
}

View file

@ -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';

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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}
</>
);
}

View file

@ -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) => {

View file

@ -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()}

View file

@ -42,6 +42,7 @@ describe('ESGeoGridSource', () => {
get() {
return {
getIndexPattern: () => 'foo-*',
getName: () => 'foo-*',
fields: {
getByName() {
return {

View file

@ -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(

View file

@ -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) => {

View file

@ -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}
/>

View file

@ -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(

View file

@ -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(

View file

@ -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}
/>

View file

@ -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);
});
});
});

View file

@ -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> {

View file

@ -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}
/>

View file

@ -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>

View file

@ -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>
);
}

View file

@ -27,6 +27,7 @@ const defaultProps = {
supportsClustering: true,
termFields: [],
numberOfJoins: 0,
hasSpatialJoins: false,
};
describe('scaling form', () => {

View file

@ -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>
);
}

View file

@ -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();
}
}

View file

@ -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';

View file

@ -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);
});

View file

@ -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;
}

View file

@ -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,

View file

@ -6,3 +6,4 @@
*/
export * from './es_term_source';
export { isTermSourceComplete } from './is_term_source_complete';

View file

@ -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;
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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>;
/*

View file

@ -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 [];

View file

@ -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;

View file

@ -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;
}

View file

@ -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"

View file

@ -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>
);

View file

@ -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();
});

View file

@ -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>
</>
);
}

View file

@ -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)}

View file

@ -44,7 +44,7 @@ function fieldsToOptions(
});
}
type Props = Omit<
export type Props = Omit<
EuiComboBoxProps<DataViewField>,
'isDisabled' | 'onChange' | 'options' | 'renderOption' | 'selectedOptions' | 'singleSelection'
> & {

View file

@ -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],
}

View file

@ -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 = {

View file

@ -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(),

View file

@ -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 {

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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();
});

View file

@ -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
);

View file

@ -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));
},
};

View file

@ -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();
});

View file

@ -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>
);
}

View file

@ -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 {}}
/>

View file

@ -3,6 +3,7 @@
background: tintOrShade($euiColorLightShade, 85%, 0);
border-radius: $euiBorderRadius;
padding: $euiSizeS $euiSizeXS;
margin-bottom: $euiSizeM;
.mapJoinItem__inner {
@include euiScrollBar;

View file

@ -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}

View file

@ -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>
);
}

View file

@ -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',
});
}

View file

@ -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()}

View file

@ -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';

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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';

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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">

View file

@ -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',
}),
};

View file

@ -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',

View file

@ -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;
}

View file

@ -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": {

View file

@ -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 daffichage",
"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",

View file

@ -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": "送信元",

View file

@ -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": "源",

View file

@ -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 },

View file

@ -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