remove inspector from Vis (#24112)

This commit is contained in:
Peter Pisljar 2018-11-06 13:49:38 +01:00 committed by GitHub
parent 830e149787
commit 23cd191d39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 253 additions and 258 deletions

View file

@ -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.';
}
}

View file

@ -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() {

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 { 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 };
}

View file

@ -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;

View file

@ -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;
}
};

View file

@ -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 = <T>(vis: Vis, params: RequestHandlerParams) => T;

View file

@ -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;

View file

@ -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(), {});

View file

@ -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<typeof Inspector.open> {
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);
};