diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.js b/src/core_plugins/kibana/public/visualize/editor/editor.js index c0e0ac1624d0e..fff62db1c2e13 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/core_plugins/kibana/public/visualize/editor/editor.js @@ -212,13 +212,13 @@ function VisEditor( description: 'Open Inspector for visualization', testId: 'openInspectorButton', disableButton() { - return !vis.hasInspector(); + return !vis.hasInspector || !vis.hasInspector(); }, run() { vis.openInspector().bindToAngularScope($scope); }, tooltip() { - if (!vis.hasInspector()) { + if (!vis.hasInspector || !vis.hasInspector()) { return 'This visualization doesn\'t support any inspectors.'; } } diff --git a/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.js b/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.js index fbe4beda7e644..04d731dbd77b0 100644 --- a/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.js +++ b/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.js @@ -46,7 +46,10 @@ export class VisualizeEmbeddable extends Embeddable { }; getInspectorAdapters() { - return this.savedVisualization.vis.API.inspectorAdapters; + if (!this.handler) { + return undefined; + } + return this.handler.inspectorAdapters; } getEmbeddableState() { diff --git a/src/ui/public/inspector/build_tabular_inspector_data.js b/src/ui/public/inspector/build_tabular_inspector_data.js new file mode 100644 index 0000000000000..83d9acd72f82d --- /dev/null +++ b/src/ui/public/inspector/build_tabular_inspector_data.js @@ -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 { FormattedData } from './adapters/data'; + +/** + * This function builds tabular data from the response and attaches it to the + * inspector. It will only be called when the data view in the inspector is opened. + */ +export async function buildTabularInspectorData(table, queryFilter) { + const columns = table.columns.map((col, index) => { + const field = col.aggConfig.getField(); + const isCellContentFilterable = + col.aggConfig.isFilterable() + && (!field || field.filterable); + return ({ + name: col.name, + field: `col${index}`, + filter: isCellContentFilterable && ((value) => { + const filter = col.aggConfig.createFilter(value.raw); + queryFilter.addFilters(filter); + }), + filterOut: isCellContentFilterable && ((value) => { + const filter = col.aggConfig.createFilter(value.raw); + filter.meta = filter.meta || {}; + filter.meta.negate = true; + queryFilter.addFilters(filter); + }), + }); + }); + const rows = table.rows.map(row => { + return table.columns.reduce((prev, cur, index) => { + const value = row[cur.id]; + const fieldFormatter = cur.aggConfig.fieldFormatter('text'); + prev[`col${index}`] = new FormattedData(value, fieldFormatter(value)); + return prev; + }, {}); + }); + + return { columns, rows }; +} diff --git a/src/ui/public/vis/__tests__/_vis.js b/src/ui/public/vis/__tests__/_vis.js index a5765b8b8fe94..f40e88ec99a7b 100644 --- a/src/ui/public/vis/__tests__/_vis.js +++ b/src/ui/public/vis/__tests__/_vis.js @@ -24,8 +24,6 @@ import expect from 'expect.js'; import { VisProvider } from '..'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { VisTypesRegistryProvider } from '../../registry/vis_types'; -import { DataAdapter, RequestAdapter } from '../../inspector/adapters'; -import { Inspector } from '../../inspector/inspector'; describe('Vis Class', function () { let indexPattern; @@ -118,145 +116,6 @@ describe('Vis Class', function () { }); }); - describe('inspector', () => { - - describe('hasInspector()', () => { - it('should forward to inspectors hasInspector', () => { - const vis = new Vis(indexPattern, state({ - inspectorAdapters: { - data: true, - requests: true, - } - })); - sinon.spy(Inspector, 'isAvailable'); - vis.hasInspector(); - expect(Inspector.isAvailable.calledOnce).to.be(true); - const adapters = Inspector.isAvailable.lastCall.args[0]; - expect(adapters.data).to.be.a(DataAdapter); - expect(adapters.requests).to.be.a(RequestAdapter); - }); - - it('should return hasInspectors result', () => { - const vis = new Vis(indexPattern, state({})); - const stub = sinon.stub(Inspector, 'isAvailable'); - stub.returns(true); - expect(vis.hasInspector()).to.be(true); - stub.returns(false); - expect(vis.hasInspector()).to.be(false); - }); - - afterEach(() => { - Inspector.isAvailable.restore(); - }); - }); - - describe('openInspector()', () => { - - beforeEach(() => { - sinon.stub(Inspector, 'open'); - }); - - it('should call openInspector with all attached inspectors', () => { - const Foodapter = class {}; - const vis = new Vis(indexPattern, state({ - inspectorAdapters: { - data: true, - custom: { - foo: Foodapter - } - } - })); - vis.openInspector(); - expect(Inspector.open.calledOnce).to.be(true); - const adapters = Inspector.open.lastCall.args[0]; - expect(adapters).to.be(vis.API.inspectorAdapters); - }); - - it('should pass the vis title to the openInspector call', () => { - const vis = new Vis(indexPattern, { ...state(), title: 'beautifulVis' }); - vis.openInspector(); - expect(Inspector.open.calledOnce).to.be(true); - const params = Inspector.open.lastCall.args[1]; - expect(params.title).to.be('beautifulVis'); - }); - - afterEach(() => { - Inspector.open.restore(); - }); - }); - - describe('inspectorAdapters', () => { - - it('should register none for none requestHandler', () => { - const vis = new Vis(indexPattern, state({ requestHandler: 'none' })); - expect(vis.API.inspectorAdapters).to.eql({}); - }); - - it('should attach data and request handler for courier', () => { - const vis = new Vis(indexPattern, state({ requestHandler: 'courier' })); - expect(vis.API.inspectorAdapters.data).to.be.a(DataAdapter); - expect(vis.API.inspectorAdapters.requests).to.be.a(RequestAdapter); - }); - - it('should allow enabling data adapter manually', () => { - const vis = new Vis(indexPattern, state({ - requestHandler: 'none', - inspectorAdapters: { - data: true, - } - })); - expect(vis.API.inspectorAdapters.data).to.be.a(DataAdapter); - }); - - it('should allow enabling requests adapter manually', () => { - const vis = new Vis(indexPattern, state({ - requestHandler: 'none', - inspectorAdapters: { - requests: true, - } - })); - expect(vis.API.inspectorAdapters.requests).to.be.a(RequestAdapter); - }); - - it('should allow adding custom inspector adapters via the custom key', () => { - const Foodapter = class {}; - const Bardapter = class {}; - const vis = new Vis(indexPattern, state({ - requestHandler: 'none', - inspectorAdapters: { - custom: { - foo: Foodapter, - bar: Bardapter, - } - } - })); - expect(vis.API.inspectorAdapters.foo).to.be.a(Foodapter); - expect(vis.API.inspectorAdapters.bar).to.be.a(Bardapter); - }); - - it('should not share adapter instances between vis instances', () => { - const Foodapter = class {}; - const visState = state({ - inspectorAdapters: { - data: true, - custom: { - foo: Foodapter - } - } - }); - const vis1 = new Vis(indexPattern, visState); - const vis2 = new Vis(indexPattern, visState); - expect(vis1.API.inspectorAdapters.foo).to.be.a(Foodapter); - expect(vis2.API.inspectorAdapters.foo).to.be.a(Foodapter); - expect(vis1.API.inspectorAdapters.foo).not.to.be(vis2.API.inspectorAdapters.foo); - expect(vis1.API.inspectorAdapters.data).to.be.a(DataAdapter); - expect(vis2.API.inspectorAdapters.data).to.be.a(DataAdapter); - expect(vis1.API.inspectorAdapters.data).not.to.be(vis2.API.inspectorAdapters.data); - }); - }); - - }); - describe('vis addFilter method', () => { let aggConfig; let data; diff --git a/src/ui/public/vis/request_handlers/courier.js b/src/ui/public/vis/request_handlers/courier.js index 2fb4b3d599b9e..fba0c221f0188 100644 --- a/src/ui/public/vis/request_handlers/courier.js +++ b/src/ui/public/vis/request_handlers/courier.js @@ -18,62 +18,28 @@ */ import { cloneDeep, has } from 'lodash'; - +import { i18n } from '@kbn/i18n'; import { VisRequestHandlersRegistryProvider } from '../../registry/vis_request_handlers'; import { calculateObjectHash } from '../lib/calculate_object_hash'; import { getRequestInspectorStats, getResponseInspectorStats } from '../../courier/utils/courier_inspector_utils'; import { tabifyAggResponse } from '../../agg_response/tabify/tabify'; - -import { FormattedData } from '../../inspector/adapters'; +import { buildTabularInspectorData } from '../../inspector/build_tabular_inspector_data'; import { getTime } from '../../timefilter/get_time'; -import { i18n } from '@kbn/i18n'; const CourierRequestHandlerProvider = function () { - /** - * This function builds tabular data from the response and attaches it to the - * inspector. It will only be called when the data view in the inspector is opened. - */ - async function buildTabularInspectorData(vis, searchSource, aggConfigs) { - const table = tabifyAggResponse(aggConfigs, searchSource.finalResponse, { - partialRows: true, - metricsAtAllLevels: vis.isHierarchical(), - }); - const columns = table.columns.map((col, index) => { - const field = col.aggConfig.getField(); - const isCellContentFilterable = - col.aggConfig.isFilterable() - && (!field || field.filterable); - return ({ - name: col.name, - field: `col${index}`, - filter: isCellContentFilterable && ((value) => { - const filter = col.aggConfig.createFilter(value.raw); - vis.API.queryFilter.addFilters(filter); - }), - filterOut: isCellContentFilterable && ((value) => { - const filter = col.aggConfig.createFilter(value.raw); - filter.meta = filter.meta || {}; - filter.meta.negate = true; - vis.API.queryFilter.addFilters(filter); - }), - }); - }); - const rows = table.rows.map(row => { - return table.columns.reduce((prev, cur, index) => { - const value = row[cur.id]; - const fieldFormatter = cur.aggConfig.fieldFormatter('text'); - prev[`col${index}`] = new FormattedData(value, fieldFormatter(value)); - return prev; - }, {}); - }); - - return { columns, rows }; - } - return { name: 'courier', - handler: async function (vis, { searchSource, aggs, timeRange, query, filters, forceFetch, partialRows }) { + handler: async function (vis, { + searchSource, + aggs, + timeRange, + query, + filters, + forceFetch, + partialRows, + inspectorAdapters, + }) { // Create a new search source that inherits the original search source // but has the appropriate timeRange applied via a filter. @@ -123,14 +89,14 @@ const CourierRequestHandlerProvider = function () { const shouldQuery = forceFetch || (searchSource.lastQuery !== queryHash); if (shouldQuery) { - const lastAggConfig = aggs; - vis.API.inspectorAdapters.requests.reset(); - const request = vis.API.inspectorAdapters.requests.start( + inspectorAdapters.requests.reset(); + const request = inspectorAdapters.requests.start( i18n.translate('common.ui.vis.courier.inspector.dataRequest.title', { defaultMessage: 'Data' }), { description: i18n.translate('common.ui.vis.courier.inspector.dataRequest.description', { defaultMessage: 'This request queries Elasticsearch to fetch the data for the visualization.' }), - }); + } + ); request.stats(getRequestInspectorStats(requestSearchSource)); const response = await requestSearchSource.fetch(); @@ -151,18 +117,13 @@ const CourierRequestHandlerProvider = function () { aggs, agg, requestSearchSource, - vis.API.inspectorAdapters + inspectorAdapters ); } } searchSource.finalResponse = resp; - vis.API.inspectorAdapters.data.setTabularLoader( - () => buildTabularInspectorData(vis, searchSource, lastAggConfig), - { returnsFormattedValues: true } - ); - requestSearchSource.getSearchRequestBody().then(req => { request.json(req); }); @@ -185,6 +146,11 @@ const CourierRequestHandlerProvider = function () { searchSource.tabifiedResponse = tabifyAggResponse(tabifyAggs, searchSource.finalResponse, tabifyParams); } + inspectorAdapters.data.setTabularLoader( + () => buildTabularInspectorData(searchSource.tabifiedResponse, vis.API.queryFilter), + { returnsFormattedValues: true } + ); + return searchSource.tabifiedResponse; } }; diff --git a/src/ui/public/vis/request_handlers/request_handlers.d.ts b/src/ui/public/vis/request_handlers/request_handlers.d.ts index 8c193f77b0e84..e8261dd9b34f0 100644 --- a/src/ui/public/vis/request_handlers/request_handlers.d.ts +++ b/src/ui/public/vis/request_handlers/request_handlers.d.ts @@ -19,6 +19,7 @@ import { SearchSource } from '../../courier'; import { QueryFilter } from '../../filter_bar/query_filter'; +import { Adapters } from '../../inspector/types'; import { PersistedState } from '../../persisted_state'; import { Filters, Query, TimeRange } from '../../visualize'; import { AggConfigs } from '../agg_configs'; @@ -34,6 +35,7 @@ export interface RequestHandlerParams { queryFilter: QueryFilter; uiState: PersistedState; partialRows?: boolean; + inspectorAdapters?: Adapters; } export type RequestHandler = (vis: Vis, params: RequestHandlerParams) => T; diff --git a/src/ui/public/vis/vis.js b/src/ui/public/vis/vis.js index 2c8b11d99ef97..bdb6ff4d3106f 100644 --- a/src/ui/public/vis/vis.js +++ b/src/ui/public/vis/vis.js @@ -40,9 +40,6 @@ import { SearchSourceProvider } from '../courier/search_source'; import { SavedObjectsClientProvider } from '../saved_objects'; import { timefilter } from 'ui/timefilter'; -import { Inspector } from '../inspector'; -import { RequestAdapter, DataAdapter } from '../inspector/adapters'; - const getTerms = (table, columnIndex, rowIndex) => { if (rowIndex === -1) { return []; @@ -132,59 +129,10 @@ export function VisProvider(Private, indexPatterns, getAppState) { onBrushEvent(event, getAppState()); } }, - inspectorAdapters: this._getActiveInspectorAdapters(), getAppState, }; } - /** - * Open the inspector for this visualization. - * @return {InspectorSession} the handler for the session of this inspector. - */ - openInspector() { - return Inspector.open(this.API.inspectorAdapters, { - title: this.title - }); - } - - hasInspector() { - return Inspector.isAvailable(this.API.inspectorAdapters); - } - - /** - * Returns an object of all inspectors for this vis object. - * This must only be called after this.type has properly be initialized, - * since we need to read out data from the the vis type to check which - * inspectors are available. - */ - _getActiveInspectorAdapters() { - const adapters = {}; - const { inspectorAdapters: typeAdapters } = this.type; - - // Add the requests inspector adapters if the vis type explicitly requested it via - // inspectorAdapters.requests: true in its definition or if it's using the courier - // request handler, since that will automatically log its requests. - if (typeAdapters && typeAdapters.requests || this.type.requestHandler === 'courier') { - adapters.requests = new RequestAdapter(); - } - - // Add the data inspector adapter if the vis type requested it or if the - // vis is using courier, since we know that courier supports logging - // its data. - if (typeAdapters && typeAdapters.data || this.type.requestHandler === 'courier') { - adapters.data = new DataAdapter(); - } - - // Add all inspectors, that are explicitly registered with this vis type - if (typeAdapters && typeAdapters.custom) { - Object.entries(typeAdapters.custom).forEach(([key, Adapter]) => { - adapters[key] = new Adapter(); - }); - } - - return adapters; - } - setCurrentState(state) { this.title = state.title || ''; const type = state.type || this.type; diff --git a/src/ui/public/visualize/loader/__tests__/visualize_loader.js b/src/ui/public/visualize/loader/__tests__/visualize_loader.js index a13a993ed287e..3f870e94d65e9 100644 --- a/src/ui/public/visualize/loader/__tests__/visualize_loader.js +++ b/src/ui/public/visualize/loader/__tests__/visualize_loader.js @@ -34,6 +34,8 @@ import { Inspector } from '../../../inspector/inspector'; import { dispatchRenderComplete } from '../../../render_complete'; import { VisualizeDataLoader } from '../visualize_data_loader'; import { PersistedState } from '../../../persisted_state'; +import { DataAdapter } from '../../../inspector/adapters/data'; +import { RequestAdapter } from '../../../inspector/adapters/request'; describe('visualize loader', () => { @@ -79,6 +81,7 @@ describe('visualize loader', () => { const Vis = Private(VisProvider); vis = new Vis(indexPattern, { type: 'pie', + title: 'testVis', params: {}, aggs: [ { type: 'count', schema: 'metric' }, @@ -95,7 +98,7 @@ describe('visualize loader', () => { } ] }); - vis.type.requestHandler = 'none'; + vis.type.requestHandler = 'courier'; vis.type.responseHandler = 'none'; vis.type.requiresSearch = false; @@ -239,6 +242,116 @@ describe('visualize loader', () => { inspectorSession.close(); }); + describe('inspector', () => { + + describe('hasInspector()', () => { + it('should forward to inspectors hasInspector', () => { + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); + sinon.spy(Inspector, 'isAvailable'); + handler.hasInspector(); + expect(Inspector.isAvailable.calledOnce).to.be(true); + const adapters = Inspector.isAvailable.lastCall.args[0]; + expect(adapters.data).to.be.a(DataAdapter); + expect(adapters.requests).to.be.a(RequestAdapter); + }); + + it('should return hasInspectors result', () => { + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); + const stub = sinon.stub(Inspector, 'isAvailable'); + stub.returns(true); + expect(handler.hasInspector()).to.be(true); + stub.returns(false); + expect(handler.hasInspector()).to.be(false); + }); + + afterEach(() => { + Inspector.isAvailable.restore(); + }); + }); + + describe('openInspector()', () => { + + beforeEach(() => { + sinon.stub(Inspector, 'open'); + }); + + it('should call openInspector with all attached inspectors', () => { + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); + handler.openInspector(); + expect(Inspector.open.calledOnce).to.be(true); + const adapters = Inspector.open.lastCall.args[0]; + expect(adapters).to.be(handler.inspectorAdapters); + }); + + it('should pass the vis title to the openInspector call', () => { + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); + handler.openInspector(); + expect(Inspector.open.calledOnce).to.be(true); + const params = Inspector.open.lastCall.args[1]; + expect(params.title).to.be('testVis'); + }); + + afterEach(() => { + Inspector.open.restore(); + }); + }); + + describe('inspectorAdapters', () => { + + it('should register none for none requestHandler', () => { + const savedObj = createSavedObject(); + savedObj.vis.type.requestHandler = 'none'; + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj, {}); + expect(handler.inspectorAdapters).to.eql({}); + }); + + it('should attach data and request handler for courier', () => { + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); + expect(handler.inspectorAdapters.data).to.be.a(DataAdapter); + expect(handler.inspectorAdapters.requests).to.be.a(RequestAdapter); + }); + + it('should allow enabling data adapter manually', () => { + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); + expect(handler.inspectorAdapters.data).to.be.a(DataAdapter); + }); + + it('should allow enabling requests adapter manually', () => { + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); + expect(handler.inspectorAdapters.requests).to.be.a(RequestAdapter); + }); + + it('should allow adding custom inspector adapters via the custom key', () => { + const Foodapter = class {}; + const Bardapter = class {}; + const savedObj = createSavedObject(); + savedObj.vis.type.inspectorAdapters = { + custom: { foo: Foodapter, bar: Bardapter } + }; + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj, {}); + expect(handler.inspectorAdapters.foo).to.be.a(Foodapter); + expect(handler.inspectorAdapters.bar).to.be.a(Bardapter); + }); + + it('should not share adapter instances between vis instances', () => { + const Foodapter = class {}; + const savedObj1 = createSavedObject(); + const savedObj2 = createSavedObject(); + savedObj1.vis.type.inspectorAdapters = { custom: { foo: Foodapter } }; + savedObj2.vis.type.inspectorAdapters = { custom: { foo: Foodapter } }; + const handler1 = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj1, {}); + const handler2 = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj2, {}); + expect(handler1.inspectorAdapters.foo).to.be.a(Foodapter); + expect(handler2.inspectorAdapters.foo).to.be.a(Foodapter); + expect(handler1.inspectorAdapters.foo).not.to.be(handler2.inspectorAdapters.foo); + expect(handler1.inspectorAdapters.data).to.be.a(DataAdapter); + expect(handler2.inspectorAdapters.data).to.be.a(DataAdapter); + expect(handler1.inspectorAdapters.data).not.to.be(handler2.inspectorAdapters.data); + }); + }); + + }); + it('should have whenFirstRenderComplete returns a promise resolving on first renderComplete event', async () => { const container = newContainer(); const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); diff --git a/src/ui/public/visualize/loader/embedded_visualize_handler.ts b/src/ui/public/visualize/loader/embedded_visualize_handler.ts index c11df35afae30..f5310e5a61def 100644 --- a/src/ui/public/visualize/loader/embedded_visualize_handler.ts +++ b/src/ui/public/visualize/loader/embedded_visualize_handler.ts @@ -19,8 +19,7 @@ import { EventEmitter } from 'events'; import { debounce } from 'lodash'; -import { Inspector } from '../../inspector'; - +import { Adapters } from '../../inspector/types'; import { PersistedState } from '../../persisted_state'; import { IPrivate } from '../../private'; import { RenderCompleteHelper } from '../../render_complete'; @@ -30,6 +29,9 @@ import { RequestHandlerParams, Vis } from '../../vis'; import { visualizationLoader } from './visualization_loader'; import { VisualizeDataLoader } from './visualize_data_loader'; +import { Inspector } from '../../inspector'; +import { DataAdapter, RequestAdapter } from '../../inspector/adapters'; + import { VisSavedObject, VisualizeLoaderParams, VisualizeUpdateParams } from './types'; interface EmbeddedVisualizeHandlerParams extends VisualizeLoaderParams { @@ -67,6 +69,7 @@ export class EmbeddedVisualizeHandler { private readonly appState?: AppState; private uiState: PersistedState; private dataLoader: VisualizeDataLoader; + private inspectorAdapters: Adapters = {}; constructor( private readonly element: HTMLElement, @@ -110,6 +113,9 @@ export class EmbeddedVisualizeHandler { this.dataLoader = new VisualizeDataLoader(vis, Private); this.renderCompleteHelper = new RenderCompleteHelper(element); + this.inspectorAdapters = this.getActiveInspectorAdapters(); + this.vis.openInspector = this.openInspector; + this.vis.hasInspector = this.hasInspector; this.render(); } @@ -184,9 +190,15 @@ export class EmbeddedVisualizeHandler { * handler to the inspector to close and interact with it. * @return An inspector session to interact with the opened inspector. */ - public openInspector(): ReturnType { - return this.vis.openInspector(); - } + public openInspector = () => { + return Inspector.open(this.inspectorAdapters, { + title: this.vis.title, + }); + }; + + public hasInspector = () => { + return Inspector.isAvailable(this.inspectorAdapters); + }; /** * Returns a promise, that will resolve (without a value) once the first rendering of @@ -230,6 +242,40 @@ export class EmbeddedVisualizeHandler { this.fetchAndRender(); }; + /** + * Returns an object of all inspectors for this vis object. + * This must only be called after this.type has properly be initialized, + * since we need to read out data from the the vis type to check which + * inspectors are available. + */ + private getActiveInspectorAdapters = (): Adapters => { + const adapters: Adapters = {}; + const { inspectorAdapters: typeAdapters } = this.vis.type; + + // Add the requests inspector adapters if the vis type explicitly requested it via + // inspectorAdapters.requests: true in its definition or if it's using the courier + // request handler, since that will automatically log its requests. + if ((typeAdapters && typeAdapters.requests) || this.vis.type.requestHandler === 'courier') { + adapters.requests = new RequestAdapter(); + } + + // Add the data inspector adapter if the vis type requested it or if the + // vis is using courier, since we know that courier supports logging + // its data. + if ((typeAdapters && typeAdapters.data) || this.vis.type.requestHandler === 'courier') { + adapters.data = new DataAdapter(); + } + + // Add all inspectors, that are explicitly registered with this vis type + if (typeAdapters && typeAdapters.custom) { + Object.entries(typeAdapters.custom).forEach(([key, Adapter]) => { + adapters[key] = new (Adapter as any)(); + }); + } + + return adapters; + }; + /** * Fetches new data and renders the chart. This will happen debounced for a couple * of milliseconds, to bundle fast successive calls into one fetch and render, @@ -265,6 +311,7 @@ export class EmbeddedVisualizeHandler { private fetch = (forceFetch: boolean = false) => { this.dataLoaderParams.aggs = this.vis.getAggConfig(); this.dataLoaderParams.forceFetch = forceFetch; + this.dataLoaderParams.inspectorAdapters = this.inspectorAdapters; return this.dataLoader.fetch(this.dataLoaderParams); };