mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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
This commit is contained in:
parent
de109fc344
commit
e5d2ff8219
8 changed files with 529 additions and 146 deletions
|
@ -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 `<visualize>` directive.
|
||||
To reuse an existing Visualization implementation for a more custom purpose, use the `<visualization>` 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 `<visualization>` 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 `<visualize>` directive
|
||||
- `uiState`: uiState to pass to `<visualize>` directive
|
||||
- `appState`: appState to pass to `<visualize>` directive
|
||||
- `showSpyPanel`: showSpyPanel property to pass to `<visualize>` directive
|
||||
|
||||
|
||||
==== `<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.
|
||||
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 `<visualization>` directive.
|
||||
|
||||
The simplest way is to just pass `saved-id` to `<visualize>`:
|
||||
|
||||
`<visualize saved-id="'447d2930-9eb2-11e7-a956-5558df96e706'"></visualize>`
|
||||
|
||||
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:
|
||||
|
||||
`<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>`
|
||||
|
||||
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 { 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 <visualize> 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 `<visualize>` Angular directive directly.
|
||||
|
||||
==== `<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
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = $('<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 };
|
262
src/ui/public/visualize/loader/__tests__/loader.js
Normal file
262
src/ui/public/visualize/loader/__tests__/loader.js
Normal file
|
@ -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('<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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
73
src/ui/public/visualize/loader/embedded_visualize_handler.js
Normal file
73
src/ui/public/visualize/loader/embedded_visualize_handler.js
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
1
src/ui/public/visualize/loader/index.js
Normal file
1
src/ui/public/visualize/loader/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './loader';
|
132
src/ui/public/visualize/loader/loader.js
Normal file
132
src/ui/public/visualize/loader/loader.js
Normal file
|
@ -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.<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 };
|
8
src/ui/public/visualize/loader/loader_template.html
Normal file
8
src/ui/public/visualize/loader/loader_template.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<visualize
|
||||
saved-obj="savedObj"
|
||||
app-state="appState"
|
||||
ui-state="uiState"
|
||||
time-range="timeRange"
|
||||
show-spy-panel="showSpyPanel"
|
||||
render-counter
|
||||
></visualize>
|
Loading…
Add table
Add a link
Reference in a new issue