From e5d2ff8219561af15fe2f32c3f6a07bbcf5ab763 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Sat, 2 Dec 2017 00:44:28 +0100 Subject: [PATCH] Refactor and improve Visualize Loader (#15157) * Simplify promise setup logic * Import template from own file * Use angular.element instead of jquery * Add documentation for loader methods * Add params.append * Remove params.editorMode * Clarify when returned promise resolves * Add element to handler * Allow setting CSS class via loader * Use render-counter on visualize * Use Angular run method to get access to Private service * Allow adding data-attributes to the vis element * Refactor loader to return an EmbeddedVisualizeHandler instance * Use this.destroy for previous API * Remove fallback then method, due to bugs * Reject promise from withId when id not found * Add tests * Change developer documentation * Revert "Use Angular run method to get access to Private service" This reverts commit 160e47d7709484c0478415436b3c2e8a8fc8aed3. * Rename parameter for more clarity * Add more documentation about appState * Fix broken test utils * Use chrome to get access to Angular * Move loader to its own folder * Use a method instead of getter for element * Add listeners for renderComplete events * Use typedef to document params * Fix documentation --- ...elopment-embedding-visualizations.asciidoc | 120 ++++---- .../public/stub_get_active_injector.js | 4 +- src/ui/public/visualize/loader.js | 75 ----- .../visualize/loader/__tests__/loader.js | 262 ++++++++++++++++++ .../loader/embedded_visualize_handler.js | 73 +++++ src/ui/public/visualize/loader/index.js | 1 + src/ui/public/visualize/loader/loader.js | 132 +++++++++ .../visualize/loader/loader_template.html | 8 + 8 files changed, 529 insertions(+), 146 deletions(-) delete mode 100644 src/ui/public/visualize/loader.js create mode 100644 src/ui/public/visualize/loader/__tests__/loader.js create mode 100644 src/ui/public/visualize/loader/embedded_visualize_handler.js create mode 100644 src/ui/public/visualize/loader/index.js create mode 100644 src/ui/public/visualize/loader/loader.js create mode 100644 src/ui/public/visualize/loader/loader_template.html diff --git a/docs/development/visualize/development-embedding-visualizations.asciidoc b/docs/development/visualize/development-embedding-visualizations.asciidoc index af25826805d2..421d04b8a5b6 100644 --- a/docs/development/visualize/development-embedding-visualizations.asciidoc +++ b/docs/development/visualize/development-embedding-visualizations.asciidoc @@ -1,89 +1,71 @@ [[development-embedding-visualizations]] === Embedding Visualizations -There are two different angular directives you can use to insert a visualization in your page. -To display an already saved visualization, use the `` directive. -To reuse an existing Visualization implementation for a more custom purpose, use the `` directive instead. +There are two different methods you can use to insert a visualization in your page. + +To display an already saved visualization, use the `VisualizeLoader`. +To reuse an existing visualization implementation for a more custom purpose, +use the Angular `` directive instead. ==== VisualizeLoader -The `VisualizeLoader` class i the easiest way to embed a visualization into your plugin. It exposes -two methods: -- `getVisualizationList()`: which returns promise which gets resolved with list of saved visualizations -- `embedVisualizationWithId(container, savedId, params)`: which embeds visualization by id -- `embedVisualizationWithSavedObject(container, savedObject, params)`: which embeds visualization from saved object +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. -`container` should be a dom element to which visualization should be embedded -`params` is a parameter object where the following properties can be defined: +To get an instance of the loader, do the following: -- `timeRange`: time range to pass to `` directive -- `uiState`: uiState to pass to `` directive -- `appState`: appState to pass to `` directive -- `showSpyPanel`: showSpyPanel property to pass to `` directive - - -==== `` directive -The `` directive takes care of loading data, parsing data, rendering the editor -(if the Visualization is in edit mode) and rendering the visualization. -The directive takes a savedVis object for its configuration. -It is the easiest way to add visualization to your page under the assumption that -the visualization you are trying to display is saved in kibana. -If that is not the case, take a look at using `` directive. - -The simplest way is to just pass `saved-id` to ``: - -`` - -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: - -`` - -Once 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 `` - -`` 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`. 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, `` 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 should render in editor or in view mode. - -*code example: Showing a saved visualization, without linking to querybar or filterbar.* -["source","html"] ------------ -
- -
------------ ["source","js"] ----------- -import { uiModules } from 'ui/modules'; +import { getVisualizeLoader } from 'ui/visualize/loader'; -uiModules.get('kibana') -.controller('KbnTestController', function ($scope, AppState, savedVisualizations) { - const visId = 'enter_your_vis_id'; - savedVisualizations.get(visId).then(savedVis => $scope.savedObj = savedVis); +getVisualizeLoader().then((loader) => { + // You now have access to the loader }); ----------- -When is done rendering it will emit `renderComplete` event on the element. +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]. + +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. + +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]. + +We recommend *not* to use the internal `` Angular directive directly. ==== `` directive The `` 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. `` where diff --git a/src/test_utils/public/stub_get_active_injector.js b/src/test_utils/public/stub_get_active_injector.js index 170c1ed8f020..6ab191dbe91f 100644 --- a/src/test_utils/public/stub_get_active_injector.js +++ b/src/test_utils/public/stub_get_active_injector.js @@ -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.getActiveInjector. + * but BEFORE you first execute code, that uses chrome.dangerouslyGetActiveInjector. */ export function setupInjectorStub() { beforeEach(ngMock.inject(($injector) => { @@ -30,7 +30,7 @@ export function setupInjectorStub() { */ export function teardownInjectorStub() { afterEach(() => { - chrome.getActiveInjector.restore(); + chrome.dangerouslyGetActiveInjector.restore(); }); } diff --git a/src/ui/public/visualize/loader.js b/src/ui/public/visualize/loader.js deleted file mode 100644 index b88822b7782e..000000000000 --- a/src/ui/public/visualize/loader.js +++ /dev/null @@ -1,75 +0,0 @@ -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 = $(''); - 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 }; diff --git a/src/ui/public/visualize/loader/__tests__/loader.js b/src/ui/public/visualize/loader/__tests__/loader.js new file mode 100644 index 000000000000..5664a9d59f15 --- /dev/null +++ b/src/ui/public/visualize/loader/__tests__/loader.js @@ -0,0 +1,262 @@ +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('
'); + } + + 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('
'); + 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('
'); + 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); + }); + }); + + }); +}); diff --git a/src/ui/public/visualize/loader/embedded_visualize_handler.js b/src/ui/public/visualize/loader/embedded_visualize_handler.js new file mode 100644 index 000000000000..a6d8f943448b --- /dev/null +++ b/src/ui/public/visualize/loader/embedded_visualize_handler.js @@ -0,0 +1,73 @@ +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); + } + +} diff --git a/src/ui/public/visualize/loader/index.js b/src/ui/public/visualize/loader/index.js new file mode 100644 index 000000000000..3f3b9e6635d5 --- /dev/null +++ b/src/ui/public/visualize/loader/index.js @@ -0,0 +1 @@ +export * from './loader'; diff --git a/src/ui/public/visualize/loader/loader.js b/src/ui/public/visualize/loader/loader.js new file mode 100644 index 000000000000..2eda20730c05 --- /dev/null +++ b/src/ui/public/visualize/loader/loader.js @@ -0,0 +1,132 @@ +/** + * 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.} 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 }; diff --git a/src/ui/public/visualize/loader/loader_template.html b/src/ui/public/visualize/loader/loader_template.html new file mode 100644 index 000000000000..80ca5319ac7b --- /dev/null +++ b/src/ui/public/visualize/loader/loader_template.html @@ -0,0 +1,8 @@ +