mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[maps] allow by value styling for EMS boundary fields (#166306)
Closes https://github.com/elastic/kibana/issues/166305 PR: 1) adds getFields implementation to EMSFileSource so that fields can be used for by-value styling <img width="600" alt="Screen Shot 2023-09-12 at 4 20 44 PM" src="5540e18f
-4a0f-408a-91ed-f3cea5cc9747"> 2) removes `createFields` method from `IVectorStyle`. Duplicate of `getFieldByNamd` method. 3) Refactored EMSFileSource update editor to functional component and added loading state. <img width="497" alt="Screen Shot 2023-09-12 at 4 08 18 PM" src="d15fddd8
-af30-4c9b-8c93-6ab0a431bdcb"> ### Test instructions 1) create map and add "EMS boundaries layer". 2) Verify layer allows styling by-value for label with EMS property fields --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d67bafdc3e
commit
1abe8c02c3
12 changed files with 146 additions and 163 deletions
|
@ -27,7 +27,7 @@ const rightSource = {
|
|||
} as ESTermSourceDescriptor;
|
||||
|
||||
const mockSource = {
|
||||
createField({ fieldName }: { fieldName: string }) {
|
||||
getFieldByName(fieldName: string) {
|
||||
return {
|
||||
getName() {
|
||||
return fieldName;
|
||||
|
|
|
@ -56,13 +56,13 @@ export function createJoinSource(
|
|||
export class InnerJoin {
|
||||
private readonly _descriptor: Partial<JoinDescriptor>;
|
||||
private readonly _rightSource?: IJoinSource;
|
||||
private readonly _leftField?: IField;
|
||||
private readonly _leftField?: IField | null;
|
||||
|
||||
constructor(joinDescriptor: Partial<JoinDescriptor>, leftSource: IVectorSource) {
|
||||
this._descriptor = joinDescriptor;
|
||||
this._rightSource = createJoinSource(this._descriptor.right);
|
||||
this._leftField = joinDescriptor.leftField
|
||||
? leftSource.createField({ fieldName: joinDescriptor.leftField })
|
||||
? leftSource.getFieldByName(joinDescriptor.leftField)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ const mockVectorSource = {
|
|||
getInspectorAdapters: () => {
|
||||
return undefined;
|
||||
},
|
||||
createField: () => {
|
||||
getFieldByName: () => {
|
||||
return {
|
||||
getName: () => {
|
||||
return LEFT_FIELD;
|
||||
|
|
|
@ -66,18 +66,10 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc
|
|||
super(EMSFileSource.createDescriptor(descriptor));
|
||||
this._descriptor = EMSFileSource.createDescriptor(descriptor);
|
||||
this._tooltipFields = this._descriptor.tooltipProperties.map((propertyKey) =>
|
||||
this.createField({ fieldName: propertyKey })
|
||||
this.getFieldByName(propertyKey)
|
||||
);
|
||||
}
|
||||
|
||||
createField({ fieldName }: { fieldName: string }): IField {
|
||||
return new EMSFileField({
|
||||
fieldName,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
});
|
||||
}
|
||||
|
||||
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement<any> | null {
|
||||
return (
|
||||
<UpdateSourceEditor
|
||||
|
@ -188,10 +180,22 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc
|
|||
};
|
||||
}
|
||||
|
||||
async getLeftJoinFields() {
|
||||
async getFields(): Promise<IField[]> {
|
||||
const emsFileLayer = await this.getEMSFileLayer();
|
||||
const fields = emsFileLayer.getFieldsInLanguage();
|
||||
return fields.map((f) => this.createField({ fieldName: f.name }));
|
||||
return fields.map((f) => this.getFieldByName(f.name));
|
||||
}
|
||||
|
||||
getFieldByName(fieldName: string): IField {
|
||||
return new EMSFileField({
|
||||
fieldName,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
});
|
||||
}
|
||||
|
||||
async getLeftJoinFields() {
|
||||
return this.getFields();
|
||||
}
|
||||
|
||||
hasTooltipProperties() {
|
||||
|
@ -216,4 +220,38 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc
|
|||
const emsSettings = getEMSSettings();
|
||||
return emsSettings.isEMSUrlSet() ? [LICENSED_FEATURES.ON_PREM_EMS] : [];
|
||||
}
|
||||
|
||||
private async _getFieldValues(fieldName: string): Promise<string[]> {
|
||||
try {
|
||||
const emsFileLayer = await this.getEMSFileLayer();
|
||||
const targetEmsField = emsFileLayer.getFields().find(({ id }) => id === fieldName);
|
||||
if (targetEmsField?.values?.length) {
|
||||
return targetEmsField.values;
|
||||
}
|
||||
|
||||
// Fallback to pulling values from feature properties when values are not available in file definition
|
||||
const valuesSet = new Set<string>(); // use set to avoid duplicate values
|
||||
const featureCollection = await emsFileLayer.getGeoJson();
|
||||
featureCollection?.features.forEach((feature) => {
|
||||
if (
|
||||
feature.properties &&
|
||||
fieldName in feature.properties &&
|
||||
feature.properties[fieldName] != null
|
||||
) {
|
||||
valuesSet.add(feature.properties[fieldName].toString());
|
||||
}
|
||||
});
|
||||
return Array.from(valuesSet);
|
||||
} catch (error) {
|
||||
// ignore errors
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getValueSuggestions = async (field: IField, query: string): Promise<string[]> => {
|
||||
const values = await this._getFieldValues(field.getName());
|
||||
return query.length
|
||||
? values.filter((value) => value.toLowerCase().includes(query.toLowerCase()))
|
||||
: values;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiTitle, EuiPanel, EuiSkeletonText, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { TooltipSelector } from '../../../components/tooltip_selector';
|
||||
import { getEmsFileLayers } from '../../../util';
|
||||
import { IEmsFileSource } from './ems_file_source';
|
||||
import { IField } from '../../fields/field';
|
||||
import { OnSourceChangeArgs } from '../source';
|
||||
|
@ -21,74 +20,62 @@ interface Props {
|
|||
tooltipFields: IField[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
fields: IField[] | null;
|
||||
}
|
||||
export function UpdateSourceEditor(props: Props) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [fields, setFields] = useState<IField[]>([]);
|
||||
|
||||
export class UpdateSourceEditor extends Component<Props, State> {
|
||||
private _isMounted: boolean = false;
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
setIsLoading(true);
|
||||
props.source
|
||||
.getFields()
|
||||
.then((nextFields) => {
|
||||
if (!ignore) {
|
||||
setFields(nextFields);
|
||||
setIsLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!ignore) {
|
||||
// When a matching EMS-config cannot be found, the source already will have thrown errors during the data request.
|
||||
// This will propagate to the vector-layer and be displayed in the UX
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
state = {
|
||||
fields: null,
|
||||
};
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
// only run onMount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.loadFields();
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.emsSource.tooltipsTitle"
|
||||
defaultMessage="Tooltip fields"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
async loadFields() {
|
||||
let fields: IField[] = [];
|
||||
try {
|
||||
const emsFiles = await getEmsFileLayers();
|
||||
const targetEmsFile = emsFiles.find((emsFile) => emsFile.getId() === this.props.layerId);
|
||||
if (targetEmsFile) {
|
||||
fields = targetEmsFile
|
||||
.getFieldsInLanguage()
|
||||
.map((field) => this.props.source.createField({ fieldName: field.name }));
|
||||
}
|
||||
} catch (e) {
|
||||
// When a matching EMS-config cannot be found, the source already will have thrown errors during the data request.
|
||||
// This will propagate to the vector-layer and be displayed in the UX
|
||||
}
|
||||
|
||||
if (this._isMounted) {
|
||||
this.setState({ fields });
|
||||
}
|
||||
}
|
||||
|
||||
_onTooltipPropertiesSelect = (selectedFieldNames: string[]) => {
|
||||
this.props.onChange({ propName: 'tooltipProperties', value: selectedFieldNames });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.emsSource.tooltipsTitle"
|
||||
defaultMessage="Tooltip fields"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiSkeletonText isLoading={isLoading}>
|
||||
<TooltipSelector
|
||||
tooltipFields={this.props.tooltipFields}
|
||||
onChange={this._onTooltipPropertiesSelect}
|
||||
fields={this.state.fields}
|
||||
tooltipFields={props.tooltipFields}
|
||||
onChange={(selectedFieldNames: string[]) => {
|
||||
props.onChange({ propName: 'tooltipProperties', value: selectedFieldNames });
|
||||
}}
|
||||
fields={fields}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiSkeletonText>
|
||||
</EuiPanel>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -152,19 +152,11 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
|
|||
this._descriptor = sourceDescriptor;
|
||||
this._tooltipFields = this._descriptor.tooltipProperties
|
||||
? this._descriptor.tooltipProperties.map((property) => {
|
||||
return this.createField({ fieldName: property });
|
||||
return this.getFieldByName(property);
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
createField({ fieldName }: { fieldName: string }): ESDocField {
|
||||
return new ESDocField({
|
||||
fieldName,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
});
|
||||
}
|
||||
|
||||
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> | null {
|
||||
if (this._isTopHits()) {
|
||||
return (
|
||||
|
@ -213,7 +205,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
|
|||
});
|
||||
|
||||
return fields.map((field): IField => {
|
||||
return this.createField({ fieldName: field.name });
|
||||
return this.getFieldByName(field.name);
|
||||
});
|
||||
} catch (error) {
|
||||
// failed index-pattern retrieval will show up as error-message in the layer-toc-entry
|
||||
|
@ -221,6 +213,14 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
|
|||
}
|
||||
}
|
||||
|
||||
getFieldByName(fieldName: string): ESDocField {
|
||||
return new ESDocField({
|
||||
fieldName,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
});
|
||||
}
|
||||
|
||||
isMvt() {
|
||||
return this._descriptor.scalingType === SCALING_TYPES.MVT;
|
||||
}
|
||||
|
@ -747,7 +747,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
|
|||
const indexPattern = await this.getIndexPattern();
|
||||
// Left fields are retrieved from _source.
|
||||
return getSourceFields(indexPattern.fields).map((field): IField => {
|
||||
return this.createField({ fieldName: field.name });
|
||||
return this.getFieldByName(field.name);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -58,44 +58,33 @@ export class GeoJsonFileSource extends AbstractVectorSource {
|
|||
super(normalizedDescriptor);
|
||||
}
|
||||
|
||||
_getFields(): InlineFieldDescriptor[] {
|
||||
private _getFieldDescriptors(): InlineFieldDescriptor[] {
|
||||
const fields = (this._descriptor as GeojsonFileSourceDescriptor).__fields;
|
||||
return fields ? fields : [];
|
||||
}
|
||||
|
||||
createField({ fieldName }: { fieldName: string }): IField {
|
||||
const fields = this._getFields();
|
||||
const descriptor: InlineFieldDescriptor | undefined = fields.find((field) => {
|
||||
return field.name === fieldName;
|
||||
});
|
||||
|
||||
if (!descriptor) {
|
||||
throw new Error(
|
||||
`Cannot find corresponding field ${fieldName} in __fields array ${JSON.stringify(
|
||||
this._getFields()
|
||||
)} `
|
||||
);
|
||||
}
|
||||
private _createField(fieldDescriptor: InlineFieldDescriptor): IField {
|
||||
return new InlineField<GeoJsonFileSource>({
|
||||
fieldName: descriptor.name,
|
||||
fieldName: fieldDescriptor.name,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
dataType: descriptor.type,
|
||||
dataType: fieldDescriptor.type,
|
||||
});
|
||||
}
|
||||
|
||||
async getFields(): Promise<IField[]> {
|
||||
const fields = this._getFields();
|
||||
return fields.map((field: InlineFieldDescriptor) => {
|
||||
return new InlineField<GeoJsonFileSource>({
|
||||
fieldName: field.name,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
dataType: field.type,
|
||||
});
|
||||
return this._getFieldDescriptors().map((fieldDescriptor: InlineFieldDescriptor) => {
|
||||
return this._createField(fieldDescriptor);
|
||||
});
|
||||
}
|
||||
|
||||
getFieldByName(fieldName: string): IField | null {
|
||||
const fieldDescriptor = this._getFieldDescriptors().find((findFieldDescriptor) => {
|
||||
return findFieldDescriptor.name === fieldName;
|
||||
});
|
||||
return fieldDescriptor ? this._createField(fieldDescriptor) : null;
|
||||
}
|
||||
|
||||
isBoundsAware(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -139,14 +139,6 @@ export class TableSource extends AbstractVectorSource implements ITermJoinSource
|
|||
return false;
|
||||
}
|
||||
|
||||
createField({ fieldName }: { fieldName: string }): IField {
|
||||
const field = this.getFieldByName(fieldName);
|
||||
if (!field) {
|
||||
throw new Error(`Cannot find field for ${fieldName}`);
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
async getBoundsForFilters(
|
||||
boundsFilters: BoundsRequestMeta,
|
||||
registerCancelCallback: (callback: () => void) => void
|
||||
|
|
|
@ -112,26 +112,17 @@ export class MVTSingleLayerVectorSource extends AbstractSource implements IMvtVe
|
|||
}
|
||||
|
||||
getFieldByName(fieldName: string): MVTField | null {
|
||||
try {
|
||||
return this.createField({ fieldName });
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
createField({ fieldName }: { fieldName: string }): MVTField {
|
||||
const field = this._descriptor.fields.find((f: MVTFieldDescriptor) => {
|
||||
return f.name === fieldName;
|
||||
});
|
||||
if (!field) {
|
||||
throw new Error(`Cannot create field for fieldName ${fieldName}`);
|
||||
}
|
||||
return new MVTField({
|
||||
fieldName: field.name,
|
||||
type: field.type,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
});
|
||||
return field
|
||||
? new MVTField({
|
||||
fieldName: field.name,
|
||||
type: field.type,
|
||||
source: this,
|
||||
origin: FIELD_ORIGIN.SOURCE,
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
getGeoJsonWithMeta(): Promise<GeoJsonWithMeta> {
|
||||
|
|
|
@ -113,7 +113,6 @@ export interface IVectorSource extends ISource {
|
|||
*/
|
||||
getSyncMeta(dataFilters: DataFilters): object | null;
|
||||
|
||||
createField({ fieldName }: { fieldName: string }): IField;
|
||||
hasTooltipProperties(): boolean;
|
||||
getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]>;
|
||||
isBoundsAware(): boolean;
|
||||
|
@ -143,14 +142,6 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
|
|||
return false;
|
||||
}
|
||||
|
||||
createField({ fieldName }: { fieldName: string }): IField {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
getFieldByName(fieldName: string): IField | null {
|
||||
return this.createField({ fieldName });
|
||||
}
|
||||
|
||||
isFilterByMapBounds() {
|
||||
return false;
|
||||
}
|
||||
|
@ -174,6 +165,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
|
|||
return [];
|
||||
}
|
||||
|
||||
getFieldByName(fieldName: string): IField | null {
|
||||
throw new Error('Must implement VectorSource#getFieldByName');
|
||||
}
|
||||
|
||||
async getLeftJoinFields(): Promise<IField[]> {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -26,9 +26,6 @@ class MockSource {
|
|||
getFieldByName(fieldName) {
|
||||
return new MockField({ fieldName });
|
||||
}
|
||||
createField({ fieldName }) {
|
||||
return new MockField({ fieldName });
|
||||
}
|
||||
}
|
||||
|
||||
describe('getDescriptorWithUpdatedStyleProps', () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FileLayer } from '@elastic/ems-client';
|
||||
import type { FileLayer, FileLayerField } from '@elastic/ems-client';
|
||||
import { getEmsFileLayers } from '../util';
|
||||
|
||||
export interface SampleValuesConfig {
|
||||
|
@ -23,12 +23,6 @@ interface UniqueMatch {
|
|||
config: EMSTermJoinConfig;
|
||||
count: number;
|
||||
}
|
||||
interface FileLayerFieldShim {
|
||||
id: string;
|
||||
values?: string[];
|
||||
regex?: string;
|
||||
alias?: string[];
|
||||
}
|
||||
|
||||
export async function suggestEMSTermJoinConfig(
|
||||
sampleValuesConfig: SampleValuesConfig
|
||||
|
@ -94,8 +88,8 @@ function suggestByName(
|
|||
): EMSTermJoinConfig[] {
|
||||
const matches: EMSTermJoinConfig[] = [];
|
||||
fileLayers.forEach((fileLayer) => {
|
||||
const emsFields: FileLayerFieldShim[] = fileLayer.getFields();
|
||||
emsFields.forEach((emsField: FileLayerFieldShim) => {
|
||||
const emsFields: FileLayerField[] = fileLayer.getFields();
|
||||
emsFields.forEach((emsField: FileLayerField) => {
|
||||
if (!emsField.alias || !emsField.alias.length) {
|
||||
return;
|
||||
}
|
||||
|
@ -148,8 +142,8 @@ function suggestByIdValues(
|
|||
): EMSTermJoinConfig[] {
|
||||
const matches: EMSTermJoinConfig[] = [];
|
||||
fileLayers.forEach((fileLayer) => {
|
||||
const emsFields: FileLayerFieldShim[] = fileLayer.getFields();
|
||||
emsFields.forEach((emsField: FileLayerFieldShim) => {
|
||||
const emsFields: FileLayerField[] = fileLayer.getFields();
|
||||
emsFields.forEach((emsField: FileLayerField) => {
|
||||
if (!emsField.values || !emsField.values.length) {
|
||||
return;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue