[Maps] Handle unavailable tilemap services#28852
Conversation
…attribution errors
… both obtaining url and tile loading
… list from reordering logic
…or logic for tile and vector layers
💔 Build Failed |
💔 Build Failed |
|
Pinging @elastic/kibana-gis |
…nsolidate redundant code
…loading via callback. Add interval to cancel timer
💔 Build Failed |
💔 Build Failed |
💔 Build Failed |
💔 Build Failed |
|
retest |
💚 Build Succeeded |
# Conflicts: # x-pack/plugins/gis/public/shared/layers/layer.js
💚 Build Succeeded |
nreese
left a comment
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
|
|
||
| dataHasLoadError() { | ||
| return this._dataRequests.some(dataRequest => dataRequest.hasLoadError()); | ||
| return this._descriptor && this._descriptor.errorState || false; |
There was a problem hiding this comment.
Should errorState be errorMessage? What is the difference between the two?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 : ''; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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`); |
There was a problem hiding this comment.
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('|'); |
There was a problem hiding this comment.
Need to check that service exists because _getTMSOptions can return null
| map.off('dataloading'); | ||
| }; | ||
|
|
||
| checkInterval = setInterval(() => { |
There was a problem hiding this comment.
Why is this interval needed? Isn't the timer sufficient?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Is isInErrorState needed? Why not just check if this._descriptor.errorMessage exists or not?
There was a problem hiding this comment.
Not needed, just preferred
|
|
||
| _getTMSOptions() { | ||
| return this._emsTileServices.find(service => { | ||
| if(!this._emsTileServices || !this._emsTileServices.length) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
};
});
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Where did 32 seconds come from?
There was a problem hiding this comment.
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
💔 Build Failed |
💔 Build Failed |
|
retest |
💔 Build Failed |
…Disabling # Conflicts: # x-pack/plugins/gis/public/components/widget_overlay/layer_control/layer_toc/toc_entry/view.js
💔 Build Failed |
nreese
left a comment
There was a problem hiding this comment.
Looks like some dataHasLoadError and getDataLoadError function names snuck in with latest rebase
| static createDescriptor(options = {}) { | ||
| const layerDescriptor = { ...options }; | ||
|
|
||
| layerDescriptor.source = this._source; |
There was a problem hiding this comment.
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 ''; |
There was a problem hiding this comment.
Should return [] to be consistent with this._source.getAttributions()
| export class TileLayer extends AbstractLayer { | ||
|
|
||
| static type = "TILE"; | ||
| TMS_LOAD_TIMEOUT = 32000; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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,
});
}
💚 Build Succeeded |
thomasneirynck
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
This causes the syncing operation to unhook from the callstack, and completion is not guaranteed.
* 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
* 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
Resolves #27832 & #28119. Handles errors from:
A few details: