[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:
Nathan Reese 2023-09-13 12:51:16 -06:00 committed by GitHub
parent d67bafdc3e
commit 1abe8c02c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 146 additions and 163 deletions

View file

@ -27,7 +27,7 @@ const rightSource = {
} as ESTermSourceDescriptor;
const mockSource = {
createField({ fieldName }: { fieldName: string }) {
getFieldByName(fieldName: string) {
return {
getName() {
return fieldName;

View file

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

View file

@ -69,7 +69,7 @@ const mockVectorSource = {
getInspectorAdapters: () => {
return undefined;
},
createField: () => {
getFieldByName: () => {
return {
getName: () => {
return LEFT_FIELD;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,9 +26,6 @@ class MockSource {
getFieldByName(fieldName) {
return new MockField({ fieldName });
}
createField({ fieldName }) {
return new MockField({ fieldName });
}
}
describe('getDescriptorWithUpdatedStyleProps', () => {

View file

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