kibana/x-pack/test/api_integration/apis/maps/get_tile.js
Nathan Reese 30c17e0222
[maps] fix layer shows no data instead of error (#170084)
Closes https://github.com/elastic/kibana/issues/169545
Closes https://github.com/elastic/kibana/issues/170657

While investigating https://github.com/elastic/kibana/issues/169545, it
was determined that Maps current error handling leaves a lot to be
desired. This PR cleans up several problems at once since they are all
intertwined.

#### Problem 1 - layer error removed when another data request finished
Redux store contains error state in a single location, `__errorMessage`
key in `LayerDescriptor`. This resulted in other operations, like
"fitting to bounds", "fetching supports feature state", or "fetching
style meta" clearing layer error state.

#### Solution to problem 1
Redux store updated to contain isolated error state
1) `error` key added `DataRequestDescriptor`, allowing each data request
to store independent error state. This will capture data fetching errors
when fetching features and join metrics.
2) `error` key added to `JoinDescriptor`, allowing each join to store
independent error state. This will capture join errors like mismatched
join keys
3) `__tileErrors` added to `LayerDescriptor`, allowing each tile error
to be stored independently. This will capture tile fetch errors.

#### Problem 2 - tile status tracker clears error cache when map center
tile changes
This resulted in removing tile errors that may still be relevant if
tiles have not been refetched.

#### Solution to problem 2
Updated tile status tracker to only clear a tile error when the tile is
reloaded.

#### Problem 3 - Tile Errors do not surface elasticsearch ErrorCause
This results in useless error messages like in the screen shot below
<img width="300" alt="Screenshot 2023-11-01 at 2 39 01 PM"
src="75546228-24c6-4855-bea7-39ed421ee3f4">

#### Solution to problem 3
Updated tile status tracker to read and persist elasticsearch ErrorCause
from tile error. Now tile error messages contain more relevant
information about the problem.
<img width="200" alt="Screenshot 2023-11-03 at 9 56 41 AM"
src="b9ddff98-049e-4f22-8249-3f5988fa93a5">

#### Problem 4 - error UI is not interactive when layer editor is not
available, in dashboards or read only user

#### Solution to problem 4
* Updated layer tooltip to only display error title
<img width="200" alt="Screenshot 2023-11-03 at 11 22 50 AM"
src="6943aead-a7d6-4da3-8ecc-bb6065e0406a">
* Moved error callout from editor to legend so its visible when map is
in dashboard and by readonly users.
<img width="200" alt="Screenshot 2023-11-03 at 11 23 45 AM"
src="358fe133-4c5a-4f06-a03e-e96a16b7afb6">

Moving error details from tooltip to legend allowed error details to
contain interactive elements. For example, display a tile picker so that
users can see each tile's error. This will be useful in the future where
search source requests can display "view details" button that opens
request in inspector.

#### Problem 5 - error UI displayed as warning
This results in inconsistent view between kibana applications

#### Solution to problem 5
Updated error UI to use danger callout and error icon

### test instructions
1) install sample web logs
2) create map
3) add documents layer with vector tiles scaling
4) add documents layer with geojson scaling
5) add join layer
6) add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "kibana_sample_data_logs"
          }
        ]
      }
    }
    ```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
2023-11-06 17:00:15 -07:00

223 lines
7.2 KiB
JavaScript

/*
* 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 { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import expect from '@kbn/expect';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { getTileUrlParams } from '@kbn/maps-vector-tile-utils';
function findFeature(layer, callbackFn) {
for (let i = 0; i < layer.length; i++) {
const feature = layer.feature(i);
if (callbackFn(feature)) {
return feature;
}
}
}
export default function ({ getService }) {
const supertest = getService('supertest');
describe('getTile', () => {
const defaultParams = {
geometryFieldName: 'geo.coordinates',
hasLabels: false,
index: 'logstash-*',
requestBody: {
fields: [
'bytes',
'machine.os.raw',
{
field: '@timestamp',
format: 'epoch_millis',
},
],
query: {
bool: {
filter: [
{
match_all: {},
},
{
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: '2015-09-20T00:00:00.000Z',
lte: '2015-09-20T01:00:00.000Z',
},
},
},
],
must: [],
must_not: [],
should: [],
},
},
runtime_mappings: {
hour_of_day: {
script: {
source: "// !@#$%^&*()_+ %%%\nemit(doc['timestamp'].value.getHour());",
},
type: 'long',
},
},
size: 10000,
},
};
it('should return ES vector tile containing documents and metadata', async () => {
const resp = await supertest
.get(`/internal/maps/mvt/getTile/2/1/1.pbf?${getTileUrlParams(defaultParams)}`)
.set('kbn-xsrf', 'kibana')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.responseType('blob')
.expect(200);
expect(resp.headers['content-encoding']).to.be('gzip');
expect(resp.headers['content-disposition']).to.be('inline');
expect(resp.headers['content-type']).to.be('application/x-protobuf');
expect(resp.headers['cache-control']).to.be('public, max-age=3600');
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers.hits;
expect(layer.length).to.be(2); // 2 docs
// Verify ES document
const feature = findFeature(layer, (feature) => {
return feature.properties._id === 'AU_x3_BsGFA8no6Qjjug';
});
expect(feature).not.to.be(undefined);
expect(feature.type).to.be(1);
expect(feature.extent).to.be(4096);
expect(feature.id).to.be(undefined);
expect(feature.properties).to.eql({
'@timestamp': '1442709961071',
_id: 'AU_x3_BsGFA8no6Qjjug',
_index: 'logstash-2015.09.20',
bytes: 9252,
'machine.os.raw': 'ios',
});
expect(feature.loadGeometry()).to.eql([[{ x: 44, y: 2382 }]]);
// Verify metadata feature
const metaDataLayer = jsonTile.layers.meta;
const metadataFeature = metaDataLayer.feature(0);
expect(metadataFeature).not.to.be(undefined);
expect(metadataFeature.type).to.be(3);
expect(metadataFeature.extent).to.be(4096);
expect(metadataFeature.id).to.be(undefined);
// This is dropping some irrelevant properties from the comparison
expect(metadataFeature.properties['hits.total.relation']).to.eql('eq');
expect(metadataFeature.properties['hits.total.value']).to.eql(2);
expect(metadataFeature.properties.timed_out).to.eql(false);
expect(metadataFeature.loadGeometry()).to.eql([
[
{ x: 44, y: 2382 },
{ x: 44, y: 1913 },
{ x: 550, y: 1913 },
{ x: 550, y: 2382 },
{ x: 44, y: 2382 },
],
]);
});
it('should return ES vector tile containing label features when hasLabels is true', async () => {
const tileUrlParams = getTileUrlParams({
...defaultParams,
hasLabels: true,
});
const resp = await supertest
.get(`/internal/maps/mvt/getTile/2/1/1.pbf?${tileUrlParams}`)
.set('kbn-xsrf', 'kibana')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.responseType('blob')
.expect(200);
expect(resp.headers['content-encoding']).to.be('gzip');
expect(resp.headers['content-disposition']).to.be('inline');
expect(resp.headers['content-type']).to.be('application/x-protobuf');
expect(resp.headers['cache-control']).to.be('public, max-age=3600');
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers.hits;
expect(layer.length).to.be(4); // 2 docs + 2 label features
// Verify ES document
const feature = findFeature(layer, (feature) => {
return (
feature.properties._id === 'AU_x3_BsGFA8no6Qjjug' &&
feature.properties._mvt_label_position === true
);
});
expect(feature).not.to.be(undefined);
expect(feature.type).to.be(1);
expect(feature.extent).to.be(4096);
expect(feature.id).to.be(undefined);
expect(feature.properties).to.eql({
'@timestamp': '1442709961071',
_id: 'AU_x3_BsGFA8no6Qjjug',
_index: 'logstash-2015.09.20',
bytes: 9252,
'machine.os.raw': 'ios',
_mvt_label_position: true,
});
expect(feature.loadGeometry()).to.eql([[{ x: 44, y: 2382 }]]);
});
it('should return error when index does not exist', async () => {
const tileUrlParams = getTileUrlParams({
...defaultParams,
index: 'notRealIndex',
});
await supertest
.get(`/internal/maps/mvt/getTile/2/1/1.pbf?${tileUrlParams}`)
.set('kbn-xsrf', 'kibana')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.responseType('blob')
.expect(404);
});
it('should return elasticsearch error', async () => {
const tileUrlParams = getTileUrlParams({
...defaultParams,
requestBody: {
...defaultParams.requestBody,
query: {
error_query: {
indices: [
{
error_type: 'exception',
message: 'local shard failure message 123',
name: 'logstash-*',
},
],
},
},
},
});
const resp = await supertest
.get(`/internal/maps/mvt/getTile/2/1/1.pbf?${tileUrlParams}`)
.set('kbn-xsrf', 'kibana')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(400);
expect(resp.body.error.reason).to.be('all shards failed');
});
});
}