Revert "Refactor and improve Visualize Loader (#15157) (#15359)"

This reverts commit bead35811f.

This backport was botched, so a bunch of inline diffs were committed.
This commit is contained in:
Court Ewing 2017-12-02 14:13:41 -05:00
parent bead35811f
commit fa3c2d5e83
8 changed files with 113 additions and 546 deletions

View file

@ -1,7 +1,6 @@
[[development-embedding-visualizations]]
=== Embedding Visualizations
<<<<<<< HEAD
There are two different angular directives you can use to insert a visualization in your page.
To display an already saved visualization, use the `<visualize>` directive.
To reuse an existing Visualization implementation for a more custom purpose, use the `<visualization>` directive instead.
@ -21,15 +20,8 @@ two methods:
- `uiState`: uiState to pass to `<visualize>` directive
- `appState`: appState to pass to `<visualize>` directive
- `showSpyPanel`: showSpyPanel property to pass to `<visualize>` directive
=======
There are two different methods you can use to insert a visualization in your page.
>>>>>>> e5d2ff821... Refactor and improve Visualize Loader (#15157)
To display an already saved visualization, use the `VisualizeLoader`.
To reuse an existing visualization implementation for a more custom purpose,
use the Angular `<visualization>` directive instead.
<<<<<<< HEAD
==== `<visualize>` directive
The `<visualize>` directive takes care of loading data, parsing data, rendering the editor
(if the Visualization is in edit mode) and rendering the visualization.
@ -41,82 +33,58 @@ If that is not the case, take a look at using `<visualization>` directive.
The simplest way is to just pass `saved-id` to `<visualize>`:
`<visualize saved-id="'447d2930-9eb2-11e7-a956-5558df96e706'"></visualize>`
=======
==== VisualizeLoader
>>>>>>> e5d2ff821... Refactor and improve Visualize Loader (#15157)
The `VisualizeLoader` class is the easiest way to embed a visualization into your plugin.
It will take care of loading the data and rendering the visualization.
For the above to work with time based visualizations time picker must be present (enabled) on the page. For scenarios
where timepicker is not available time range can be passed in as additional parameter:
<<<<<<< HEAD
`<visualize saved-id="'447d2930-9eb2-11e7-a956-5558df96e706'"
time-range="{ max: '2017-09-21T21:59:59.999Z', min: '2017-09-18T22:00:00.000Z' }"></visualize>`
=======
To get an instance of the loader, do the following:
>>>>>>> e5d2ff821... Refactor and improve Visualize Loader (#15157)
Once <visualize> is done rendering the element will emit `renderComplete` event.
When more control is required over the visualization you may prefer to load the saved object yourself and then pass it
to `<visualize>`
`<visualize saved-obj='savedVis' app-state='appState' ui-state='uiState' editor-mode='false'></visualize>` where
`savedVis` is an instance of savedVisualization object, which can be retrieved using `savedVisualizations` service
which is explained later in this documentation.
`appState` is an instance of `AppState`. <visualize> is expecting two keys defined on AppState:
- `filters` which is an instance of searchSource filter object and
- `query` which is an instance of searchSource query object
If `appState` is not provided, `<visualize>` will not monitor the `AppState`.
`uiState` should be an instance of `PersistedState`. if not provided visualize will generate one,
but you will have no access to it. It is used to store viewer specific information like whether the legend is toggled on or off.
`editor-mode` defines if <visualize> should render in editor or in view mode.
*code example: Showing a saved visualization, without linking to querybar or filterbar.*
["source","html"]
-----------
<div ng-controller="KbnTestController" class="test_vis">
<visualize saved-obj='savedVis'></visualize>
</div>
-----------
["source","js"]
-----------
import { getVisualizeLoader } from 'ui/visualize/loader';
import { uiModules } from 'ui/modules';
getVisualizeLoader().then((loader) => {
// You now have access to the loader
uiModules.get('kibana')
.controller('KbnTestController', function ($scope, AppState, savedVisualizations) {
const visId = 'enter_your_vis_id';
savedVisualizations.get(visId).then(savedVis => $scope.savedObj = savedVis);
});
-----------
The loader exposes the following methods:
- `getVisualizationList()`: which returns promise which gets resolved with a list of saved visualizations
- `embedVisualizationWithId(container, savedId, params)`: which embeds visualization by id
- `embedVisualizationWithSavedObject(container, savedObject, params)`: which embeds visualization from saved object
Depending on which embed method you are using, you either pass in the id of the
saved object for the visualization, or a `savedObject`, that you can retrieve via
the `savedVisualizations` Angular service by its id. The `savedObject` give you access
to the filter and query logic and allows you to attach listeners to the visualizations.
For a more complex use-case you usually want to use that method.
`container` should be a DOM element (jQuery wrapped or regular DOM element) into which the visualization should be embedded
`params` is a parameter object specifying several parameters, that influence rendering.
You will find a detailed description of all the parameters in the inline docs
in the {repo}blob/{branch}/src/ui/public/visualize/loader/loader.js[loader source code].
<<<<<<< HEAD
`uiState` should be an instance of `PersistedState`. if not provided visualize will generate one,
but you will have no access to it. It is used to store viewer specific information like whether the legend is toggled on or off.
=======
Both methods return an `EmbeddedVisualizeHandler`, that gives you some access
to the visualization. The `embedVisualizationWithSavedObject` method will return
the handler immediately from the method call, whereas the `embedVisualizationWithId`
will return a promise, that resolves with the handler, as soon as the `id` could be
found. It will reject, if the `id` is invalid.
>>>>>>> e5d2ff821... Refactor and improve Visualize Loader (#15157)
The returned `EmbeddedVisualizeHandler` itself has the following methods and properties:
- `destroy()`: destroys the underlying Angualr scope of the visualization
- `getElement()`: a reference to the jQuery wrapped DOM element, that renders the visualization
- `whenFirstRenderComplete()`: will return a promise, that resolves as soon as the visualization has
finished rendering for the first time
- `addRenderCompleteListener(listener)`: will register a listener to be called whenever
a rendering of this visualization finished (not just the first one)
- `removeRenderCompleteListener(listener)`: removes an event listener from the handler again
You can find the detailed `EmbeddedVisualizeHandler` documentation in its
{repo}blob/{branch}/src/ui/public/visualize/loader/embedded_visualize_handler.js[source code].
<<<<<<< HEAD
When <visualize> is ready it will emit `ready:vis` event on the root scope.
When <visualize> is done rendering it will emit `renderComplete` event on the element.
=======
We recommend *not* to use the internal `<visualize>` Angular directive directly.
>>>>>>> e5d2ff821... Refactor and improve Visualize Loader (#15157)
==== `<visualization>` directive
The `<visualization>` directive takes a visualization configuration and data.
It should be used, if you don't want to render a saved visualization, but specify
the config and data directly.
`<visualization vis='vis' vis-data='visData' ui-state='uiState' ></visualization>` where

View file

@ -16,7 +16,7 @@ import sinon from 'sinon';
* This method setups the stub for chrome.dangerouslyGetActiveInjector. You must call it in
* a place where beforeEach is allowed to be called (read: inside your describe)
* method. You must call this AFTER you've called `ngMock.module` to setup the modules,
* but BEFORE you first execute code, that uses chrome.dangerouslyGetActiveInjector.
* but BEFORE you first execute code, that uses chrome.getActiveInjector.
*/
export function setupInjectorStub() {
beforeEach(ngMock.inject(($injector) => {
@ -30,7 +30,7 @@ export function setupInjectorStub() {
*/
export function teardownInjectorStub() {
afterEach(() => {
chrome.dangerouslyGetActiveInjector.restore();
chrome.getActiveInjector.restore();
});
}

View file

@ -0,0 +1,75 @@
import $ from 'jquery';
import uiRoutes from 'ui/routes';
import 'ui/visualize';
const VisualizeLoaderProvider = ($compile, $rootScope, savedVisualizations) => {
const renderVis = (el, savedObj, params) => {
const scope = $rootScope.$new();
scope.savedObj = savedObj;
scope.appState = params.appState;
scope.uiState = params.uiState;
scope.timeRange = params.timeRange;
scope.showSpyPanel = params.showSpyPanel;
scope.editorMode = params.editorMode;
const container = el instanceof $ ? el : $(el);
container.html('');
const visEl = $('<visualize saved-obj="savedObj" app-state="appState" ui-state="uiState" ' +
'time-range="timeRange" editor-mode="editorMode" show-spy-panel="showSpyPanel"></visualize>');
const visHtml = $compile(visEl)(scope);
container.html(visHtml);
const handler = { destroy: scope.$destroy };
return new Promise((resolve) => {
visEl.on('renderComplete', () => {
resolve(handler);
});
});
};
return {
embedVisualizationWithId: async (el, savedVisualizationId, params) => {
return new Promise((resolve) => {
savedVisualizations.get(savedVisualizationId).then(savedObj => {
renderVis(el, savedObj, params).then(handler => {
resolve(handler);
});
});
});
},
embedVisualizationWithSavedObject: (el, savedObj, params) => {
return renderVis(el, savedObj, params);
},
getVisualizationList: () => {
return savedVisualizations.find().then(result => result.hits);
},
};
};
let visualizeLoader = null;
let pendingPromise = null;
let pendingResolve = null;
uiRoutes.addSetupWork(function (Private) {
visualizeLoader = Private(VisualizeLoaderProvider);
if (pendingResolve) {
pendingResolve(visualizeLoader);
}
});
async function getVisualizeLoader() {
if (!pendingResolve) {
pendingPromise = new Promise((resolve)=> {
pendingResolve = resolve;
if (visualizeLoader) resolve(visualizeLoader);
});
}
return pendingPromise;
}
export { getVisualizeLoader, VisualizeLoaderProvider };

View file

@ -1,262 +0,0 @@
import angular from 'angular';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import { setupAndTeardownInjectorStub } from 'test_utils/stub_get_active_injector';
import FixturesStubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { VisProvider } from 'ui/vis';
import { getVisualizeLoader } from '../loader';
import { EmbeddedVisualizeHandler } from '../embedded_visualize_handler';
describe('visualize loader', () => {
let searchSource;
let vis;
let $rootScope;
let loader;
let mockedSavedObject;
function createSavedObject() {
return {
vis: vis,
searchSource: searchSource
};
}
async function timeout(delay = 0) {
return new Promise(resolve => {
setTimeout(resolve, delay);
});
}
function newContainer() {
return angular.element('<div></div>');
}
function embedWithParams(params) {
const container = newContainer();
loader.embedVisualizationWithSavedObject(container, createSavedObject(), params);
$rootScope.$digest();
return container.find('visualize');
}
beforeEach(ngMock.module('kibana', 'kibana/directive'));
beforeEach(ngMock.inject((_$rootScope_, savedVisualizations, Private) => {
$rootScope = _$rootScope_;
searchSource = Private(FixturesStubbedSearchSourceProvider);
const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
// Create a new Vis object
const Vis = Private(VisProvider);
vis = new Vis(indexPattern, {
type: 'pie',
params: {},
aggs: [
{ type: 'count', schema: 'metric' },
{
type: 'range',
schema: 'bucket',
params: {
field: 'bytes',
ranges: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 }
]
}
}
]
});
vis.type.requestHandler = 'none';
vis.type.responseHandler = 'none';
vis.type.requiresSearch = false;
// Setup savedObject
mockedSavedObject = createSavedObject();
// Mock savedVisualizations.get to return 'mockedSavedObject' when id is 'exists'
sinon.stub(savedVisualizations, 'get', (id) =>
id === 'exists' ? Promise.resolve(mockedSavedObject) : Promise.reject()
);
}));
setupAndTeardownInjectorStub();
beforeEach(async () => {
loader = await getVisualizeLoader();
});
describe('getVisualizeLoader', () => {
it('should return a promise', () => {
expect(getVisualizeLoader().then).to.be.a('function');
});
it('should resolve to an object', async () => {
const visualizeLoader = await getVisualizeLoader();
expect(visualizeLoader).to.be.an('object');
});
});
describe('service', () => {
describe('getVisualizationList', () => {
it('should be a function', async () => {
expect(loader.getVisualizationList).to.be.a('function');
});
});
describe('embedVisualizationWithSavedObject', () => {
it('should be a function', () => {
expect(loader.embedVisualizationWithSavedObject).to.be.a('function');
});
it('should render the visualize element', () => {
const container = newContainer();
loader.embedVisualizationWithSavedObject(container, createSavedObject(), { });
expect(container.find('visualize').length).to.be(1);
});
it('should replace content of container by default', () => {
const container = angular.element('<div><div id="prevContent"></div></div>');
loader.embedVisualizationWithSavedObject(container, createSavedObject(), {});
expect(container.find('#prevContent').length).to.be(0);
});
it('should append content to container when using append parameter', () => {
const container = angular.element('<div><div id="prevContent"></div></div>');
loader.embedVisualizationWithSavedObject(container, createSavedObject(), {
append: true
});
expect(container.children().length).to.be(2);
expect(container.find('#prevContent').length).to.be(1);
});
it('should apply css classes from parameters', () => {
const vis = embedWithParams({ cssClass: 'my-css-class another-class' });
expect(vis.hasClass('my-css-class')).to.be(true);
expect(vis.hasClass('another-class')).to.be(true);
});
it('should apply data attributes from dataAttrs parameter', () => {
const vis = embedWithParams({
dataAttrs: {
'foo': '',
'with-dash': 'value',
}
});
expect(vis.attr('data-foo')).to.be('');
expect(vis.attr('data-with-dash')).to.be('value');
});
it('should hide spy panel control by default', () => {
const vis = embedWithParams({});
expect(vis.find('[data-test-subj="spyToggleButton"]').length).to.be(0);
});
});
describe('embedVisualizationWithId', () => {
it('should be a function', async () => {
expect(loader.embedVisualizationWithId).to.be.a('function');
});
it('should reject if the id was not found', () => {
const resolveSpy = sinon.spy();
const rejectSpy = sinon.spy();
return loader.embedVisualizationWithId(newContainer(), 'not-existing', {})
.then(resolveSpy, rejectSpy)
.then(() => {
expect(resolveSpy.called).to.be(false);
expect(rejectSpy.calledOnce).to.be(true);
});
});
it('should render a visualize element, if id was found', async () => {
const container = newContainer();
await loader.embedVisualizationWithId(container, 'exists', {});
expect(container.find('visualize').length).to.be(1);
});
});
describe('EmbeddedVisualizeHandler', () => {
it('should be returned from embedVisualizationWithId via a promise', async () => {
const handler = await loader.embedVisualizationWithId(newContainer(), 'exists', {});
expect(handler instanceof EmbeddedVisualizeHandler).to.be(true);
});
it('should be returned from embedVisualizationWithSavedObject', async () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer(), createSavedObject(), {});
expect(handler instanceof EmbeddedVisualizeHandler).to.be(true);
});
it('should give access to the visualzie element', () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), {});
expect(handler.getElement()[0]).to.be(container.find('visualize')[0]);
});
it('should use a jquery wrapper for handler.element', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer(), createSavedObject(), {});
// Every jquery wrapper has a .jquery property with the version number
expect(handler.getElement().jquery).to.be.ok();
});
it('should have whenFirstRenderComplete returns a promise resolving on first renderComplete event', async () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), {});
const spy = sinon.spy();
handler.whenFirstRenderComplete().then(spy);
expect(spy.notCalled).to.be(true);
container.find('visualize').trigger('renderComplete');
await timeout();
expect(spy.calledOnce).to.be(true);
});
it('should add listeners via addRenderCompleteListener that triggers on renderComplete events', async () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), {});
const spy = sinon.spy();
handler.addRenderCompleteListener(spy);
expect(spy.notCalled).to.be(true);
container.find('visualize').trigger('renderComplete');
await timeout();
expect(spy.calledOnce).to.be(true);
});
it('should call render complete listeners once per renderComplete event', async () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), {});
const spy = sinon.spy();
handler.addRenderCompleteListener(spy);
expect(spy.notCalled).to.be(true);
container.find('visualize').trigger('renderComplete');
container.find('visualize').trigger('renderComplete');
container.find('visualize').trigger('renderComplete');
expect(spy.callCount).to.be(3);
});
it('should successfully remove listeners from render complete', async () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), {});
const spy = sinon.spy();
handler.addRenderCompleteListener(spy);
expect(spy.notCalled).to.be(true);
container.find('visualize').trigger('renderComplete');
expect(spy.calledOnce).to.be(true);
spy.reset();
handler.removeRenderCompleteListener(spy);
container.find('visualize').trigger('renderComplete');
expect(spy.notCalled).to.be(true);
});
});
});
});

View file

@ -1,73 +0,0 @@
import { EventEmitter } from 'events';
const RENDER_COMPLETE_EVENT = 'render_complete';
/**
* A handler to the embedded visualization. It offers several methods to interact
* with the visualization.
*/
export class EmbeddedVisualizeHandler {
constructor(element, scope) {
this._element = element;
this._scope = scope;
this._listeners = new EventEmitter();
// Listen to the first RENDER_COMPLETE_EVENT to resolve this promise
this._firstRenderComplete = new Promise(resolve => {
this._listeners.once(RENDER_COMPLETE_EVENT, resolve);
});
this._element.on('renderComplete', () => {
this._listeners.emit(RENDER_COMPLETE_EVENT);
});
}
/**
* Destroy the underlying Angular scope of the visualization. This should be
* called whenever you remove the visualization.
*/
destroy() {
this._scope.$destroy();
}
/**
* Return the actual DOM element (wrapped in jQuery) of the rendered visualization.
* This is especially useful if you used `append: true` in the parameters where
* the visualization will be appended to the specified container.
*/
getElement() {
return this._element;
}
/**
* Returns a promise, that will resolve (without a value) once the first rendering of
* the visualization has finished. If you want to listen to concecutive rendering
* events, look into the `addRenderCompleteListener` method.
*
* @returns {Promise} Promise, that resolves as soon as the visualization is done rendering
* for the first time.
*/
whenFirstRenderComplete() {
return this._firstRenderComplete;
}
/**
* Adds a listener to be called whenever the visualization finished rendering.
* This can be called multiple times, when the visualization rerenders, e.g. due
* to new data.
*
* @param {function} listener The listener to be notified about complete renders.
*/
addRenderCompleteListener(listener) {
this._listeners.addListener(RENDER_COMPLETE_EVENT, listener);
}
/**
* Removes a previously registered render complete listener from this handler.
* This listener will no longer be called when the visualization finished rendering.
*
* @param {function} listener The listener to remove from this handler.
*/
removeRenderCompleteListener(listener) {
this._listeners.removeListener(RENDER_COMPLETE_EVENT, listener);
}
}

View file

@ -1 +0,0 @@
export * from './loader';

View file

@ -1,132 +0,0 @@
/**
* IMPORTANT: If you make changes to this API, please make sure to check that
* the docs (docs/development/visualize/development-create-visualization.asciidoc)
* are up to date.
*/
import angular from 'angular';
import chrome from 'ui/chrome';
import 'ui/visualize';
import visTemplate from './loader_template.html';
import { EmbeddedVisualizeHandler } from './embedded_visualize_handler';
/**
* The parameters accepted by the embedVisualize calls.
* @typedef {object} VisualizeLoaderParams
* @property {AppState} params.appState The appState this visualization should use.
* If you don't spyecify it, the global AppState (that is decoded in the URL)
* will be used. Usually you don't need to overwrite this, unless you don't
* want the visualization to use the global AppState.
* @property {UiState} params.uiState The current uiState of the application. If you
* don't pass a uiState, the visualization will creates it's own uiState to
* store information like whether the legend is open or closed, but you don't
* have access to it from the outside. Pass one in if you need that access.
* @property {object} params.timeRange An object with a min/max key, that must be
* either a date in ISO format, or a valid datetime Elasticsearch expression,
* e.g.: { min: 'now-7d/d', max: 'now' }
* @property {boolean} params.showSpyPanel Whether or not the spy panel should be available
* on this chart. (default: false)
* @property {boolean} params.append If set to true, the visualization will be appended
* to the passed element instead of replacing all its content. (default: false)
* @property {string} params.cssClass If specified this CSS class (or classes with space separated)
* will be set to the root visuzalize element.
* @property {object} params.dataAttrs An object of key-value pairs, that will be set
* as data-{key}="{value}" attributes on the visualization element.
*/
const VisualizeLoaderProvider = ($compile, $rootScope, savedVisualizations) => {
const renderVis = (el, savedObj, params) => {
const scope = $rootScope.$new();
params = params || {};
scope.savedObj = savedObj;
scope.appState = params.appState;
scope.uiState = params.uiState;
scope.timeRange = params.timeRange;
scope.showSpyPanel = params.showSpyPanel;
const container = angular.element(el);
const visHtml = $compile(visTemplate)(scope);
// If params specified cssClass, we will set this to the element.
if (params.cssClass) {
visHtml.addClass(params.cssClass);
}
// Apply data- attributes to the element if specified
if (params.dataAttrs) {
Object.keys(params.dataAttrs).forEach(key => {
visHtml.attr(`data-${key}`, params.dataAttrs[key]);
});
}
// If params.append was true append instead of replace content
if (params.append) {
container.append(visHtml);
} else {
container.html(visHtml);
}
return new EmbeddedVisualizeHandler(visHtml, scope);
};
return {
/**
* Renders a saved visualization specified by its id into a DOM element.
*
* @param {Element} element The DOM element to render the visualization into.
* You can alternatively pass a jQuery element instead.
* @param {String} id The id of the saved visualization. This is the id of the
* saved object that is stored in the .kibana index.
* @param {VisualizeLoaderParams} params A list of parameters that will influence rendering.
*
* @return {Promise.<EmbeddedVisualizeHandler>} A promise that resolves to the
* handler for this visualization as soon as the saved object could be found.
*/
embedVisualizationWithId: async (element, savedVisualizationId, params) => {
return new Promise((resolve, reject) => {
savedVisualizations.get(savedVisualizationId).then(savedObj => {
const handler = renderVis(element, savedObj, params);
resolve(handler);
}, reject);
});
},
/**
* Renders a saved visualization specified by its savedObject into a DOM element.
* In most of the cases you will need this method, since it allows you to specify
* filters, handlers, queries, etc. on the savedObject before rendering.
*
* @param {Element} element The DOM element to render the visualization into.
* You can alternatively pass a jQuery element instead.
* @param {Object} savedObj The savedObject as it could be retrieved by the
* `savedVisualizations` service.
* @param {VisualizeLoaderParams} params A list of paramters that will influence rendering.
*
* @return {EmbeddedVisualizeHandler} The handler to the visualization.
*/
embedVisualizationWithSavedObject: (el, savedObj, params) => {
return renderVis(el, savedObj, params);
},
/**
* Returns a promise, that resolves to a list of all saved visualizations.
*
* @return {Promise} Resolves with a list of all saved visualizations as
* returned by the `savedVisualizations` service in Kibana.
*/
getVisualizationList: () => {
return savedVisualizations.find().then(result => result.hits);
},
};
};
/**
* Returns a promise, that resolves with the visualize loader, once it's ready.
* @return {Promise} A promise, that resolves to the visualize loader.
*/
function getVisualizeLoader() {
return chrome.dangerouslyGetActiveInjector().then($injector => {
const Private = $injector.get('Private');
return Private(VisualizeLoaderProvider);
});
}
export { getVisualizeLoader, VisualizeLoaderProvider };

View file

@ -1,8 +0,0 @@
<visualize
saved-obj="savedObj"
app-state="appState"
ui-state="uiState"
time-range="timeRange"
show-spy-panel="showSpyPanel"
render-counter
></visualize>