mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[maps] vector tile inspector (#131565)
* [maps] Vector tile inspector adapter * empty prompt * add layer select * tile request view * show gridTile es path and body * show error message * hits request * tab with editor * clean up * open in console * do not track same tile multiple times * remove layer from vector tile inspector when layer is removed * refactor tile request generation * show path in inspector * requests view callout * remove duplicated server side code * remove unused files * fix map_actions test * open requests view when getting requests from inspector * only show view when adapter is present * fix open in console link not matching tile request * tslint * fix search sessions functional test * update trouble shooting docs * use bold in docs * fix tiles at zoom level 0 * revert changes to mb_map * include path when copying to clipboard * clear inspector when layer type changes * tslint fix * clean-up * update callout copy * empty prompt copy * copy updates Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
03e1233408
commit
72ec630f91
42 changed files with 1098 additions and 283 deletions
Binary file not shown.
Before Width: | Height: | Size: 879 KiB |
BIN
docs/maps/images/requests_inspector.png
Normal file
BIN
docs/maps/images/requests_inspector.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 802 KiB |
BIN
docs/maps/images/vector_tile_inspector.png
Normal file
BIN
docs/maps/images/vector_tile_inspector.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 784 KiB |
|
@ -12,10 +12,13 @@ Use the information in this section to inspect Elasticsearch requests and find s
|
|||
[float]
|
||||
=== Inspect Elasticsearch requests
|
||||
|
||||
Maps uses the {ref}/search-search.html[{es} search API] to get documents and aggregation results from {es}. To troubleshoot these requests, open the Inspector, which shows the most recent requests for each layer. You can switch between different requests using the *Request* dropdown.
|
||||
Maps uses the {ref}/search-vector-tile-api.html[{es} vector tile search API] and the {ref}/search-search.html[{es} search API] to get documents and aggregation results from {es}. Use *Vector tiles* inspector to view {es} vector tile search API requests. Use *Requests* inspector to view {es} search API requests.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/inspector.png[]
|
||||
image::maps/images/vector_tile_inspector.png[]
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/requests_inspector.png[]
|
||||
|
||||
[float]
|
||||
=== Solutions to common problems
|
||||
|
|
|
@ -20,7 +20,7 @@ export const getRequestsViewDescription = (): InspectorViewDescription => ({
|
|||
}),
|
||||
order: 20,
|
||||
help: i18n.translate('inspector.requests.requestsDescriptionTooltip', {
|
||||
defaultMessage: 'View the requests that collected the data',
|
||||
defaultMessage: 'View the search requests used to collect the data',
|
||||
}),
|
||||
shouldShow(adapters: Adapters) {
|
||||
return Boolean(adapters.requests);
|
||||
|
|
|
@ -6,9 +6,14 @@
|
|||
*/
|
||||
|
||||
import { isUndefined, omitBy } from 'lodash';
|
||||
import type { KibanaExecutionContext } from '@kbn/core/public';
|
||||
import { APP_ID } from './constants';
|
||||
|
||||
export function makeExecutionContext(context: { id?: string; url?: string; description?: string }) {
|
||||
export function makeExecutionContext(context: {
|
||||
id?: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
}): KibanaExecutionContext {
|
||||
return omitBy(
|
||||
{
|
||||
name: APP_ID,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { RisonValue } from 'rison-node';
|
||||
import rison from 'rison-node';
|
||||
import { RENDER_AS } from './constants';
|
||||
|
||||
export function decodeMvtResponseBody(encodedRequestBody: string): object {
|
||||
return rison.decode(decodeURIComponent(encodedRequestBody)) as object;
|
||||
|
@ -15,3 +16,107 @@ export function decodeMvtResponseBody(encodedRequestBody: string): object {
|
|||
export function encodeMvtResponseBody(unencodedRequestBody: object): string {
|
||||
return encodeURIComponent(rison.encode(unencodedRequestBody as RisonValue));
|
||||
}
|
||||
|
||||
export function getAggsTileRequest({
|
||||
encodedRequestBody,
|
||||
geometryFieldName,
|
||||
gridPrecision,
|
||||
index,
|
||||
renderAs = RENDER_AS.POINT,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
}: {
|
||||
encodedRequestBody: string;
|
||||
geometryFieldName: string;
|
||||
gridPrecision: number;
|
||||
index: string;
|
||||
renderAs: RENDER_AS;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}) {
|
||||
const requestBody = decodeMvtResponseBody(encodedRequestBody) as any;
|
||||
return {
|
||||
path: `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`,
|
||||
body: {
|
||||
size: 0, // no hits
|
||||
grid_precision: gridPrecision,
|
||||
exact_bounds: false,
|
||||
extent: 4096, // full resolution,
|
||||
query: requestBody.query,
|
||||
grid_agg: renderAs === RENDER_AS.HEX ? 'geohex' : 'geotile',
|
||||
grid_type: renderAs === RENDER_AS.GRID || renderAs === RENDER_AS.HEX ? 'grid' : 'centroid',
|
||||
aggs: requestBody.aggs,
|
||||
fields: requestBody.fields,
|
||||
runtime_mappings: requestBody.runtime_mappings,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getHitsTileRequest({
|
||||
encodedRequestBody,
|
||||
geometryFieldName,
|
||||
index,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
}: {
|
||||
encodedRequestBody: string;
|
||||
geometryFieldName: string;
|
||||
index: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}) {
|
||||
const requestBody = decodeMvtResponseBody(encodedRequestBody) as any;
|
||||
return {
|
||||
path: `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`,
|
||||
body: {
|
||||
grid_precision: 0, // no aggs
|
||||
exact_bounds: true,
|
||||
extent: 4096, // full resolution,
|
||||
query: requestBody.query,
|
||||
fields: mergeFields(
|
||||
[
|
||||
requestBody.docvalue_fields as Field[] | undefined,
|
||||
requestBody.stored_fields as Field[] | undefined,
|
||||
],
|
||||
[geometryFieldName]
|
||||
),
|
||||
runtime_mappings: requestBody.runtime_mappings,
|
||||
track_total_hits: typeof requestBody.size === 'number' ? requestBody.size + 1 : false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// can not use "import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey"
|
||||
// SearchRequest is incorrectly typed and does not support Field as object
|
||||
// https://github.com/elastic/elasticsearch-js/issues/1615
|
||||
type Field =
|
||||
| string
|
||||
| {
|
||||
field: string;
|
||||
format: string;
|
||||
};
|
||||
|
||||
function mergeFields(fieldsList: Array<Field[] | undefined>, excludeNames: string[]): Field[] {
|
||||
const fieldNames: string[] = [];
|
||||
const mergedFields: Field[] = [];
|
||||
|
||||
fieldsList.forEach((fields) => {
|
||||
if (!fields) {
|
||||
return;
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
const fieldName = typeof field === 'string' ? field : field.field;
|
||||
if (!excludeNames.includes(fieldName) && !fieldNames.includes(fieldName)) {
|
||||
fieldNames.push(fieldName);
|
||||
mergedFields.push(field);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return mergedFields;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
getSelectedLayerId,
|
||||
} from '../selectors/map_selectors';
|
||||
import { FLYOUT_STATE } from '../reducers/ui';
|
||||
import { cancelRequest } from '../reducers/non_serializable_instances';
|
||||
import { cancelRequest, getInspectorAdapters } from '../reducers/non_serializable_instances';
|
||||
import { setDrawMode, updateFlyout } from './ui_actions';
|
||||
import {
|
||||
ADD_LAYER,
|
||||
|
@ -451,6 +451,9 @@ function updateLayerType(layerId: string, newLayerType: string) {
|
|||
return;
|
||||
}
|
||||
dispatch(clearDataRequests(layer));
|
||||
if (layer.getSource().isESSource()) {
|
||||
getInspectorAdapters(getState()).vectorTiles?.removeLayer(layerId);
|
||||
}
|
||||
dispatch({
|
||||
type: UPDATE_LAYER_PROP,
|
||||
id: layerId,
|
||||
|
@ -587,6 +590,9 @@ function removeLayerFromLayerList(layerId: string) {
|
|||
});
|
||||
dispatch(updateTooltipStateForLayer(layerGettingRemoved));
|
||||
layerGettingRemoved.destroy();
|
||||
if (layerGettingRemoved.getSource().isESSource()) {
|
||||
getInspectorAdapters(getState())?.vectorTiles.removeLayer(layerId);
|
||||
}
|
||||
dispatch({
|
||||
type: REMOVE_LAYER,
|
||||
id: layerId,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
/* eslint @typescript-eslint/no-var-requires: 0 */
|
||||
|
||||
jest.mock('../selectors/map_selectors', () => ({}));
|
||||
jest.mock('../reducers/non_serializable_instances', () => ({}));
|
||||
jest.mock('./data_request_actions', () => {
|
||||
return {
|
||||
syncDataForAllLayers: () => {},
|
||||
|
@ -25,6 +26,9 @@ import { mapExtentChanged, setMouseCoordinates, setQuery } from './map_actions';
|
|||
|
||||
const getStoreMock = jest.fn();
|
||||
const dispatchMock = jest.fn();
|
||||
const vectorTileAdapterMock = {
|
||||
setTiles: jest.fn(),
|
||||
};
|
||||
|
||||
describe('map_actions', () => {
|
||||
afterEach(() => {
|
||||
|
@ -43,6 +47,12 @@ describe('map_actions', () => {
|
|||
require('../selectors/map_selectors').getLayerList = () => {
|
||||
return [];
|
||||
};
|
||||
|
||||
require('../reducers/non_serializable_instances').getInspectorAdapters = () => {
|
||||
return {
|
||||
vectorTiles: vectorTileAdapterMock,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
it('should set buffer', () => {
|
||||
|
@ -61,6 +71,8 @@ describe('map_actions', () => {
|
|||
});
|
||||
action(dispatchMock, getStoreMock);
|
||||
|
||||
expect(vectorTileAdapterMock.setTiles.mock.calls[0]).toEqual([[{ x: 24, y: 15, z: 5 }]]);
|
||||
|
||||
expect(dispatchMock.mock.calls[0]).toEqual([
|
||||
{
|
||||
mapViewContext: {
|
||||
|
@ -101,6 +113,12 @@ describe('map_actions', () => {
|
|||
minLon: 92.5,
|
||||
},
|
||||
};
|
||||
|
||||
require('../reducers/non_serializable_instances').getInspectorAdapters = () => {
|
||||
return {
|
||||
vectorTiles: vectorTileAdapterMock,
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -120,6 +138,8 @@ describe('map_actions', () => {
|
|||
});
|
||||
action(dispatchMock, getStoreMock);
|
||||
|
||||
expect(vectorTileAdapterMock.setTiles.mock.calls.length).toBe(0);
|
||||
|
||||
expect(dispatchMock.mock.calls[0]).toEqual([
|
||||
{
|
||||
mapViewContext: {
|
||||
|
@ -162,6 +182,8 @@ describe('map_actions', () => {
|
|||
});
|
||||
action(dispatchMock, getStoreMock);
|
||||
|
||||
expect(vectorTileAdapterMock.setTiles.mock.calls.length).toBe(1);
|
||||
|
||||
expect(dispatchMock.mock.calls[0]).toEqual([
|
||||
{
|
||||
mapViewContext: {
|
||||
|
@ -204,6 +226,8 @@ describe('map_actions', () => {
|
|||
});
|
||||
action(dispatchMock, getStoreMock);
|
||||
|
||||
expect(vectorTileAdapterMock.setTiles.mock.calls.length).toBe(1);
|
||||
|
||||
expect(dispatchMock.mock.calls[0]).toEqual([
|
||||
{
|
||||
mapViewContext: {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { Geometry, Position } from 'geojson';
|
|||
import { asyncForEach, asyncMap } from '@kbn/std';
|
||||
import { DRAW_MODE, DRAW_SHAPE, LAYER_STYLE_TYPE } from '../../common/constants';
|
||||
import type { MapExtentState, MapViewContext } from '../reducers/map/types';
|
||||
import { getInspectorAdapters } from '../reducers/non_serializable_instances';
|
||||
import { MapStoreState } from '../reducers/store';
|
||||
import { IVectorStyle } from '../classes/styles/vector/vector_style';
|
||||
import {
|
||||
|
@ -73,7 +74,7 @@ import { INITIAL_LOCATION } from '../../common/constants';
|
|||
import { updateTooltipStateForLayer } from './tooltip_actions';
|
||||
import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer';
|
||||
import { SET_DRAW_MODE, pushDeletedFeatureId, clearDeletedFeatureIds } from './ui_actions';
|
||||
import { expandToTileBoundaries } from '../classes/util/geo_tile_utils';
|
||||
import { expandToTileBoundaries, getTilesForExtent } from '../classes/util/geo_tile_utils';
|
||||
import { getToasts } from '../kibana_services';
|
||||
import { getDeletedFeatureIds } from '../selectors/ui_selectors';
|
||||
|
||||
|
@ -217,14 +218,18 @@ export function mapExtentChanged(mapExtentState: MapExtentState) {
|
|||
doesPrevBufferContainNextExtent = turfBooleanContains(bufferGeometry, extentGeometry);
|
||||
}
|
||||
|
||||
const requiresNewBuffer =
|
||||
!prevBuffer || !doesPrevBufferContainNextExtent || prevZoom !== nextZoom;
|
||||
if (requiresNewBuffer) {
|
||||
getInspectorAdapters(getState()).vectorTiles.setTiles(getTilesForExtent(nextZoom, extent));
|
||||
}
|
||||
dispatch({
|
||||
type: MAP_EXTENT_CHANGED,
|
||||
mapViewContext: {
|
||||
...mapExtentState,
|
||||
buffer:
|
||||
!prevBuffer || !doesPrevBufferContainNextExtent || prevZoom !== nextZoom
|
||||
? expandToTileBoundaries(extent, Math.ceil(nextZoom))
|
||||
: prevBuffer,
|
||||
buffer: requiresNewBuffer
|
||||
? expandToTileBoundaries(extent, Math.ceil(nextZoom))
|
||||
: prevBuffer,
|
||||
} as MapViewContext,
|
||||
});
|
||||
|
||||
|
|
|
@ -94,6 +94,7 @@ export class HeatmapLayer extends AbstractLayer {
|
|||
async syncData(syncContext: DataRequestContext) {
|
||||
await syncMvtSourceData({
|
||||
layerId: this.getId(),
|
||||
layerName: await this.getDisplayName(),
|
||||
prevDataRequest: this.getSourceDataRequest(),
|
||||
requestMeta: buildVectorRequestMeta(
|
||||
this.getSource(),
|
||||
|
|
|
@ -42,6 +42,9 @@ const mockSource = {
|
|||
isGeoGridPrecisionAware: () => {
|
||||
return false;
|
||||
},
|
||||
isESSource: () => {
|
||||
return false;
|
||||
},
|
||||
} as unknown as IMvtVectorSource;
|
||||
|
||||
describe('syncMvtSourceData', () => {
|
||||
|
@ -50,6 +53,7 @@ describe('syncMvtSourceData', () => {
|
|||
|
||||
await syncMvtSourceData({
|
||||
layerId: 'layer1',
|
||||
layerName: 'my layer',
|
||||
prevDataRequest: undefined,
|
||||
requestMeta: {
|
||||
...syncContext.dataFilters,
|
||||
|
@ -96,6 +100,7 @@ describe('syncMvtSourceData', () => {
|
|||
|
||||
await syncMvtSourceData({
|
||||
layerId: 'layer1',
|
||||
layerName: 'my layer',
|
||||
prevDataRequest: {
|
||||
getMeta: () => {
|
||||
return prevRequestMeta;
|
||||
|
@ -138,6 +143,7 @@ describe('syncMvtSourceData', () => {
|
|||
|
||||
await syncMvtSourceData({
|
||||
layerId: 'layer1',
|
||||
layerName: 'my layer',
|
||||
prevDataRequest: {
|
||||
getMeta: () => {
|
||||
return prevRequestMeta;
|
||||
|
@ -177,6 +183,7 @@ describe('syncMvtSourceData', () => {
|
|||
|
||||
await syncMvtSourceData({
|
||||
layerId: 'layer1',
|
||||
layerName: 'my layer',
|
||||
prevDataRequest: {
|
||||
getMeta: () => {
|
||||
return prevRequestMeta;
|
||||
|
@ -224,6 +231,7 @@ describe('syncMvtSourceData', () => {
|
|||
|
||||
await syncMvtSourceData({
|
||||
layerId: 'layer1',
|
||||
layerName: 'my layer',
|
||||
prevDataRequest: {
|
||||
getMeta: () => {
|
||||
return prevRequestMeta;
|
||||
|
@ -263,6 +271,7 @@ describe('syncMvtSourceData', () => {
|
|||
|
||||
await syncMvtSourceData({
|
||||
layerId: 'layer1',
|
||||
layerName: 'my layer',
|
||||
prevDataRequest: {
|
||||
getMeta: () => {
|
||||
return prevRequestMeta;
|
||||
|
@ -302,6 +311,7 @@ describe('syncMvtSourceData', () => {
|
|||
|
||||
await syncMvtSourceData({
|
||||
layerId: 'layer1',
|
||||
layerName: 'my layer',
|
||||
prevDataRequest: {
|
||||
getMeta: () => {
|
||||
return prevRequestMeta;
|
||||
|
@ -325,4 +335,38 @@ describe('syncMvtSourceData', () => {
|
|||
// @ts-expect-error
|
||||
sinon.assert.calledOnce(syncContext.stopLoading);
|
||||
});
|
||||
|
||||
test('Should add layer to vector tile inspector when source is synced', async () => {
|
||||
const syncContext = new MockSyncContext({ dataFilters: {} });
|
||||
const mockVectorTileAdapter = {
|
||||
addLayer: sinon.spy(),
|
||||
};
|
||||
|
||||
await syncMvtSourceData({
|
||||
layerId: 'layer1',
|
||||
layerName: 'my layer',
|
||||
prevDataRequest: undefined,
|
||||
requestMeta: {
|
||||
...syncContext.dataFilters,
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
applyForceRefresh: true,
|
||||
fieldNames: [],
|
||||
sourceMeta: {},
|
||||
isForceRefresh: false,
|
||||
isFeatureEditorOpenForLayer: false,
|
||||
},
|
||||
source: {
|
||||
...mockSource,
|
||||
isESSource: () => {
|
||||
return true;
|
||||
},
|
||||
getInspectorAdapters: () => {
|
||||
return { vectorTiles: mockVectorTileAdapter };
|
||||
},
|
||||
},
|
||||
syncContext,
|
||||
});
|
||||
sinon.assert.calledOnce(mockVectorTileAdapter.addLayer);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,12 +24,14 @@ export interface MvtSourceData {
|
|||
|
||||
export async function syncMvtSourceData({
|
||||
layerId,
|
||||
layerName,
|
||||
prevDataRequest,
|
||||
requestMeta,
|
||||
source,
|
||||
syncContext,
|
||||
}: {
|
||||
layerId: string;
|
||||
layerName: string;
|
||||
prevDataRequest: DataRequest | undefined;
|
||||
requestMeta: VectorSourceRequestMeta;
|
||||
source: IMvtVectorSource;
|
||||
|
@ -71,6 +73,9 @@ export async function syncMvtSourceData({
|
|||
: prevData.refreshToken;
|
||||
|
||||
const tileUrl = await source.getTileUrl(requestMeta, refreshToken);
|
||||
if (source.isESSource()) {
|
||||
source.getInspectorAdapters()?.vectorTiles.addLayer(layerId, layerName, tileUrl);
|
||||
}
|
||||
const sourceData = {
|
||||
tileUrl,
|
||||
tileSourceLayer: source.getTileSourceLayer(),
|
||||
|
|
|
@ -220,6 +220,7 @@ export class MvtVectorLayer extends AbstractVectorLayer {
|
|||
|
||||
await syncMvtSourceData({
|
||||
layerId: this.getId(),
|
||||
layerName: await this.getDisplayName(),
|
||||
prevDataRequest: this.getSourceDataRequest(),
|
||||
requestMeta: await this._getVectorSourceRequestMeta(
|
||||
syncContext.isForceRefresh,
|
||||
|
|
|
@ -9,10 +9,11 @@ import {
|
|||
getTileKey,
|
||||
parseTileKey,
|
||||
getTileBoundingBox,
|
||||
getTilesForExtent,
|
||||
expandToTileBoundaries,
|
||||
} from './geo_tile_utils';
|
||||
|
||||
it('Should parse tile key', () => {
|
||||
test('Should parse tile key', () => {
|
||||
expect(parseTileKey('15/23423/1867')).toEqual({
|
||||
zoom: 15,
|
||||
x: 23423,
|
||||
|
@ -21,11 +22,76 @@ it('Should parse tile key', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Should get tile key', () => {
|
||||
test('Should get tiles for extent', () => {
|
||||
const extent = {
|
||||
minLon: -132.19235,
|
||||
minLat: 12.05834,
|
||||
maxLon: -83.6593,
|
||||
maxLat: 30.03121,
|
||||
};
|
||||
|
||||
expect(getTilesForExtent(4.74, extent)).toEqual([
|
||||
{ x: 2, y: 6, z: 4 },
|
||||
{ x: 2, y: 7, z: 4 },
|
||||
{ x: 3, y: 6, z: 4 },
|
||||
{ x: 3, y: 7, z: 4 },
|
||||
{ x: 4, y: 6, z: 4 },
|
||||
{ x: 4, y: 7, z: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should get tiles for extent that crosses dateline', () => {
|
||||
const extent = {
|
||||
minLon: -267.34624,
|
||||
minLat: 10,
|
||||
maxLon: 33.8355,
|
||||
maxLat: 79.16772,
|
||||
};
|
||||
|
||||
expect(getTilesForExtent(2.12, extent)).toEqual([
|
||||
{ x: 3, y: 0, z: 2 },
|
||||
{ x: 3, y: 1, z: 2 },
|
||||
{ x: 0, y: 0, z: 2 },
|
||||
{ x: 0, y: 1, z: 2 },
|
||||
{ x: 1, y: 0, z: 2 },
|
||||
{ x: 1, y: 1, z: 2 },
|
||||
{ x: 2, y: 0, z: 2 },
|
||||
{ x: 2, y: 1, z: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should get tiles for extent that crosses dateline and not add tiles in between right and left', () => {
|
||||
const extent = {
|
||||
minLon: -183.25917,
|
||||
minLat: 50.10446,
|
||||
maxLon: -176.63722,
|
||||
maxLat: 53.06071,
|
||||
};
|
||||
|
||||
expect(getTilesForExtent(6.8, extent)).toEqual([
|
||||
{ x: 63, y: 20, z: 6 },
|
||||
{ x: 63, y: 21, z: 6 },
|
||||
{ x: 0, y: 20, z: 6 },
|
||||
{ x: 0, y: 21, z: 6 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should return single tile for zoom level 0', () => {
|
||||
const extent = {
|
||||
minLon: -180.39426,
|
||||
minLat: -85.05113,
|
||||
maxLon: 270.66456,
|
||||
maxLat: 85.05113,
|
||||
};
|
||||
|
||||
expect(getTilesForExtent(0, extent)).toEqual([{ x: 0, y: 0, z: 0 }]);
|
||||
});
|
||||
|
||||
test('Should get tile key', () => {
|
||||
expect(getTileKey(45, 120, 10)).toEqual('10/853/368');
|
||||
});
|
||||
|
||||
it('Should convert tile key to geojson Polygon', () => {
|
||||
test('Should convert tile key to geojson Polygon', () => {
|
||||
const geometry = getTileBoundingBox('15/23423/1867');
|
||||
expect(geometry).toEqual({
|
||||
top: 82.92546,
|
||||
|
@ -35,7 +101,7 @@ it('Should convert tile key to geojson Polygon', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Should convert tile key to geojson Polygon with extra precision', () => {
|
||||
test('Should convert tile key to geojson Polygon with extra precision', () => {
|
||||
const geometry = getTileBoundingBox('26/19762828/25222702');
|
||||
expect(geometry).toEqual({
|
||||
top: 40.7491508,
|
||||
|
@ -45,7 +111,7 @@ it('Should convert tile key to geojson Polygon with extra precision', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Should expand extent to align boundaries with tile boundaries', () => {
|
||||
test('Should expand extent to align boundaries with tile boundaries', () => {
|
||||
const extent = {
|
||||
maxLat: 12.5,
|
||||
maxLon: 102.5,
|
||||
|
|
|
@ -60,6 +60,31 @@ export function parseTileKey(tileKey: string): {
|
|||
return { x, y, zoom, tileCount };
|
||||
}
|
||||
|
||||
export function getTilesForExtent(
|
||||
zoom: number,
|
||||
extent: MapExtent
|
||||
): Array<{ x: number; y: number; z: number }> {
|
||||
const tileCount = getTileCount(Math.floor(zoom));
|
||||
const minX = longitudeToTile(extent.minLon, tileCount);
|
||||
const maxX = longitudeToTile(extent.maxLon, tileCount);
|
||||
const minY = latitudeToTile(extent.maxLat, tileCount);
|
||||
const maxY = latitudeToTile(extent.minLat, tileCount);
|
||||
|
||||
const tiles: Array<{ x: number; y: number; z: number }> = [];
|
||||
for (let x = 0; x < tileCount && minX + x <= maxX; x++) {
|
||||
const tileX = minX + x;
|
||||
for (let y = 0; y < tileCount && minY + y <= maxY; y++) {
|
||||
const tileY = minY + y;
|
||||
tiles.push({
|
||||
x: tileX < 0 ? tileCount - Math.abs(tileX) : tileX,
|
||||
y: tileY < 0 ? tileCount - Math.abs(tileY) : tileY,
|
||||
z: Math.floor(zoom),
|
||||
});
|
||||
}
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
export function getTileKey(lat: number, lon: number, zoom: number): string {
|
||||
const tileCount = getTileCount(zoom);
|
||||
|
||||
|
|
9
x-pack/plugins/maps/public/inspector/index.ts
Normal file
9
x-pack/plugins/maps/public/inspector/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { MapAdapter, MapInspectorView } from './map_adapter';
|
||||
export { VectorTileAdapter, VectorTileInspectorView } from './vector_tile_adapter';
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { MapAdapter } from './map_adapter';
|
||||
export { MapInspectorView } from './map_inspector_view';
|
|
@ -8,7 +8,7 @@
|
|||
import React, { lazy } from 'react';
|
||||
import type { Adapters } from '@kbn/inspector-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LazyWrapper } from '../lazy_wrapper';
|
||||
import { LazyWrapper } from '../../lazy_wrapper';
|
||||
|
||||
const getLazyComponent = () => {
|
||||
return lazy(() => import('./map_view_component'));
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
export function EmptyPrompt() {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.inspector.vectorTile.noRequestsLoggedTitle"
|
||||
defaultMessage="No requests logged for vector tiles"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.inspector.vectorTile.noRequestsLoggedDescription.mapHasNotLoggedAnyRequestsText"
|
||||
defaultMessage="This map does not have any vector tile search results."
|
||||
/>
|
||||
</p>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getTileRequest } from './get_tile_request';
|
||||
|
||||
test('Should return elasticsearch vector tile request for aggs tiles', () => {
|
||||
expect(
|
||||
getTileRequest({
|
||||
layerId: '1',
|
||||
tileUrl: `/pof/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=kibana_sample_data_logs&gridPrecision=8&requestBody=(_source%3A(excludes%3A!())%2Caggs%3A()%2Cfields%3A!((field%3A'%40timestamp'%2Cformat%3Adate_time)%2C(field%3Atimestamp%2Cformat%3Adate_time)%2C(field%3Autc_time%2Cformat%3Adate_time))%2Cquery%3A(bool%3A(filter%3A!((match_phrase%3A(machine.os.keyword%3Aios))%2C(range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A'2022-04-22T16%3A46%3A00.744Z'%2Clte%3A'2022-04-29T16%3A46%3A05.345Z'))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A'emit(doc%5B!'timestamp!'%5D.value.getHour())%3B')%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A0%2Cstored_fields%3A!('*'))&renderAs=heatmap&token=e8bff005-ccea-464a-ae56-2061b4f8ce68`,
|
||||
x: 3,
|
||||
y: 0,
|
||||
z: 2,
|
||||
})
|
||||
).toEqual({
|
||||
path: '/kibana_sample_data_logs/_mvt/geo.coordinates/2/3/0',
|
||||
body: {
|
||||
size: 0,
|
||||
grid_precision: 8,
|
||||
exact_bounds: false,
|
||||
extent: 4096,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
match_phrase: {
|
||||
'machine.os.keyword': 'ios',
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
timestamp: {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: '2022-04-22T16:46:00.744Z',
|
||||
lte: '2022-04-29T16:46:05.345Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
},
|
||||
grid_agg: 'geotile',
|
||||
grid_type: 'centroid',
|
||||
aggs: {},
|
||||
fields: [
|
||||
{
|
||||
field: '@timestamp',
|
||||
format: 'date_time',
|
||||
},
|
||||
{
|
||||
field: 'timestamp',
|
||||
format: 'date_time',
|
||||
},
|
||||
{
|
||||
field: 'utc_time',
|
||||
format: 'date_time',
|
||||
},
|
||||
],
|
||||
runtime_mappings: {
|
||||
hour_of_day: {
|
||||
script: {
|
||||
source: "emit(doc['timestamp'].value.getHour());",
|
||||
},
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Should return elasticsearch vector tile request for hits tiles', () => {
|
||||
expect(
|
||||
getTileRequest({
|
||||
layerId: '1',
|
||||
tileUrl: `http://localhost:5601/pof/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=kibana_sample_data_logs&requestBody=(_source%3A!f%2Cdocvalue_fields%3A!()%2Cquery%3A(bool%3A(filter%3A!((range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A%272022-04-22T16%3A46%3A00.744Z%27%2Clte%3A%272022-04-29T16%3A46%3A05.345Z%27))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A%27emit(doc%5B!%27timestamp!%27%5D.value.getHour())%3B%27)%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A10000%2Cstored_fields%3A!(geo.coordinates))&token=415049b6-bb0a-444a-a7b9-89717db5183c`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 2,
|
||||
})
|
||||
).toEqual({
|
||||
path: '/kibana_sample_data_logs/_mvt/geo.coordinates/2/0/0',
|
||||
body: {
|
||||
grid_precision: 0,
|
||||
exact_bounds: true,
|
||||
extent: 4096,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
timestamp: {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: '2022-04-22T16:46:00.744Z',
|
||||
lte: '2022-04-29T16:46:05.345Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
runtime_mappings: {
|
||||
hour_of_day: {
|
||||
script: {
|
||||
source: "emit(doc['timestamp'].value.getHour());",
|
||||
},
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
track_total_hits: 10001,
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
MVT_GETGRIDTILE_API_PATH,
|
||||
MVT_GETTILE_API_PATH,
|
||||
RENDER_AS,
|
||||
} from '../../../../common/constants';
|
||||
import { getAggsTileRequest, getHitsTileRequest } from '../../../../common/mvt_request_body';
|
||||
import type { TileRequest } from '../types';
|
||||
|
||||
function getSearchParams(url: string): URLSearchParams {
|
||||
const split = url.split('?');
|
||||
const queryString = split.length <= 1 ? '' : split[1];
|
||||
return new URLSearchParams(queryString);
|
||||
}
|
||||
|
||||
export function getTileRequest(tileRequest: TileRequest): { path?: string; body?: object } {
|
||||
const searchParams = getSearchParams(tileRequest.tileUrl);
|
||||
const encodedRequestBody = searchParams.has('requestBody')
|
||||
? (searchParams.get('requestBody') as string)
|
||||
: '()';
|
||||
|
||||
if (!searchParams.has('index')) {
|
||||
throw new Error(`Required query parameter 'index' not provided.`);
|
||||
}
|
||||
const index = searchParams.get('index') as string;
|
||||
|
||||
if (!searchParams.has('geometryFieldName')) {
|
||||
throw new Error(`Required query parameter 'geometryFieldName' not provided.`);
|
||||
}
|
||||
const geometryFieldName = searchParams.get('geometryFieldName') as string;
|
||||
|
||||
if (tileRequest.tileUrl.includes(MVT_GETGRIDTILE_API_PATH)) {
|
||||
return getAggsTileRequest({
|
||||
encodedRequestBody,
|
||||
geometryFieldName,
|
||||
gridPrecision: parseInt(searchParams.get('gridPrecision') as string, 10),
|
||||
index,
|
||||
renderAs: searchParams.get('renderAs') as RENDER_AS,
|
||||
x: tileRequest.x,
|
||||
y: tileRequest.y,
|
||||
z: tileRequest.z,
|
||||
});
|
||||
}
|
||||
|
||||
if (tileRequest.tileUrl.includes(MVT_GETTILE_API_PATH)) {
|
||||
return getHitsTileRequest({
|
||||
encodedRequestBody,
|
||||
geometryFieldName,
|
||||
index,
|
||||
x: tileRequest.x,
|
||||
y: tileRequest.y,
|
||||
z: tileRequest.z,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('Unexpected path');
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
export function RequestsViewCallout() {
|
||||
return (
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={i18n.translate('xpack.maps.inspector.vectorTile.requestsView', {
|
||||
defaultMessage: `You're viewing vector tile search requests. To view requests submitted to the search API, set View to Requests.`,
|
||||
})}
|
||||
iconType="iInCircle"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiCopy,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { XJsonLang } from '@kbn/monaco';
|
||||
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
|
||||
import { compressToEncodedURIComponent } from 'lz-string';
|
||||
import {
|
||||
getDevToolsCapabilities,
|
||||
getNavigateToUrl,
|
||||
getShareService,
|
||||
} from '../../../kibana_services';
|
||||
import type { TileRequest } from '../types';
|
||||
import { getTileRequest } from './get_tile_request';
|
||||
|
||||
interface Props {
|
||||
tileRequest: TileRequest;
|
||||
}
|
||||
|
||||
export function TileRequestTab(props: Props) {
|
||||
try {
|
||||
const { path, body } = getTileRequest(props.tileRequest);
|
||||
const consoleRequest = `POST ${path}\n${JSON.stringify(body, null, 2)}`;
|
||||
let consoleHref: string | undefined;
|
||||
if (getDevToolsCapabilities().show) {
|
||||
const devToolsDataUri = compressToEncodedURIComponent(consoleRequest);
|
||||
consoleHref = getShareService()
|
||||
.url.locators.get('CONSOLE_APP_LOCATOR')
|
||||
?.useUrl({ loadFrom: `data:text/plain,${devToolsDataUri}` });
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
wrap={false}
|
||||
responsive={true}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="m" wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
<EuiCopy textToCopy={consoleRequest}>
|
||||
{(copy) => (
|
||||
<EuiButtonEmpty size="xs" flush="right" iconType="copyClipboard" onClick={copy}>
|
||||
{i18n.translate(
|
||||
'xpack.maps.inspector.vectorTileRequest.copyToClipboardLabel',
|
||||
{
|
||||
defaultMessage: 'Copy to clipboard',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
{consoleHref !== undefined && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
flush="right"
|
||||
onClick={() => {
|
||||
const navigateToUrl = getNavigateToUrl();
|
||||
navigateToUrl(consoleHref!);
|
||||
}}
|
||||
iconType="wrench"
|
||||
>
|
||||
{i18n.translate('xpack.maps.inspector.vectorTileRequest.openInConsoleLabel', {
|
||||
defaultMessage: 'Open in Console',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<CodeEditor
|
||||
languageId={XJsonLang.ID}
|
||||
value={consoleRequest}
|
||||
options={{
|
||||
readOnly: true,
|
||||
lineNumbers: 'off',
|
||||
fontSize: 12,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
folding: true,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'indent',
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
} catch (e) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.maps.inspector.vectorTileRequest.errorMessage', {
|
||||
defaultMessage: 'Unable to create Elasticsearch vector tile search request',
|
||||
})}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('xpack.maps.inspector.vectorTileRequest.errorTitle', {
|
||||
defaultMessage: `Could not convert tile request, '{tileUrl}', to Elasticesarch vector tile search request, error: {error}`,
|
||||
values: {
|
||||
tileUrl: props.tileRequest.tileUrl,
|
||||
error: e.message,
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Adapters } from '@kbn/inspector-plugin/public';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui';
|
||||
import { EmptyPrompt } from './empty_prompt';
|
||||
import type { TileRequest } from '../types';
|
||||
import { TileRequestTab } from './tile_request_tab';
|
||||
import { RequestsViewCallout } from './requests_view_callout';
|
||||
|
||||
interface Props {
|
||||
adapters: Adapters;
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedLayer: EuiComboBoxOptionOption<string> | null;
|
||||
selectedTileRequest: TileRequest | null;
|
||||
tileRequests: TileRequest[];
|
||||
layerOptions: Array<EuiComboBoxOptionOption<string>>;
|
||||
}
|
||||
|
||||
class VectorTileInspector extends Component<Props, State> {
|
||||
private _isMounted = false;
|
||||
|
||||
state: State = {
|
||||
selectedLayer: null,
|
||||
selectedTileRequest: null,
|
||||
tileRequests: [],
|
||||
layerOptions: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._onAdapterChange();
|
||||
this.props.adapters.vectorTiles.on('change', this._debouncedOnAdapterChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
this.props.adapters.vectorTiles.removeListener('change', this._debouncedOnAdapterChange);
|
||||
}
|
||||
|
||||
_onAdapterChange = () => {
|
||||
const layerOptions = this.props.adapters.vectorTiles.getLayerOptions() as Array<
|
||||
EuiComboBoxOptionOption<string>
|
||||
>;
|
||||
if (layerOptions.length === 0) {
|
||||
this.setState({
|
||||
selectedLayer: null,
|
||||
selectedTileRequest: null,
|
||||
tileRequests: [],
|
||||
layerOptions: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedLayer =
|
||||
this.state.selectedLayer &&
|
||||
layerOptions.some((layerOption) => {
|
||||
return this.state.selectedLayer?.value === layerOption.value;
|
||||
})
|
||||
? this.state.selectedLayer
|
||||
: layerOptions[0];
|
||||
const tileRequests = this.props.adapters.vectorTiles.getTileRequests(selectedLayer.value);
|
||||
const selectedTileRequest =
|
||||
this.state.selectedTileRequest &&
|
||||
tileRequests.some((tileRequest: TileRequest) => {
|
||||
return (
|
||||
this.state.selectedTileRequest?.layerId === tileRequest.layerId &&
|
||||
this.state.selectedTileRequest?.x === tileRequest.x &&
|
||||
this.state.selectedTileRequest?.y === tileRequest.y &&
|
||||
this.state.selectedTileRequest?.z === tileRequest.z
|
||||
);
|
||||
})
|
||||
? this.state.selectedTileRequest
|
||||
: tileRequests.length
|
||||
? tileRequests[0]
|
||||
: null;
|
||||
|
||||
this.setState({
|
||||
selectedLayer,
|
||||
selectedTileRequest,
|
||||
tileRequests,
|
||||
layerOptions,
|
||||
});
|
||||
};
|
||||
|
||||
_debouncedOnAdapterChange = _.debounce(() => {
|
||||
if (this._isMounted) {
|
||||
this._onAdapterChange();
|
||||
}
|
||||
}, 256);
|
||||
|
||||
_onLayerSelect = (selectedOptions: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
if (selectedOptions.length === 0) {
|
||||
this.setState({
|
||||
selectedLayer: null,
|
||||
selectedTileRequest: null,
|
||||
tileRequests: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedLayer = selectedOptions[0];
|
||||
const tileRequests = this.props.adapters.vectorTiles.getTileRequests(selectedLayer.value);
|
||||
this.setState({
|
||||
selectedLayer,
|
||||
selectedTileRequest: tileRequests.length ? tileRequests[0] : null,
|
||||
tileRequests,
|
||||
});
|
||||
};
|
||||
|
||||
renderTabs() {
|
||||
return this.state.tileRequests.map((tileRequest) => {
|
||||
const tileLabel = `${tileRequest.z}/${tileRequest.x}/${tileRequest.y}`;
|
||||
return (
|
||||
<EuiTab
|
||||
key={`${tileRequest.layerId}${tileLabel}`}
|
||||
onClick={() => {
|
||||
this.setState({ selectedTileRequest: tileRequest });
|
||||
}}
|
||||
isSelected={
|
||||
tileRequest.layerId === this.state.selectedTileRequest?.layerId &&
|
||||
tileRequest.x === this.state.selectedTileRequest?.x &&
|
||||
tileRequest.y === this.state.selectedTileRequest?.y &&
|
||||
tileRequest.z === this.state.selectedTileRequest?.z
|
||||
}
|
||||
>
|
||||
{tileLabel}
|
||||
</EuiTab>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.layerOptions.length === 0 ? (
|
||||
<>
|
||||
<RequestsViewCallout />
|
||||
<EmptyPrompt />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RequestsViewCallout />
|
||||
<EuiSpacer />
|
||||
<EuiComboBox
|
||||
singleSelection={true}
|
||||
options={this.state.layerOptions}
|
||||
selectedOptions={this.state.selectedLayer ? [this.state.selectedLayer] : []}
|
||||
onChange={this._onLayerSelect}
|
||||
isClearable={false}
|
||||
prepend={i18n.translate('xpack.maps.inspector.vectorTile.layerSelectPrepend', {
|
||||
defaultMessage: 'Layer',
|
||||
})}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiTabs size="s">{this.renderTabs()}</EuiTabs>
|
||||
<EuiSpacer size="s" />
|
||||
{this.state.selectedTileRequest && (
|
||||
<TileRequestTab
|
||||
key={`${this.state.selectedTileRequest.layerId}${this.state.selectedTileRequest.x}${this.state.selectedTileRequest.y}${this.state.selectedTileRequest.z}`}
|
||||
tileRequest={this.state.selectedTileRequest}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// default export required for React.Lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default VectorTileInspector;
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { VectorTileAdapter } from './vector_tile_adapter';
|
||||
export { VectorTileInspectorView } from './vector_tile_inspector_view';
|
|
@ -5,8 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
|
||||
export function isAbortError(error: Error) {
|
||||
return error instanceof errors.RequestAbortedError;
|
||||
export interface TileRequest {
|
||||
layerId: string;
|
||||
tileUrl: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { TileRequest } from './types';
|
||||
|
||||
export class VectorTileAdapter extends EventEmitter {
|
||||
private _layers: Record<string, { label: string; tileUrl: string }> = {};
|
||||
private _tiles: Array<{ x: number; y: number; z: number }> = [];
|
||||
|
||||
addLayer(layerId: string, label: string, tileUrl: string) {
|
||||
this._layers[layerId] = { label, tileUrl };
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
removeLayer(layerId: string) {
|
||||
delete this._layers[layerId];
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
setTiles(tiles: Array<{ x: number; y: number; z: number }>) {
|
||||
this._tiles = tiles;
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
getLayerOptions(): Array<{ value: string; label: string }> {
|
||||
return Object.keys(this._layers).map((layerId) => {
|
||||
return {
|
||||
value: layerId,
|
||||
label: this._layers[layerId].label,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getTileRequests(layerId: string): TileRequest[] {
|
||||
if (!this._layers[layerId]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { tileUrl } = this._layers[layerId];
|
||||
return this._tiles.map((tile) => {
|
||||
return {
|
||||
layerId,
|
||||
tileUrl,
|
||||
...tile,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_onChange() {
|
||||
this.emit('change');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { lazy } from 'react';
|
||||
import type { Adapters } from '@kbn/inspector-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LazyWrapper } from '../../lazy_wrapper';
|
||||
|
||||
const getLazyComponent = () => {
|
||||
return lazy(() => import('./components/vector_tile_inspector'));
|
||||
};
|
||||
|
||||
export const VectorTileInspectorView = {
|
||||
title: i18n.translate('xpack.maps.inspector.vectorTileViewTitle', {
|
||||
defaultMessage: 'Vector tiles',
|
||||
}),
|
||||
order: 10,
|
||||
help: i18n.translate('xpack.maps.inspector.vectorTileViewHelpText', {
|
||||
defaultMessage: 'View the vector tile search requests used to collect the data',
|
||||
}),
|
||||
shouldShow(adapters: Adapters) {
|
||||
return Boolean(adapters.vectorTiles);
|
||||
},
|
||||
component: (props: { adapters: Adapters }) => {
|
||||
return <LazyWrapper getLazyComponent={getLazyComponent} lazyComponentProps={props} />;
|
||||
},
|
||||
};
|
|
@ -46,6 +46,7 @@ export const getTimeFilter = () => pluginsStart.data.query.timefilter.timefilter
|
|||
export const getToasts = () => coreStart.notifications.toasts;
|
||||
export const getSavedObjectsClient = () => coreStart.savedObjects.client;
|
||||
export const getCoreChrome = () => coreStart.chrome;
|
||||
export const getDevToolsCapabilities = () => coreStart.application.capabilities.dev_tools;
|
||||
export const getMapsCapabilities = () => coreStart.application.capabilities.maps;
|
||||
export const getVisualizeCapabilities = () => coreStart.application.capabilities.visualize;
|
||||
export const getDocLinks = () => coreStart.docLinks;
|
||||
|
@ -58,6 +59,7 @@ export const getCoreI18n = () => coreStart.i18n;
|
|||
export const getSearchService = () => pluginsStart.data.search;
|
||||
export const getEmbeddableService = () => pluginsStart.embeddable;
|
||||
export const getNavigateToApp = () => coreStart.application.navigateToApp;
|
||||
export const getNavigateToUrl = () => coreStart.application.navigateToUrl;
|
||||
export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging;
|
||||
export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider;
|
||||
export const getSecurityService = () => pluginsStart.security;
|
||||
|
|
|
@ -74,7 +74,7 @@ import { APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/cons
|
|||
import { getMapsVisTypeAlias } from './maps_vis_type_alias';
|
||||
import { featureCatalogueEntry } from './feature_catalogue_entry';
|
||||
import { setIsCloudEnabled, setMapAppConfig, setStartServices } from './kibana_services';
|
||||
import { MapInspectorView } from './inspector/map_inspector_view';
|
||||
import { MapInspectorView, VectorTileInspectorView } from './inspector';
|
||||
|
||||
import { setupLensChoroplethChart } from './lens';
|
||||
|
||||
|
@ -172,6 +172,7 @@ export class MapsPlugin
|
|||
})
|
||||
);
|
||||
|
||||
plugins.inspector.registerView(VectorTileInspectorView);
|
||||
plugins.inspector.registerView(MapInspectorView);
|
||||
if (plugins.home) {
|
||||
plugins.home.featureCatalogue.register(featureCatalogueEntry);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/common/adapters/request';
|
||||
import { MapAdapter } from '../inspector/map_adapter';
|
||||
import { MapAdapter, VectorTileAdapter } from '../inspector';
|
||||
import { getShowMapsInspectorAdapter } from '../kibana_services';
|
||||
|
||||
const REGISTER_CANCEL_CALLBACK = 'REGISTER_CANCEL_CALLBACK';
|
||||
|
@ -17,6 +17,7 @@ const SET_CHARTS_PALETTE_SERVICE_GET_COLOR = 'SET_CHARTS_PALETTE_SERVICE_GET_COL
|
|||
function createInspectorAdapters() {
|
||||
const inspectorAdapters = {
|
||||
requests: new RequestAdapter(),
|
||||
vectorTiles: new VectorTileAdapter(),
|
||||
};
|
||||
if (getShowMapsInspectorAdapter()) {
|
||||
inspectorAdapters.map = new MapAdapter();
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreStart, Logger } from '@kbn/core/server';
|
||||
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { Stream } from 'stream';
|
||||
import { RENDER_AS } from '../../common/constants';
|
||||
import { isAbortError } from './util';
|
||||
import { makeExecutionContext } from '../../common/execution_context';
|
||||
|
||||
export async function getEsGridTile({
|
||||
url,
|
||||
core,
|
||||
logger,
|
||||
context,
|
||||
index,
|
||||
geometryFieldName,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
requestBody = {},
|
||||
renderAs = RENDER_AS.POINT,
|
||||
gridPrecision,
|
||||
abortController,
|
||||
}: {
|
||||
url: string;
|
||||
core: CoreStart;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
geometryFieldName: string;
|
||||
index: string;
|
||||
context: DataRequestHandlerContext;
|
||||
logger: Logger;
|
||||
requestBody: any;
|
||||
renderAs: RENDER_AS;
|
||||
gridPrecision: number;
|
||||
abortController: AbortController;
|
||||
}): Promise<{ stream: Stream | null; headers: IncomingHttpHeaders; statusCode: number }> {
|
||||
try {
|
||||
const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`;
|
||||
const body = {
|
||||
size: 0, // no hits
|
||||
grid_precision: gridPrecision,
|
||||
exact_bounds: false,
|
||||
extent: 4096, // full resolution,
|
||||
query: requestBody.query,
|
||||
grid_agg: renderAs === RENDER_AS.HEX ? 'geohex' : 'geotile',
|
||||
grid_type: renderAs === RENDER_AS.GRID || renderAs === RENDER_AS.HEX ? 'grid' : 'centroid',
|
||||
aggs: requestBody.aggs,
|
||||
fields: requestBody.fields,
|
||||
runtime_mappings: requestBody.runtime_mappings,
|
||||
};
|
||||
|
||||
const esClient = (await context.core).elasticsearch.client;
|
||||
const tile = await core.executionContext.withContext(
|
||||
makeExecutionContext({
|
||||
description: 'mvt:get_grid_tile',
|
||||
url,
|
||||
}),
|
||||
async () => {
|
||||
return await esClient.asCurrentUser.transport.request(
|
||||
{
|
||||
method: 'GET',
|
||||
path,
|
||||
body,
|
||||
},
|
||||
{
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'Accept-Encoding': 'gzip',
|
||||
},
|
||||
asStream: true,
|
||||
meta: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return { stream: tile.body as Stream, headers: tile.headers, statusCode: tile.statusCode };
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
return { stream: null, headers: {}, statusCode: 200 };
|
||||
}
|
||||
|
||||
// These are often circuit breaking exceptions
|
||||
// Should return a tile with some error message
|
||||
logger.warn(`Cannot generate ES-grid-tile for ${z}/${x}/${y}: ${e.message}`);
|
||||
return { stream: null, headers: {}, statusCode: 500 };
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreStart, Logger } from '@kbn/core/server';
|
||||
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { Stream } from 'stream';
|
||||
import { isAbortError } from './util';
|
||||
import { makeExecutionContext } from '../../common/execution_context';
|
||||
import { Field, mergeFields } from './merge_fields';
|
||||
|
||||
export async function getEsTile({
|
||||
url,
|
||||
core,
|
||||
logger,
|
||||
context,
|
||||
index,
|
||||
geometryFieldName,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
requestBody = {},
|
||||
abortController,
|
||||
}: {
|
||||
url: string;
|
||||
core: CoreStart;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
geometryFieldName: string;
|
||||
index: string;
|
||||
context: DataRequestHandlerContext;
|
||||
logger: Logger;
|
||||
requestBody: any;
|
||||
abortController: AbortController;
|
||||
}): Promise<{ stream: Stream | null; headers: IncomingHttpHeaders; statusCode: number }> {
|
||||
try {
|
||||
const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`;
|
||||
|
||||
const body = {
|
||||
grid_precision: 0, // no aggs
|
||||
exact_bounds: true,
|
||||
extent: 4096, // full resolution,
|
||||
query: requestBody.query,
|
||||
fields: mergeFields(
|
||||
[
|
||||
requestBody.docvalue_fields as Field[] | undefined,
|
||||
requestBody.stored_fields as Field[] | undefined,
|
||||
],
|
||||
[geometryFieldName]
|
||||
),
|
||||
runtime_mappings: requestBody.runtime_mappings,
|
||||
track_total_hits: requestBody.size + 1,
|
||||
};
|
||||
|
||||
const esClient = (await context.core).elasticsearch.client;
|
||||
const tile = await core.executionContext.withContext(
|
||||
makeExecutionContext({
|
||||
description: 'mvt:get_tile',
|
||||
url,
|
||||
}),
|
||||
async () => {
|
||||
return await esClient.asCurrentUser.transport.request(
|
||||
{
|
||||
method: 'GET',
|
||||
path,
|
||||
body,
|
||||
},
|
||||
{
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'Accept-Encoding': 'gzip',
|
||||
},
|
||||
asStream: true,
|
||||
meta: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return { stream: tile.body as Stream, headers: tile.headers, statusCode: tile.statusCode };
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
return { stream: null, headers: {}, statusCode: 200 };
|
||||
}
|
||||
|
||||
// These are often circuit breaking exceptions
|
||||
// Should return a tile with some error message
|
||||
logger.warn(`Cannot generate ES-grid-tile for ${z}/${x}/${y}: ${e.message}`);
|
||||
return { stream: null, headers: {}, statusCode: 500 };
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
// can not use "import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey"
|
||||
// SearchRequest is incorrectly typed and does not support Field as object
|
||||
// https://github.com/elastic/elasticsearch-js/issues/1615
|
||||
export type Field =
|
||||
| string
|
||||
| {
|
||||
field: string;
|
||||
format: string;
|
||||
};
|
||||
|
||||
export function mergeFields(
|
||||
fieldsList: Array<Field[] | undefined>,
|
||||
excludeNames: string[]
|
||||
): Field[] {
|
||||
const fieldNames: string[] = [];
|
||||
const mergedFields: Field[] = [];
|
||||
|
||||
fieldsList.forEach((fields) => {
|
||||
if (!fields) {
|
||||
return;
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
const fieldName = typeof field === 'string' ? field : field.field;
|
||||
if (!excludeNames.includes(fieldName) && !fieldNames.includes(fieldName)) {
|
||||
fieldNames.push(fieldName);
|
||||
mergedFields.push(field);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return mergedFields;
|
||||
}
|
|
@ -8,18 +8,19 @@
|
|||
import { Stream } from 'stream';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { KibanaExecutionContext } from '@kbn/core/public';
|
||||
import { CoreStart, KibanaRequest, KibanaResponseFactory, Logger } from '@kbn/core/server';
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import {
|
||||
MVT_GETTILE_API_PATH,
|
||||
API_ROOT_PATH,
|
||||
MVT_GETGRIDTILE_API_PATH,
|
||||
RENDER_AS,
|
||||
} from '../../common/constants';
|
||||
import { decodeMvtResponseBody } from '../../common/mvt_request_body';
|
||||
import { getEsTile } from './get_tile';
|
||||
import { getEsGridTile } from './get_grid_tile';
|
||||
import { makeExecutionContext } from '../../common/execution_context';
|
||||
import { getAggsTileRequest, getHitsTileRequest } from '../../common/mvt_request_body';
|
||||
|
||||
const CACHE_TIMEOUT_SECONDS = 60 * 60;
|
||||
|
||||
|
@ -55,21 +56,35 @@ export function initMVTRoutes({
|
|||
response: KibanaResponseFactory
|
||||
) => {
|
||||
const { query, params } = request;
|
||||
const x = parseInt((params as any).x, 10) as number;
|
||||
const y = parseInt((params as any).y, 10) as number;
|
||||
const z = parseInt((params as any).z, 10) as number;
|
||||
|
||||
const abortController = makeAbortController(request);
|
||||
let tileRequest: { path: string; body: object } | undefined;
|
||||
try {
|
||||
tileRequest = getHitsTileRequest({
|
||||
encodedRequestBody: query.requestBody as string,
|
||||
geometryFieldName: query.geometryFieldName as string,
|
||||
index: query.index as string,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
});
|
||||
} catch (e) {
|
||||
return response.badRequest();
|
||||
}
|
||||
|
||||
const { stream, headers, statusCode } = await getEsTile({
|
||||
url: `${API_ROOT_PATH}/${MVT_GETTILE_API_PATH}/{z}/{x}/{y}.pbf`,
|
||||
core,
|
||||
logger,
|
||||
const { stream, headers, statusCode } = await getTile({
|
||||
abortController: makeAbortController(request),
|
||||
body: tileRequest.body,
|
||||
context,
|
||||
geometryFieldName: query.geometryFieldName as string,
|
||||
x: parseInt((params as any).x, 10) as number,
|
||||
y: parseInt((params as any).y, 10) as number,
|
||||
z: parseInt((params as any).z, 10) as number,
|
||||
index: query.index as string,
|
||||
requestBody: decodeMvtResponseBody(query.requestBody as string) as any,
|
||||
abortController,
|
||||
core,
|
||||
executionContext: makeExecutionContext({
|
||||
description: 'mvt:get_hits_tile',
|
||||
url: `${API_ROOT_PATH}/${MVT_GETTILE_API_PATH}/${z}/${x}/${y}.pbf`,
|
||||
}),
|
||||
logger,
|
||||
path: tileRequest.path,
|
||||
});
|
||||
|
||||
return sendResponse(response, stream, headers, statusCode);
|
||||
|
@ -101,23 +116,37 @@ export function initMVTRoutes({
|
|||
response: KibanaResponseFactory
|
||||
) => {
|
||||
const { query, params } = request;
|
||||
const x = parseInt((params as any).x, 10) as number;
|
||||
const y = parseInt((params as any).y, 10) as number;
|
||||
const z = parseInt((params as any).z, 10) as number;
|
||||
|
||||
const abortController = makeAbortController(request);
|
||||
let tileRequest: { path: string; body: object } | undefined;
|
||||
try {
|
||||
tileRequest = getAggsTileRequest({
|
||||
encodedRequestBody: query.requestBody as string,
|
||||
geometryFieldName: query.geometryFieldName as string,
|
||||
gridPrecision: parseInt(query.gridPrecision, 10),
|
||||
index: query.index as string,
|
||||
renderAs: query.renderAs as RENDER_AS,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
});
|
||||
} catch (e) {
|
||||
return response.badRequest();
|
||||
}
|
||||
|
||||
const { stream, headers, statusCode } = await getEsGridTile({
|
||||
url: `${API_ROOT_PATH}/${MVT_GETGRIDTILE_API_PATH}/{z}/{x}/{y}.pbf`,
|
||||
core,
|
||||
logger,
|
||||
const { stream, headers, statusCode } = await getTile({
|
||||
abortController: makeAbortController(request),
|
||||
body: tileRequest.body,
|
||||
context,
|
||||
geometryFieldName: query.geometryFieldName as string,
|
||||
x: parseInt((params as any).x, 10) as number,
|
||||
y: parseInt((params as any).y, 10) as number,
|
||||
z: parseInt((params as any).z, 10) as number,
|
||||
index: query.index as string,
|
||||
requestBody: decodeMvtResponseBody(query.requestBody as string) as any,
|
||||
renderAs: query.renderAs as RENDER_AS,
|
||||
gridPrecision: parseInt(query.gridPrecision, 10),
|
||||
abortController,
|
||||
core,
|
||||
executionContext: makeExecutionContext({
|
||||
description: 'mvt:get_aggs_tile',
|
||||
url: `${API_ROOT_PATH}/${MVT_GETGRIDTILE_API_PATH}/${z}/${x}/${y}.pbf`,
|
||||
}),
|
||||
logger,
|
||||
path: tileRequest.path,
|
||||
});
|
||||
|
||||
return sendResponse(response, stream, headers, statusCode);
|
||||
|
@ -125,6 +154,56 @@ export function initMVTRoutes({
|
|||
);
|
||||
}
|
||||
|
||||
async function getTile({
|
||||
abortController,
|
||||
body,
|
||||
context,
|
||||
core,
|
||||
executionContext,
|
||||
logger,
|
||||
path,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
body: object;
|
||||
context: DataRequestHandlerContext;
|
||||
core: CoreStart;
|
||||
executionContext: KibanaExecutionContext;
|
||||
logger: Logger;
|
||||
path: string;
|
||||
}) {
|
||||
try {
|
||||
const esClient = (await context.core).elasticsearch.client;
|
||||
const tile = await core.executionContext.withContext(executionContext, async () => {
|
||||
return await esClient.asCurrentUser.transport.request(
|
||||
{
|
||||
method: 'POST',
|
||||
path,
|
||||
body,
|
||||
},
|
||||
{
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'Accept-Encoding': 'gzip',
|
||||
},
|
||||
asStream: true,
|
||||
meta: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return { stream: tile.body as Stream, headers: tile.headers, statusCode: tile.statusCode };
|
||||
} catch (e) {
|
||||
if (e instanceof errors.RequestAbortedError) {
|
||||
return { stream: null, headers: {}, statusCode: 200 };
|
||||
}
|
||||
|
||||
// These are often circuit breaking exceptions
|
||||
// Should return a tile with some error message
|
||||
logger.warn(`Cannot generate tile for ${executionContext.url}: ${e.message}`);
|
||||
return { stream: null, headers: {}, statusCode: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
export function sendResponse(
|
||||
response: KibanaResponseFactory,
|
||||
tileStream: Stream | null,
|
||||
|
|
|
@ -600,6 +600,7 @@ export class GisPageObject extends FtrService {
|
|||
}
|
||||
|
||||
async _getResponse(requestName: string) {
|
||||
await this.inspector.openInspectorView('inspectorViewChooserRequests');
|
||||
if (requestName) {
|
||||
await this.testSubjects.click('inspectorRequestChooser');
|
||||
await this.testSubjects.click(`inspectorRequestChooser${requestName}`);
|
||||
|
|
|
@ -98,6 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await find.existsByCssSelector('.vgaVis__view')).to.be(true);
|
||||
log.debug('Checking map rendered');
|
||||
await dashboardPanelActions.openInspectorByTitle('[Flights] Origin Time Delayed');
|
||||
await inspector.openInspectorView('inspectorViewChooserRequests');
|
||||
const requestStats = await inspector.getTableData();
|
||||
const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits');
|
||||
expect(totalHits).to.equal('0');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue