Skip to content

[Maps] Handle unavailable tilemap services#28852

Merged
kindsun merged 20 commits intoelastic:masterfrom
kindsun:fix/mapsAppHandleDisabling
Jan 28, 2019
Merged

[Maps] Handle unavailable tilemap services#28852
kindsun merged 20 commits intoelastic:masterfrom
kindsun:fix/mapsAppHandleDisabling

Conversation

@kindsun
Copy link
Copy Markdown
Contributor

@kindsun kindsun commented Jan 16, 2019

Resolves #27832 & #28119. Handles errors from:

  • EMS TMS
  • custom TMS
  • TMS from url
  • Also handles EMS disabling via configuration

A few details:

  • If the app is unable to obtain the EMS manifest, EMS fails immediately
  • Timeout for valid urls is set at 32 seconds
  • Errors are show both on the layer TOC/Legend and in the console. We could port messaging logic recently written for the region/coordinate maps over in a future PR

@kindsun kindsun changed the title Fix/maps app handle disabling Handle unavailable tilemap services Jan 16, 2019
@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@nreese nreese self-requested a review January 16, 2019 22:03
@thomasneirynck thomasneirynck added Project:Accessibility v7.0.0 Team:Geo Former Team Label for Geo Team. Now use Team:Presentation v6.7.0 labels Jan 17, 2019
@elasticmachine
Copy link
Copy Markdown
Contributor

Pinging @elastic/kibana-gis

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@kindsun
Copy link
Copy Markdown
Contributor Author

kindsun commented Jan 17, 2019

retest

@elasticmachine
Copy link
Copy Markdown
Contributor

💚 Build Succeeded

# Conflicts:
#	x-pack/plugins/gis/public/shared/layers/layer.js
@elasticmachine
Copy link
Copy Markdown
Contributor

💚 Build Succeeded

Copy link
Copy Markdown
Contributor

@nreese nreese left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works really well and the app now works without any EMS connection.

export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER';
export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER';
export const ADD_LAYER = 'ADD_LAYER';
export const SET_TMS_ERROR_STATUS = 'SET_TMS_ERROR_STATUS';
Copy link
Copy Markdown
Contributor

@nreese nreese Jan 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this a generic action for setting any error status on a layer. Not one thats tied to TMS. So maybe rename to SET_LAYER_ERROR_MESSAGE?

layerList.forEach((layer) => {
layer.syncLayerWithMB(this._mbMap);
layerList.forEach(layer => {
if (!layer.dataHasLoadError()) {
Copy link
Copy Markdown
Contributor

@nreese nreese Jan 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dataHasLoadError maybe this should be more generic and just be hasErrors? That way it will cover all errors, not just load errors.

if (!layer.dataHasLoadError()) {
Promise.resolve(layer.syncLayerWithMB(this._mbMap))
.catch(({ message }) => {
switch(layer._descriptor.type) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this switch needed? I would say all errors have the same action - update the layer state with the error message

return this._source.renderSourceSettingsEditor({ onChange });
}

_findDataRequestForSource(sourceDataId) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete unused method


dataHasLoadError() {
return this._dataRequests.some(dataRequest => dataRequest.hasLoadError());
return this._descriptor && this._descriptor.errorState || false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should errorState be errorMessage? What is the difference between the two?

Copy link
Copy Markdown
Contributor Author

@kindsun kindsun Jan 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error state is a boolean value. When an error occurs, the layer is put in errorState and is assigned an errorMessage, assuming there is one. I could change it to something like isInErrorState if we wanted to be explicit on the boolean status

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rename to isInErrorState, that makes a lot of sense. Also this._descriptor.errorState || false should use lodash _.get to avoid problems like this #29275

return dataRequest._descriptor.dataLoadError;
});
return loadErrors.join(',');
return this.dataHasLoadError() ? this._descriptor.errorMessage : '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe errorMessage should be an array errors so multiple message could be displayed. For example lets say you had a EMS vector layer with a join. Then EMS is not available and the index pattern of the join got deleted. Should have two messages - 1) Can not load EMS vector layer. 2) Can not access index pattern X

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to hold off on this since we're primarily focused on TMS for this issue, which should have limited errors. I do think our messaging deserves further fleshing out. If we don't already have one, we likely need a future issue around better error messaging to the user. We have fairly robust messaging for the legacy maps now (some of it pending merge). We could port that over and rethink how errors are relayed in the process.

return service.id === this._descriptor.id;
});
if (!emsTmsService) {
console.error(`EMS TMS Service: ${this._descriptor.id} currently unavailable`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this console error is needed. Just going to spam the console. The server should log an error if it can not reach EMS. Not need for the console to log errors since there will be something visible to the user through the UI

let attributions;
try {
service = this._getTMSOptions();
attributions = service.attributionMarkdown.split('|');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check that service exists because _getTMSOptions can return null

map.off('dataloading');
};

checkInterval = setInterval(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this interval needed? Isn't the timer sufficient?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running timers stress me out :), especially ones that run for 32 seconds as this one does. I added this on a second pass to clear the timer as soon as the tiles have loaded rather than keep running. I don't mind removing it though. It's a little cluttery and we can always bring it back if we want.

}, 1000);
tileLoadTimer = setTimeout(() => {
if (!tileLoad) {
try {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you throw from within a try and then catch it? Could just just be

if (!tileLoadCount) {
  reject(new Error(`Tiles from "${url}" could not be loaded`));
  return;
}
resolve();

…Disabling

# Conflicts:
#	x-pack/plugins/gis/public/shared/layers/layer.js
#	x-pack/plugins/gis/public/shared/layers/tile_layer.js
#	x-pack/plugins/gis/public/shared/layers/util/data_request.js
#	x-pack/plugins/gis/public/shared/layers/vector_layer.js
dataHasLoadError() {
return this._dataRequests.some(dataRequest => dataRequest.hasLoadError());
hasErrors() {
return _.get(this._descriptor, 'isInErrorState', false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is isInErrorState needed? Why not just check if this._descriptor.errorMessage exists or not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed, just preferred


_getTMSOptions() {
return this._emsTileServices.find(service => {
if(!this._emsTileServices || !this._emsTileServices.length) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method can be simplified to just

_getTMSOptions() {
    if(!this._emsTileServices) {
      return;
    }

    return this._emsTileServices.find(service => {
      return service.id === this._descriptor.id;
    });
  }

});
let service;
let attributions;
try {
Copy link
Copy Markdown
Contributor

@nreese nreese Jan 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this try/catch block going to catch? I don't think its needed since this._getTMSOptions does not throw and error and service.attributionMarkdown.split should not throw an error. This entire method code be rewritten to the below which I think is much more readable

async getAttributions() {
  const service = this._getTMSOptions();
  if (!service || !service.attributionMarkdown) {
    return [];
  }

  return service.attributionMarkdown.split('|').map((attribution) => {
    attribution = attribution.trim();
    //this assumes attribution is plain markdown link
    const extractLink = /\[(.*)\]\((.*)\)/;
    const result = extractLink.exec(attribution);
    return {
      label: result ? result[1] : null,
      url: result ? result[2] : null
    };
  });
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was an error getting thrown specific to attributions, otherwise I wouldn't have modified it at all since it's not directly related. I believe it was attempting to create attributions on unresolved layers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But where is the error getting thrown? Is it getting thrown by _getTMSOptions? This method needs some clean-up. It is very difficult to read and has some problems. For example, if an exception is thrown and attributions is not set when you will call attributions.map with will throw an exception. Also, your return returns mixed types. Sometimes it will return an array and sometimes it will return an empty string. It should return an [] if there are not attributes or there are problems getting them. The implementation I provided should be fine and I don't think needs to be a try/catch block

export class TileLayer extends ALayer {

static type = "TILE";
TMS_LOAD_TIMEOUT = 32000;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did 32 seconds come from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offline discussion with @thomasneirynck but for resolution of the TMS layers in the legacy map apps. For consistency, I made the timeout the same here

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@kindsun
Copy link
Copy Markdown
Contributor Author

kindsun commented Jan 25, 2019

retest

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

…Disabling

# Conflicts:
#	x-pack/plugins/gis/public/components/widget_overlay/layer_control/layer_toc/toc_entry/view.js
@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

Copy link
Copy Markdown
Contributor

@nreese nreese left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some dataHasLoadError and getDataLoadError function names snuck in with latest rebase

https://github.com/elastic/kibana/blob/master/x-pack/plugins/gis/public/shared/components/layer_toc_actions.js#L80

static createDescriptor(options = {}) {
const layerDescriptor = { ...options };

layerDescriptor.source = this._source;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you setting descriptor.source to an object instance? I thought the descriptors just held pure data and not instances. I don't think this is needed and may cause problems when storing the layer in the store since this._source is not serializable.

if (!this.hasErrors()) {
return await this._source.getAttributions();
}
return '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should return [] to be consistent with this._source.getAttributions()

export class TileLayer extends AbstractLayer {

static type = "TILE";
TMS_LOAD_TIMEOUT = 32000;
Copy link
Copy Markdown
Contributor

@nreese nreese Jan 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right now TMS_LOAD_TIMEOUT on this is editable. Move this out of class and make const

map.on('dataloading', ({ tile }) => {
if (tile && tile.request) {
// If at least one tile loads, endpoint/resource is valid
tile.request.onloadend = ({ loaded }) => loaded && (tileLoad = true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tile.request.onloadend = ({ loaded }) => loaded && (tileLoad = true); is difficult to read in a single line. How about breaking it out so its easier to read and under stand that tileLoad is getting set to true

        tile.request.onloadend = ({ loaded }) => {
          if (loaded) {
            tileLoad = true;
          }
        };

}

let url;
return new Promise((resolve, reject) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in this first promise block is async. Why does the first new Promise exist? It really hurts readability to have all of these nested Promises and .then callbacks.

This function can be rewritten to the below which is much easier to follow. If this._source.getUrlTemplate or await this._tileLoadErrorTracker(mbMap, url) fail the function will throw an error which will be catch by the caller x-pack/plugins/gis/public/components/map/mb/view.js

  async syncLayerWithMB(mbMap) {
    const source = mbMap.getSource(this.getId());
    const layerId = this.getId() + '_raster';

    if (source) {
      return;
    }

    const url = this._source.getUrlTemplate();
    const sourceId = this.getId();
    mbMap.addSource(sourceId, {
      type: 'raster',
      tiles: [url],
      tileSize: 256,
      scheme: 'xyz',
    });

    mbMap.addLayer({
      id: layerId,
      type: 'raster',
      source: sourceId,
      minzoom: 0,
      maxzoom: 22,
    });

    await this._tileLoadErrorTracker(mbMap, url);

    mbMap.setLayoutProperty(layerId, 'visibility', this.isVisible() ? 'visible' : 'none');
    mbMap.setLayerZoomRange(layerId, this._descriptor.minZoom, this._descriptor.maxZoom);
    this._style && this._style.setMBPaintProperties({
      alpha: this.getAlpha(),
      mbMap,
      layerId,
    });
  }

@elasticmachine
Copy link
Copy Markdown
Contributor

💚 Build Succeeded

Copy link
Copy Markdown
Contributor

@thomasneirynck thomasneirynck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment. I'm running into the same for #29367.

If we change the signature to async of the layer syncing with mapbox-gl, the client-code needs to handle this asynchronous nature.

});
}

async syncLayerWithMB(mbMap) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we make this signature async, than the client-code should take into account the async-nature of this method.

In practice, the completion of this method (https://github.com/thomasneirynck/kibana/blob/93fe5b7b2f566cc4519cc257834b110eb8f8248b/x-pack/plugins/gis/public/components/map/mb/view.js#L194) now no longer guarantees that the map has synced.

layer.syncLayerWithMB(this._mbMap);
layerList.forEach(layer => {
if (!layer.hasErrors()) {
Promise.resolve(layer.syncLayerWithMB(this._mbMap))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This causes the syncing operation to unhook from the callstack, and completion is not guaranteed.

@thomasneirynck thomasneirynck changed the title Handle unavailable tilemap services [Maps] Handle unavailable tilemap services Jan 28, 2019
Copy link
Copy Markdown
Contributor

@thomasneirynck thomasneirynck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@kindsun kindsun merged commit 20c5c3b into elastic:master Jan 28, 2019
kindsun added a commit to kindsun/kibana that referenced this pull request Jan 29, 2019
* Update ems utils to better handle no service results. Prevent excess attribution errors

* Update tile layer sync to return promise and handle errors related to both obtaining url and tile loading

* Add flow for updating tms layers with error status/message

* Handle promises, if returned, on syncLayerWithMB. Update TMS error status

* Exclude layers that mapbox didn't add to map but are tracked in layer list from reordering logic

* Move datarequest handling to vector layer. Use relevant data load/error logic for tile and vector layers

* Don't try to get attributions on errored layer

* Handle 'includeElasticMapsService' configuration

* Move data requests back to layer level for heatmap usage

* Update all layers to set top-level layer error status and message. Consolidate redundant code

* Update tile sync function to more reliably confirm load status after loading via callback. Add interval to cancel timer

* Remove unnecessary, and annoying, clear temp layers on tms error

* Clean up

* More clean up

* Review feedback

* Review feedback. Test cleanup

* Test fixes and review feedback
kindsun added a commit that referenced this pull request Jan 29, 2019
* Update ems utils to better handle no service results. Prevent excess attribution errors

* Update tile layer sync to return promise and handle errors related to both obtaining url and tile loading

* Add flow for updating tms layers with error status/message

* Handle promises, if returned, on syncLayerWithMB. Update TMS error status

* Exclude layers that mapbox didn't add to map but are tracked in layer list from reordering logic

* Move datarequest handling to vector layer. Use relevant data load/error logic for tile and vector layers

* Don't try to get attributions on errored layer

* Handle 'includeElasticMapsService' configuration

* Move data requests back to layer level for heatmap usage

* Update all layers to set top-level layer error status and message. Consolidate redundant code

* Update tile sync function to more reliably confirm load status after loading via callback. Add interval to cancel timer

* Remove unnecessary, and annoying, clear temp layers on tms error

* Clean up

* More clean up

* Review feedback

* Review feedback. Test cleanup

* Test fixes and review feedback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Team:Geo Former Team Label for Geo Team. Now use Team:Presentation v6.7.0 v7.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants