[Maps] better error handling when layer configs can not be found (#29534)

* [Maps] better error handling when layer configs can not be found

* remove kibana.yml changes

* gis-map to map

* throw exception for getStringFields and getAttributions and then handle exceptions in caller
This commit is contained in:
Nathan Reese 2019-02-01 15:50:51 -07:00 committed by GitHub
parent fafa9fa9ae
commit b1e1ab6385
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 289 additions and 77 deletions

View file

@ -101,7 +101,12 @@ export class Join extends Component {
}
async _loadLeftFields() {
const stringFields = await this.props.layer.getStringFields();
let stringFields;
try {
stringFields = await this.props.layer.getStringFields();
} catch (error) {
stringFields = [];
}
if (!this._isMounted) {
return;
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { Fragment } from 'react';
import {
EuiFlexGroup,
@ -15,6 +15,7 @@ import {
EuiFieldText,
EuiRange,
EuiSpacer,
EuiCallOut,
} from '@elastic/eui';
import { ValidatedRange } from '../../../shared/components/validated_range';
@ -43,6 +44,26 @@ export function SettingsPanel(props) {
props.updateSourceProp(props.layerId, propName, value);
};
const renderLayerErrors = () => {
if (!props.layer.hasErrors()) {
return null;
}
return (
<Fragment>
<EuiCallOut
color="warning"
title="Unable to load layer"
>
<p data-test-subj="layerErrorMessage">
{props.layer.getErrors()}
</p>
</EuiCallOut>
<EuiSpacer margin="m"/>
</Fragment>
);
};
const renderZoomSliders = () => {
return (
<EuiFormRow
@ -117,23 +138,27 @@ export function SettingsPanel(props) {
};
return (
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs"><h5>Settings</h5></EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<Fragment>
<EuiSpacer margin="m"/>
{renderLayerErrors()}
{renderLabel()}
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs"><h5>Settings</h5></EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
{renderZoomSliders()}
<EuiSpacer margin="m"/>
{renderAlphaSlider()}
{renderLabel()}
{props.layer.renderSourceSettingsEditor({ onChange: onSourceChange })}
{renderZoomSliders()}
</EuiPanel>
{renderAlphaSlider()}
{props.layer.renderSourceSettingsEditor({ onChange: onSourceChange })}
</EuiPanel>
</Fragment>
);
}

View file

@ -37,8 +37,12 @@ export class AttributionControl extends React.Component {
_syncMbMapWithAttribution = async () => {
const attributionPromises = this.props.layerList.map(layer => {
return layer.getAttributions();
const attributionPromises = this.props.layerList.map(async (layer) => {
try {
return await layer.getAttributions();
} catch (error) {
return [];
}
});
const attributions = await Promise.all(attributionPromises);
if (!this._isMounted) {

View file

@ -15,10 +15,8 @@ import { LayerTocActions } from '../../../../../shared/components/layer_toc_acti
export class TOCEntry extends React.Component {
constructor() {
super();
this.state = {
displayName: null };
state = {
displayName: null
}
componentDidMount() {
@ -30,7 +28,6 @@ export class TOCEntry extends React.Component {
this._isMounted = false;
}
async _updateDisplayName() {
const label = await this.props.layer.getDisplayName();
if (this._isMounted) {

View file

@ -41,11 +41,16 @@ export class LayerTOC extends React.Component {
}
_renderLayers() {
return [ ...this.props.layerList ]
.reverse()
return this.props.layerList
.map((layer) => {
return (<TOCEntry key={layer.getId()} layer={layer} displayName={layer.getDisplayName()}/>);
});
return (
<TOCEntry
key={layer.getId()}
layer={layer}
/>
);
})
.reverse();
}
render() {

View file

@ -39,11 +39,19 @@ export class EMSFileSource extends AbstractVectorSource {
super(descriptor);
}
async getGeoJsonWithMeta() {
async _getEmsVectorFileMeta() {
const emsFiles = await getEmsVectorFilesMeta();
const fileSource = emsFiles.find((source => source.id === this._descriptor.id));
const meta = emsFiles.find((source => source.id === this._descriptor.id));
if (!meta) {
throw new Error(`Unable to find EMS vector shapes for id: ${this._descriptor.id}`);
}
return meta;
}
async getGeoJsonWithMeta() {
const emsVectorFileMeta = await this._getEmsVectorFileMeta();
const fetchUrl = `../${GIS_API_PATH}/data/ems?id=${encodeURIComponent(this._descriptor.id)}`;
const featureCollection = await AbstractVectorSource.getGeoJson(fileSource, fetchUrl);
const featureCollection = await AbstractVectorSource.getGeoJson(emsVectorFileMeta, fetchUrl);
return {
data: featureCollection,
meta: {}
@ -59,22 +67,23 @@ export class EMSFileSource extends AbstractVectorSource {
}
async getDisplayName() {
const emsFiles = await getEmsVectorFilesMeta();
const fileSource = emsFiles.find((source => source.id === this._descriptor.id));
return fileSource.name;
try {
const emsVectorFileMeta = await this._getEmsVectorFileMeta();
return emsVectorFileMeta.name;
} catch (error) {
return this._descriptor.id;
}
}
async getAttributions() {
const emsFiles = await getEmsVectorFilesMeta();
const fileSource = emsFiles.find((source => source.id === this._descriptor.id));
return fileSource.attributions;
const emsVectorFileMeta = await this._getEmsVectorFileMeta();
return emsVectorFileMeta.attributions;
}
async getStringFields() {
const emsFiles = await getEmsVectorFilesMeta();
const fileSource = emsFiles.find((source => source.id === this._descriptor.id));
return fileSource.fields.map(f => {
const emsVectorFileMeta = await this._getEmsVectorFileMeta();
return emsVectorFileMeta.fields.map(f => {
return { name: f.name, label: f.description };
});
}

View file

@ -48,15 +48,15 @@ export class EMSTMSSource extends AbstractTMSSource {
];
}
async _getTMSOptions() {
async _getEmsTmsMeta() {
const emsTileServices = await getEmsTMSServices();
if(!emsTileServices) {
return;
}
return emsTileServices.find(service => {
const meta = emsTileServices.find(service => {
return service.id === this._descriptor.id;
});
if (!meta) {
throw new Error(`Unable to find EMS tile configuration for id: ${this._descriptor.id}`);
}
return meta;
}
_createDefaultLayerDescriptor(options) {
@ -78,12 +78,12 @@ export class EMSTMSSource extends AbstractTMSSource {
}
async getAttributions() {
const service = await this._getTMSOptions();
if (!service || !service.attributionMarkdown) {
const emsTmsMeta = await this._getEmsTmsMeta();
if (!emsTmsMeta.attributionMarkdown) {
return [];
}
return service.attributionMarkdown.split('|').map((attribution) => {
return emsTmsMeta.attributionMarkdown.split('|').map((attribution) => {
attribution = attribution.trim();
//this assumes attribution is plain markdown link
const extractLink = /\[(.*)\]\((.*)\)/;
@ -96,10 +96,7 @@ export class EMSTMSSource extends AbstractTMSSource {
}
async getUrlTemplate() {
const service = await this._getTMSOptions();
if (!service || !service.url) {
throw new Error('Cannot generate EMS TMS url template');
}
return service.url;
const emsTmsMeta = await this._getEmsTmsMeta();
return emsTmsMeta.url;
}
}

View file

@ -9,9 +9,6 @@ import React from 'react';
import uuid from 'uuid/v4';
import { AbstractESSource } from '../es_source';
import {
indexPatternService,
} from '../../../../kibana_services';
import { hitsToGeoJson } from '../../../../elasticsearch_geo_utils';
import { CreateSourceEditor } from './create_source_editor';
import { UpdateSourceEditor } from './update_source_editor';
@ -59,10 +56,14 @@ export class ESSearchSource extends AbstractESSource {
}
async getNumberFields() {
const indexPattern = await indexPatternService.get(this._descriptor.indexPatternId);
return indexPattern.fields.byType.number.map(field => {
return { name: field.name, label: field.name };
});
try {
const indexPattern = await this._getIndexPattern();
return indexPattern.fields.byType.number.map(field => {
return { name: field.name, label: field.name };
});
} catch (error) {
return [];
}
}
getFieldNames() {

View file

@ -108,20 +108,26 @@ export class AbstractESSource extends AbstractVectorSource {
}
async isTimeAware() {
const indexPattern = await this._getIndexPattern();
const timeField = indexPattern.timeFieldName;
return !!timeField;
try {
const indexPattern = await this._getIndexPattern();
const timeField = indexPattern.timeFieldName;
return !!timeField;
} catch (error) {
return false;
}
}
async _getIndexPattern() {
let indexPattern;
try {
indexPattern = await indexPatternService.get(this._descriptor.indexPatternId);
} catch (error) {
throw new Error(`Unable to find Index pattern ${this._descriptor.indexPatternId}`);
if (this.indexPattern) {
return this.indexPattern;
}
try {
this.indexPattern = await indexPatternService.get(this._descriptor.indexPatternId);
return this.indexPattern;
} catch (error) {
throw new Error(`Unable to find Index pattern for id: ${this._descriptor.indexPatternId}`);
}
return indexPattern;
}
async _getGeoField() {
@ -134,8 +140,13 @@ export class AbstractESSource extends AbstractVectorSource {
}
async getDisplayName() {
const indexPattern = await this._getIndexPattern();
return indexPattern.title;
try {
const indexPattern = await this._getIndexPattern();
return indexPattern.title;
} catch (error) {
// Unable to load index pattern, just return id as display name
return this._descriptor.indexPatternId;
}
}
isBoundsAware() {

View file

@ -49,20 +49,26 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
];
}
async getGeoJsonWithMeta() {
async _getVectorFileMeta() {
const regionList = await getKibanaRegionList();
const fileSource = regionList.find(source => source.name === this._descriptor.name);
const featureCollection = await AbstractVectorSource.getGeoJson(fileSource, fileSource.url);
const meta = regionList.find(source => source.name === this._descriptor.name);
if (!meta) {
throw new Error(`Unable to find map.regionmap configuration for ${this._descriptor.name}`);
}
return meta;
}
async getGeoJsonWithMeta() {
const vectorFileMeta = await this._getVectorFileMeta();
const featureCollection = await AbstractVectorSource.getGeoJson(vectorFileMeta, vectorFileMeta.url);
return {
data: featureCollection
};
}
async getStringFields() {
const regionList = await getKibanaRegionList();
const fileSource = regionList.find((source => source.name === this._descriptor.name));
return fileSource.fields.map(f => {
const vectorFileMeta = await this._getVectorFileMeta();
return vectorFileMeta.fields.map(f => {
return { name: f.name, label: f.description };
});
}

View file

@ -35,5 +35,6 @@ export default function ({ loadTestFile, getService }) {
loadTestFile(require.resolve('./es_search_source'));
loadTestFile(require.resolve('./es_geo_grid_source'));
loadTestFile(require.resolve('./joins'));
loadTestFile(require.resolve('./layer_errors'));
});
}

View file

@ -0,0 +1,100 @@
/*
* 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 'expect.js';
export default function ({ getPageObjects }) {
const PageObjects = getPageObjects(['gis', 'header']);
describe('layer errors', () => {
before(async () => {
await PageObjects.gis.loadSavedMap('layer with errors');
});
describe('ESSearchSource with missing index pattern id', async () => {
const MISSING_INDEX_ID = 'idThatDoesNotExitForESSearchSource';
const LAYER_NAME = MISSING_INDEX_ID;
it('should diplay error message in layer panel', async () => {
const errorMsg = await PageObjects.gis.getLayerErrorText(LAYER_NAME);
expect(errorMsg).to.equal(`Unable to find Index pattern for id: ${MISSING_INDEX_ID}`);
});
it('should allow deletion of layer', async () => {
await PageObjects.gis.removeLayer(LAYER_NAME);
const exists = await PageObjects.gis.doesLayerExist(LAYER_NAME);
expect(exists).to.be(false);
});
});
describe('ESGeoGridSource with missing index pattern id', async () => {
const MISSING_INDEX_ID = 'idThatDoesNotExitForESGeoGridSource';
const LAYER_NAME = MISSING_INDEX_ID;
it('should diplay error message in layer panel', async () => {
const errorMsg = await PageObjects.gis.getLayerErrorText(LAYER_NAME);
expect(errorMsg).to.equal(`Unable to find Index pattern for id: ${MISSING_INDEX_ID}`);
});
it('should allow deletion of layer', async () => {
await PageObjects.gis.removeLayer(LAYER_NAME);
const exists = await PageObjects.gis.doesLayerExist(LAYER_NAME);
expect(exists).to.be(false);
});
});
describe('EMSFileSource with missing EMS id', async () => {
const MISSING_EMS_ID = 'idThatDoesNotExitForEMSFileSource';
const LAYER_NAME = 'EMS_vector_shapes';
it('should diplay error message in layer panel', async () => {
const errorMsg = await PageObjects.gis.getLayerErrorText(LAYER_NAME);
expect(errorMsg).to.equal(`Unable to find EMS vector shapes for id: ${MISSING_EMS_ID}`);
});
it('should allow deletion of layer', async () => {
await PageObjects.gis.removeLayer(LAYER_NAME);
const exists = await PageObjects.gis.doesLayerExist(LAYER_NAME);
expect(exists).to.be(false);
});
});
describe('EMSTMSSource with missing EMS id', async () => {
const MISSING_EMS_ID = 'idThatDoesNotExitForEMSTile';
const LAYER_NAME = 'EMS_tiles';
it('should diplay error message in layer panel', async () => {
const errorMsg = await PageObjects.gis.getLayerErrorText(LAYER_NAME);
expect(errorMsg).to.equal(`Unable to find EMS tile configuration for id: ${MISSING_EMS_ID}`);
});
it('should allow deletion of layer', async () => {
await PageObjects.gis.removeLayer(LAYER_NAME);
const exists = await PageObjects.gis.doesLayerExist(LAYER_NAME);
expect(exists).to.be(false);
});
});
describe('KibanaRegionmapSource with missing region map configuration', async () => {
const MISSING_REGION_NAME = 'nameThatDoesNotExitForKibanaRegionmapSource';
const LAYER_NAME = 'Custom_vector_shapes';
it('should diplay error message in layer panel', async () => {
const errorMsg = await PageObjects.gis.getLayerErrorText(LAYER_NAME);
expect(errorMsg).to.equal(`Unable to find map.regionmap configuration for ${MISSING_REGION_NAME}`);
});
it('should allow deletion of layer', async () => {
await PageObjects.gis.removeLayer(LAYER_NAME);
const exists = await PageObjects.gis.doesLayerExist(LAYER_NAME);
expect(exists).to.be(false);
});
});
});
}

View file

@ -332,3 +332,48 @@
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana",
"id": "map:745c98b0-23e1-11e9-a048-6fef5a3e0d1e",
"source": {
"type": "map",
"map": {
"title" : "layer with errors",
"description" : "",
"mapStateJSON" : "{\"zoom\":0.71,\"center\":{\"lon\":0.10268,\"lat\":0},\"timeFilters\":{\"from\":\"now-7d\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"}}",
"layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"KIBANA_TILEMAP\",\"url\":\"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png\"},\"id\":\"ap0ys\",\"label\":\"Custom TMS\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"TILE\",\"isInErrorState\":true,\"errorMessage\":\"Tiles from \\\"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png\\\" could not be loaded\"},{\"sourceDescriptor\":{\"type\":\"REGIONMAP_FILE\",\"name\":\"nameThatDoesNotExitForKibanaRegionmapSource\"},\"temporary\":false,\"id\":\"0sabv\",\"label\":\"Custom_vector_shapes\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#3cb44b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\",\"isInErrorState\":true,\"errorMessage\":\"Cannot read property 'url' of undefined\"},{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"idThatDoesNotExitForEMSTile\"},\"temporary\":false,\"id\":\"plw9l\",\"label\":\"EMS_tiles\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"idThatDoesNotExitForEMSFileSource\"},\"temporary\":false,\"id\":\"2gro0\",\"label\":\"EMS_vector_shapes\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"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\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"f67fe707-95dd-46d6-89b8-82617b251b61\",\"indexPatternId\":\"idThatDoesNotExitForESGeoGridSource\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"resolution\":\"COARSE\"},\"temporary\":false,\"id\":\"pl5qd\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Blues\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}},\"previousStyle\":null},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"id\":\"a07072bb-3a92-4320-bd37-250ef6d04db7\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"idThatDoesNotExitForESSearchSource\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"temporary\":false,\"id\":\"9bw8h\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"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" : "{}",
"bounds" : {
"type" : "polygon",
"coordinates" : [
[
[
-180,
85.05113
],
[
-180,
-85.05113
],
[
180,
-85.05113
],
[
180,
85.05113
],
[
-180,
85.05113
]
]
]
}
}
}
}
}

View file

@ -143,6 +143,12 @@ export function GisPageProvider({ getService, getPageObjects }) {
await testSubjects.click(`mapRemoveLayerButton`);
}
async getLayerErrorText(layerName) {
log.debug(`Remove layer ${layerName}`);
await this.openLayerPanel(layerName);
return await testSubjects.getVisibleText(`layerErrorMessage`);
}
async openInspectorView(viewId) {
await inspector.open();
log.debug(`Open Inspector view ${viewId}`);