mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
c3864a5d10
commit
9e920faacc
24 changed files with 558 additions and 103 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -63,6 +63,7 @@ const mockLayer = {
|
|||
|
||||
const defaultProps = {
|
||||
depth: 0,
|
||||
inspectorAdapters: {},
|
||||
layer: mockLayer,
|
||||
selectedLayer: undefined,
|
||||
openLayerPanel: async () => {},
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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} />;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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に送信された要求を表示するには、[表示]を[要求]に設定します。",
|
||||
|
|
|
@ -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 的请求,请将视图设置为“请求”。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue