Add inspector for VEGA (#70941)

* [WIP] Add inspector for VEGA

Closes: #31189

* view -> dataset

* cleanup

* add spec viewer

* cleanup code

* use rx to retrieve data from adapters

* Make custom inspector adapters registerable from the visType

* fix flex-box size

* cleanup

* remove visTypesWithoutInspector from visualize_embeddable

* fix PR comments

* add vega folder to sass-lint

* fix jest

* Update src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx

Co-authored-by: Wylie Conlon <wylieconlon@gmail.com>

* use addSignalListener

* cleanup

* add onColumnResize handler

* EuiCodeEditor -> CodeEditor

* fix type_check

* fix issue with pagination

* fix extra vertical scroll

* add area-label for EuiButtonIcon

* add area-label for EuiComboBox

* Design Commit

- Fixing up layout trying to remove any `.eui` classes and uses flex instead of percentage
- Fixing text to use `Sentence case` not `Title Case`

* Wrapper around signal viewer table

* fix Jest snapshot

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Wylie Conlon <wylieconlon@gmail.com>
Co-authored-by: cchaos <caroline.horn@elastic.co>
This commit is contained in:
Alexey Antonov 2020-07-21 14:45:51 +03:00 committed by GitHub
parent 81cbd13db4
commit e1ffcccb96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 993 additions and 84 deletions

View file

@ -3,6 +3,7 @@ files:
- 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss'
- 'src/plugins/timelion/**/*.s+(a|c)ss'
- 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss'
- 'src/plugins/vis_type_vega/**/*.s+(a|c)ss'
- 'src/plugins/vis_type_xy/**/*.s+(a|c)ss'
- 'x-pack/plugins/canvas/**/*.s+(a|c)ss'
- 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss'

View file

@ -52,7 +52,6 @@ import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiComboBoxProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
import { EuiGlobalToastListToast } from '@elastic/eui';
import { EventEmitter } from 'events';
import { ExclusiveUnion } from '@elastic/eui';
import { ExistsParams } from 'elasticsearch';
import { ExplainParams } from 'elasticsearch';
@ -148,7 +147,7 @@ import { ReindexRethrottleParams } from 'elasticsearch';
import { RenderSearchTemplateParams } from 'elasticsearch';
import { Reporter } from '@kbn/analytics';
import { RequestAdapter } from 'src/plugins/inspector/common';
import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common';
import { RequestStatistics } from 'src/plugins/inspector/common';
import { Required } from '@kbn/utility-types';
import * as Rx from 'rxjs';
import { SavedObject } from 'src/core/server';

View file

@ -129,7 +129,7 @@ export const getTermsBucketAgg = () =>
const response = await nestedSearchSource.fetch({ abortSignal });
request
.stats(getResponseInspectorStats(nestedSearchSource, response))
.stats(getResponseInspectorStats(response, nestedSearchSource))
.ok({ json: response });
resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg());
}

View file

@ -160,7 +160,7 @@ const handleCourierRequest = async ({
(searchSource as any).lastQuery = queryHash;
request.stats(getResponseInspectorStats(searchSource, response)).ok({ json: response });
request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response });
(searchSource as any).rawResponse = response;
} catch (e) {

View file

@ -61,10 +61,11 @@ export function getRequestInspectorStats(searchSource: ISearchSource) {
/** @public */
export function getResponseInspectorStats(
searchSource: ISearchSource,
resp: SearchResponse<unknown>
resp: SearchResponse<unknown>,
searchSource?: ISearchSource
) {
const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1];
const lastRequest =
searchSource?.history && searchSource.history[searchSource.history.length - 1];
const stats: RequestStatistics = {};
if (resp && resp.took) {

View file

@ -39,7 +39,6 @@ import { DeleteTemplateParams } from 'elasticsearch';
import { DetailedPeerCertificate } from 'tls';
import { Duration } from 'moment';
import { ErrorToastOptions } from 'src/core/public/notifications';
import { EventEmitter } from 'events';
import { ExistsParams } from 'elasticsearch';
import { ExplainParams } from 'elasticsearch';
import { FieldStatsParams } from 'elasticsearch';

View file

@ -874,7 +874,7 @@ function discoverController(
}
function onResults(resp) {
inspectorRequest.stats(getResponseInspectorStats($scope.searchSource, resp)).ok({ json: resp });
inspectorRequest.stats(getResponseInspectorStats(resp, $scope.searchSource)).ok({ json: resp });
if (getTimeField()) {
const tabifiedData = tabifyAggResponse($scope.vis.data.aggs, resp);

View file

@ -307,7 +307,7 @@ export class SearchEmbeddable extends Embeddable<SearchInput, SearchOutput>
this.updateOutput({ loading: false, error: undefined });
// Log response to inspector
inspectorRequest.stats(getResponseInspectorStats(searchSource, resp)).ok({ json: resp });
inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp });
// Apply the changes to the angular scope
this.searchScope.$apply(() => {

View file

@ -23,7 +23,7 @@ import { createExecutionContainer, ExecutionContainer } from './container';
import { createError } from '../util';
import { Defer, now } from '../../../kibana_utils/common';
import { toPromise } from '../../../data/common/utils/abort_utils';
import { RequestAdapter, DataAdapter } from '../../../inspector/common';
import { RequestAdapter, DataAdapter, Adapters } from '../../../inspector/common';
import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error';
import {
ExpressionAstExpression,
@ -70,7 +70,7 @@ export class Execution<
ExtraContext extends Record<string, unknown> = Record<string, unknown>,
Input = unknown,
Output = unknown,
InspectorAdapters = ExtraContext['inspectorAdapters'] extends object
InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object
? ExtraContext['inspectorAdapters']
: DefaultInspectorAdapters
> {

View file

@ -18,7 +18,7 @@
*/
import { ExpressionType } from '../expression_types';
import { DataAdapter, RequestAdapter } from '../../../inspector/common';
import { Adapters, DataAdapter, RequestAdapter } from '../../../inspector/common';
import { TimeRange, Query, Filter } from '../../../data/common';
import { SavedObject, SavedObjectAttributes } from '../../../../core/public';
@ -26,7 +26,7 @@ import { SavedObject, SavedObjectAttributes } from '../../../../core/public';
* `ExecutionContext` is an object available to all functions during a single execution;
* it provides various methods to perform side-effects.
*/
export interface ExecutionContext<Input = unknown, InspectorAdapters = DefaultInspectorAdapters> {
export interface ExecutionContext<Input = unknown, InspectorAdapters extends Adapters = Adapters> {
/**
* Get initial input with which execution started.
*/
@ -75,7 +75,7 @@ export interface ExecutionContext<Input = unknown, InspectorAdapters = DefaultIn
/**
* Default inspector adapters created if inspector adapters are not set explicitly.
*/
export interface DefaultInspectorAdapters {
export interface DefaultInspectorAdapters extends Adapters {
requests: RequestAdapter;
data: DataAdapter;
}

View file

@ -150,7 +150,7 @@ export class ExpressionLoader {
variables: params.variables || {},
inspectorAdapters: params.inspectorAdapters,
});
if (!params.inspectorAdapters) params.inspectorAdapters = this.execution.inspect() as Adapters;
const prevDataHandler = this.execution;
const data = await prevDataHandler.getData();
if (this.execution !== prevDataHandler) {
@ -181,6 +181,9 @@ export class ExpressionLoader {
if (params.variables && this.params) {
this.params.variables = params.variables;
}
this.params.inspectorAdapters = (params.inspectorAdapters ||
this.execution?.inspect()) as Adapters;
}
}

View file

@ -65,6 +65,7 @@ export function createInputControlVisTypeDefinition(deps: InputControlVisDepende
},
],
},
inspectorAdapters: {},
requestHandler: 'none',
responseHandler: 'none',
};

View file

@ -17,5 +17,12 @@
* under the License.
*/
export { Adapters } from './types';
export { DataAdapter, FormattedData } from './data';
export { RequestAdapter, RequestStatistic, RequestStatistics, RequestStatus } from './request';
export {
RequestAdapter,
RequestStatistic,
RequestStatistics,
RequestStatus,
RequestResponder,
} from './request';

View file

@ -19,3 +19,4 @@
export { RequestStatistic, RequestStatistics, RequestStatus } from './types';
export { RequestAdapter } from './request_adapter';
export { RequestResponder } from './request_responder';

View file

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The interface that the adapters used to open an inspector have to fullfill.
*/
export interface Adapters {
[key: string]: any;
}

View file

@ -1 +1 @@
@import 'views/index'
@import 'views/index';

View file

@ -22,8 +22,9 @@ import * as React from 'react';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { toMountPoint } from '../../kibana_react/public';
import { InspectorViewRegistry } from './view_registry';
import { Adapters, InspectorOptions, InspectorSession } from './types';
import { InspectorOptions, InspectorSession } from './types';
import { InspectorPanel } from './ui/inspector_panel';
import { Adapters } from '../common';
import { getRequestsViewDescription, getDataViewDescription } from './views';

View file

@ -18,23 +18,17 @@
*/
import { OverlayRef } from '../../../core/public';
/**
* The interface that the adapters used to open an inspector have to fullfill.
*/
export interface Adapters {
[key: string]: any;
}
import { Adapters } from '../common';
/**
* The props interface that a custom inspector view component, that will be passed
* to {@link InspectorViewDescription#component}, must use.
*/
export interface InspectorViewProps {
export interface InspectorViewProps<TAdapters extends Adapters = Adapters> {
/**
* Adapters used to open the inspector.
*/
adapters: Adapters;
adapters: TAdapters;
/**
* The title that the inspector is currently using e.g. a visualization name.
*/

View file

@ -306,9 +306,11 @@ exports[`InspectorPanel should render as expected 1`] = `
</EuiFlexGroup>
</div>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlyoutBody
className="insInspectorPanel__flyoutBody"
>
<div
className="euiFlyoutBody"
className="euiFlyoutBody insInspectorPanel__flyoutBody"
>
<div
className="euiFlyoutBody__overflow"

View file

@ -0,0 +1,12 @@
.insInspectorPanel__flyoutBody {
// TODO: EUI to allow for custom classNames to inner elements
// Or supply this as default
> div {
display: flex;
flex-direction: column;
> div {
flex-grow: 1;
}
}
}

View file

@ -20,7 +20,8 @@
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { InspectorPanel } from './inspector_panel';
import { Adapters, InspectorViewDescription } from '../types';
import { InspectorViewDescription } from '../types';
import { Adapters } from '../../common';
describe('InspectorPanel', () => {
let adapters: Adapters;

View file

@ -17,11 +17,13 @@
* under the License.
*/
import './inspector_panel.scss';
import { i18n } from '@kbn/i18n';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import { Adapters, InspectorViewDescription } from '../types';
import { InspectorViewDescription } from '../types';
import { Adapters } from '../../common';
import { InspectorViewChooser } from './inspector_view_chooser';
function hasAdaptersChanged(oldAdapters: Adapters, newAdapters: Adapters) {
@ -122,7 +124,9 @@ export class InspectorPanel extends Component<InspectorPanelProps, InspectorPane
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>{this.renderSelectedPanel()}</EuiFlyoutBody>
<EuiFlyoutBody className="insInspectorPanel__flyoutBody">
{this.renderSelectedPanel()}
</EuiFlyoutBody>
</React.Fragment>
);
}

View file

@ -20,7 +20,7 @@
import { InspectorViewRegistry } from './view_registry';
import { InspectorViewDescription } from './types';
import { Adapters } from './types';
import { Adapters } from '../common';
function createMockView(
params: {

View file

@ -18,7 +18,8 @@
*/
import { EventEmitter } from 'events';
import { Adapters, InspectorViewDescription } from './types';
import { InspectorViewDescription } from './types';
import { Adapters } from '../common';
/**
* @callback viewShouldShowFunc

View file

@ -30,7 +30,8 @@ import {
} from '@elastic/eui';
import { DataTableFormat } from './data_table';
import { InspectorViewProps, Adapters } from '../../../types';
import { InspectorViewProps } from '../../../types';
import { Adapters } from '../../../../common';
import {
TabularLoaderOptions,
TabularData,

View file

@ -20,7 +20,8 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { DataViewComponent } from './components/data_view';
import { Adapters, InspectorViewDescription, InspectorViewProps } from '../../types';
import { InspectorViewDescription, InspectorViewProps } from '../../types';
import { Adapters } from '../../../common';
import { IUiSettingsClient } from '../../../../../core/public';
export const getDataViewDescription = (

View file

@ -19,7 +19,8 @@
import { i18n } from '@kbn/i18n';
import { RequestsViewComponent } from './components/requests_view';
import { Adapters, InspectorViewDescription } from '../../types';
import { InspectorViewDescription } from '../../types';
import { Adapters } from '../../../common';
export const getRequestsViewDescription = (): InspectorViewDescription => ({
title: i18n.translate('inspector.requests.requestsTitle', {

View file

@ -66,4 +66,5 @@ export const markdownVisDefinition = {
},
requestHandler: 'none',
responseHandler: 'none',
inspectorAdapters: {},
};

View file

@ -62,6 +62,7 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies)
},
requestHandler: timelionRequestHandler,
responseHandler: 'none',
inspectorAdapters: {},
options: {
showIndexSelection: false,
showQueryBar: false,

View file

@ -78,5 +78,6 @@ export const metricsVisDefinition = {
showIndexSelection: false,
},
requestHandler: metricsRequestHandler,
inspectorAdapters: {},
responseHandler: 'none',
};

View file

@ -3,6 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"],
"requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions", "inspector"],
"requiredBundles": ["kibanaUtils", "kibanaReact"]
}

View file

@ -17,6 +17,7 @@
// BUG #23514: Make sure Vega doesn't display the controls in two places
.vega-bindings {
// sass-lint:disable no-important
display: none !important;
}
}
@ -47,7 +48,7 @@
width: $euiSizeM * 10 - $euiSize;
}
input[type="range"] {
input[type='range'] {
width: $euiSizeM * 10;
display: inline-block;
vertical-align: middle;
@ -74,7 +75,7 @@
top: 0;
width: 100%;
margin: auto;
opacity: 0.8;
opacity: .8;
z-index: 1;
list-style: none;
}
@ -115,25 +116,30 @@
@include euiTextTruncate;
padding-top: $euiSizeXS;
padding-bottom: $euiSizeXS;
}
td.key {
color: $euiColorMediumShade;
max-width: $euiSize * 10;
text-align: right;
padding-right: $euiSizeXS;
}
td.value {
max-width: $euiSizeL * 10;
text-align: left;
}
@media only screen and (max-width: map-get($euiBreakpoints, 'm')){
td.key {
max-width: $euiSize * 6;
}
td.value {
&.key {
color: $euiColorMediumShade;
max-width: $euiSize * 10;
text-align: right;
padding-right: $euiSizeXS;
}
&.value {
max-width: $euiSizeL * 10;
text-align: left;
}
}
@media only screen and (max-width: map-get($euiBreakpoints, 'm')) {
td {
&.key {
max-width: $euiSize * 6;
}
&.value {
max-width: $euiSize * 10;
}
}
}
}

View file

@ -18,13 +18,17 @@
*/
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { CoreStart, IUiSettingsClient } from 'kibana/public';
import {
getSearchParamsFromRequest,
SearchRequest,
DataPublicPluginStart,
IEsSearchResponse,
} from '../../../data/public';
import { search as dataPluginSearch } from '../../../data/public';
import { VegaInspectorAdapters } from '../vega_inspector';
import { RequestResponder } from '../../../inspector/public';
export interface SearchAPIDependencies {
uiSettings: IUiSettingsClient;
@ -35,26 +39,52 @@ export interface SearchAPIDependencies {
export class SearchAPI {
constructor(
private readonly dependencies: SearchAPIDependencies,
private readonly abortSignal?: AbortSignal
private readonly abortSignal?: AbortSignal,
public readonly inspectorAdapters?: VegaInspectorAdapters
) {}
search(searchRequests: SearchRequest[]) {
const { search } = this.dependencies.search;
const requestResponders: any = {};
return combineLatest(
searchRequests.map((request, index) => {
const requestId: number = index;
const params = getSearchParamsFromRequest(request, {
uiSettings: this.dependencies.uiSettings,
injectedMetadata: this.dependencies.injectedMetadata,
});
if (this.inspectorAdapters) {
requestResponders[requestId] = this.inspectorAdapters.requests.start(
`#${requestId}`,
request
);
requestResponders[requestId].json(params.body);
}
return search({ params }, { signal: this.abortSignal }).pipe(
tap((data) => this.inspectSearchResult(data, requestResponders[requestId])),
map((data) => ({
id: index,
id: requestId,
rawResponse: data.rawResponse,
}))
);
})
);
}
public resetSearchStats() {
if (this.inspectorAdapters) {
this.inspectorAdapters.requests.reset();
}
}
private inspectSearchResult(response: IEsSearchResponse, requestResponder: RequestResponder) {
if (requestResponder) {
requestResponder
.stats(dataPluginSearch.getResponseInspectorStats(response.rawResponse))
.ok({ json: response.rawResponse });
}
}
}

View file

@ -97,6 +97,7 @@ describe('VegaParser._resolveEsQueries', () => {
search: jest.fn(() => ({
toPromise: jest.fn(() => Promise.resolve(data)),
})),
resetSearchStats: jest.fn(),
};
});

View file

@ -79,6 +79,7 @@ export class VegaParser {
paddingHeight?: number;
containerDir?: ControlsLocation | ControlsDirection;
controlsDir?: ControlsLocation;
searchAPI: SearchAPI;
constructor(
spec: VegaSpec | string,
@ -92,10 +93,11 @@ export class VegaParser {
this.error = undefined;
this.warnings = [];
this.searchAPI = searchAPI;
const onWarn = this._onWarning.bind(this);
this._urlParsers = {
elasticsearch: new EsQueryParser(timeCache, searchAPI, filters, onWarn),
elasticsearch: new EsQueryParser(timeCache, this.searchAPI, filters, onWarn),
emsfile: new EmsFileParser(serviceSettings),
url: new UrlParser(onWarn),
};
@ -541,6 +543,8 @@ export class VegaParser {
async _resolveDataUrls() {
const pending: PendingType = {};
this.searchAPI.resetSearchStats();
this._findObjectDataUrls(this.spec!, (obj: Data) => {
const url = obj.url;
delete obj.url;

View file

@ -18,8 +18,10 @@
*/
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public';
import { Plugin as DataPublicPlugin } from '../../data/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public';
import { VisualizationsSetup } from '../../visualizations/public';
import { Setup as InspectorSetup } from '../../inspector/public';
import {
setNotifications,
setData,
@ -37,11 +39,13 @@ import { IServiceSettings } from '../../maps_legacy/public';
import './index.scss';
import { ConfigSchema } from '../config';
import { getVegaInspectorView } from './vega_inspector';
/** @internal */
export interface VegaVisualizationDependencies {
core: CoreSetup;
plugins: {
data: ReturnType<DataPublicPlugin['setup']>;
data: DataPublicPluginSetup;
};
serviceSettings: IServiceSettings;
}
@ -50,13 +54,14 @@ export interface VegaVisualizationDependencies {
export interface VegaPluginSetupDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
visualizations: VisualizationsSetup;
data: ReturnType<DataPublicPlugin['setup']>;
inspector: InspectorSetup;
data: DataPublicPluginSetup;
mapsLegacy: any;
}
/** @internal */
export interface VegaPluginStartDependencies {
data: ReturnType<DataPublicPlugin['start']>;
data: DataPublicPluginStart;
}
/** @internal */
@ -69,7 +74,7 @@ export class VegaPlugin implements Plugin<Promise<void>, void> {
public async setup(
core: CoreSetup,
{ data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies
{ inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies
) {
setInjectedVars({
enableExternalUrls: this.initializerContext.config.get().enableExternalUrls,
@ -88,6 +93,8 @@ export class VegaPlugin implements Plugin<Promise<void>, void> {
serviceSettings: mapsLegacy.serviceSettings,
};
inspector.registerView(getVegaInspectorView({ uiSettings: core.uiSettings }));
expressions.registerFunction(() => createVegaFn(visualizationDependencies));
visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies));

View file

@ -19,9 +19,15 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public';
import {
ExecutionContext,
ExpressionFunctionDefinition,
KibanaContext,
Render,
} from '../../expressions/public';
import { VegaVisualizationDependencies } from './plugin';
import { createVegaRequestHandler } from './vega_request_handler';
import { VegaInspectorAdapters } from './vega_inspector/index';
import { TimeRange, Query } from '../../data/public';
import { VegaParser } from './data_model/vega_parser';
@ -42,7 +48,13 @@ interface RenderValue {
export const createVegaFn = (
dependencies: VegaVisualizationDependencies
): ExpressionFunctionDefinition<'vega', Input, Arguments, Output> => ({
): ExpressionFunctionDefinition<
'vega',
Input,
Arguments,
Output,
ExecutionContext<unknown, VegaInspectorAdapters>
> => ({
name: 'vega',
type: 'render',
inputTypes: ['kibana_context', 'null'],
@ -57,7 +69,7 @@ export const createVegaFn = (
},
},
async fn(input, args, context) {
const vegaRequestHandler = createVegaRequestHandler(dependencies, context.abortSignal);
const vegaRequestHandler = createVegaRequestHandler(dependencies, context);
const response = await vegaRequestHandler({
timeRange: get(input, 'timeRange') as TimeRange,

View file

@ -0,0 +1,114 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiComboBox,
EuiFlexGroup,
EuiComboBoxProps,
EuiFlexItem,
EuiSpacer,
CommonProps,
} from '@elastic/eui';
import { VegaAdapter, InspectDataSets } from '../vega_adapter';
import { InspectorDataGrid } from './inspector_data_grid';
interface DataViewerProps extends CommonProps {
vegaAdapter: VegaAdapter;
}
const getDataGridArialabel = (view: InspectDataSets) =>
i18n.translate('visTypeVega.inspector.dataViewer.gridAriaLabel', {
defaultMessage: '{name} data grid',
values: {
name: view.id,
},
});
const dataSetAriaLabel = i18n.translate('visTypeVega.inspector.dataViewer.dataSetAriaLabel', {
defaultMessage: 'Data set',
});
export const DataViewer = ({ vegaAdapter, ...rest }: DataViewerProps) => {
const [inspectDataSets, setInspectDataSets] = useState<InspectDataSets[]>([]);
const [selectedView, setSelectedView] = useState<InspectDataSets>();
const [dataGridAriaLabel, setDataGridAriaLabel] = useState<string>('');
const onViewChange: EuiComboBoxProps<unknown>['onChange'] = useCallback(
(selectedOptions) => {
const newView = inspectDataSets!.find((view) => view.id === selectedOptions[0].label);
if (newView) {
setDataGridAriaLabel(getDataGridArialabel(newView));
setSelectedView(newView);
}
},
[inspectDataSets]
);
useEffect(() => {
const subscription = vegaAdapter.getDataSetsSubscription().subscribe((dataSets) => {
setInspectDataSets(dataSets);
});
return () => {
subscription.unsubscribe();
};
}, [vegaAdapter]);
useEffect(() => {
if (inspectDataSets) {
if (!selectedView) {
setSelectedView(inspectDataSets[0]);
} else {
setDataGridAriaLabel(getDataGridArialabel(selectedView));
}
}
}, [selectedView, inspectDataSets]);
if (!selectedView) {
return null;
}
return (
<EuiFlexGroup direction="column" gutterSize="s" wrap={false} responsive={false} {...rest}>
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
<EuiComboBox
fullWidth
options={inspectDataSets.map((item: any) => ({
label: item.id,
}))}
aria-label={dataSetAriaLabel}
onChange={onViewChange}
isClearable={false}
singleSelection={{ asPlainText: true }}
selectedOptions={[{ label: selectedView.id }]}
/>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<InspectorDataGrid
columns={selectedView.columns}
data={selectedView.data}
dataGridAriaLabel={dataGridAriaLabel}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { DataViewer } from './data_viewer';
export { SignalViewer } from './signal_viewer';
export { SpecViewer } from './spec_viewer';

View file

@ -0,0 +1,144 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { EuiDataGrid, EuiDataGridSorting, EuiDataGridProps } from '@elastic/eui';
import { VegaRuntimeData } from '../vega_adapter';
const DEFAULT_PAGE_SIZE = 15;
interface InspectorDataGridProps extends VegaRuntimeData {
dataGridAriaLabel: string;
}
export const InspectorDataGrid = ({ columns, data, dataGridAriaLabel }: InspectorDataGridProps) => {
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: DEFAULT_PAGE_SIZE });
const onChangeItemsPerPage = useCallback(
(pageSize) => setPagination((p) => ({ ...p, pageSize, pageIndex: 0 })),
[setPagination]
);
const onChangePage = useCallback((pageIndex) => setPagination((p) => ({ ...p, pageIndex })), [
setPagination,
]);
// Column visibility
const [visibleColumns, setVisibleColumns] = useState<string[]>([]);
useEffect(
() => {
setPagination({
...pagination,
pageIndex: 0,
});
setVisibleColumns(columns.map((column) => column.id));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[dataGridAriaLabel]
);
// Sorting
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
const onSort = useCallback(
(newSortingColumns: EuiDataGridSorting['columns']) => {
setSortingColumns(newSortingColumns);
},
[setSortingColumns]
);
let gridData = useMemo(() => {
return [...data].sort((a, b) => {
for (let i = 0; i < sortingColumns.length; i++) {
const column = sortingColumns[i];
const aValue = a[column.id];
const bValue = b[column.id];
if (aValue < bValue) return column.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return column.direction === 'asc' ? 1 : -1;
}
return 0;
});
}, [data, sortingColumns]);
const renderCellValue = useMemo(() => {
return (({ rowIndex, columnId }) => {
let adjustedRowIndex = rowIndex;
// If we are doing the pagination (instead of leaving that to the grid)
// then the row index must be adjusted as `data` has already been pruned to the page size
adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize;
return gridData.hasOwnProperty(adjustedRowIndex)
? gridData[adjustedRowIndex][columnId] || null
: null;
}) as EuiDataGridProps['renderCellValue'];
}, [gridData, pagination.pageIndex, pagination.pageSize]);
// Pagination
gridData = useMemo(() => {
const rowStart = pagination.pageIndex * pagination.pageSize;
const rowEnd = Math.min(rowStart + pagination.pageSize, gridData.length);
return gridData.slice(rowStart, rowEnd);
}, [gridData, pagination]);
// Resize
const [columnsWidth, setColumnsWidth] = useState<Record<string, number>>({});
const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback(
({ columnId, width }) => {
setColumnsWidth({
...columnsWidth,
[columnId]: width,
});
},
[columnsWidth]
);
return (
<EuiDataGrid
aria-label={dataGridAriaLabel}
columns={columns.map((column) => {
if (columnsWidth[column.id]) {
return {
...column,
initialWidth: columnsWidth[column.id],
};
}
return column;
})}
columnVisibility={{
visibleColumns,
setVisibleColumns,
}}
rowCount={data.length}
renderCellValue={renderCellValue}
sorting={{ columns: sortingColumns, onSort }}
toolbarVisibility={{
showFullScreenSelector: false,
}}
onColumnResize={onColumnResize}
pagination={{
...pagination,
pageSizeOptions: [DEFAULT_PAGE_SIZE, 25, 50],
onChangeItemsPerPage,
onChangePage,
}}
/>
);
};

View file

@ -0,0 +1,72 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { VegaAdapter, InspectSignalsSets } from '../vega_adapter';
import { InspectorDataGrid } from './inspector_data_grid';
interface SignalViewerProps {
vegaAdapter: VegaAdapter;
}
const initialSignalColumnWidth = 150;
const signalDataGridAriaLabel = i18n.translate('visTypeVega.inspector.signalViewer.gridAriaLabel', {
defaultMessage: 'Signal values data grid',
});
export const SignalViewer = ({ vegaAdapter }: SignalViewerProps) => {
const [inspectSignalsSets, setInspectSignalsSets] = useState<InspectSignalsSets>();
useEffect(() => {
const subscription = vegaAdapter.getSignalsSetsSubscription().subscribe((signalSets) => {
if (signalSets) {
setInspectSignalsSets(signalSets);
}
});
return () => {
subscription.unsubscribe();
};
}, [vegaAdapter]);
if (!inspectSignalsSets) {
return null;
}
return (
<div>
<EuiSpacer size="s" />
<InspectorDataGrid
columns={inspectSignalsSets.columns.map((column, index) => {
if (index === 0) {
return {
...column,
initialWidth: initialSignalColumnWidth,
};
}
return column;
})}
data={inspectSignalsSets.data}
dataGridAriaLabel={signalDataGridAriaLabel}
/>
</div>
);
};

View file

@ -0,0 +1,97 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexItem,
EuiFlexGroup,
EuiCopy,
EuiButtonEmpty,
EuiSpacer,
CommonProps,
} from '@elastic/eui';
import { VegaAdapter } from '../vega_adapter';
import { CodeEditor } from '../../../../kibana_react/public';
interface SpecViewerProps extends CommonProps {
vegaAdapter: VegaAdapter;
}
const copyToClipboardLabel = i18n.translate(
'visTypeVega.inspector.specViewer.copyToClipboardLabel',
{
defaultMessage: 'Copy to clipboard',
}
);
export const SpecViewer = ({ vegaAdapter, ...rest }: SpecViewerProps) => {
const [spec, setSpec] = useState<string>();
useEffect(() => {
const subscription = vegaAdapter.getSpecSubscription().subscribe((data) => {
if (data) {
setSpec(data);
}
});
return () => {
subscription.unsubscribe();
};
}, [vegaAdapter]);
if (!spec) {
return null;
}
return (
<EuiFlexGroup direction="column" gutterSize="s" wrap={false} responsive={false} {...rest}>
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
<div className="eui-textRight">
<EuiCopy textToCopy={spec}>
{(copy) => (
<EuiButtonEmpty size="xs" flush="right" iconType="copyClipboard" onClick={copy}>
{copyToClipboardLabel}
</EuiButtonEmpty>
)}
</EuiCopy>
</div>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<CodeEditor
languageId="json"
value={spec}
onChange={() => {}}
options={{
readOnly: true,
lineNumbers: 'off',
fontSize: 12,
minimap: {
enabled: false,
},
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'indent',
automaticLayout: true,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export {
createInspectorAdapters,
getVegaInspectorView,
VegaInspectorAdapters,
} from './vega_inspector';

View file

@ -0,0 +1,148 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable, ReplaySubject, fromEventPattern, merge, timer } from 'rxjs';
import { map, switchMap, filter, debounce } from 'rxjs/operators';
import { View, Runtime, Spec } from 'vega';
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
interface DebugValues {
view: View;
spec: Spec;
}
export interface VegaRuntimeData {
columns: Array<{
id: string;
}>;
data: Array<Record<string, string>>;
}
export type InspectDataSets = Assign<VegaRuntimeData, { id: string }>;
export type InspectSignalsSets = VegaRuntimeData;
const vegaAdapterSignalLabel = i18n.translate('visTypeVega.inspector.vegaAdapter.signal', {
defaultMessage: 'Signal',
});
const vegaAdapterValueLabel = i18n.translate('visTypeVega.inspector.vegaAdapter.value', {
defaultMessage: 'Value',
});
/** Get Runtime Scope for Vega View
* @link https://vega.github.io/vega/docs/api/debugging/#scope
**/
const getVegaRuntimeScope = (debugValues: DebugValues) =>
(debugValues.view as any)._runtime as Runtime;
const serializeColumns = (item: Record<string, unknown>, columns: string[]) => {
const nonSerializableFieldLabel = '(..)';
return columns.reduce((row: Record<string, string>, column) => {
try {
const cell = item[column];
row[column] = typeof cell === 'object' ? JSON.stringify(cell) : `${cell}`;
} catch (e) {
row[column] = nonSerializableFieldLabel;
}
return row;
}, {});
};
export class VegaAdapter {
private debugValuesSubject = new ReplaySubject<DebugValues>();
bindInspectValues(debugValues: DebugValues) {
this.debugValuesSubject.next(debugValues);
}
getDataSetsSubscription(): Observable<InspectDataSets[]> {
return this.debugValuesSubject.pipe(
filter((debugValues) => Boolean(debugValues)),
map((debugValues) => {
const runtimeScope = getVegaRuntimeScope(debugValues);
return Object.keys(runtimeScope.data || []).reduce((acc: InspectDataSets[], key) => {
const value = runtimeScope.data[key].values.value;
if (value && value[0]) {
const columns = Object.keys(value[0]);
acc.push({
id: key,
columns: columns.map((column) => ({ id: column, schema: 'json' })),
data: value.map((item: Record<string, unknown>) => serializeColumns(item, columns)),
});
}
return acc;
}, []);
})
);
}
getSignalsSetsSubscription(): Observable<InspectSignalsSets> {
const signalsListener = this.debugValuesSubject.pipe(
filter((debugValues) => Boolean(debugValues)),
switchMap((debugValues) => {
const runtimeScope = getVegaRuntimeScope(debugValues);
return merge(
...Object.keys(runtimeScope.signals).map((key: string) =>
fromEventPattern(
(handler) => debugValues.view.addSignalListener(key, handler),
(handler) => debugValues.view.removeSignalListener(key, handler)
)
)
).pipe(
debounce((val) => timer(350)),
map(() => debugValues)
);
})
);
return merge(this.debugValuesSubject, signalsListener).pipe(
filter((debugValues) => Boolean(debugValues)),
map((debugValues) => {
const runtimeScope = getVegaRuntimeScope(debugValues);
return {
columns: [
{ id: vegaAdapterSignalLabel, schema: 'text' },
{ id: vegaAdapterValueLabel, schema: 'json' },
],
data: Object.keys(runtimeScope.signals).map((key: string) =>
serializeColumns(
{
[vegaAdapterSignalLabel]: key,
[vegaAdapterValueLabel]: runtimeScope.signals[key].value,
},
[vegaAdapterSignalLabel, vegaAdapterValueLabel]
)
),
};
})
);
}
getSpecSubscription(): Observable<string> {
return this.debugValuesSubject.pipe(
filter((debugValues) => Boolean(debugValues)),
map((debugValues) => JSON.stringify(debugValues.spec, null, 2))
);
}
}

View file

@ -0,0 +1,18 @@
.vgaVegaDataInspector,
.vgaVegaDataInspector__specViewer {
height: 100%;
}
.vgaVegaDataInspector {
// TODO: EUI needs to provide props to pass down from EuiTabbedContent to tabs and content
display: flex;
flex-direction: column;
[role='tablist'] {
flex-shrink: 0;
}
[role='tabpanel'] {
flex-grow: 1;
}
}

View file

@ -0,0 +1,74 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import './vega_data_inspector.scss';
import React from 'react';
import { EuiTabbedContent } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { VegaInspectorAdapters } from './vega_inspector';
import { DataViewer, SignalViewer, SpecViewer } from './components';
import { InspectorViewProps } from '../../../inspector/public';
export type VegaDataInspectorProps = InspectorViewProps<VegaInspectorAdapters>;
const dataSetsLabel = i18n.translate('visTypeVega.inspector.dataSetsLabel', {
defaultMessage: 'Data sets',
});
const signalValuesLabel = i18n.translate('visTypeVega.inspector.signalValuesLabel', {
defaultMessage: 'Signal values',
});
const specLabel = i18n.translate('visTypeVega.inspector.specLabel', {
defaultMessage: 'Spec',
});
export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => {
const tabs = [
{
id: 'data-viewer--id',
name: dataSetsLabel,
content: <DataViewer vegaAdapter={adapters.vega} />,
},
{
id: 'signal-viewer--id',
name: signalValuesLabel,
content: <SignalViewer vegaAdapter={adapters.vega} />,
},
{
id: 'spec-viewer--id',
name: specLabel,
content: (
<SpecViewer className="vgaVegaDataInspector__specViewer" vegaAdapter={adapters.vega} />
),
},
];
return (
<EuiTabbedContent
className="vgaVegaDataInspector"
size="s"
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
/>
);
};

View file

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'kibana/public';
import { VegaAdapter } from './vega_adapter';
import { VegaDataInspector, VegaDataInspectorProps } from './vega_data_inspector';
import { KibanaContextProvider } from '../../../kibana_react/public';
import { Adapters, RequestAdapter, InspectorViewDescription } from '../../../inspector/public';
export interface VegaInspectorAdapters extends Adapters {
requests: RequestAdapter;
vega: VegaAdapter;
}
const vegaDebugLabel = i18n.translate('visTypeVega.inspector.vegaDebugLabel', {
defaultMessage: 'Vega debug',
});
interface VegaInspectorViewDependencies {
uiSettings: IUiSettingsClient;
}
export const getVegaInspectorView = (dependencies: VegaInspectorViewDependencies) =>
({
title: vegaDebugLabel,
shouldShow(adapters) {
return Boolean(adapters.vega);
},
component: (props) => (
<KibanaContextProvider services={dependencies}>
<VegaDataInspector {...(props as VegaDataInspectorProps)}> </VegaDataInspector>
</KibanaContextProvider>
),
} as InspectorViewDescription);
export const createInspectorAdapters = (): VegaInspectorAdapters => ({
requests: new RequestAdapter(),
vega: new VegaAdapter(),
});

View file

@ -25,6 +25,7 @@ import { TimeCache } from './data_model/time_cache';
import { VegaVisualizationDependencies } from './plugin';
import { VisParams } from './vega_fn';
import { getData, getInjectedMetadata } from './services';
import { VegaInspectorAdapters } from './vega_inspector';
interface VegaRequestHandlerParams {
query: Query;
@ -33,9 +34,14 @@ interface VegaRequestHandlerParams {
visParams: VisParams;
}
interface VegaRequestHandlerContext {
abortSignal?: AbortSignal;
inspectorAdapters?: VegaInspectorAdapters;
}
export function createVegaRequestHandler(
{ plugins: { data }, core: { uiSettings }, serviceSettings }: VegaVisualizationDependencies,
abortSignal?: AbortSignal
context: VegaRequestHandlerContext = {}
) {
let searchAPI: SearchAPI;
const { timefilter } = data.query.timefilter;
@ -54,7 +60,8 @@ export function createVegaRequestHandler(
search: getData().search,
injectedMetadata: getInjectedMetadata(),
},
abortSignal
context.abortSignal,
context.inspectorAdapters
);
}

View file

@ -23,9 +23,10 @@ import { VegaVisualizationDependencies } from './plugin';
import { VegaVisEditor } from './components';
import { createVegaRequestHandler } from './vega_request_handler';
// @ts-ignore
// @ts-expect-error
import { createVegaVisualization } from './vega_visualization';
import { getDefaultSpec } from './default_spec';
import { createInspectorAdapters } from './vega_inspector';
export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => {
const requestHandler = createVegaRequestHandler(dependencies);
@ -54,5 +55,6 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen
showFilterBar: true,
},
stage: 'experimental',
inspectorAdapters: createInspectorAdapters,
};
};

View file

@ -364,6 +364,11 @@ export class VegaBaseView {
* Set global debug variable to simplify vega debugging in console. Show info message first time
*/
setDebugValues(view, spec, vlspec) {
this._parser.searchAPI.inspectorAdapters?.vega.bindInspectValues({
view,
spec: vlspec || spec,
});
if (window) {
if (window.VEGA_DEBUG === undefined && console) {
console.log('%cWelcome to Kibana Vega Plugin!', 'font-size: 16px; font-weight: bold;');

View file

@ -142,7 +142,7 @@ export class VegaMapView extends VegaBaseView {
});
const vegaView = vegaMapLayer.getVegaView();
this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec);
await this.setView(vegaView);
this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec);
}
}

View file

@ -26,7 +26,6 @@ export class VegaView extends VegaBaseView {
if (!this._$container) return;
const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig);
this.setDebugValues(view, this._parser.spec, this._parser.vlspec);
view.warn = this.onWarn.bind(this);
view.error = this.onError.bind(this);
@ -36,5 +35,6 @@ export class VegaView extends VegaBaseView {
if (this._parser.useHover) view.hover();
await this.setView(view);
this.setDebugValues(view, this._parser.spec, this._parser.vlspec);
}
}

View file

@ -34,6 +34,7 @@ import {
EmbeddableOutput,
Embeddable,
IContainer,
Adapters,
} from '../../../../plugins/embeddable/public';
import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public';
import {
@ -78,8 +79,6 @@ export interface VisualizeOutput extends EmbeddableOutput {
type ExpressionLoader = InstanceType<ExpressionsStart['ExpressionLoader']>;
const visTypesWithoutInspector = ['markdown', 'input_control_vis', 'metrics', 'vega', 'timelion'];
export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOutput> {
private handler?: ExpressionLoader;
private timefilter: TimefilterContract;
@ -96,6 +95,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
private autoRefreshFetchSubscription: Subscription;
private abortController?: AbortController;
private readonly deps: VisualizeEmbeddableFactoryDeps;
private readonly inspectorAdapters?: Adapters;
constructor(
timefilter: TimefilterContract,
@ -131,13 +131,20 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
this.handleChanges();
})
);
const inspectorAdapters = this.vis.type.inspectorAdapters;
if (inspectorAdapters) {
this.inspectorAdapters =
typeof inspectorAdapters === 'function' ? inspectorAdapters() : inspectorAdapters;
}
}
public getVisualizationDescription() {
return this.vis.description;
}
public getInspectorAdapters = () => {
if (!this.handler || visTypesWithoutInspector.includes(this.vis.type.name)) {
if (!this.handler || (this.inspectorAdapters && !Object.keys(this.inspectorAdapters).length)) {
return undefined;
}
return this.handler.inspect();
@ -349,6 +356,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
filters: this.input.filters,
},
uiState: this.vis.uiState,
inspectorAdapters: this.inspectorAdapters,
};
if (this.abortController) {
this.abortController.abort();

View file

@ -20,6 +20,7 @@
import _ from 'lodash';
import { VisualizationControllerConstructor } from '../types';
import { TriggerContextMapping } from '../../../ui_actions/public';
import { Adapters } from '../../../inspector/public';
export interface BaseVisTypeOptions {
name: string;
@ -40,6 +41,7 @@ export interface BaseVisTypeOptions {
hierarchicalData?: boolean | unknown;
setup?: unknown;
useCustomNoDataScreen?: boolean;
inspectorAdapters?: Adapters | (() => Adapters);
}
export class BaseVisType {
@ -63,6 +65,7 @@ export class BaseVisType {
hierarchicalData: boolean | unknown;
setup?: unknown;
useCustomNoDataScreen: boolean;
inspectorAdapters?: Adapters | (() => Adapters);
constructor(opts: BaseVisTypeOptions) {
if (!opts.icon && !opts.image) {
@ -98,6 +101,7 @@ export class BaseVisType {
this.requiresSearch = this.requestHandler !== 'none';
this.hierarchicalData = opts.hierarchicalData || false;
this.useCustomNoDataScreen = opts.useCustomNoDataScreen || false;
this.inspectorAdapters = opts.inspectorAdapters;
}
public get schemas() {

View file

@ -22,7 +22,6 @@ import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['timePicker', 'visualize', 'visChart', 'vegaChart']);
const filterBar = getService('filterBar');
const inspector = getService('inspector');
const log = getService('log');
describe('vega chart in visualize app', () => {
@ -35,10 +34,6 @@ export default function ({ getService, getPageObjects }) {
describe('vega chart', () => {
describe('initial render', () => {
it('should not have inspector enabled', async function () {
await inspector.expectIsNotEnabled();
});
it.skip('should have some initial vega spec text', async function () {
const vegaSpec = await PageObjects.vegaChart.getSpec();
expect(vegaSpec).to.contain('{').and.to.contain('data');

View file

@ -80,7 +80,7 @@ export async function fetchSearchSourceAndRecordWithInspector({
inspectorRequest.json(body);
});
resp = await searchSource.fetch({ abortSignal });
inspectorRequest.stats(getResponseInspectorStats(searchSource, resp)).ok({ json: resp });
inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp });
} catch (error) {
inspectorRequest.error({ error });
throw error;