diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index d3acd33d73d01..5c8483cf21369 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -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()); } diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 4ac6c823d2e3b..8e11c475e10bf 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -175,7 +175,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) { diff --git a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts b/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts index 96d0aaa16f6ba..c933e8cd3e961 100644 --- a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts +++ b/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts @@ -61,10 +61,11 @@ export function getRequestInspectorStats(searchSource: ISearchSource) { /** @public */ export function getResponseInspectorStats( - searchSource: ISearchSource, - resp: SearchResponse + resp: SearchResponse, + 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) { diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 9b8b32b51cfd8..c791bdd850151 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -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); diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index e03a6b938bc4f..38efd5a83bce0 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -307,7 +307,7 @@ export class SearchEmbeddable extends Embeddable 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(() => { diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index f42ee18965309..3533500a2fbc5 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -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 = Record, Input = unknown, Output = unknown, - InspectorAdapters = ExtraContext['inspectorAdapters'] extends object + InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object ? ExtraContext['inspectorAdapters'] : DefaultInspectorAdapters > { diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 51538394cd125..7c26e586fb790 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -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 { +export interface ExecutionContext { /** * Get initial input with which execution started. */ @@ -75,7 +75,7 @@ export interface ExecutionContext { /** * Adapters used to open the inspector. */ - adapters: Adapters; + adapters: TAdapters; /** * The title that the inspector is currently using e.g. a visualization name. */ diff --git a/src/plugins/inspector/public/ui/inspector_panel.test.tsx b/src/plugins/inspector/public/ui/inspector_panel.test.tsx index c482b6fa8033b..23f698c23793b 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.test.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.test.tsx @@ -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; diff --git a/src/plugins/inspector/public/ui/inspector_panel.tsx b/src/plugins/inspector/public/ui/inspector_panel.tsx index 85705b6b74f55..2c91e937d0ee1 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.tsx @@ -21,7 +21,8 @@ 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) { diff --git a/src/plugins/inspector/public/view_registry.test.ts b/src/plugins/inspector/public/view_registry.test.ts index 542328d4f48da..13e109f50243c 100644 --- a/src/plugins/inspector/public/view_registry.test.ts +++ b/src/plugins/inspector/public/view_registry.test.ts @@ -20,7 +20,7 @@ import { InspectorViewRegistry } from './view_registry'; import { InspectorViewDescription } from './types'; -import { Adapters } from './types'; +import { Adapters } from '../common'; function createMockView( params: { diff --git a/src/plugins/inspector/public/view_registry.ts b/src/plugins/inspector/public/view_registry.ts index 800d917af28ca..be84a62a11712 100644 --- a/src/plugins/inspector/public/view_registry.ts +++ b/src/plugins/inspector/public/view_registry.ts @@ -18,7 +18,8 @@ */ import { EventEmitter } from 'events'; -import { Adapters, InspectorViewDescription } from './types'; +import { InspectorViewDescription } from './types'; +import { Adapters } from '../common'; /** * @callback viewShouldShowFunc diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx index e03c165d96a27..1a2b6f9922d2d 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.tsx @@ -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, diff --git a/src/plugins/inspector/public/views/data/index.tsx b/src/plugins/inspector/public/views/data/index.tsx index 0cd88442bf8f8..b02e02bbe6b6b 100644 --- a/src/plugins/inspector/public/views/data/index.tsx +++ b/src/plugins/inspector/public/views/data/index.tsx @@ -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 = ( diff --git a/src/plugins/inspector/public/views/requests/index.ts b/src/plugins/inspector/public/views/requests/index.ts index 741da76872710..00a223e1e30fa 100644 --- a/src/plugins/inspector/public/views/requests/index.ts +++ b/src/plugins/inspector/public/views/requests/index.ts @@ -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', { diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index f1f82e7f5b7ad..e2f2a0660d10c 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"] + "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions", "inspector"] } diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index c2eecf13c2d51..98e51a7e64a9a 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -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,54 @@ 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 = {}; + + if (this.inspectorAdapters) { + this.inspectorAdapters.requests.reset(); + } return combineLatest( searchRequests.map((request, index) => { + const requestId = index.toString(); 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( map((data) => ({ - id: index, + id: requestId, rawResponse: data.rawResponse, })) ); }) - ); + ).pipe(tap((data) => this.inspectSearchResults(data, requestResponders))); + } + + private inspectSearchResults( + requests: IEsSearchResponse[], + requestResponders: Record + ) { + requests.forEach((request) => { + if (request.id && requestResponders[request.id]) { + requestResponders[request.id] + .stats(dataPluginSearch.getResponseInspectorStats(request.rawResponse)) + .ok({ json: request.rawResponse }); + } + }); } } diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.js index 377567e47ced8..a911702ab1ddd 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.js @@ -48,10 +48,11 @@ export class VegaParser { this.hideWarnings = false; 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), }; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index c20a104736291..9d93a3f678a2c 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -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, Start } from '../../inspector/public'; + import { setNotifications, setData, @@ -37,11 +39,14 @@ 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; + data: DataPublicPluginSetup; + inspector: Setup; }; serviceSettings: IServiceSettings; } @@ -50,13 +55,15 @@ export interface VegaVisualizationDependencies { export interface VegaPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; - data: ReturnType; + inspector: Setup; + data: DataPublicPluginSetup; mapsLegacy: any; } /** @internal */ export interface VegaPluginStartDependencies { - data: ReturnType; + data: DataPublicPluginStart; + inspector: Start; } /** @internal */ @@ -69,7 +76,7 @@ export class VegaPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies + { inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { setInjectedVars({ enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, @@ -84,16 +91,19 @@ export class VegaPlugin implements Plugin, void> { core, plugins: { data, + inspector, }, serviceSettings: mapsLegacy.serviceSettings, }; + inspector.registerView(getVegaInspectorView()); + expressions.registerFunction(() => createVegaFn(visualizationDependencies)); visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); } - public start(core: CoreStart, { data }: VegaPluginStartDependencies) { + public start(core: CoreStart, { data, inspector }: VegaPluginStartDependencies) { setNotifications(core.notifications); setSavedObjects(core.savedObjects); setData(data); diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index 6b1af6044a2c4..237bc374dca34 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -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 { createInspectorAdapters, VegaInspectorAdapters } from './vega_inspector/index'; import { TimeRange, Query } from '../../data/public'; type Input = KibanaContext | null; @@ -41,7 +47,13 @@ interface RenderValue { export const createVegaFn = ( dependencies: VegaVisualizationDependencies -): ExpressionFunctionDefinition<'vega', Input, Arguments, Output> => ({ +): ExpressionFunctionDefinition< + 'vega', + Input, + Arguments, + Output, + ExecutionContext +> => ({ name: 'vega', type: 'render', inputTypes: ['kibana_context', 'null'], @@ -56,7 +68,10 @@ export const createVegaFn = ( }, }, async fn(input, args, context) { - const vegaRequestHandler = createVegaRequestHandler(dependencies, context.abortSignal); + // Temporary solution. we should find a more legal way to set custom adapters; + context.inspectorAdapters = createInspectorAdapters(); + + const vegaRequestHandler = createVegaRequestHandler(dependencies, context); const response = await vegaRequestHandler({ timeRange: get(input, 'timeRange') as TimeRange, diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx new file mode 100644 index 0000000000000..10aeca5c8ba81 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx @@ -0,0 +1,79 @@ +/* + * 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, useMemo, useCallback } from 'react'; + +import { EuiComboBox, EuiSpacer, EuiFormRow, EuiComboBoxProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { VegaAdapter, InspectDataSets } from '../vega_adapter'; +import { InspectorDataGrid } from './inspector_data_grid'; + +interface DataViewerProps { + vegaAdapter: VegaAdapter; +} + +export const DataViewer = ({ vegaAdapter }: DataViewerProps) => { + const inspectDataSets = useMemo(() => vegaAdapter.getInspectDataSets(), [ + vegaAdapter, + ]); + const [selectedView, setSelectedView] = useState(inspectDataSets[0]); + + const onViewChange: EuiComboBoxProps['onChange'] = useCallback( + (selectedOptions) => { + const newView = inspectDataSets.find((view) => view.id === selectedOptions[0].label); + + if (newView) { + setSelectedView(newView); + } + }, + [inspectDataSets] + ); + + return ( + <> + + + ({ + label: item.id, + }))} + onChange={onViewChange} + isClearable={false} + fullWidth={true} + singleSelection={{ asPlainText: true }} + selectedOptions={[{ label: selectedView.id }]} + /> + + + + + + + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts b/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts new file mode 100644 index 0000000000000..a60ef218d79da --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts @@ -0,0 +1,21 @@ +/* + * 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'; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx new file mode 100644 index 0000000000000..aa9fbbc3cf42d --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx @@ -0,0 +1,118 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { VegaRuntimeData } from '../vega_adapter'; + +const inspectorDataGridAriaLabel = i18n.translate('visTypeVega.inspector.dataGrid.areaLabel', { + defaultMessage: 'Data grid', +}); + +export const InspectorDataGrid = ({ columns, data }: VegaRuntimeData) => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + + 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([]); + + useEffect(() => { + setVisibleColumns(columns.map((column) => column.id)); + }, [columns]); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + + 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; + }) 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]); + + return ( + + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx new file mode 100644 index 0000000000000..c50dea8a6a54c --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx @@ -0,0 +1,48 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiFormRow, 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; +} + +export const SignalViewer = ({ vegaAdapter }: SignalViewerProps) => { + const inspectSignalsSets = useMemo( + () => vegaAdapter.getInspectSignalsSets(), + [vegaAdapter] + ); + + return ( + <> + + + + + + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/index.ts b/src/plugins/vis_type_vega/public/vega_inspector/index.ts new file mode 100644 index 0000000000000..24da27d2d742d --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/index.ts @@ -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'; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts b/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts new file mode 100644 index 0000000000000..1f16ed898a8ff --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts @@ -0,0 +1,102 @@ +/* + * 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 { View, Runtime } from 'vega'; +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; + +export interface VegaRuntimeData { + columns: Array<{ + id: string; + }>; + data: Array>; +} + +export type InspectDataSets = Assign; +export type InspectSignalsSets = VegaRuntimeData; + +/** Get Runtime Scope for Vega View + * @link https://vega.github.io/vega/docs/api/debugging/#scope + **/ +const getVegaRuntimeScope = (view: View) => (view as any)._runtime as Runtime; + +const serializeColumns = (item: Record, columns: string[]) => { + const nonSerializableFieldLabel = '(..)'; + + return columns.reduce((row: Record, column) => { + try { + row[column] = JSON.stringify(item[column]); + } catch (e) { + row[column] = nonSerializableFieldLabel; + } + return row; + }, {}); +}; + +const mapColumns = (columns: string[]) => columns.map((column) => ({ id: column, schema: 'json' })); + +export class VegaAdapter { + private vegaView?: View; + + bindVegaView(vegaView: View) { + this.vegaView = vegaView; + } + + getInspectDataSets(): InspectDataSets[] { + const runtimeScope = getVegaRuntimeScope(this.vegaView!); + + 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: mapColumns(columns), + data: value.map((item: Record) => serializeColumns(item, columns)), + }); + } + return acc; + }, []); + } + + getInspectSignalsSets(): InspectSignalsSets { + const runtimeScope = getVegaRuntimeScope(this.vegaView!); + const columns = [ + i18n.translate('visTypeVega.inspector.vegaAdapter.signal', { + defaultMessage: 'Signal', + }), + i18n.translate('visTypeVega.inspector.vegaAdapter.value', { + defaultMessage: 'Value', + }), + ]; + + return { + columns: mapColumns(columns), + data: Object.keys(runtimeScope.signals).map((key: string) => { + return serializeColumns( + { + [columns[0]]: key, + [columns[1]]: runtimeScope.signals[key].value, + }, + columns + ); + }), + }; + } +} diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx new file mode 100644 index 0000000000000..a56c43ee918a7 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx @@ -0,0 +1,48 @@ +/* + * 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 { EuiTabbedContent } from '@elastic/eui'; + +import { VegaInspectorAdapters } from './vega_inspector'; +import { DataViewer, SignalViewer } from './components'; +import { InspectorViewProps } from '../../../inspector/public'; + +interface VegaDataInspectorProps extends InspectorViewProps { + adapters: VegaInspectorAdapters; +} + +export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { + const tabs = [ + { + id: 'data-viewer--id', + name: 'Inspect Data Sets', + content: , + }, + { + id: 'signal-viewer--id', + name: 'Inspect Signal Values', + content: , + }, + ]; + + return ( + + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.ts b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.ts new file mode 100644 index 0000000000000..f622a16f4f12a --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.ts @@ -0,0 +1,41 @@ +/* + * 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 { VegaAdapter } from './vega_adapter'; +import { VegaDataInspector } from './vega_data_inspector'; +import { Adapters, RequestAdapter, InspectorViewDescription } from '../../../inspector/public'; + +export interface VegaInspectorAdapters extends Adapters { + requests: RequestAdapter; + vega: VegaAdapter; +} + +export const getVegaInspectorView = () => + ({ + title: 'Vega Debug', + help: `And additional help text, that will be shown in the inspector help.`, + shouldShow(adapters) { + return Boolean(adapters.vega); + }, + component: VegaDataInspector, + } as InspectorViewDescription); + +export const createInspectorAdapters = (): VegaInspectorAdapters => ({ + requests: new RequestAdapter(), + vega: new VegaAdapter(), +}); diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index ac28f0b3782b2..cde73cad1d167 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -21,12 +21,13 @@ import { Filter, esQuery, TimeRange, Query } from '../../data/public'; import { SearchAPI } from './data_model/search_api'; -// @ts-ignore +// @ts-expect-error 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; @@ -35,9 +36,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; @@ -56,7 +62,8 @@ export function createVegaRequestHandler( search: getData().search, injectedMetadata: getInjectedMetadata(), }, - abortSignal + context.abortSignal, + context.inspectorAdapters ); } @@ -64,7 +71,7 @@ export function createVegaRequestHandler( const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); - // @ts-ignore + // @ts-expect-error const { VegaParser } = await import('./data_model/vega_parser'); const vp = new VegaParser(visParams.spec, searchAPI, timeCache, filtersDsl, serviceSettings); diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 55ad134c05301..ddaa2786fbd85 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -23,9 +23,9 @@ import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; import { createVegaRequestHandler } from './vega_request_handler'; -// @ts-ignore +// @ts-expect-error import { createVegaVisualization } from './vega_visualization'; -// @ts-ignore +// @ts-expect-error import defaultSpec from '!!raw-loader!./default.spec.hjson'; export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => { diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 069664a9c91c1..6d74f76f52dcd 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -364,6 +364,8 @@ 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.bindVegaView(view); + if (window) { if (window.VEGA_DEBUG === undefined && console) { console.log('%cWelcome to Kibana Vega Plugin!', 'font-size: 16px; font-weight: bold;'); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 2f9cda32fccdc..da31d585278d3 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -78,7 +78,7 @@ export interface VisualizeOutput extends EmbeddableOutput { type ExpressionLoader = InstanceType; -const visTypesWithoutInspector = ['markdown', 'input_control_vis', 'metrics', 'vega', 'timelion']; +const visTypesWithoutInspector = ['markdown', 'input_control_vis', 'metrics', 'timelion']; export class VisualizeEmbeddable extends Embeddable { private handler?: ExpressionLoader; diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.js index 4442e1f969b4b..c530c6f823133 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.js @@ -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'); diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 1684acfb0f463..940afa89846a7 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -77,7 +77,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;