[Maps] convert EMS file source to TS (#65373)

* [Maps] convert EMS file source to TS

* ts more ems_file_source

* ts lint cleanup

* more ts clean up

* clean up some ts-ignores

* review feedback
This commit is contained in:
Nathan Reese 2020-05-06 12:28:30 -06:00 committed by GitHub
parent 552bac5dc7
commit 2eaa074923
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 449 additions and 351 deletions

View file

@ -28,7 +28,7 @@ export type EMSTMSSourceDescriptor = AbstractSourceDescriptor & {
export type EMSFileSourceDescriptor = AbstractSourceDescriptor & {
// id: EMS file id
id: string;
tooltipProperties: string[];
};

View file

@ -1,3 +1,3 @@
@import 'metric_editors';
@import './geometry_filter';
@import 'tooltip_selector';
@import 'tooltip_selector/tooltip_selector';

View file

@ -1,229 +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, { Component } from 'react';
import classNames from 'classnames';
import {
EuiButtonIcon,
EuiDragDropContext,
EuiDraggable,
EuiDroppable,
EuiText,
EuiTextAlign,
EuiSpacer,
} from '@elastic/eui';
import { AddTooltipFieldPopover } from './add_tooltip_field_popover';
import { i18n } from '@kbn/i18n';
// TODO import reorder from EUI once its exposed as service
// https://github.com/elastic/eui/issues/2372
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const getProps = async field => {
return new Promise(async (resolve, reject) => {
try {
const label = await field.getLabel();
const type = await field.getDataType();
resolve({
label: label,
type: type,
name: field.getName(),
});
} catch (e) {
reject(e);
}
});
};
export class TooltipSelector extends Component {
state = {
fieldProps: [],
selectedFieldProps: [],
};
constructor() {
super();
this._isMounted = false;
this._previousFields = null;
this._previousSelectedTooltips = null;
}
componentDidMount() {
this._isMounted = true;
this._loadFieldProps();
this._loadTooltipFieldProps();
}
componentWillUnmount() {
this._isMounted = false;
}
componentDidUpdate() {
this._loadTooltipFieldProps();
this._loadFieldProps();
}
async _loadTooltipFieldProps() {
if (!this.props.tooltipFields || this.props.tooltipFields === this._previousSelectedTooltips) {
return;
}
this._previousSelectedTooltips = this.props.tooltipFields;
const selectedProps = this.props.tooltipFields.map(getProps);
const selectedFieldProps = await Promise.all(selectedProps);
if (this._isMounted) {
this.setState({ selectedFieldProps });
}
}
async _loadFieldProps() {
if (!this.props.fields || this.props.fields === this._previousFields) {
return;
}
this._previousFields = this.props.fields;
const props = this.props.fields.map(getProps);
const fieldProps = await Promise.all(props);
if (this._isMounted) {
this.setState({ fieldProps });
}
}
_getPropertyLabel = propertyName => {
if (!this.state.fieldProps.length) {
return propertyName;
}
const prop = this.state.fieldProps.find(field => {
return field.name === propertyName;
});
return prop.label ? prop.label : propertyName;
};
_getTooltipProperties() {
return this.props.tooltipFields.map(field => field.getName());
}
_onAdd = properties => {
if (!this.props.tooltipFields) {
this.props.onChange([...properties]);
} else {
const existingProperties = this._getTooltipProperties();
this.props.onChange([...existingProperties, ...properties]);
}
};
_removeProperty = index => {
if (!this.props.tooltipFields) {
this.props.onChange([]);
} else {
const tooltipProperties = this._getTooltipProperties();
tooltipProperties.splice(index, 1);
this.props.onChange(tooltipProperties);
}
};
_onDragEnd = ({ source, destination }) => {
// Dragging item out of EuiDroppable results in destination of null
if (!destination) {
return;
}
this.props.onChange(reorder(this._getTooltipProperties(), source.index, destination.index));
};
_renderProperties() {
if (!this.state.selectedFieldProps.length) {
return null;
}
return (
<EuiDragDropContext onDragEnd={this._onDragEnd}>
<EuiDroppable droppableId="mapLayerTOC" spacing="none">
{(provided, snapshot) =>
this.state.selectedFieldProps.map((field, idx) => (
<EuiDraggable
spacing="none"
key={field.name}
index={idx}
draggableId={field.name}
customDragHandle={true}
disableInteractiveElementBlocking // Allows button to be drag handle
>
{(provided, state) => (
<div
className={classNames('mapTooltipSelector__propertyRow', {
'mapTooltipSelector__propertyRow-isDragging': state.isDragging,
'mapTooltipSelector__propertyRow-isDraggingOver': snapshot.isDraggingOver,
})}
>
<EuiText className="mapTooltipSelector__propertyContent" size="s">
{this._getPropertyLabel(field.name)}
</EuiText>
<div className="mapTooltipSelector__propertyIcons">
<EuiButtonIcon
iconType="trash"
color="danger"
onClick={this._removeProperty.bind(null, idx)}
title={i18n.translate('xpack.maps.tooltipSelector.trashButtonTitle', {
defaultMessage: 'Remove property',
})}
aria-label={i18n.translate(
'xpack.maps.tooltipSelector.trashButtonAriaLabel',
{
defaultMessage: 'Remove property',
}
)}
/>
<EuiButtonIcon
className="mapTooltipSelector__grab"
iconType="grab"
color="subdued"
title={i18n.translate('xpack.maps.tooltipSelector.grabButtonTitle', {
defaultMessage: 'Reorder property',
})}
aria-label={i18n.translate(
'xpack.maps.tooltipSelector.grabButtonAriaLabel',
{
defaultMessage: 'Reorder property',
}
)}
{...provided.dragHandleProps}
/>
</div>
</div>
)}
</EuiDraggable>
))
}
</EuiDroppable>
</EuiDragDropContext>
);
}
render() {
return (
<div>
{this._renderProperties()}
<EuiSpacer size="s" />
<EuiTextAlign textAlign="center">
<AddTooltipFieldPopover
onAdd={this._onAdd}
fields={this.state.fieldProps}
selectedFields={this.state.selectedFieldProps}
/>
</EuiTextAlign>
</div>
);
}
}

View file

@ -30,7 +30,7 @@ exports[`Should remove selected fields from selectable 1`] = `
options={
Array [
Object {
"label": "@timestamp",
"label": "@timestamp-label",
"prepend": <FieldIcon
className="eui-alignMiddle"
fill="none"
@ -100,7 +100,7 @@ exports[`Should render 1`] = `
options={
Array [
Object {
"label": "@timestamp",
"label": "@timestamp-label",
"prepend": <FieldIcon
className="eui-alignMiddle"
fill="none"
@ -118,7 +118,7 @@ exports[`Should render 1`] = `
"value": "prop1",
},
Object {
"label": "prop2",
"label": "prop2-label",
"prepend": <FieldIcon
className="eui-alignMiddle"
fill="none"

View file

@ -39,7 +39,7 @@ exports[`TooltipSelector should render component 1`] = `
Object {
"label": "foobar_label",
"name": "iso2",
"type": "foobar_type",
"type": "string",
},
]
}

View file

@ -18,15 +18,17 @@ const defaultProps = {
},
{
name: 'prop2',
label: 'prop2-label',
type: 'string',
},
{
name: '@timestamp',
label: '@timestamp-label',
type: 'date',
},
],
selectedFields: [],
onSelect: () => {},
onAdd: () => {},
};
test('Should render', () => {
@ -39,7 +41,10 @@ test('Should remove selected fields from selectable', () => {
const component = shallow(
<AddTooltipFieldPopover
{...defaultProps}
selectedFields={[{ name: 'prop2' }, { name: 'prop1' }]}
selectedFields={[
{ name: 'prop2', label: 'prop2-label', type: 'string' },
{ name: 'prop1', label: 'prop1-label', type: 'string' },
]}
/>
);

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import React, { Component, Fragment } from 'react';
import {
@ -11,19 +12,26 @@ import {
EuiPopoverTitle,
EuiButtonEmpty,
EuiSelectable,
EuiSelectableOption,
EuiButton,
EuiSpacer,
EuiTextAlign,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FieldIcon } from '../../../../../src/plugins/kibana_react/public';
import { FieldIcon } from '../../../../../../src/plugins/kibana_react/public';
const sortByLabel = (a, b) => {
return a.label.localeCompare(b.label);
export type FieldProps = {
label: string;
type: string;
name: string;
};
function getOptions(fields, selectedFields) {
function sortByLabel(a: EuiSelectableOption, b: EuiSelectableOption): number {
return a.label.localeCompare(b.label);
}
function getOptions(fields: FieldProps[], selectedFields: FieldProps[]): EuiSelectableOption[] {
if (!fields) {
return [];
}
@ -43,19 +51,33 @@ function getOptions(fields, selectedFields) {
'type' in field ? (
<FieldIcon className="eui-alignMiddle" type={field.type} fill="none" />
) : null,
label: 'label' in field ? field.label : field.name,
label: field.label,
};
})
.sort(sortByLabel);
}
export class AddTooltipFieldPopover extends Component {
state = {
interface Props {
onAdd: (checkedFieldNames: string[]) => void;
fields: FieldProps[];
selectedFields: FieldProps[];
}
interface State {
isPopoverOpen: boolean;
checkedFields: string[];
options?: EuiSelectableOption[];
prevFields?: FieldProps[];
prevSelectedFields?: FieldProps[];
}
export class AddTooltipFieldPopover extends Component<Props, State> {
state: State = {
isPopoverOpen: false,
checkedFields: [],
};
static getDerivedStateFromProps(nextProps, prevState) {
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
if (
nextProps.fields !== prevState.prevFields ||
nextProps.selectedFields !== prevState.prevSelectedFields
@ -83,13 +105,13 @@ export class AddTooltipFieldPopover extends Component {
});
};
_onSelect = options => {
const checkedFields = options
_onSelect = (options: EuiSelectableOption[]) => {
const checkedFields: string[] = options
.filter(option => {
return option.checked === 'on';
})
.map(option => {
return option.value;
return option.value as string;
});
this.setState({

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { TooltipSelector } from './tooltip_selector';

View file

@ -8,25 +8,19 @@ import React from 'react';
import { shallow } from 'enzyme';
import { TooltipSelector } from './tooltip_selector';
import { AbstractField } from '../../layers/fields/field';
import { FIELD_ORIGIN } from '../../../common/constants';
class MockField {
constructor({ name, label, type }) {
this._name = name;
class MockField extends AbstractField {
private _label?: string;
constructor({ name, label }: { name: string; label?: string }) {
super({ fieldName: name, origin: FIELD_ORIGIN.SOURCE });
this._label = label;
this._type = type;
}
getName() {
return this._name;
}
async getLabel() {
return this._label || 'foobar_label';
}
async getDataType() {
return this._type || 'foobar_type';
}
}
const defaultProps = {
@ -36,11 +30,9 @@ const defaultProps = {
new MockField({
name: 'iso2',
label: 'ISO 3166-1 alpha-2 code',
type: 'string',
}),
new MockField({
name: 'iso3',
type: 'string',
}),
],
};

View file

@ -0,0 +1,245 @@
/*
* 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, { Component, Fragment } from 'react';
import classNames from 'classnames';
import {
EuiButtonIcon,
EuiDragDropContext,
EuiDraggable,
EuiDroppable,
EuiText,
EuiTextAlign,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AddTooltipFieldPopover, FieldProps } from './add_tooltip_field_popover';
import { IField } from '../../layers/fields/field';
// TODO import reorder from EUI once its exposed as service
// https://github.com/elastic/eui/issues/2372
const reorder = (list: string[], startIndex: number, endIndex: number) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
async function getFieldProps(field: IField): Promise<FieldProps> {
return {
label: await field.getLabel(),
type: await field.getDataType(),
name: field.getName(),
};
}
interface Props {
fields: IField[] | null;
onChange: (selectedFieldNames: string[]) => void;
tooltipFields: IField[];
}
interface State {
fieldProps: FieldProps[];
selectedFieldProps: FieldProps[];
}
export class TooltipSelector extends Component<Props, State> {
private _isMounted: boolean;
private _previousFields: IField[] | null;
private _previousSelectedTooltips: IField[] | null;
state = {
fieldProps: [],
selectedFieldProps: [],
};
constructor(props: Props) {
super(props);
this._isMounted = false;
this._previousFields = null;
this._previousSelectedTooltips = null;
}
componentDidMount() {
this._isMounted = true;
this._loadFieldProps();
this._loadTooltipFieldProps();
}
componentWillUnmount() {
this._isMounted = false;
}
componentDidUpdate() {
this._loadTooltipFieldProps();
this._loadFieldProps();
}
async _loadTooltipFieldProps() {
if (!this.props.tooltipFields || this.props.tooltipFields === this._previousSelectedTooltips) {
return;
}
this._previousSelectedTooltips = this.props.tooltipFields;
const promises = this.props.tooltipFields.map(getFieldProps);
const selectedFieldProps = await Promise.all(promises);
if (this._isMounted) {
this.setState({ selectedFieldProps });
}
}
async _loadFieldProps() {
if (!this.props.fields || this.props.fields === this._previousFields) {
return;
}
this._previousFields = this.props.fields;
const promises = this.props.fields.map(getFieldProps);
const fieldProps = await Promise.all(promises);
if (this._isMounted) {
this.setState({ fieldProps });
}
}
_getPropertyLabel = (propertyName: string) => {
if (!this.state.fieldProps.length) {
return propertyName;
}
const prop: FieldProps | undefined = this.state.fieldProps.find((field: FieldProps) => {
return field.name === propertyName;
});
return prop ? prop!.label : propertyName;
};
_getTooltipFieldNames(): string[] {
return this.props.tooltipFields ? this.props.tooltipFields.map(field => field.getName()) : [];
}
_onAdd = (properties: string[]) => {
if (!this.props.tooltipFields) {
this.props.onChange([...properties]);
} else {
const existingProperties = this._getTooltipFieldNames();
this.props.onChange([...existingProperties, ...properties]);
}
};
_removeProperty = (index: number) => {
if (!this.props.tooltipFields) {
this.props.onChange([]);
} else {
const tooltipProperties = this._getTooltipFieldNames();
tooltipProperties.splice(index, 1);
this.props.onChange(tooltipProperties);
}
};
_onDragEnd = ({
source,
destination,
}: {
source: { index: number };
destination?: { index: number };
}) => {
// Dragging item out of EuiDroppable results in destination of null
if (!destination) {
return;
}
this.props.onChange(reorder(this._getTooltipFieldNames(), source.index, destination.index));
};
_renderProperties() {
if (!this.state.selectedFieldProps.length) {
return null;
}
return (
<EuiDragDropContext onDragEnd={this._onDragEnd}>
<EuiDroppable droppableId="mapLayerTOC" spacing="none">
{(droppableProvided, snapshot) => (
<Fragment>
{this.state.selectedFieldProps.map((field: FieldProps, idx: number) => (
<EuiDraggable
spacing="none"
key={field.name}
index={idx}
draggableId={field.name}
customDragHandle={true}
disableInteractiveElementBlocking // Allows button to be drag handle
>
{(provided, state) => (
<div
className={classNames('mapTooltipSelector__propertyRow', {
'mapTooltipSelector__propertyRow-isDragging': state.isDragging,
'mapTooltipSelector__propertyRow-isDraggingOver': snapshot.isDraggingOver,
})}
>
<EuiText className="mapTooltipSelector__propertyContent" size="s">
{this._getPropertyLabel(field.name)}
</EuiText>
<div className="mapTooltipSelector__propertyIcons">
<EuiButtonIcon
iconType="trash"
color="danger"
onClick={this._removeProperty.bind(null, idx)}
title={i18n.translate('xpack.maps.tooltipSelector.trashButtonTitle', {
defaultMessage: 'Remove property',
})}
aria-label={i18n.translate(
'xpack.maps.tooltipSelector.trashButtonAriaLabel',
{
defaultMessage: 'Remove property',
}
)}
/>
<EuiButtonIcon
className="mapTooltipSelector__grab"
iconType="grab"
color="subdued"
title={i18n.translate('xpack.maps.tooltipSelector.grabButtonTitle', {
defaultMessage: 'Reorder property',
})}
aria-label={i18n.translate(
'xpack.maps.tooltipSelector.grabButtonAriaLabel',
{
defaultMessage: 'Reorder property',
}
)}
{...provided.dragHandleProps}
/>
</div>
</div>
)}
</EuiDraggable>
))}
</Fragment>
)}
</EuiDroppable>
</EuiDragDropContext>
);
}
render() {
return (
<div>
{this._renderProperties()}
<EuiSpacer size="s" />
<EuiTextAlign textAlign="center">
<AddTooltipFieldPopover
onAdd={this._onAdd}
fields={this.state.fieldProps}
selectedFields={this.state.selectedFieldProps}
/>
</EuiTextAlign>
</div>
);
}
}

View file

@ -7,7 +7,7 @@
import { FIELD_ORIGIN } from '../../../common/constants';
import { IField, AbstractField } from './field';
import { IVectorSource } from '../sources/vector_source';
import { IEmsFileSource } from '../sources/ems_file_source/ems_file_source';
import { IEmsFileSource } from '../sources/ems_file_source';
export class EMSFileField extends AbstractField implements IField {
private readonly _source: IEmsFileSource;

View file

@ -28,7 +28,7 @@ import {
MapFilters,
StyleDescriptor,
} from '../../common/descriptor_types';
import { Attribution, ImmutableSourceProperty, ISource } from './sources/source';
import { Attribution, ImmutableSourceProperty, ISource, SourceEditorArgs } from './sources/source';
import { SyncContext } from '../actions/map_actions';
import { IStyle } from './styles/style';
@ -58,7 +58,7 @@ export interface ILayer {
getStyleForEditing(): IStyle;
getCurrentStyle(): IStyle;
getImmutableSourceProperties(): Promise<ImmutableSourceProperty[]>;
renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement<any> | null;
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement<any> | null;
isLayerLoading(): boolean;
hasErrors(): boolean;
getErrors(): string;
@ -368,7 +368,7 @@ export class AbstractLayer implements ILayer {
return await source.getImmutableProperties();
}
renderSourceSettingsEditor({ onChange }: { onChange: () => void }) {
renderSourceSettingsEditor({ onChange }: SourceEditorArgs) {
const source = this.getSourceForEditing();
return source.renderSourceSettingsEditor({ onChange });
}

View file

@ -4,31 +4,51 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiComboBox, EuiFormRow } from '@elastic/eui';
import React, { Component } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { getEMSClient } from '../../../meta';
import { getEmsUnavailableMessage } from '../ems_unavailable_message';
import { i18n } from '@kbn/i18n';
import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types';
interface Props {
onSourceConfigChange: (sourceConfig: Partial<EMSFileSourceDescriptor>) => void;
}
interface State {
hasLoadedOptions: boolean;
emsFileOptions: Array<EuiComboBoxOptionOption<string>>;
selectedOption: EuiComboBoxOptionOption<string> | null;
}
export class EMSFileCreateSourceEditor extends Component<Props, State> {
private _isMounted: boolean = false;
export class EMSFileCreateSourceEditor extends React.Component {
state = {
emsFileOptionsRaw: null,
hasLoadedOptions: false,
emsFileOptions: [],
selectedOption: null,
};
_loadFileOptions = async () => {
// @ts-ignore
const emsClient = getEMSClient();
const fileLayers = await emsClient.getFileLayers();
// @ts-ignore
const fileLayers: unknown[] = await emsClient.getFileLayers();
const options = fileLayers.map(fileLayer => {
return {
id: fileLayer.getId(),
name: fileLayer.getDisplayName(),
// @ts-ignore
value: fileLayer.getId(),
// @ts-ignore
label: fileLayer.getDisplayName(),
};
});
if (this._isMounted) {
this.setState({
emsFileOptionsRaw: options,
hasLoadedOptions: true,
emsFileOptions: options,
});
}
};
@ -42,7 +62,7 @@ export class EMSFileCreateSourceEditor extends React.Component {
this._loadFileOptions();
}
_onChange = selectedOptions => {
_onChange = (selectedOptions: Array<EuiComboBoxOptionOption<string>>) => {
if (selectedOptions.length === 0) {
return;
}
@ -54,32 +74,28 @@ export class EMSFileCreateSourceEditor extends React.Component {
};
render() {
if (!this.state.emsFileOptionsRaw) {
if (!this.state.hasLoadedOptions) {
// TODO display loading message
return null;
}
const options = this.state.emsFileOptionsRaw.map(({ id, name }) => {
return { label: name, value: id };
});
return (
<EuiFormRow
label={i18n.translate('xpack.maps.source.emsFile.layerLabel', {
defaultMessage: 'Layer',
})}
helpText={this.state.emsFileOptionsRaw.length === 0 ? getEmsUnavailableMessage() : null}
helpText={this.state.emsFileOptions.length === 0 ? getEmsUnavailableMessage() : null}
>
<EuiComboBox
placeholder={i18n.translate('xpack.maps.source.emsFile.selectPlaceholder', {
defaultMessage: 'Select EMS vector shapes',
})}
options={options}
selectedOptions={this.state.selectedOption ? [this.state.selectedOption] : []}
options={this.state.emsFileOptions}
selectedOptions={this.state.selectedOption ? [this.state.selectedOption!] : []}
onChange={this._onChange}
isClearable={false}
singleSelection={true}
isDisabled={this.state.emsFileOptionsRaw.length === 0}
isDisabled={this.state.emsFileOptions.length === 0}
data-test-subj="emsVectorComboBox"
/>
</EuiFormRow>

View file

@ -8,10 +8,9 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { VectorLayer } from '../../vector_layer';
import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry';
// @ts-ignore
import { EMSFileCreateSourceEditor } from './create_source_editor';
// @ts-ignore
import { EMSFileSource, sourceTitle } from './ems_file_source';
import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types';
// @ts-ignore
import { isEmsEnabled } from '../../../meta';
@ -24,8 +23,7 @@ export const emsBoundariesLayerWizardConfig: LayerWizard = {
}),
icon: 'emsApp',
renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: unknown) => {
// @ts-ignore
const onSourceConfigChange = (sourceConfig: Partial<EMSFileSourceDescriptor>) => {
const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig);
const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
previewLayer(layerDescriptor);

View file

@ -1,15 +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 { AbstractVectorSource, IVectorSource } from '../vector_source';
export interface IEmsFileSource extends IVectorSource {
getEMSFileLayer(): Promise<unknown>;
}
export class EMSFileSource extends AbstractVectorSource implements IEmsFileSource {
getEMSFileLayer(): Promise<unknown>;
}

View file

@ -9,11 +9,9 @@ import { EMSFileSource } from './ems_file_source';
jest.mock('ui/new_platform');
jest.mock('../../vector_layer', () => {});
function makeEMSFileSource(tooltipProperties) {
const emsFileSource = new EMSFileSource({
tooltipProperties: tooltipProperties,
});
emsFileSource.getEMSFileLayer = () => {
function makeEMSFileSource(tooltipProperties: string[]) {
const emsFileSource = new EMSFileSource({ tooltipProperties });
emsFileSource.getEMSFileLayer = async () => {
return {
getFieldsInLanguage() {
return [

View file

@ -4,40 +4,56 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AbstractVectorSource } from '../vector_source';
import { VECTOR_SHAPE_TYPES } from '../vector_feature_types';
import React from 'react';
import { SOURCE_TYPES, FIELD_ORIGIN } from '../../../../common/constants';
import { getEMSClient } from '../../../meta';
import React, { ReactElement } from 'react';
import { i18n } from '@kbn/i18n';
import { Feature } from 'geojson';
import { Adapters } from 'src/plugins/inspector/public';
import { Attribution, ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { AbstractVectorSource, GeoJsonWithMeta, IVectorSource } from '../vector_source';
import { VECTOR_SHAPE_TYPES } from '../vector_feature_types';
import { SOURCE_TYPES, FIELD_ORIGIN } from '../../../../common/constants';
// @ts-ignore
import { getEMSClient } from '../../../meta';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { UpdateSourceEditor } from './update_source_editor';
import { EMSFileField } from '../../fields/ems_file_field';
import { registerSource } from '../source_registry';
import { IField } from '../../fields/field';
import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
export interface IEmsFileSource extends IVectorSource {
getEMSFileLayer(): Promise<unknown>;
createField({ fieldName }: { fieldName: string }): IField;
}
export const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', {
defaultMessage: 'EMS Boundaries',
});
export class EMSFileSource extends AbstractVectorSource {
export class EMSFileSource extends AbstractVectorSource implements IEmsFileSource {
static type = SOURCE_TYPES.EMS_FILE;
static createDescriptor({ id, tooltipProperties = [] }) {
static createDescriptor({ id, tooltipProperties = [] }: Partial<EMSFileSourceDescriptor>) {
return {
type: EMSFileSource.type,
id,
id: id!,
tooltipProperties,
};
}
constructor(descriptor, inspectorAdapters) {
private readonly _tooltipFields: IField[];
readonly _descriptor: EMSFileSourceDescriptor;
constructor(descriptor: Partial<EMSFileSourceDescriptor>, inspectorAdapters?: Adapters) {
super(EMSFileSource.createDescriptor(descriptor), inspectorAdapters);
this._descriptor = EMSFileSource.createDescriptor(descriptor);
this._tooltipFields = this._descriptor.tooltipProperties.map(propertyKey =>
this.createField({ fieldName: propertyKey })
);
}
createField({ fieldName }) {
createField({ fieldName }: { fieldName: string }): IField {
return new EMSFileField({
fieldName,
source: this,
@ -45,7 +61,7 @@ export class EMSFileSource extends AbstractVectorSource {
});
}
renderSourceSettingsEditor({ onChange }) {
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement<any> | null {
return (
<UpdateSourceEditor
onChange={onChange}
@ -56,9 +72,12 @@ export class EMSFileSource extends AbstractVectorSource {
);
}
async getEMSFileLayer() {
async getEMSFileLayer(): Promise<unknown> {
// @ts-ignore
const emsClient = getEMSClient();
// @ts-ignore
const emsFileLayers = await emsClient.getFileLayers();
// @ts-ignore
const emsFileLayer = emsFileLayers.find(fileLayer => fileLayer.getId() === this._descriptor.id);
if (!emsFileLayer) {
throw new Error(
@ -73,19 +92,23 @@ export class EMSFileSource extends AbstractVectorSource {
return emsFileLayer;
}
async getGeoJsonWithMeta() {
async getGeoJsonWithMeta(): Promise<GeoJsonWithMeta> {
const emsFileLayer = await this.getEMSFileLayer();
// @ts-ignore
const featureCollection = await AbstractVectorSource.getGeoJson({
// @ts-ignore
format: emsFileLayer.getDefaultFormatType(),
featureCollectionPath: 'data',
// @ts-ignore
fetchUrl: emsFileLayer.getDefaultFormatUrl(),
});
// @ts-ignore
const emsIdField = emsFileLayer._config.fields.find(field => {
return field.type === 'id';
});
featureCollection.features.forEach((feature, index) => {
feature.id = emsIdField ? feature.properties[emsIdField.id] : index;
featureCollection.features.forEach((feature: Feature, index: number) => {
feature.id = emsIdField ? feature!.properties![emsIdField.id] : index;
});
return {
@ -94,10 +117,11 @@ export class EMSFileSource extends AbstractVectorSource {
};
}
async getImmutableProperties() {
async getImmutableProperties(): Promise<ImmutableSourceProperty[]> {
let emsLink;
try {
const emsFileLayer = await this.getEMSFileLayer();
// @ts-ignore
emsLink = emsFileLayer.getEMSHotLink();
} catch (error) {
// ignore error if EMS layer id could not be found
@ -118,23 +142,27 @@ export class EMSFileSource extends AbstractVectorSource {
];
}
async getDisplayName() {
async getDisplayName(): Promise<string> {
try {
const emsFileLayer = await this.getEMSFileLayer();
// @ts-ignore
return emsFileLayer.getDisplayName();
} catch (error) {
return this._descriptor.id;
}
}
async getAttributions() {
async getAttributions(): Promise<Attribution[]> {
const emsFileLayer = await this.getEMSFileLayer();
// @ts-ignore
return emsFileLayer.getAttributions();
}
async getLeftJoinFields() {
const emsFileLayer = await this.getEMSFileLayer();
// @ts-ignore
const fields = emsFileLayer.getFieldsInLanguage();
// @ts-ignore
return fields.map(f => this.createField({ fieldName: f.name }));
}
@ -142,16 +170,17 @@ export class EMSFileSource extends AbstractVectorSource {
return this._tooltipFields.length > 0;
}
async filterAndFormatPropertiesToHtml(properties) {
const tooltipProperties = this._tooltipFields.map(field => {
async filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]> {
const promises = this._tooltipFields.map(field => {
// @ts-ignore
const value = properties[field.getName()];
return field.createTooltipProperty(value);
});
return Promise.all(tooltipProperties);
return Promise.all(promises);
}
async getSupportedShapeTypes() {
async getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPES[]> {
return [VECTOR_SHAPE_TYPES.POLYGON];
}
}

View file

@ -5,4 +5,4 @@
*/
export { emsBoundariesLayerWizardConfig } from './ems_boundaries_layer_wizard';
export { EMSFileSource } from './ems_file_source';
export { EMSFileSource, IEmsFileSource } from './ems_file_source';

View file

@ -5,18 +5,28 @@
*/
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { TooltipSelector } from '../../../components/tooltip_selector';
import { getEMSClient } from '../../../meta';
import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TooltipSelector } from '../../../components/tooltip_selector';
// @ts-ignore
import { getEMSClient } from '../../../meta';
import { IEmsFileSource } from './ems_file_source';
import { IField } from '../../fields/field';
import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view';
export class UpdateSourceEditor extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired,
source: PropTypes.object,
};
interface Props {
layerId: string;
onChange: (args: OnSourceChangeArgs) => void;
source: IEmsFileSource;
tooltipFields: IField[];
}
interface State {
fields: IField[] | null;
}
export class UpdateSourceEditor extends Component<Props, State> {
private _isMounted: boolean = false;
state = {
fields: null,
@ -34,23 +44,29 @@ export class UpdateSourceEditor extends Component {
async loadFields() {
let fields;
try {
// @ts-ignore
const emsClient = getEMSClient();
// @ts-ignore
const emsFiles = await emsClient.getFileLayers();
const emsFile = emsFiles.find(emsFile => emsFile.getId() === this.props.layerId);
const emsFields = emsFile.getFieldsInLanguage();
// @ts-ignore
const taregetEmsFile = emsFiles.find(emsFile => emsFile.getId() === this.props.layerId);
// @ts-ignore
const emsFields = taregetEmsFile.getFieldsInLanguage();
// @ts-ignore
fields = emsFields.map(field => this.props.source.createField({ fieldName: field.name }));
} catch (e) {
//swallow this error. 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
// 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
fields = [];
}
if (this._isMounted) {
this.setState({ fields: fields });
this.setState({ fields });
}
}
_onTooltipPropertiesSelect = propertyNames => {
this.props.onChange({ propName: 'tooltipProperties', value: propertyNames });
_onTooltipPropertiesSelect = (selectedFieldNames: string[]) => {
this.props.onChange({ propName: 'tooltipProperties', value: selectedFieldNames });
};
render() {

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getInjectedVarFunc } from '../../kibana_services';
import { i18n } from '@kbn/i18n';
import { getInjectedVarFunc } from '../../kibana_services';
export function getEmsUnavailableMessage() {
export function getEmsUnavailableMessage(): string {
const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true);
if (isEmsEnabled) {
return i18n.translate('xpack.maps.source.ems.noAccessDescription', {

View file

@ -20,6 +20,7 @@ import {
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
export const sourceTitle = i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle',
@ -152,6 +153,10 @@ export class MVTSingleLayerVectorSource extends AbstractSource
getApplyGlobalQuery(): boolean {
return false;
}
async filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]> {
return [];
}
}
registerSource({

View file

@ -16,10 +16,16 @@ import { copyPersistentState } from '../../reducers/util';
import { SourceDescriptor } from '../../../common/descriptor_types';
import { IField } from '../fields/field';
import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view';
export type SourceEditorArgs = {
onChange: (args: OnSourceChangeArgs) => void;
};
export type ImmutableSourceProperty = {
label: string;
value: string;
link?: string;
};
export type Attribution = {
@ -48,7 +54,7 @@ export interface ISource {
getImmutableProperties(): Promise<ImmutableSourceProperty[]>;
getAttributions(): Promise<Attribution[]>;
isESSource(): boolean;
renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement<any> | null;
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement<any> | null;
supportsFitToBounds(): Promise<boolean>;
isJoinable(): boolean;
cloneDescriptor(): SourceDescriptor;
@ -124,7 +130,7 @@ export class AbstractSource implements ISource {
return [];
}
renderSourceSettingsEditor() {
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement<any> | null {
return null;
}

View file

@ -15,6 +15,7 @@ import {
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { VECTOR_SHAPE_TYPES } from '../vector_feature_types';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
export type GeoJsonFetchMeta = ESSearchSourceResponseMeta;
@ -24,6 +25,7 @@ export type GeoJsonWithMeta = {
};
export interface IVectorSource extends ISource {
filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]>;
getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent;
getGeoJsonWithMeta(
layerName: 'string',
@ -39,6 +41,7 @@ export interface IVectorSource extends ISource {
}
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]>;
getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent;
getGeoJsonWithMeta(
layerName: 'string',