mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Maps] Show joins disabled message (#70826)
Show feedback in the layer-settings when the scaling-method does not support Term-joins.
This commit is contained in:
parent
2c19feb55f
commit
c44f019790
19 changed files with 383 additions and 169 deletions
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
import { RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants';
|
||||
import { RENDER_AS, SORT_ORDER, SCALING_TYPES, SOURCE_TYPES } from '../constants';
|
||||
import { MapExtent, MapQuery } from './map_descriptor';
|
||||
import { Filter, TimeRange } from '../../../../../src/plugins/data/common';
|
||||
|
||||
|
@ -26,10 +26,12 @@ type ESSearchSourceSyncMeta = {
|
|||
scalingType: SCALING_TYPES;
|
||||
topHitsSplitField: string;
|
||||
topHitsSize: number;
|
||||
sourceType: SOURCE_TYPES.ES_SEARCH;
|
||||
};
|
||||
|
||||
type ESGeoGridSourceSyncMeta = {
|
||||
requestType: RENDER_AS;
|
||||
sourceType: SOURCE_TYPES.ES_GEO_GRID;
|
||||
};
|
||||
|
||||
export type VectorSourceSyncMeta = ESSearchSourceSyncMeta | ESGeoGridSourceSyncMeta | null;
|
||||
|
@ -51,7 +53,6 @@ export type VectorStyleRequestMeta = MapFilters & {
|
|||
|
||||
export type ESSearchSourceResponseMeta = {
|
||||
areResultsTrimmed?: boolean;
|
||||
sourceType?: string;
|
||||
|
||||
// top hits meta
|
||||
areEntitiesTrimmed?: boolean;
|
||||
|
|
|
@ -77,8 +77,8 @@ export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & {
|
|||
};
|
||||
|
||||
export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & {
|
||||
indexPatternTitle: string;
|
||||
term: string; // term field name
|
||||
indexPatternTitle?: string;
|
||||
term?: string; // term field name
|
||||
whereQuery?: Query;
|
||||
};
|
||||
|
||||
|
@ -138,7 +138,7 @@ export type GeojsonFileSourceDescriptor = {
|
|||
};
|
||||
|
||||
export type JoinDescriptor = {
|
||||
leftField: string;
|
||||
leftField?: string;
|
||||
right: ESTermSourceDescriptor;
|
||||
};
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ function getClusterStyleDescriptor(
|
|||
),
|
||||
}
|
||||
: undefined;
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
clusterStyleDescriptor.properties[styleName] = {
|
||||
type: STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
|
@ -136,7 +136,7 @@ function getClusterStyleDescriptor(
|
|||
};
|
||||
} else {
|
||||
// copy static styles to cluster style
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
clusterStyleDescriptor.properties[styleName] = {
|
||||
type: STYLE_TYPE.STATIC,
|
||||
options: { ...styleProperty.getOptions() },
|
||||
|
@ -192,8 +192,8 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
|
|||
const requestMeta = sourceDataRequest.getMeta();
|
||||
if (
|
||||
requestMeta &&
|
||||
requestMeta.sourceType &&
|
||||
requestMeta.sourceType === SOURCE_TYPES.ES_GEO_GRID
|
||||
requestMeta.sourceMeta &&
|
||||
requestMeta.sourceMeta.sourceType === SOURCE_TYPES.ES_GEO_GRID
|
||||
) {
|
||||
isClustered = true;
|
||||
}
|
||||
|
@ -220,8 +220,12 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
|
|||
: displayName;
|
||||
}
|
||||
|
||||
isJoinable() {
|
||||
return false;
|
||||
showJoinEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getJoinsDisabledReason() {
|
||||
return this._documentSource.getJoinsDisabledReason();
|
||||
}
|
||||
|
||||
getJoins() {
|
||||
|
|
|
@ -78,6 +78,8 @@ export interface ILayer {
|
|||
isPreviewLayer: () => boolean;
|
||||
areLabelsOnTop: () => boolean;
|
||||
supportsLabelsOnTop: () => boolean;
|
||||
showJoinEditor(): boolean;
|
||||
getJoinsDisabledReason(): string | null;
|
||||
}
|
||||
export type Footnote = {
|
||||
icon: ReactElement<any>;
|
||||
|
@ -141,13 +143,12 @@ export class AbstractLayer implements ILayer {
|
|||
}
|
||||
|
||||
static getBoundDataForSource(mbMap: unknown, sourceId: string): FeatureCollection {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
const mbStyle = mbMap.getStyle();
|
||||
return mbStyle.sources[sourceId].data;
|
||||
}
|
||||
|
||||
async cloneDescriptor(): Promise<LayerDescriptor> {
|
||||
// @ts-ignore
|
||||
const clonedDescriptor = copyPersistentState(this._descriptor);
|
||||
// layer id is uuid used to track styles/layers in mapbox
|
||||
clonedDescriptor.id = uuid();
|
||||
|
@ -155,14 +156,10 @@ export class AbstractLayer implements ILayer {
|
|||
clonedDescriptor.label = `Clone of ${displayName}`;
|
||||
clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor();
|
||||
|
||||
// todo: remove this
|
||||
// This should not be in AbstractLayer. It relies on knowledge of VectorLayerDescriptor
|
||||
// @ts-ignore
|
||||
if (clonedDescriptor.joins) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
clonedDescriptor.joins.forEach((joinDescriptor) => {
|
||||
// right.id is uuid used to track requests in inspector
|
||||
// @ts-ignore
|
||||
joinDescriptor.right.id = uuid();
|
||||
});
|
||||
}
|
||||
|
@ -173,8 +170,12 @@ export class AbstractLayer implements ILayer {
|
|||
return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`;
|
||||
}
|
||||
|
||||
isJoinable(): boolean {
|
||||
return this.getSource().isJoinable();
|
||||
showJoinEditor(): boolean {
|
||||
return this.getSource().showJoinEditor();
|
||||
}
|
||||
|
||||
getJoinsDisabledReason() {
|
||||
return this.getSource().getJoinsDisabledReason();
|
||||
}
|
||||
|
||||
isPreviewLayer(): boolean {
|
||||
|
@ -394,7 +395,6 @@ export class AbstractLayer implements ILayer {
|
|||
const requestTokens = this._dataRequests.map((dataRequest) => dataRequest.getRequestToken());
|
||||
|
||||
// Compact removes all the undefineds
|
||||
// @ts-ignore
|
||||
return _.compact(requestTokens);
|
||||
}
|
||||
|
||||
|
@ -478,7 +478,7 @@ export class AbstractLayer implements ILayer {
|
|||
}
|
||||
|
||||
syncVisibilityWithMb(mbMap: unknown, mbLayerId: string) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
|
|||
getSyncMeta() {
|
||||
return {
|
||||
requestType: this._descriptor.requestType,
|
||||
sourceType: SOURCE_TYPES.ES_GEO_GRID,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -103,7 +104,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
|
|||
return true;
|
||||
}
|
||||
|
||||
isJoinable() {
|
||||
showJoinEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -307,7 +308,6 @@ export class ESGeoGridSource extends AbstractESAggSource {
|
|||
},
|
||||
meta: {
|
||||
areResultsTrimmed: false,
|
||||
sourceType: SOURCE_TYPES.ES_GEO_GRID,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export class ESPewPewSource extends AbstractESAggSource {
|
|||
return true;
|
||||
}
|
||||
|
||||
isJoinable() {
|
||||
showJoinEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -385,7 +385,7 @@ export class ESSearchSource extends AbstractESSource {
|
|||
|
||||
return {
|
||||
data: featureCollection,
|
||||
meta: { ...meta, sourceType: SOURCE_TYPES.ES_SEARCH },
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -540,6 +540,7 @@ export class ESSearchSource extends AbstractESSource {
|
|||
scalingType: this._descriptor.scalingType,
|
||||
topHitsSplitField: this._descriptor.topHitsSplitField,
|
||||
topHitsSize: this._descriptor.topHitsSize,
|
||||
sourceType: SOURCE_TYPES.ES_SEARCH,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -551,6 +552,14 @@ export class ESSearchSource extends AbstractESSource {
|
|||
path: geoField.name,
|
||||
};
|
||||
}
|
||||
|
||||
getJoinsDisabledReason() {
|
||||
return this._descriptor.scalingType === SCALING_TYPES.CLUSTERS
|
||||
? i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', {
|
||||
defaultMessage: 'Joins are not supported when scaling by clusters',
|
||||
})
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
registerSource({
|
||||
|
|
|
@ -54,7 +54,8 @@ export interface ISource {
|
|||
isESSource(): boolean;
|
||||
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement<any> | null;
|
||||
supportsFitToBounds(): Promise<boolean>;
|
||||
isJoinable(): boolean;
|
||||
showJoinEditor(): boolean;
|
||||
getJoinsDisabledReason(): string | null;
|
||||
cloneDescriptor(): SourceDescriptor;
|
||||
getFieldNames(): string[];
|
||||
getApplyGlobalQuery(): boolean;
|
||||
|
@ -80,7 +81,6 @@ export class AbstractSource implements ISource {
|
|||
destroy(): void {}
|
||||
|
||||
cloneDescriptor(): SourceDescriptor {
|
||||
// @ts-ignore
|
||||
return copyPersistentState(this._descriptor);
|
||||
}
|
||||
|
||||
|
@ -148,10 +148,14 @@ export class AbstractSource implements ISource {
|
|||
return 0;
|
||||
}
|
||||
|
||||
isJoinable(): boolean {
|
||||
showJoinEditor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getJoinsDisabledReason() {
|
||||
return null;
|
||||
}
|
||||
|
||||
isESSource(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ export class AbstractVectorSource extends AbstractSource {
|
|||
return false;
|
||||
}
|
||||
|
||||
isJoinable() {
|
||||
showJoinEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -96,8 +96,8 @@ exports[`LayerPanel is rendered 1`] = `
|
|||
"getId": [Function],
|
||||
"getImmutableSourceProperties": [Function],
|
||||
"getLayerTypeIconName": [Function],
|
||||
"isJoinable": [Function],
|
||||
"renderSourceSettingsEditor": [Function],
|
||||
"showJoinEditor": [Function],
|
||||
"supportsElasticsearchFilters": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -107,6 +107,17 @@ exports[`LayerPanel is rendered 1`] = `
|
|||
</div>
|
||||
<EuiPanel>
|
||||
<JoinEditor
|
||||
layer={
|
||||
Object {
|
||||
"getDisplayName": [Function],
|
||||
"getId": [Function],
|
||||
"getImmutableSourceProperties": [Function],
|
||||
"getLayerTypeIconName": [Function],
|
||||
"renderSourceSettingsEditor": [Function],
|
||||
"showJoinEditor": [Function],
|
||||
"supportsElasticsearchFilters": [Function],
|
||||
}
|
||||
}
|
||||
layerDisplayName="layer 1"
|
||||
leftJoinFields={Array []}
|
||||
/>
|
||||
|
|
|
@ -12,7 +12,7 @@ import { updateSourceProp } from '../../actions';
|
|||
function mapStateToProps(state = {}) {
|
||||
const selectedLayer = getSelectedLayer(state);
|
||||
return {
|
||||
key: selectedLayer ? `${selectedLayer.getId()}${selectedLayer.isJoinable()}` : '',
|
||||
key: selectedLayer ? `${selectedLayer.getId()}${selectedLayer.showJoinEditor()}` : '',
|
||||
selectedLayer,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Should render callout when joins are disabled 1`] = `
|
||||
<div>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
>
|
||||
<h5>
|
||||
<EuiToolTip
|
||||
content="Use term joins to augment this layer with properties for data driven styling."
|
||||
delay="regular"
|
||||
position="top"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Term joins"
|
||||
id="xpack.maps.layerPanel.joinEditor.termJoinsTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
>
|
||||
Simulated disabled reason
|
||||
</EuiCallOut>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Should render join editor 1`] = `
|
||||
<div>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
>
|
||||
<h5>
|
||||
<EuiToolTip
|
||||
content="Use term joins to augment this layer with properties for data driven styling."
|
||||
delay="regular"
|
||||
position="top"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Term joins"
|
||||
id="xpack.maps.layerPanel.joinEditor.termJoinsTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<Join
|
||||
join={
|
||||
Object {
|
||||
"leftField": "iso2",
|
||||
"right": Object {
|
||||
"id": "673ff994-fc75-4c67-909b-69fcb0e1060e",
|
||||
"indexPatternId": "abcde",
|
||||
"indexPatternTitle": "kibana_sample_data_logs",
|
||||
"metrics": Array [
|
||||
Object {
|
||||
"label": "web logs count",
|
||||
"type": "count",
|
||||
},
|
||||
],
|
||||
"term": "geo.src",
|
||||
},
|
||||
}
|
||||
}
|
||||
layer={
|
||||
MockLayer {
|
||||
"_disableReason": null,
|
||||
}
|
||||
}
|
||||
leftFields={Array []}
|
||||
leftSourceName="myLeftJoinField"
|
||||
onChange={[Function]}
|
||||
onRemove={[Function]}
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiTextAlign
|
||||
textAlign="center"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
aria-label="Add join"
|
||||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add join"
|
||||
id="xpack.maps.layerPanel.joinEditor.addJoinButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiTextAlign>
|
||||
</div>
|
||||
`;
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { JoinEditor } from './view';
|
||||
import {
|
||||
getSelectedLayer,
|
||||
getSelectedLayerJoinDescriptors,
|
||||
} from '../../../selectors/map_selectors';
|
||||
import { setJoinsForLayer } from '../../../actions';
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onChange: (layer, joins) => {
|
||||
dispatch(setJoinsForLayer(layer, joins));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
joins: getSelectedLayerJoinDescriptors(state),
|
||||
layer: getSelectedLayer(state),
|
||||
};
|
||||
}
|
||||
|
||||
const connectedJoinEditor = connect(mapStateToProps, mapDispatchToProps)(JoinEditor);
|
||||
export { connectedJoinEditor as JoinEditor };
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AnyAction, Dispatch } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { JoinEditor } from './join_editor';
|
||||
import { getSelectedLayerJoinDescriptors } from '../../../selectors/map_selectors';
|
||||
import { setJoinsForLayer } from '../../../actions';
|
||||
import { MapStoreState } from '../../../reducers/store';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
import { JoinDescriptor } from '../../../../common/descriptor_types';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
joins: getSelectedLayerJoinDescriptors(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
|
||||
return {
|
||||
onChange: (layer: ILayer, joins: JoinDescriptor[]) => {
|
||||
dispatch<any>(setJoinsForLayer(layer, joins));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const connectedJoinEditor = connect(mapStateToProps, mapDispatchToProps)(JoinEditor);
|
||||
export { connectedJoinEditor as JoinEditor };
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
import { JoinEditor } from './join_editor';
|
||||
import { shallow } from 'enzyme';
|
||||
import { JoinDescriptor } from '../../../../common/descriptor_types';
|
||||
|
||||
class MockLayer {
|
||||
private readonly _disableReason: string | null;
|
||||
|
||||
constructor(disableReason: string | null) {
|
||||
this._disableReason = disableReason;
|
||||
}
|
||||
|
||||
getJoinsDisabledReason() {
|
||||
return this._disableReason;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
joins: [
|
||||
{
|
||||
leftField: 'iso2',
|
||||
right: {
|
||||
id: '673ff994-fc75-4c67-909b-69fcb0e1060e',
|
||||
indexPatternTitle: 'kibana_sample_data_logs',
|
||||
term: 'geo.src',
|
||||
indexPatternId: 'abcde',
|
||||
metrics: [
|
||||
{
|
||||
type: 'count',
|
||||
label: 'web logs count',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as JoinDescriptor,
|
||||
],
|
||||
layerDisplayName: 'myLeftJoinField',
|
||||
leftJoinFields: [],
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
test('Should render join editor', () => {
|
||||
const component = shallow(
|
||||
<JoinEditor {...defaultProps} layer={(new MockLayer(null) as unknown) as ILayer} />
|
||||
);
|
||||
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 ILayer}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiToolTip,
|
||||
EuiTextAlign,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-expect-error
|
||||
import { Join } from './resources/join';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
import { JoinDescriptor } from '../../../../common/descriptor_types';
|
||||
import { IField } from '../../../classes/fields/field';
|
||||
|
||||
interface Props {
|
||||
joins: JoinDescriptor[];
|
||||
layer: ILayer;
|
||||
layerDisplayName: string;
|
||||
leftJoinFields: IField[];
|
||||
onChange: (layer: ILayer, joins: JoinDescriptor[]) => void;
|
||||
}
|
||||
|
||||
export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }: Props) {
|
||||
const renderJoins = () => {
|
||||
return joins.map((joinDescriptor: JoinDescriptor, index: number) => {
|
||||
const handleOnChange = (updatedDescriptor: JoinDescriptor) => {
|
||||
onChange(layer, [...joins.slice(0, index), updatedDescriptor, ...joins.slice(index + 1)]);
|
||||
};
|
||||
|
||||
const handleOnRemove = () => {
|
||||
onChange(layer, [...joins.slice(0, index), ...joins.slice(index + 1)]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<EuiSpacer size="m" />
|
||||
<Join
|
||||
join={joinDescriptor}
|
||||
layer={layer}
|
||||
onChange={handleOnChange}
|
||||
onRemove={handleOnRemove}
|
||||
leftFields={leftJoinFields}
|
||||
leftSourceName={layerDisplayName}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const addJoin = () => {
|
||||
onChange(layer, [
|
||||
...joins,
|
||||
{
|
||||
right: {
|
||||
id: uuid(),
|
||||
applyGlobalQuery: true,
|
||||
},
|
||||
} as JoinDescriptor,
|
||||
]);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
const disabledReason = layer.getJoinsDisabledReason();
|
||||
return disabledReason ? (
|
||||
<EuiCallOut color="warning">{disabledReason}</EuiCallOut>
|
||||
) : (
|
||||
<Fragment>
|
||||
{renderJoins()}
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiTextAlign textAlign="center">
|
||||
<EuiButtonEmpty
|
||||
onClick={addJoin}
|
||||
size="xs"
|
||||
iconType="plusInCircle"
|
||||
aria-label={i18n.translate('xpack.maps.layerPanel.joinEditor.addJoinAriaLabel', {
|
||||
defaultMessage: 'Add join',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.joinEditor.addJoinButtonLabel"
|
||||
defaultMessage="Add join"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiTextAlign>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.maps.layerPanel.joinEditor.termJoinTooltip', {
|
||||
defaultMessage:
|
||||
'Use term joins to augment this layer with properties for data driven styling.',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.joinEditor.termJoinsTitle"
|
||||
defaultMessage="Term joins"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,103 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Join } from './resources/join';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }) {
|
||||
const renderJoins = () => {
|
||||
return joins.map((joinDescriptor, index) => {
|
||||
const handleOnChange = (updatedDescriptor) => {
|
||||
onChange(layer, [...joins.slice(0, index), updatedDescriptor, ...joins.slice(index + 1)]);
|
||||
};
|
||||
|
||||
const handleOnRemove = () => {
|
||||
onChange(layer, [...joins.slice(0, index), ...joins.slice(index + 1)]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<EuiSpacer size="m" />
|
||||
<Join
|
||||
join={joinDescriptor}
|
||||
layer={layer}
|
||||
onChange={handleOnChange}
|
||||
onRemove={handleOnRemove}
|
||||
leftFields={leftJoinFields}
|
||||
leftSourceName={layerDisplayName}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const addJoin = () => {
|
||||
onChange(layer, [
|
||||
...joins,
|
||||
{
|
||||
right: {
|
||||
id: uuid(),
|
||||
applyGlobalQuery: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
if (!layer.isJoinable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.maps.layerPanel.joinEditor.termJoinTooltip', {
|
||||
defaultMessage:
|
||||
'Use term joins to augment this layer with properties for data driven styling.',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.joinEditor.termJoinsTitle"
|
||||
defaultMessage="Term joins"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircle"
|
||||
onClick={addJoin}
|
||||
aria-label={i18n.translate('xpack.maps.layerPanel.joinEditor.addJoinAriaLabel', {
|
||||
defaultMessage: 'Add join',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.layerPanel.joinEditor.addJoinButtonLabel', {
|
||||
defaultMessage: 'Add join',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{renderJoins()}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -75,7 +75,7 @@ export class LayerPanel extends React.Component {
|
|||
};
|
||||
|
||||
async _loadLeftJoinFields() {
|
||||
if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) {
|
||||
if (!this.props.selectedLayer || !this.props.selectedLayer.showJoinEditor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -120,7 +120,7 @@ export class LayerPanel extends React.Component {
|
|||
}
|
||||
|
||||
_renderJoinSection() {
|
||||
if (!this.props.selectedLayer.isJoinable()) {
|
||||
if (!this.props.selectedLayer.showJoinEditor()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -128,6 +128,7 @@ export class LayerPanel extends React.Component {
|
|||
<Fragment>
|
||||
<EuiPanel>
|
||||
<JoinEditor
|
||||
layer={this.props.selectedLayer}
|
||||
leftJoinFields={this.state.leftJoinFields}
|
||||
layerDisplayName={this.state.displayName}
|
||||
/>
|
||||
|
|
|
@ -55,7 +55,7 @@ const mockLayer = {
|
|||
getImmutableSourceProperties: () => {
|
||||
return [{ label: 'source prop1', value: 'you get one chance to set me' }];
|
||||
},
|
||||
isJoinable: () => {
|
||||
showJoinEditor: () => {
|
||||
return true;
|
||||
},
|
||||
supportsElasticsearchFilters: () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue