[maps] display vector tile results in vector tile inspector (#172627)

Closes https://github.com/elastic/kibana/issues/172717

PR updates Vector tile inspector to display vector tile response. When
tile has an error, error displays "View details" button to open response
in inspector.

### Test setup
1. install sample web logs data set
2. create new map
3. add heatmap layer. 
4. Save map

### Test response displayed in inspector
1. Click "Inspector" button in top nav
2. Click "Response" tab. Verify response is displayed
<img width="500" alt="Screenshot 2023-12-05 at 1 28 41 PM"
src="a90ea2bf-92c5-45aa-b98a-3edf45c3d9fa">

### Vector tile error contains "View details" button that opens
inspector
1. add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "kibana_sample_data_logs"
          }
        ]
      }
    }
    ```
<img width="250" alt="Screenshot 2023-12-05 at 1 29 27 PM"
src="10fa5341-6901-477f-a2dc-b77bb2891ae0">
5. Click "View details". Verify inspector is showing response for
correct layer and tile
<img width="500" alt="Screenshot 2023-12-05 at 1 29 33 PM"
src="2b78a865-44e9-42c2-9e61-f09e7491e17f">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-12-07 08:25:32 -07:00 committed by GitHub
parent c3864a5d10
commit 9e920faacc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 558 additions and 103 deletions

View file

@ -826,6 +826,14 @@ export function setTileState(
newValue: tileErrors,
});
if (!isLayerGroup(layer) && layer.getSource().isESSource()) {
getInspectorAdapters(getState()).vectorTiles.setTileResults(
layerId,
tileMetaFeatures,
tileErrors
);
}
if (!tileMetaFeatures && !layer.getDescriptor().__tileMetaFeatures) {
return;
}

View file

@ -9,6 +9,7 @@
import { i18n } from '@kbn/i18n';
import type { Map as MbMap } from '@kbn/mapbox-gl';
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
import type { Query } from '@kbn/es-query';
import {
getWarningsTitle,
@ -46,6 +47,7 @@ import { IStyle } from '../styles/style';
import { LICENSED_FEATURES } from '../../licensed_features';
import { IESSource } from '../sources/es_source';
import { TileErrorsList } from './tile_errors_list';
import { isLayerGroup } from './layer_group';
export const INCOMPLETE_RESULTS_WARNING = i18n.translate(
'xpack.maps.layer.incompleteResultsWarning',
@ -90,7 +92,7 @@ export interface ILayer {
isLayerLoading(zoom: number): boolean;
isFilteredByGlobalTime(): Promise<boolean>;
hasErrors(): boolean;
getErrors(): LayerMessage[];
getErrors(inspectorAdapters: Adapters): LayerMessage[];
hasWarnings(): boolean;
getWarnings(): LayerMessage[];
@ -411,7 +413,8 @@ export class AbstractLayer implements ILayer {
}
hasErrors(): boolean {
return this.getErrors().length > 0;
const inspectorAdapters = {}; // errors are not interacted with so empty Adapters can be passed to getErrors
return this.getErrors(inspectorAdapters).length > 0;
}
_getSourceErrorTitle() {
@ -420,7 +423,7 @@ export class AbstractLayer implements ILayer {
});
}
getErrors(): LayerMessage[] {
getErrors(inspectorAdapters: Adapters): LayerMessage[] {
const errors: LayerMessage[] = [];
const sourceError = this.getSourceDataRequest()?.renderError();
@ -436,7 +439,14 @@ export class AbstractLayer implements ILayer {
title: i18n.translate('xpack.maps.layer.tileErrorTitle', {
defaultMessage: `An error occurred when loading layer tiles`,
}),
body: <TileErrorsList tileErrors={this._descriptor.__tileErrors} />,
body: (
<TileErrorsList
inspectorAdapters={inspectorAdapters}
isESSource={!isLayerGroup(this) && this.getSource().isESSource()}
layerId={this.getId()}
tileErrors={this._descriptor.__tileErrors}
/>
),
});
}

View file

@ -6,11 +6,17 @@
*/
import React, { useEffect, useState } from 'react';
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui';
import { EuiButton, EuiButtonEmpty, EuiCodeBlock, EuiContextMenu, EuiPopover } from '@elastic/eui';
import type { TileError } from '../../../common/descriptor_types';
import { getInspector } from '../../kibana_services';
import { RESPONSE_VIEW_ID } from '../../inspector/vector_tile_adapter/components/vector_tile_inspector';
interface Props {
inspectorAdapters: Adapters;
isESSource: boolean;
layerId: string;
tileErrors: TileError[];
}
@ -75,7 +81,28 @@ export function TileErrorsList(props: Props) {
>
<EuiContextMenu initialPanelId={0} panels={panels} size="s" />
</EuiPopover>
<p>{getDescription(selectedTileError)}</p>
<EuiCodeBlock isCopyable={true} paddingSize="s">
{getDescription(selectedTileError)}
</EuiCodeBlock>
{props.isESSource && (
<EuiButton
color="primary"
onClick={() => {
getInspector().open(props.inspectorAdapters, {
options: {
initialLayerId: props.layerId,
initialTileKey: selectedTileError?.tileKey,
initialTab: [RESPONSE_VIEW_ID],
},
});
}}
size="s"
>
{i18n.translate('xpack.maps.tileError.viewDetailsButtonLabel', {
defaultMessage: 'View details',
})}
</EuiButton>
)}
</>
);
}

View file

@ -7,6 +7,7 @@
import _ from 'lodash';
import React from 'react';
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
import { i18n } from '@kbn/i18n';
import { EuiIcon } from '@elastic/eui';
import { Feature, FeatureCollection } from 'geojson';
@ -157,8 +158,8 @@ export class GeoJsonVectorLayer extends AbstractVectorLayer {
);
}
getErrors(): LayerMessage[] {
const errors = super.getErrors();
getErrors(inspectorAdapters: Adapters): LayerMessage[] {
const errors = super.getErrors(inspectorAdapters);
this.getValidJoins().forEach((join) => {
const joinDescriptor = join.toDescriptor();

View file

@ -7,6 +7,7 @@
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
import { asyncForEach } from '@kbn/std';
import type { FilterSpecification, Map as MbMap, LayerSpecification } from '@kbn/mapbox-gl';
import type { KibanaExecutionContext } from '@kbn/core/public';
@ -274,8 +275,8 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
});
}
getErrors(): LayerMessage[] {
const errors = super.getErrors();
getErrors(inspectorAdapters: Adapters): LayerMessage[] {
const errors = super.getErrors(inspectorAdapters);
this.getValidJoins().forEach((join) => {
const joinDataRequest = this.getDataRequest(join.getSourceDataRequestId());

View file

@ -7,13 +7,11 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
import type { ISource } from './source';
export type SourceRegistryEntry = {
ConstructorFunction: new (
sourceDescriptor: any, // this is the source-descriptor that corresponds specifically to the particular ISource instance
inspectorAdapters?: Adapters
sourceDescriptor: any // this is the source-descriptor that corresponds specifically to the particular ISource instance
) => ISource;
type: string;
};

View file

@ -174,3 +174,13 @@ export function expandToTileBoundaries(extent: MapExtent, zoom: number): MapExte
maxLat: tileToLatitude(upperLeftY, tileCount),
};
}
export function isPointInTile(lat: number, lon: number, x: number, y: number, z: number) {
const tileCount = getTileCount(z);
const lonX = longitudeToTile(lon, tileCount);
if (lonX !== x) {
return false;
}
const latY = latitudeToTile(lat, tileCount);
return latY === y;
}

View file

@ -479,6 +479,7 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is
data-test-subj="mapLayerTOCDetailslayer_1"
>
<LegendDetails
inspectorAdapters={Object {}}
layer={
Object {
"getDisplayName": [Function],

View file

@ -22,6 +22,7 @@ import {
getOpenTOCDetails,
getFlyoutDisplay,
} from '../../../../../selectors/ui_selectors';
import { getInspectorAdapters } from '../../../../../reducers/non_serializable_instances';
import {
fitToLayerExtent,
setSelectedLayer,
@ -37,6 +38,7 @@ import { DRAW_MODE } from '../../../../../../common/constants';
function mapStateToProps(state: MapStoreState, ownProps: OwnProps): ReduxStateProps {
const flyoutDisplay = getFlyoutDisplay(state);
return {
inspectorAdapters: getInspectorAdapters(state),
isReadOnly: getIsReadOnly(state),
zoom: getMapZoom(state),
selectedLayer: getSelectedLayer(state),

View file

@ -34,7 +34,7 @@ describe('LegendDetails', () => {
} as unknown as ILayer;
test('Should only render errors when layer contains errors', () => {
render(<LegendDetails layer={mockLayer} />);
render(<LegendDetails inspectorAdapters={{}} layer={mockLayer} />);
screen.getByTestId('layer-error');
const error = screen.queryByTestId('layer-error');
expect(error).not.toBeNull();
@ -47,6 +47,7 @@ describe('LegendDetails', () => {
test('Should render warnings and legend when layer contains warnings', () => {
render(
<LegendDetails
inspectorAdapters={{}}
layer={{
...mockLayer,
getErrors: () => {

View file

@ -6,15 +6,17 @@
*/
import React from 'react';
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import type { ILayer } from '../../../../../classes/layers/layer';
interface Props {
inspectorAdapters: Adapters;
layer: ILayer;
}
export function LegendDetails({ layer }: Props) {
const errors = layer.getErrors();
export function LegendDetails({ inspectorAdapters, layer }: Props) {
const errors = layer.getErrors(inspectorAdapters);
if (errors.length) {
return (
<>

View file

@ -63,6 +63,7 @@ const mockLayer = {
const defaultProps = {
depth: 0,
inspectorAdapters: {},
layer: mockLayer,
selectedLayer: undefined,
openLayerPanel: async () => {},

View file

@ -8,6 +8,7 @@
import React, { Component } from 'react';
import classNames from 'classnames';
import type { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiIcon, EuiButtonIcon, EuiConfirmModal, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -27,6 +28,7 @@ function escapeLayerName(name: string) {
}
export interface ReduxStateProps {
inspectorAdapters: Adapters;
isReadOnly: boolean;
zoom: number;
selectedLayer: ILayer | undefined;
@ -337,7 +339,10 @@ export class TOCEntry extends Component<Props, State> {
className="mapTocEntry__layerDetails"
data-test-subj={`mapLayerTOCDetails${escapeLayerName(this.state.displayName)}`}
>
<LegendDetails layer={this.props.layer} />
<LegendDetails
inspectorAdapters={this.props.inspectorAdapters}
layer={this.props.layer}
/>
</div>
) : null}

View file

@ -8,10 +8,12 @@
import { connect } from 'react-redux';
import { MapStoreState } from '../../../../../../reducers/store';
import { getMapZoom, isUsingSearch } from '../../../../../../selectors/map_selectors';
import { getInspectorAdapters } from '../../../../../../reducers/non_serializable_instances';
import { TOCEntryButton, ReduxStateProps, OwnProps } from './toc_entry_button';
function mapStateToProps(state: MapStoreState, ownProps: OwnProps): ReduxStateProps {
return {
inspectorAdapters: getInspectorAdapters(state),
isUsingSearch: isUsingSearch(state),
zoom: getMapZoom(state),
};

View file

@ -6,7 +6,7 @@
*/
import React, { Component, Fragment, ReactNode } from 'react';
import { Adapters } from '@kbn/inspector-plugin/common/adapters';
import { EuiButtonEmpty, EuiIcon, EuiToolTip, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { type ILayer, INCOMPLETE_RESULTS_WARNING } from '../../../../../../classes/layers/layer';
@ -19,6 +19,7 @@ interface Footnote {
}
export interface ReduxStateProps {
inspectorAdapters: Adapters;
isUsingSearch: boolean;
zoom: number;
}
@ -69,7 +70,7 @@ export class TOCEntryButton extends Component<Props, State> {
footnotes: Footnote[];
postScript?: string;
} {
const errors = this.props.layer.getErrors();
const errors = this.props.layer.getErrors(this.props.inspectorAdapters);
if (errors.length) {
const errorIcon = (
<EuiIcon
@ -88,7 +89,7 @@ export class TOCEntryButton extends Component<Props, State> {
: {
icon: errorIcon,
tooltipContent: this.props.layer
.getErrors()
.getErrors(this.props.inspectorAdapters)
.map(({ title }) => <div key={title}>{title}</div>),
footnotes: [],
};

View file

@ -0,0 +1,83 @@
/*
* 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.
*/
jest.mock('./tile_request_tab', () => ({
TileRequestTab: () => {
return <div>mockTileRequestTab</div>;
},
}));
import React from 'react';
import { render, screen } from '@testing-library/react';
import VectorTileInspector, { RESPONSE_VIEW_ID } from './vector_tile_inspector';
import { VectorTileAdapter } from '../vector_tile_adapter';
describe('VectorTileInspector', () => {
let vectorTileAdapter: VectorTileAdapter | undefined;
beforeEach(() => {
vectorTileAdapter = new VectorTileAdapter();
vectorTileAdapter.addLayer(
'layer1',
'layer1 label',
'/pof/internal/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&hasLabels=false&buffer=7&index=kibana_sample_data_logs&gridPrecision=8&requestBody=()&renderAs=heatmap&token=1'
);
vectorTileAdapter.addLayer(
'layer2',
'layer2 label',
'/pof/internal/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&hasLabels=false&buffer=7&index=kibana_sample_data_logs&gridPrecision=8&requestBody=()&renderAs=heatmap&token=1'
);
vectorTileAdapter.setTiles([
{
x: 0,
y: 0,
z: 1,
},
{
x: 1,
y: 0,
z: 1,
},
{
x: 0,
y: 1,
z: 1,
},
{
x: 1,
y: 1,
z: 1,
},
]);
});
test('should show first layer, first tile, and request tab when options not provided', () => {
render(
<VectorTileInspector adapters={{ vectorTiles: vectorTileAdapter }} title="Vector tiles" />
);
screen.getByText('layer1 label');
screen.getByText('1/0/0');
screen.getByText('mockTileRequestTab');
});
test('should show layer, tile, and tab specified by options', () => {
const options = {
initialLayerId: 'layer2',
initialTileKey: '1/1/1',
initialTab: RESPONSE_VIEW_ID,
};
render(
<VectorTileInspector
adapters={{ vectorTiles: vectorTileAdapter! }}
options={options}
title="Vector tiles"
/>
);
screen.getByText('layer2 label');
screen.getByText('1/1/1');
screen.getByText('Not available'); // response is not available because tileMetaFeature or tileError where not provided
});
});

View file

@ -8,33 +8,56 @@
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 type { InspectorViewProps } from '@kbn/inspector-plugin/public';
import { XJsonLang } from '@kbn/monaco';
import {
EuiComboBox,
EuiComboBoxOptionOption,
EuiFormRow,
EuiSpacer,
EuiTabs,
EuiTab,
EuiText,
} from '@elastic/eui';
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
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;
const REQUEST_VIEW_ID = 'request_view';
export const RESPONSE_VIEW_ID = 'response_view';
interface Options {
initialLayerId?: string;
initialTileKey?: string;
initialTab?: typeof REQUEST_VIEW_ID | typeof RESPONSE_VIEW_ID;
}
interface State {
selectedLayer: EuiComboBoxOptionOption<string> | null;
selectedTileRequest: TileRequest | null;
selectedView: typeof REQUEST_VIEW_ID | typeof RESPONSE_VIEW_ID;
tileRequests: TileRequest[];
layerOptions: Array<EuiComboBoxOptionOption<string>>;
}
class VectorTileInspector extends Component<Props, State> {
class VectorTileInspector extends Component<InspectorViewProps, State> {
private _isMounted = false;
state: State = {
selectedLayer: null,
selectedTileRequest: null,
tileRequests: [],
layerOptions: [],
};
constructor(props: InspectorViewProps) {
super(props);
this.state = {
selectedLayer: null,
selectedTileRequest: null,
selectedView:
props.options && (props.options as Options).initialTab
? (props.options as Options).initialTab!
: REQUEST_VIEW_ID,
tileRequests: [],
layerOptions: [],
};
}
componentDidMount() {
this._isMounted = true;
@ -47,6 +70,62 @@ class VectorTileInspector extends Component<Props, State> {
this.props.adapters.vectorTiles.removeListener('change', this._debouncedOnAdapterChange);
}
_getDefaultLayer(layerOptions: Array<EuiComboBoxOptionOption<string>>) {
if (
this.state.selectedLayer &&
layerOptions.some((layerOption) => {
return this.state.selectedLayer?.value === layerOption.value;
})
) {
return this.state.selectedLayer;
}
if (this.props.options && (this.props.options as Options).initialLayerId) {
const initialLayer = layerOptions.find((layerOption) => {
return (this.props.options as Options).initialLayerId === layerOption.value;
});
if (initialLayer) {
return initialLayer;
}
}
return layerOptions[0];
}
_getDefaultTileRequest(tileRequests: TileRequest[]) {
if (
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
);
})
) {
return this.state.selectedTileRequest;
}
if (tileRequests.length === 0) {
return null;
}
if (this.props.options && (this.props.options as Options).initialTileKey) {
const initialTileRequest = tileRequests.find((tileRequest) => {
return (
(this.props.options as Options).initialTileKey ===
`${tileRequest.z}/${tileRequest.x}/${tileRequest.y}`
);
});
if (initialTileRequest) {
return initialTileRequest;
}
}
return tileRequests[0];
}
_onAdapterChange = () => {
const layerOptions = this.props.adapters.vectorTiles.getLayerOptions() as Array<
EuiComboBoxOptionOption<string>
@ -61,32 +140,11 @@ class VectorTileInspector extends Component<Props, State> {
return;
}
const selectedLayer =
this.state.selectedLayer &&
layerOptions.some((layerOption) => {
return this.state.selectedLayer?.value === layerOption.value;
})
? this.state.selectedLayer
: layerOptions[0];
const selectedLayer = this._getDefaultLayer(layerOptions);
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,
selectedTileRequest: this._getDefaultTileRequest(tileRequests),
tileRequests,
layerOptions,
});
@ -117,26 +175,60 @@ class VectorTileInspector extends Component<Props, State> {
});
};
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>
);
_onTileSelect = (selectedOptions: Array<EuiComboBoxOptionOption<TileRequest>>) => {
if (selectedOptions.length === 0) {
this.setState({ selectedTileRequest: null });
return;
}
this.setState({
selectedTileRequest: selectedOptions[0].value ? selectedOptions[0].value : null,
});
};
_renderTileRequest() {
if (!this.state.selectedTileRequest) {
return null;
}
if (this.state.selectedView === REQUEST_VIEW_ID) {
return (
<TileRequestTab
key={`${this.state.selectedTileRequest.layerId}${this.state.selectedTileRequest.x}${this.state.selectedTileRequest.y}${this.state.selectedTileRequest.z}`}
tileRequest={this.state.selectedTileRequest}
/>
);
}
const tileResponse = getTileResponse(this.state.selectedTileRequest);
return tileResponse ? (
<CodeEditor
languageId={XJsonLang.ID}
value={JSON.stringify(tileResponse, null, 2)}
options={{
readOnly: true,
lineNumbers: 'off',
fontSize: 12,
minimap: {
enabled: false,
},
folding: true,
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'indent',
automaticLayout: true,
}}
/>
) : (
<EuiText>
<p>
{i18n.translate('xpack.maps.inspector.vectorTile.tileMetaFeatureNotAvailable', {
defaultMessage: 'Not available',
})}
</p>
</EuiText>
);
}
render() {
@ -148,31 +240,102 @@ class VectorTileInspector extends Component<Props, State> {
) : (
<>
<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', {
<EuiFormRow
display="columnCompressed"
label={i18n.translate('xpack.maps.inspector.vectorTile.layerSelectLabel', {
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}
>
<EuiComboBox
fullWidth
singleSelection={true}
options={this.state.layerOptions}
selectedOptions={this.state.selectedLayer ? [this.state.selectedLayer] : []}
onChange={this._onLayerSelect}
isClearable={false}
/>
)}
</EuiFormRow>
<EuiFormRow
display="columnCompressed"
label={i18n.translate('xpack.maps.inspector.vectorTile.tileSelectLabel', {
defaultMessage: 'Tile',
})}
>
<EuiComboBox
fullWidth
singleSelection={true}
options={this.state.tileRequests.map((tileRequest) => {
return {
label: `${tileRequest.z}/${tileRequest.x}/${tileRequest.y}`,
value: tileRequest,
};
})}
selectedOptions={
this.state.selectedTileRequest
? [
{
label: `${this.state.selectedTileRequest.z}/${this.state.selectedTileRequest.x}/${this.state.selectedTileRequest.y}`,
value: this.state.selectedTileRequest,
},
]
: []
}
onChange={this._onTileSelect}
isClearable={false}
/>
</EuiFormRow>
<EuiTabs size="s">
<>
<EuiTab
onClick={() => {
this.setState({ selectedView: REQUEST_VIEW_ID });
}}
isSelected={this.state.selectedView === REQUEST_VIEW_ID}
>
{i18n.translate('xpack.maps.inspector.vectorTile.requestTabLabel', {
defaultMessage: 'Request',
})}
</EuiTab>
<EuiTab
onClick={() => {
this.setState({ selectedView: RESPONSE_VIEW_ID });
}}
isSelected={this.state.selectedView === RESPONSE_VIEW_ID}
>
{i18n.translate('xpack.maps.inspector.vectorTile.responseTabLabel', {
defaultMessage: 'Response',
})}
</EuiTab>
</>
</EuiTabs>
<EuiSpacer size="s" />
{this._renderTileRequest()}
</>
);
}
}
function getTileResponse(tileRequest: TileRequest) {
if (tileRequest.tileError) {
return {
error: tileRequest.tileError.error
? tileRequest.tileError.error
: tileRequest.tileError.message,
};
}
return tileRequest.tileMetaFeature
? {
meta: tileRequest.tileMetaFeature.properties,
}
: undefined;
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export default VectorTileInspector;

View file

@ -5,9 +5,13 @@
* 2.0.
*/
import type { TileError, TileMetaFeature } from '../../../common/descriptor_types';
export interface TileRequest {
layerId: string;
tileUrl: string;
tileError?: TileError;
tileMetaFeature?: TileMetaFeature;
x: number;
y: number;
z: number;

View file

@ -0,0 +1,73 @@
/*
* 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 type { TileMetaFeature } from '../../../common/descriptor_types';
import { getTileError, getTileMetaFeature } from './vector_tile_adapter';
describe('getTileError', () => {
test('should find tileError for tile', () => {
const tileErrors = [
{
message: 'simulated failure 1',
tileKey: '1/0/0',
},
{
message: 'simulated failure 2',
tileKey: '1/1/0',
},
];
const tileError = getTileError(0, 0, 1, tileErrors);
expect(tileError).not.toBeUndefined();
expect(tileError!.message).toBe('simulated failure 1');
});
});
describe('getTileMetaFeature', () => {
test('should find tileMetaFeature for tile', () => {
const tileMetaFeatures = [
{
geometry: {
coordinates: [
[
[0, -85.05112877980659],
[0, 0],
[180, 0],
[180, -85.05112877980659],
[0, -85.05112877980659],
],
],
type: 'Polygon',
},
properties: {
'hits.total.value': 0,
},
type: 'Feature',
} as TileMetaFeature,
{
geometry: {
coordinates: [
[
[-180, 0],
[-180, 85.05112877980659],
[0, 85.05112877980659],
[0, 0],
[-180, 0],
],
],
type: 'Polygon',
},
properties: {
'hits.total.value': 182,
},
type: 'Feature',
} as TileMetaFeature,
];
const tileMetaFeature = getTileMetaFeature(0, 0, 1, tileMetaFeatures);
expect(tileMetaFeature).not.toBeUndefined();
expect(tileMetaFeature!.properties['hits.total.value']).toBe(182);
});
});

View file

@ -5,33 +5,62 @@
* 2.0.
*/
// @ts-expect-error
import turfCenterOfMass from '@turf/center-of-mass';
import { EventEmitter } from 'events';
import { LAT_INDEX, LON_INDEX } from '../../../common/constants';
import type { TileError, TileMetaFeature } from '../../../common/descriptor_types';
import { TileRequest } from './types';
import { isPointInTile } from '../../classes/util/geo_tile_utils';
interface LayerState {
label: string;
tileErrors?: TileError[];
tileMetaFeatures?: TileMetaFeature[];
tileUrl: string;
}
export class VectorTileAdapter extends EventEmitter {
private _layers: Record<string, { label: string; tileUrl: string }> = {};
private _layers: Record<string, LayerState> = {};
private _tiles: Array<{ x: number; y: number; z: number }> = [];
addLayer(layerId: string, label: string, tileUrl: string) {
public addLayer(layerId: string, label: string, tileUrl: string) {
this._layers[layerId] = { label, tileUrl };
this._onChange();
}
removeLayer(layerId: string) {
public removeLayer(layerId: string) {
delete this._layers[layerId];
this._onChange();
}
hasLayers() {
public hasLayers() {
return Object.keys(this._layers).length > 0;
}
setTiles(tiles: Array<{ x: number; y: number; z: number }>) {
public setTiles(tiles: Array<{ x: number; y: number; z: number }>) {
this._tiles = tiles;
this._onChange();
}
getLayerOptions(): Array<{ value: string; label: string }> {
public setTileResults(
layerId: string,
tileMetaFeatures?: TileMetaFeature[],
tileErrors?: TileError[]
) {
if (!this._layers[layerId]) {
return;
}
this._layers[layerId] = {
...this._layers[layerId],
tileErrors,
tileMetaFeatures,
};
this._onChange();
}
public getLayerOptions(): Array<{ value: string; label: string }> {
return Object.keys(this._layers).map((layerId) => {
return {
value: layerId,
@ -40,22 +69,58 @@ export class VectorTileAdapter extends EventEmitter {
});
}
getTileRequests(layerId: string): TileRequest[] {
public getTileRequests(layerId: string): TileRequest[] {
if (!this._layers[layerId]) {
return [];
}
const { tileUrl } = this._layers[layerId];
const { tileErrors, tileMetaFeatures, tileUrl } = this._layers[layerId];
return this._tiles.map((tile) => {
return {
layerId,
tileUrl,
tileError: getTileError(tile.x, tile.y, tile.z, tileErrors),
tileMetaFeature: getTileMetaFeature(tile.x, tile.y, tile.z, tileMetaFeatures),
...tile,
};
});
}
_onChange() {
private _onChange() {
this.emit('change');
}
}
export function getTileMetaFeature(
x: number,
y: number,
z: number,
tileMetaFeatures?: TileMetaFeature[]
) {
if (!tileMetaFeatures || tileMetaFeatures.length === 0) {
return;
}
return tileMetaFeatures.find((tileMetaFeature) => {
const centerGeometry = turfCenterOfMass(tileMetaFeature).geometry;
return isPointInTile(
centerGeometry.coordinates[LAT_INDEX],
centerGeometry.coordinates[LON_INDEX],
x,
y,
z
);
});
}
export function getTileError(x: number, y: number, z: number, tileErrors?: TileError[]) {
if (!tileErrors || tileErrors.length === 0) {
return;
}
const tileKey = `${z}/${x}/${y}`;
return tileErrors.find((tileError) => {
return tileError.tileKey === tileKey;
});
}

View file

@ -6,7 +6,7 @@
*/
import React, { lazy } from 'react';
import type { Adapters } from '@kbn/inspector-plugin/public';
import type { Adapters, InspectorViewProps } from '@kbn/inspector-plugin/public';
import { i18n } from '@kbn/i18n';
import { LazyWrapper } from '../../lazy_wrapper';
@ -25,7 +25,7 @@ export const VectorTileInspectorView = {
shouldShow(adapters: Adapters) {
return Boolean(adapters.vectorTiles?.hasLayers());
},
component: (props: { adapters: Adapters }) => {
component: (props: InspectorViewProps) => {
return <LazyWrapper getLazyComponent={getLazyComponent} lazyComponentProps={props} />;
},
};

View file

@ -23475,7 +23475,6 @@
"xpack.maps.inspector.mapDetailsTitle": "Détails de la carte",
"xpack.maps.inspector.mapDetailsViewHelpText": "Voir l'état de la carte",
"xpack.maps.inspector.mapDetailsViewTitle": "Détails de la carte",
"xpack.maps.inspector.vectorTile.layerSelectPrepend": "Calque",
"xpack.maps.inspector.vectorTile.noRequestsLoggedDescription.mapHasNotLoggedAnyRequestsText": "Cette carte ne comporte aucun résultat de recherche de tuiles vectorielles.",
"xpack.maps.inspector.vectorTile.noRequestsLoggedTitle": "Aucune requête consignée dans un log pour les tuiles vectorielles",
"xpack.maps.inspector.vectorTile.requestsView": "Vous visualisez les requêtes de recherche de tuiles vectorielles. Pour afficher les requêtes envoyées à l'API de recherche, définissez Afficher sur Requêtes.",

View file

@ -23490,7 +23490,6 @@
"xpack.maps.inspector.mapDetailsTitle": "マップの詳細",
"xpack.maps.inspector.mapDetailsViewHelpText": "マップステータスを表示します",
"xpack.maps.inspector.mapDetailsViewTitle": "マップの詳細",
"xpack.maps.inspector.vectorTile.layerSelectPrepend": "レイヤー",
"xpack.maps.inspector.vectorTile.noRequestsLoggedDescription.mapHasNotLoggedAnyRequestsText": "このマップにはベクトルタイル検索結果がありません。",
"xpack.maps.inspector.vectorTile.noRequestsLoggedTitle": "ベクトルタイルの要求はログに記録されません",
"xpack.maps.inspector.vectorTile.requestsView": "ベクトルタイル検索要求を表示しています。検索APIに送信された要求を表示するには、表示要求に設定します。",

View file

@ -23489,7 +23489,6 @@
"xpack.maps.inspector.mapDetailsTitle": "地图详情",
"xpack.maps.inspector.mapDetailsViewHelpText": "查看地图状态",
"xpack.maps.inspector.mapDetailsViewTitle": "地图详情",
"xpack.maps.inspector.vectorTile.layerSelectPrepend": "图层",
"xpack.maps.inspector.vectorTile.noRequestsLoggedDescription.mapHasNotLoggedAnyRequestsText": "此地图没有任何矢量磁贴搜索结果。",
"xpack.maps.inspector.vectorTile.noRequestsLoggedTitle": "未为矢量磁贴记录任何请求",
"xpack.maps.inspector.vectorTile.requestsView": "您正查看矢量磁贴搜索请求。要查看提交到搜索 API 的请求,请将视图设置为“请求”。",