mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Maps] Handle unavailable tilemap services (#28852)
* 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
This commit is contained in:
parent
2cb94be6fd
commit
20c5c3b11f
12 changed files with 149 additions and 92 deletions
|
@ -21,6 +21,7 @@ import { timeService } from '../kibana_services';
|
|||
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_LAYER_ERROR_STATUS = 'SET_LAYER_ERROR_STATUS';
|
||||
export const ADD_WAITING_FOR_MAP_READY_LAYER = 'ADD_WAITING_FOR_MAP_READY_LAYER';
|
||||
export const CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST = 'CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST';
|
||||
export const REMOVE_LAYER = 'REMOVE_LAYER';
|
||||
|
@ -108,6 +109,16 @@ export function addLayer(layerDescriptor) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setLayerErrorStatus(id, errorMessage) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: SET_LAYER_ERROR_STATUS,
|
||||
layerId: id,
|
||||
errorMessage,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleLayerVisible(layerId) {
|
||||
return {
|
||||
type: TOGGLE_LAYER_VISIBLE,
|
||||
|
|
|
@ -12,7 +12,8 @@ import {
|
|||
mapDestroyed,
|
||||
setMouseCoordinates,
|
||||
clearMouseCoordinates,
|
||||
clearGoto
|
||||
clearGoto,
|
||||
setLayerErrorStatus,
|
||||
} from '../../../actions/store_actions';
|
||||
import { getLayerList, getMapReady, getGoto } from "../../../selectors/map_selectors";
|
||||
|
||||
|
@ -45,7 +46,9 @@ function mapDispatchToProps(dispatch) {
|
|||
},
|
||||
clearGoto: () => {
|
||||
dispatch(clearGoto());
|
||||
}
|
||||
},
|
||||
setLayerErrorStatus: (id, msg) =>
|
||||
dispatch(setLayerErrorStatus(id, msg))
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,8 @@ export function syncLayerOrder(mbMap, layerList) {
|
|||
const mbLayers = mbMap.getStyle().layers.slice();
|
||||
const currentLayerOrder = _.uniq( // Consolidate layers and remove suffix
|
||||
mbLayers.map(({ id }) => id.substring(0, id.lastIndexOf('_'))));
|
||||
const newLayerOrder = layerList.map(l => l.getId());
|
||||
const newLayerOrder = layerList.map(l => l.getId())
|
||||
.filter(layerId => currentLayerOrder.includes(layerId));
|
||||
let netPos = 0;
|
||||
let netNeg = 0;
|
||||
const movementArr = currentLayerOrder.reduce((accu, id, idx) => {
|
||||
|
|
|
@ -200,9 +200,14 @@ export class MBMapContainer extends React.Component {
|
|||
if (!isMapReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeOrphanedSourcesAndLayers(this._mbMap, layerList);
|
||||
layerList.forEach((layer) => {
|
||||
layer.syncLayerWithMB(this._mbMap);
|
||||
layerList.forEach(layer => {
|
||||
if (!layer.hasErrors()) {
|
||||
Promise.resolve(layer.syncLayerWithMB(this._mbMap))
|
||||
.catch(({ message }) =>
|
||||
this.props.setLayerErrorStatus(layer.getId(), message));
|
||||
}
|
||||
});
|
||||
syncLayerOrder(this._mbMap, layerList);
|
||||
};
|
||||
|
|
|
@ -82,7 +82,8 @@ export class TOCEntry extends React.Component {
|
|||
alignItems="center"
|
||||
responsive={false}
|
||||
className={
|
||||
layer.isVisible() && layer.showAtZoomLevel(zoom) && !layer.dataHasLoadError() ? 'gisTocEntry-visible' : 'gisTocEntry-notVisible'
|
||||
layer.isVisible() && layer.showAtZoomLevel(zoom)
|
||||
&& !layer.hasErrors() ? 'gisTocEntry-visible' : 'gisTocEntry-notVisible'
|
||||
}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -77,14 +77,14 @@ export class LayerTocActions extends Component {
|
|||
_renderIcon() {
|
||||
const { zoom, layer } = this.props;
|
||||
let smallLegendIcon;
|
||||
if (layer.dataHasLoadError()) {
|
||||
if (layer.hasErrors()) {
|
||||
smallLegendIcon = (
|
||||
<EuiIconTip
|
||||
aria-label="Load warning"
|
||||
size="m"
|
||||
type="alert"
|
||||
color="warning"
|
||||
content={layer.getDataLoadError()}
|
||||
content={layer.getErrors()}
|
||||
/>
|
||||
);
|
||||
} else if (layer.isLayerLoading()) {
|
||||
|
|
|
@ -17,7 +17,6 @@ export class AbstractLayer {
|
|||
this._descriptor = AbstractLayer.createDescriptor(layerDescriptor);
|
||||
this._source = source;
|
||||
this._style = style;
|
||||
|
||||
if (this._descriptor.dataRequests) {
|
||||
this._dataRequests = this._descriptor.dataRequests.map(dataRequest => new DataRequest(dataRequest));
|
||||
} else {
|
||||
|
@ -64,7 +63,10 @@ export class AbstractLayer {
|
|||
}
|
||||
|
||||
async getAttributions() {
|
||||
return await this._source.getAttributions();
|
||||
if (!this.hasErrors()) {
|
||||
return await this._source.getAttributions();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
|
@ -139,21 +141,20 @@ export class AbstractLayer {
|
|||
return this._source.renderSourceSettingsEditor({ onChange });
|
||||
};
|
||||
|
||||
getSourceDataRequest() {
|
||||
return this._dataRequests.find(dataRequest => dataRequest.getDataId() === 'source');
|
||||
}
|
||||
|
||||
isLayerLoading() {
|
||||
return this._dataRequests.some(dataRequest => dataRequest.isLoading());
|
||||
}
|
||||
|
||||
dataHasLoadError() {
|
||||
return this._dataRequests.some(dataRequest => dataRequest.hasLoadError());
|
||||
hasErrors() {
|
||||
return _.get(this._descriptor, 'isInErrorState', false);
|
||||
}
|
||||
|
||||
getDataLoadError() {
|
||||
const loadErrors = this._dataRequests
|
||||
.filter(dataRequest => dataRequest.hasLoadError())
|
||||
.map(dataRequest => {
|
||||
return dataRequest._descriptor.dataLoadError;
|
||||
});
|
||||
return loadErrors.join(',');
|
||||
getErrors() {
|
||||
return this.hasErrors() ? this._descriptor.errorMessage : '';
|
||||
}
|
||||
|
||||
toLayerDescriptor() {
|
||||
|
@ -219,10 +220,6 @@ export class AbstractLayer {
|
|||
return style.renderEditor(options);
|
||||
}
|
||||
|
||||
getSourceDataRequest() {
|
||||
return this._dataRequests.find(dataRequest => dataRequest.getDataId() === 'source');
|
||||
}
|
||||
|
||||
getIndexPatternIds() {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -71,6 +71,10 @@ export class EMSTMSSource extends AbstractTMSSource {
|
|||
}
|
||||
|
||||
_getTMSOptions() {
|
||||
if(!this._emsTileServices) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._emsTileServices.find(service => {
|
||||
return service.id === this._descriptor.id;
|
||||
});
|
||||
|
@ -96,9 +100,11 @@ export class EMSTMSSource extends AbstractTMSSource {
|
|||
|
||||
async getAttributions() {
|
||||
const service = this._getTMSOptions();
|
||||
const attributions = service.attributionMarkdown.split('|');
|
||||
if (!service || !service.attributionMarkdown) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return attributions.map((attribution) => {
|
||||
return service.attributionMarkdown.split('|').map((attribution) => {
|
||||
attribution = attribution.trim();
|
||||
//this assumes attribution is plain markdown link
|
||||
const extractLink = /\[(.*)\]\((.*)\)/;
|
||||
|
@ -112,8 +118,9 @@ export class EMSTMSSource extends AbstractTMSSource {
|
|||
|
||||
getUrlTemplate() {
|
||||
const service = this._getTMSOptions();
|
||||
if (!service || !service.url) {
|
||||
throw new Error('Can not generate EMS TMS url template');
|
||||
}
|
||||
return service.url;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import React from 'react';
|
|||
import { EuiIcon } from '@elastic/eui';
|
||||
import { TileStyle } from '../layers/styles/tile_style';
|
||||
|
||||
const TMS_LOAD_TIMEOUT = 32000;
|
||||
|
||||
export class TileLayer extends AbstractLayer {
|
||||
|
||||
static type = "TILE";
|
||||
|
@ -30,29 +32,65 @@ export class TileLayer extends AbstractLayer {
|
|||
return tileLayerDescriptor;
|
||||
}
|
||||
|
||||
_tileLoadErrorTracker(map, url) {
|
||||
let tileLoad;
|
||||
map.on('dataloading', ({ tile }) => {
|
||||
if (tile && tile.request) {
|
||||
// If at least one tile loads, endpoint/resource is valid
|
||||
tile.request.onloadend = ({ loaded }) => {
|
||||
if (loaded) {
|
||||
tileLoad = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
syncLayerWithMB(mbMap) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let tileLoadTimer = null;
|
||||
|
||||
const clearChecks = () => {
|
||||
clearTimeout(tileLoadTimer);
|
||||
map.off('dataloading');
|
||||
};
|
||||
|
||||
tileLoadTimer = setTimeout(() => {
|
||||
if (!tileLoad) {
|
||||
reject(new Error(`Tiles from "${url}" could not be loaded`));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
clearChecks();
|
||||
}, TMS_LOAD_TIMEOUT);
|
||||
});
|
||||
}
|
||||
|
||||
async syncLayerWithMB(mbMap) {
|
||||
const source = mbMap.getSource(this.getId());
|
||||
const layerId = this.getId() + '_raster';
|
||||
if (!source) {
|
||||
const url = this._source.getUrlTemplate();
|
||||
mbMap.addSource(this.getId(), {
|
||||
type: 'raster',
|
||||
tiles: [url],
|
||||
tileSize: 256,
|
||||
scheme: 'xyz',
|
||||
});
|
||||
|
||||
mbMap.addLayer({
|
||||
id: layerId,
|
||||
type: 'raster',
|
||||
source: this.getId(),
|
||||
minzoom: 0,
|
||||
maxzoom: 22,
|
||||
});
|
||||
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({
|
||||
|
@ -73,5 +111,8 @@ export class TileLayer extends AbstractLayer {
|
|||
/>
|
||||
);
|
||||
}
|
||||
isLayerLoading() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,14 +9,6 @@ export class DataRequest {
|
|||
this._descriptor = descriptor;
|
||||
}
|
||||
|
||||
hasLoadError() {
|
||||
return !!this._descriptor.dataHasLoadError;
|
||||
}
|
||||
|
||||
getLoadError() {
|
||||
return this._descriptor.dataLoadError;
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this._descriptor.data;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
LAYER_DATA_LOAD_ENDED,
|
||||
LAYER_DATA_LOAD_ERROR,
|
||||
ADD_LAYER,
|
||||
SET_LAYER_ERROR_STATUS,
|
||||
ADD_WAITING_FOR_MAP_READY_LAYER,
|
||||
CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST,
|
||||
REMOVE_LAYER,
|
||||
|
@ -132,8 +133,11 @@ export function map(state = INITIAL_STATE, action) {
|
|||
};
|
||||
case LAYER_DATA_LOAD_STARTED:
|
||||
return updateWithDataRequest(state, action);
|
||||
case SET_LAYER_ERROR_STATUS:
|
||||
return setErrorStatus(state, action);
|
||||
case LAYER_DATA_LOAD_ERROR:
|
||||
return updateWithDataLoadError(state, action);
|
||||
const errorRequestResetState = resetDataRequest(state, action);
|
||||
return setErrorStatus(errorRequestResetState, action);
|
||||
case LAYER_DATA_LOAD_ENDED:
|
||||
return updateWithDataResponse(state, action);
|
||||
case TOUCH_LAYER:
|
||||
|
@ -263,6 +267,15 @@ export function map(state = INITIAL_STATE, action) {
|
|||
}
|
||||
}
|
||||
|
||||
function setErrorStatus(state, { layerId, errorMessage }) {
|
||||
const tmsErrorLayer = state.layerList.find(({ id }) => id === layerId);
|
||||
return tmsErrorLayer
|
||||
? updateLayerInList(
|
||||
updateLayerInList(state, tmsErrorLayer.id, 'isInErrorState', true),
|
||||
tmsErrorLayer.id, 'errorMessage', errorMessage)
|
||||
: state;
|
||||
}
|
||||
|
||||
function findDataRequest(layerDescriptor, dataRequestAction) {
|
||||
|
||||
if (!layerDescriptor.dataRequests) {
|
||||
|
@ -276,24 +289,17 @@ function findDataRequest(layerDescriptor, dataRequestAction) {
|
|||
|
||||
|
||||
function updateWithDataRequest(state, action) {
|
||||
let dataRequest = getValidDataRequest(state, action, false);
|
||||
const layerRequestingData = findLayerById(state, action.layerId);
|
||||
if (!layerRequestingData) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (!layerRequestingData.dataRequests) {
|
||||
layerRequestingData.dataRequests = [];
|
||||
}
|
||||
|
||||
let dataRequest = findDataRequest(layerRequestingData, action);
|
||||
if (!dataRequest) {
|
||||
dataRequest = {
|
||||
dataId: action.dataId
|
||||
};
|
||||
layerRequestingData.dataRequests.push(dataRequest);
|
||||
layerRequestingData.dataRequests = [
|
||||
...(layerRequestingData.dataRequests
|
||||
? layerRequestingData.dataRequests : []), dataRequest ];
|
||||
}
|
||||
dataRequest.dataHasLoadError = false;
|
||||
dataRequest.dataLoadError = null;
|
||||
dataRequest.dataMetaAtStart = action.meta;
|
||||
dataRequest.dataRequestToken = action.requestToken;
|
||||
const layerList = [...state.layerList];
|
||||
|
@ -301,59 +307,45 @@ function updateWithDataRequest(state, action) {
|
|||
}
|
||||
|
||||
function updateWithDataResponse(state, action) {
|
||||
const layerReceivingData = findLayerById(state, action.layerId);
|
||||
if (!layerReceivingData) {
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
const dataRequest = findDataRequest(layerReceivingData, action);
|
||||
if (!dataRequest) {
|
||||
throw new Error('Data request should be initialized. Cannot call stopLoading before startLoading');
|
||||
}
|
||||
|
||||
if (
|
||||
dataRequest.dataRequestToken &&
|
||||
dataRequest.dataRequestToken !== action.requestToken
|
||||
) {
|
||||
// ignore responses to outdated requests
|
||||
return { ...state };
|
||||
}
|
||||
const dataRequest = getValidDataRequest(state, action);
|
||||
if (!dataRequest) { return state; }
|
||||
|
||||
dataRequest.data = action.data;
|
||||
dataRequest.dataMeta = { ...dataRequest.dataMetaAtStart, ...action.meta };
|
||||
dataRequest.dataMetaAtStart = null;
|
||||
return resetDataRequest(state, action, dataRequest);
|
||||
}
|
||||
|
||||
function resetDataRequest(state, action, request) {
|
||||
const dataRequest = request || getValidDataRequest(state, action);
|
||||
if (!dataRequest) { return state; }
|
||||
|
||||
dataRequest.dataRequestToken = null;
|
||||
dataRequest.dataId = action.dataId;
|
||||
const layerList = [...state.layerList];
|
||||
return { ...state, layerList };
|
||||
}
|
||||
|
||||
function updateWithDataLoadError(state, action) {
|
||||
function getValidDataRequest(state, action, checkRequestToken = true) {
|
||||
const layer = findLayerById(state, action.layerId);
|
||||
if (!layer) {
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
const dataRequest = findDataRequest(layer, action);
|
||||
if (!dataRequest) {
|
||||
throw new Error('Data request should be initialized. Cannot call loadError before startLoading');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
checkRequestToken &&
|
||||
dataRequest.dataRequestToken &&
|
||||
dataRequest.dataRequestToken !== action.requestToken
|
||||
) {
|
||||
// ignore responses to outdated requests
|
||||
return state;
|
||||
return;
|
||||
}
|
||||
|
||||
dataRequest.dataHasLoadError = true;
|
||||
dataRequest.dataLoadError = action.errorMessage;
|
||||
dataRequest.dataRequestToken = null;
|
||||
dataRequest.dataId = action.dataId;
|
||||
const layerList = [...state.layerList];
|
||||
return { ...state, layerList };
|
||||
return dataRequest;
|
||||
}
|
||||
|
||||
function findLayerById(state, id) {
|
||||
|
|
|
@ -79,6 +79,13 @@ export function initRoutes(server, licenseUid) {
|
|||
|
||||
async function getEMSResources(licenseUid) {
|
||||
|
||||
if (!mapConfig.includeElasticMapsService) {
|
||||
return {
|
||||
fileLayers: [],
|
||||
tmsServices: []
|
||||
};
|
||||
}
|
||||
|
||||
emsClient.addQueryParams({ license: licenseUid });
|
||||
const fileLayerObjs = await emsClient.getFileLayers();
|
||||
const tmsServicesObjs = await emsClient.getTMSServices();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue