[Maps] add support for Top Hits to Documents source (#38052)

* top hits

* fetch top hits

* trigger load if top hits configuration changes

* text clean-up

* add functional tests for top hits

* add functional test verify configuration updates re-fetch data

* show entity source tooltip message for top hits

* include entities cound in results trimmed message

* pass fields needed for data driven styling and joins

* fix i18n problem

* more i18n changes

* reverse hits list so most recent events are drawn on top

* review feedback

* set meta to null when no sourceDataRequest
This commit is contained in:
Nathan Reese 2019-06-11 16:12:54 -06:00 committed by GitHub
parent 2f4dc0fa6c
commit 230959cfe1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 430 additions and 38 deletions

View file

@ -49,6 +49,7 @@ export const FlyoutFooter = ({ cancelLayerPanel, saveLayerEdits, removeLayer,
<EuiButtonEmpty
onClick={cancelLayerPanel}
flush="left"
data-test-subj="layerPanelCancelButton"
>
{cancelButtonLabel}
</EuiButtonEmpty>

View file

@ -265,14 +265,14 @@ export class AbstractLayer {
//no-op by default
}
updateDueToExtent(source, meta = {}, dataFilters = {}) {
updateDueToExtent(source, prevMeta = {}, nextMeta = {}) {
const extentAware = source.isFilterByMapBounds();
if (!extentAware) {
return NO_SOURCE_UPDATE_REQUIRED;
}
const { buffer: previousBuffer } = meta;
const { buffer: newBuffer } = dataFilters;
const { buffer: previousBuffer } = prevMeta;
const { buffer: newBuffer } = nextMeta;
if (!previousBuffer) {
return SOURCE_UPDATE_REQUIRED;
@ -296,7 +296,7 @@ export class AbstractLayer {
]);
const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry);
const isTrimmed = _.get(meta, 'areResultsTrimmed', false);
const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false);
return doesPreviousBufferContainNewBuffer && !isTrimmed
? NO_SOURCE_UPDATE_REQUIRED
: SOURCE_UPDATE_REQUIRED;

View file

@ -208,7 +208,7 @@ export class CreateSourceEditor extends Component {
<EuiSwitch
label={
i18n.translate('xpack.maps.source.esSearch.extentFilterLabel', {
defaultMessage: `Dynamically filter for data in the visible map area.`
defaultMessage: `Dynamically filter for data in the visible map area`
})
}

View file

@ -56,6 +56,10 @@ export class ESSearchSource extends AbstractESSource {
geoField: descriptor.geoField,
filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS),
tooltipProperties: _.get(descriptor, 'tooltipProperties', []),
useTopHits: _.get(descriptor, 'useTopHits', false),
topHitsSplitField: descriptor.topHitsSplitField,
topHitsTimeField: descriptor.topHitsTimeField,
topHitsSize: _.get(descriptor, 'topHitsSize', 1),
}, inspectorAdapters);
}
@ -66,6 +70,10 @@ export class ESSearchSource extends AbstractESSource {
onChange={onChange}
filterByMapBounds={this._descriptor.filterByMapBounds}
tooltipProperties={this._descriptor.tooltipProperties}
useTopHits={this._descriptor.useTopHits}
topHitsSplitField={this._descriptor.topHitsSplitField}
topHitsTimeField={this._descriptor.topHitsTimeField}
topHitsSize={this._descriptor.topHitsSize}
/>
);
}
@ -127,7 +135,65 @@ export class ESSearchSource extends AbstractESSource {
];
}
async getGeoJsonWithMeta(layerName, searchFilters) {
async _getTopHits(layerName, searchFilters) {
const {
topHitsSplitField,
topHitsTimeField,
topHitsSize,
} = this._descriptor;
const searchSource = await this._makeSearchSource(searchFilters, 0);
searchSource.setField('aggs', {
entitySplit: {
terms: {
field: topHitsSplitField,
size: 10000
},
aggs: {
entityHits: {
top_hits: {
sort: [
{
[topHitsTimeField]: {
order: 'desc'
}
}
],
_source: {
includes: searchFilters.fieldNames
},
size: topHitsSize
}
}
}
}
});
const resp = await this._runEsQuery(layerName, searchSource, 'Elasticsearch document top hits request');
let hasTrimmedResults = false;
const allHits = [];
const entityBuckets = _.get(resp, 'aggregations.entitySplit.buckets', []);
entityBuckets.forEach(entityBucket => {
const total = _.get(entityBucket, 'entityHits.hits.total', 0);
const hits = _.get(entityBucket, 'entityHits.hits.hits', []);
// Reverse hits list so they are drawn from oldest to newest (per entity) so newest events are on top
allHits.push(...hits.reverse());
if (total > hits.length) {
hasTrimmedResults = true;
}
});
return {
hits: allHits,
meta: {
areResultsTrimmed: hasTrimmedResults,
entityCount: entityBuckets.length,
}
};
}
async _getSearchHits(layerName, searchFilters) {
const searchSource = await this._makeSearchSource(searchFilters, DEFAULT_ES_DOC_LIMIT);
// Setting "fields" instead of "source: { includes: []}"
// because SearchSource automatically adds the following by default
@ -136,7 +202,26 @@ export class ESSearchSource extends AbstractESSource {
// By setting "fields", SearchSource removes all of defaults
searchSource.setField('fields', searchFilters.fieldNames);
let featureCollection;
const resp = await this._runEsQuery(layerName, searchSource, 'Elasticsearch document request');
return {
hits: resp.hits.hits,
meta: {
areResultsTrimmed: resp.hits.total > resp.hits.hits.length
}
};
}
_isTopHits() {
const { useTopHits, topHitsSplitField, topHitsTimeField } = this._descriptor;
return !!(useTopHits && topHitsSplitField && topHitsTimeField);
}
async getGeoJsonWithMeta(layerName, searchFilters) {
const { hits, meta } = this._isTopHits()
? await this._getTopHits(layerName, searchFilters)
: await this._getSearchHits(layerName, searchFilters);
const indexPattern = await this._getIndexPattern();
const unusedMetaFields = indexPattern.metaFields.filter(metaField => {
return metaField !== '_id';
@ -150,10 +235,10 @@ export class ESSearchSource extends AbstractESSource {
return properties;
};
const resp = await this._runEsQuery(layerName, searchSource, 'Elasticsearch document request');
let featureCollection;
try {
const geoField = await this._getGeoField();
featureCollection = hitsToGeoJson(resp.hits.hits, flattenHit, geoField.name, geoField.type);
featureCollection = hitsToGeoJson(hits, flattenHit, geoField.name, geoField.type);
} catch(error) {
throw new Error(
i18n.translate('xpack.maps.source.esSearch.convertToGeoJsonErrorMsg', {
@ -165,9 +250,7 @@ export class ESSearchSource extends AbstractESSource {
return {
data: featureCollection,
meta: {
areResultsTrimmed: resp.hits.total > resp.hits.hits.length
}
meta
};
}
@ -252,11 +335,31 @@ export class ESSearchSource extends AbstractESSource {
getSourceTooltipContent(sourceDataRequest) {
const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null;
const meta = sourceDataRequest ? sourceDataRequest.getMeta() : {};
const meta = sourceDataRequest ? sourceDataRequest.getMeta() : null;
if (!featureCollection || !meta) {
// no tooltip content needed when there is no feature collection or meta
return null;
}
if (this._isTopHits()) {
const entitiesFoundMsg = i18n.translate('xpack.maps.esSearch.topHitsEntitiesCountMsg', {
defaultMessage: `Found {entityCount} entities.`,
values: { entityCount: meta.entityCount }
});
if (meta.areResultsTrimmed) {
const trimmedMsg = i18n.translate('xpack.maps.esSearch.topHitsResultsTrimmedMsg', {
defaultMessage: `Results limited to most recent {topHitsSize} documents per entity.`,
values: { topHitsSize: this._descriptor.topHitsSize }
});
return `${entitiesFoundMsg} ${trimmedMsg}`;
}
return entitiesFoundMsg;
}
if (meta.areResultsTrimmed) {
return i18n.translate('xpack.maps.esSearch.resultsTrimmedMsg', {
defaultMessage: `Results limited to first {count} matching documents.`,
defaultMessage: `Results limited to first {count} documents.`,
values: { count: featureCollection.features.length }
});
}
@ -266,4 +369,13 @@ export class ESSearchSource extends AbstractESSource {
values: { count: featureCollection.features.length }
});
}
getSyncMeta() {
return {
useTopHits: this._descriptor.useTopHits,
topHitsSplitField: this._descriptor.topHitsSplitField,
topHitsTimeField: this._descriptor.topHitsTimeField,
topHitsSize: this._descriptor.topHitsSize,
};
}
}

View file

@ -11,9 +11,12 @@ import {
EuiSwitch,
} from '@elastic/eui';
import { MultiFieldSelect } from '../../../components/multi_field_select';
import { SingleFieldSelect } from '../../../components/single_field_select';
import { indexPatternService } from '../../../../kibana_services';
import { i18n } from '@kbn/i18n';
import { getTermsFields } from '../../../utils/get_terms_fields';
import { ValidatedRange } from '../../../components/validated_range';
export class UpdateSourceEditor extends Component {
@ -22,10 +25,16 @@ export class UpdateSourceEditor extends Component {
onChange: PropTypes.func.isRequired,
filterByMapBounds: PropTypes.bool.isRequired,
tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired,
useTopHits: PropTypes.bool.isRequired,
topHitsSplitField: PropTypes.string,
topHitsTimeField: PropTypes.string,
topHitsSize: PropTypes.number.isRequired,
}
state = {
fields: null,
tooltipFields: null,
termFields: null,
dateFields: null,
}
componentDidMount() {
@ -59,13 +68,31 @@ export class UpdateSourceEditor extends Component {
return;
}
const dateFields = indexPattern.fields.filter(field => {
return field.type === 'date';
});
this.setState({
fields: indexPattern.fields.filter(field => {
dateFields,
tooltipFields: indexPattern.fields.filter(field => {
// Do not show multi fields as tooltip field options
// since they do not have values in _source and exist for indexing only
return field.subType !== 'multi';
})
}),
termFields: getTermsFields(indexPattern.fields),
});
if (!this.props.topHitsTimeField) {
// prefer default time field
if (indexPattern.timeFieldName) {
this.onTopHitsTimeFieldChange(indexPattern.timeFieldName);
} else {
// fall back to first date field in index
if (dateFields.length > 0) {
this.onTopHitsTimeFieldChange(dateFields[0].name);
}
}
}
}
onTooltipPropertiesSelect = (propertyNames) => {
@ -76,6 +103,94 @@ export class UpdateSourceEditor extends Component {
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
};
onUseTopHitsChange = event => {
this.props.onChange({ propName: 'useTopHits', value: event.target.checked });
};
onTopHitsSplitFieldChange = topHitsSplitField => {
this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField });
};
onTopHitsTimeFieldChange = topHitsTimeField => {
this.props.onChange({ propName: 'topHitsTimeField', value: topHitsTimeField });
};
onTopHitsSizeChange = size => {
this.props.onChange({ propName: 'topHitsSize', value: size });
};
renderTopHitsForm() {
if (!this.props.useTopHits) {
return null;
}
let timeFieldSelect;
let sizeSlider;
if (this.props.topHitsSplitField) {
timeFieldSelect = (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsTimeFieldLabel', {
defaultMessage: 'Time'
})}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.maps.source.esSearch.topHitsTimeFieldSelectPlaceholder', {
defaultMessage: 'Select time field'
})}
value={this.props.topHitsTimeField}
onChange={this.onTopHitsTimeFieldChange}
fields={this.state.dateFields}
/>
</EuiFormRow>
);
sizeSlider = (
<EuiFormRow
label={
i18n.translate('xpack.maps.source.esSearch.topHitsSizeLabel', {
defaultMessage: 'Documents per entity'
})
}
>
<ValidatedRange
min={1}
max={100}
step={1}
value={this.props.topHitsSize}
onChange={this.onTopHitsSizeChange}
showLabels
showInput
showRange
data-test-subj="layerPanelTopHitsSize"
/>
</EuiFormRow>
);
}
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.maps.source.esSearch.topHitsSplitFieldLabel', {
defaultMessage: 'Entity'
})}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder', {
defaultMessage: 'Select entity field'
})}
value={this.props.topHitsSplitField}
onChange={this.onTopHitsSplitFieldChange}
fields={this.state.termFields}
/>
</EuiFormRow>
{timeFieldSelect}
{sizeSlider}
</Fragment>
);
}
render() {
return (
<Fragment>
@ -93,7 +208,7 @@ export class UpdateSourceEditor extends Component {
}
value={this.props.tooltipProperties}
onChange={this.onTooltipPropertiesSelect}
fields={this.state.fields}
fields={this.state.tooltipFields}
/>
</EuiFormRow>
@ -101,7 +216,7 @@ export class UpdateSourceEditor extends Component {
<EuiSwitch
label={
i18n.translate('xpack.maps.source.esSearch.extentFilterLabel', {
defaultMessage: `Dynamically filter for data in the visible map area.`
defaultMessage: `Dynamically filter for data in the visible map area`
})
}
@ -109,6 +224,20 @@ export class UpdateSourceEditor extends Component {
onChange={this.onFilterByMapBoundsChange}
/>
</EuiFormRow>
<EuiFormRow>
<EuiSwitch
label={
i18n.translate('xpack.maps.source.esSearch.useTopHitsLabel', {
defaultMessage: `Show most recent documents by entity`
})
}
checked={this.props.useTopHits}
onChange={this.onUseTopHitsChange}
/>
</EuiFormRow>
{this.renderTopHitsForm()}
</Fragment>
);
}

View file

@ -101,6 +101,10 @@ export class AbstractSource {
return 0;
}
getSyncMeta() {
return {};
}
isJoinable() {
return false;
}

View file

@ -190,7 +190,7 @@ export class VectorLayer extends AbstractLayer {
return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId);
}
async _canSkipSourceUpdate(source, sourceDataId, searchFilters) {
async _canSkipSourceUpdate(source, sourceDataId, nextMeta) {
const timeAware = await source.isTimeAware();
const refreshTimerAware = await source.isRefreshTimerAware();
@ -215,24 +215,24 @@ export class VectorLayer extends AbstractLayer {
if (!sourceDataRequest) {
return false;
}
const meta = sourceDataRequest.getMeta();
if (!meta) {
const prevMeta = sourceDataRequest.getMeta();
if (!prevMeta) {
return false;
}
let updateDueToTime = false;
if (timeAware) {
updateDueToTime = !_.isEqual(meta.timeFilters, searchFilters.timeFilters);
updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters);
}
let updateDueToRefreshTimer = false;
if (refreshTimerAware && searchFilters.refreshTimerLastTriggeredAt) {
updateDueToRefreshTimer = !_.isEqual(meta.refreshTimerLastTriggeredAt, searchFilters.refreshTimerLastTriggeredAt);
if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) {
updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt);
}
let updateDueToFields = false;
if (isFieldAware) {
updateDueToFields = !_.isEqual(meta.fieldNames, searchFilters.fieldNames);
updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames);
}
let updateDueToQuery = false;
@ -240,24 +240,26 @@ export class VectorLayer extends AbstractLayer {
let updateDueToLayerQuery = false;
let updateDueToApplyGlobalQuery = false;
if (isQueryAware) {
updateDueToApplyGlobalQuery = meta.applyGlobalQuery !== searchFilters.applyGlobalQuery;
updateDueToLayerQuery = !_.isEqual(meta.layerQuery, searchFilters.layerQuery);
if (searchFilters.applyGlobalQuery) {
updateDueToQuery = !_.isEqual(meta.query, searchFilters.query);
updateDueToFilters = !_.isEqual(meta.filters, searchFilters.filters);
updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery;
updateDueToLayerQuery = !_.isEqual(prevMeta.layerQuery, nextMeta.layerQuery);
if (nextMeta.applyGlobalQuery) {
updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query);
updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters);
} else {
// Global filters and query are not applied to layer search request so no re-fetch required.
// Exception is "Refresh" query.
updateDueToQuery = isRefreshOnlyQuery(meta.query, searchFilters.query);
updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query);
}
}
let updateDueToPrecisionChange = false;
if (isGeoGridPrecisionAware) {
updateDueToPrecisionChange = !_.isEqual(meta.geogridPrecision, searchFilters.geogridPrecision);
updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision);
}
const updateDueToExtentChange = this.updateDueToExtent(source, meta, searchFilters);
const updateDueToExtentChange = this.updateDueToExtent(source, prevMeta, nextMeta);
const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta);
return !updateDueToTime
&& !updateDueToRefreshTimer
@ -267,7 +269,8 @@ export class VectorLayer extends AbstractLayer {
&& !updateDueToFilters
&& !updateDueToLayerQuery
&& !updateDueToApplyGlobalQuery
&& !updateDueToPrecisionChange;
&& !updateDueToPrecisionChange
&& !updateDueToSourceMetaChange;
}
async _syncJoin({ join, startLoading, stopLoading, onLoadError, dataFilters }) {
@ -313,7 +316,6 @@ export class VectorLayer extends AbstractLayer {
}
}
async _syncJoins({ startLoading, stopLoading, onLoadError, dataFilters }) {
const joinSyncs = this.getValidJoins().map(async join => {
return this._syncJoin({ join, startLoading, stopLoading, onLoadError, dataFilters });
@ -337,6 +339,7 @@ export class VectorLayer extends AbstractLayer {
geogridPrecision: this._source.getGeoGridPrecision(dataFilters.zoom),
layerQuery: this.getQuery(),
applyGlobalQuery: this.getApplyGlobalQuery(),
sourceMeta: this._source.getSyncMeta(),
};
}

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export default function ({ loadTestFile }) {
describe('documents source', function () {
loadTestFile(require.resolve('./search_hits'));
loadTestFile(require.resolve('./top_hits'));
});
}

View file

@ -10,7 +10,7 @@ export default function ({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['maps']);
const inspector = getService('inspector');
describe('elasticsearch document layer', () => {
describe('search hits', () => {
before(async () => {
await PageObjects.maps.loadSavedMap('document example');
});

View file

@ -0,0 +1,71 @@
/*
* 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 expect from '@kbn/expect';
const VECTOR_SOURCE_ID = 'z52lq';
export default function ({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['maps', 'common']);
const inspector = getService('inspector');
const find = getService('find');
describe('top hits', () => {
before(async () => {
await PageObjects.maps.loadSavedMap('document example top hits');
});
it('should not fetch any search hits', async () => {
await inspector.open();
await inspector.openInspectorRequestsView();
const requestStats = await inspector.getTableData();
const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits');
expect(hits).to.equal('0'); // aggregation requests do not return any documents
});
it('should display top hits per entity', async () => {
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(10);
});
describe('configuration', async () => {
before(async () => {
await PageObjects.maps.openLayerPanel('logstash');
// Can not use testSubjects because data-test-subj is placed range input and number input
const sizeInput = await find.byCssSelector(`input[data-test-subj="layerPanelTopHitsSize"][type='number']`);
await sizeInput.click();
await sizeInput.clearValue();
await sizeInput.type('3');
await PageObjects.maps.waitForLayersToLoad();
});
after(async () => {
await PageObjects.maps.closeLayerPanel();
});
it('should update top hits when configation changes', async () => {
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(15);
});
});
describe('query', async () => {
before(async () => {
await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"');
});
after(async () => {
await PageObjects.maps.setAndSubmitQuery('');
});
it('should apply query to top hits request', async () => {
await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"');
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(2);
});
});
});
}

View file

@ -30,7 +30,7 @@ export default function ({ loadTestFile, getService }) {
describe('', function () {
this.tags('ciGroup7');
loadTestFile(require.resolve('./es_search_source'));
loadTestFile(require.resolve('./documents_source'));
loadTestFile(require.resolve('./saved_object_management'));
loadTestFile(require.resolve('./sample_data'));
loadTestFile(require.resolve('./feature_controls/maps_security'));

View file

@ -151,6 +151,61 @@
}
}
{
"type": "doc",
"value": {
"id": "map:68305470-87bc-11e9-a991-3b492a7c3e09",
"index": ".kibana",
"source": {
"map": {
"title" : "document example top hits",
"description" : "",
"mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-24T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"}}",
"layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"topHitsTimeField\":\"@timestamp\",\"useTopHits\":true,\"topHitsSplitField\":\"machine.os.raw\",\"topHitsSize\":2,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]",
"uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}",
"bounds" : {
"type" : "polygon",
"coordinates" : [
[
[
-137.80011,
46.32557
],
[
-137.80011,
17.86223
],
[
-63.42171,
17.86223
],
[
-63.42171,
46.32557
],
[
-137.80011,
46.32557
]
]
]
}
},
"type": "map",
"references" : [
{
"name" : "layer_1_source_index_pattern",
"type" : "index-pattern",
"id" : "c698b940-e149-11e8-a35a-370a8516603a"
}
],
"migrationVersion" : {
"map" : "7.2.0"
}
}
}
}
{
"type": "doc",
"value": {

View file

@ -265,6 +265,11 @@ export function GisPageProvider({ getService, getPageObjects }) {
await testSubjects.click('editLayerButton');
}
async closeLayerPanel() {
await testSubjects.click('layerPanelCancelButton');
await this.waitForLayersToLoad();
}
async getLayerTOCDetails(layerName) {
return await testSubjects.getVisibleText(`mapLayerTOCDetails${escapeLayerName(layerName)}`);
}