diff --git a/src/ui/public/render_complete/render_complete_helper.ts b/src/ui/public/render_complete/render_complete_helper.ts index ebe9c57448bdd..1230638a1d709 100644 --- a/src/ui/public/render_complete/render_complete_helper.ts +++ b/src/ui/public/render_complete/render_complete_helper.ts @@ -24,21 +24,21 @@ export class RenderCompleteHelper { this.setup(); } - public destroy() { + public destroy = () => { this.element.removeEventListener('renderStart', this.start); this.element.removeEventListener('renderComplete', this.complete); - } + }; - public setup() { + public setup = () => { this.element.setAttribute(attributeName, 'false'); this.element.addEventListener('renderStart', this.start); this.element.addEventListener('renderComplete', this.complete); - } + }; - public disable() { + public disable = () => { this.element.setAttribute(attributeName, 'disabled'); this.destroy(); - } + }; private start = () => { this.element.setAttribute(attributeName, 'false'); diff --git a/src/ui/public/visualize/__tests__/visualize.js b/src/ui/public/visualize/__tests__/visualize.js deleted file mode 100644 index cea05d50b9f17..0000000000000 --- a/src/ui/public/visualize/__tests__/visualize.js +++ /dev/null @@ -1,195 +0,0 @@ -/* - * 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 $ from 'jquery'; -import expect from 'expect.js'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import { VisProvider } from '../../vis'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import FixturesStubbedSearchSourceProvider from 'fixtures/stubbed_search_source'; -import { PersistedState } from '../../persisted_state'; - -describe('visualize directive', function () { - let $rootScope; - let $compile; - let $scope; - let $el; - let Vis; - let indexPattern; - let fixtures; - let searchSource; - let updateState; - let uiState; - - beforeEach(ngMock.module('kibana', 'kibana/table_vis')); - beforeEach(ngMock.inject(function (Private, $injector) { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - fixtures = require('fixtures/fake_hierarchical_data'); - Vis = Private(VisProvider); - uiState = new PersistedState({}); - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - searchSource = Private(FixturesStubbedSearchSourceProvider); - - init(new CreateVis(null), fixtures.oneRangeBucket); - })); - - afterEach(() => { - $scope.$destroy(); - }); - - // basically a parameterized beforeEach - function init(vis, esResponse) { - vis.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); }); - - $rootScope.vis = vis; - $rootScope.esResponse = esResponse; - $rootScope.uiState = uiState; - $rootScope.searchSource = searchSource; - $rootScope.savedObject = { - vis, - searchSource, - }; - $rootScope.updateState = updateState; - - $el = $(''); - $compile($el)($rootScope); - $rootScope.$apply(); - - $scope = $el.isolateScope(); - } - - function CreateVis(params, requestHandler = 'none') { - const vis = new Vis(indexPattern, { - type: 'table', - params: 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 = requestHandler; - vis.type.responseHandler = 'none'; - vis.type.requiresSearch = false; - return vis; - } - - it('searchSource.onResults should not be called when requiresSearch is false', function () { - searchSource.crankResults(); - $scope.$digest(); - expect(searchSource.getOnResultsCount()).to.be(0); - }); - - it('fetches new data on update event', () => { - let counter = 0; - $scope.fetch = () => { counter++; }; - $scope.vis.emit('update'); - expect(counter).to.equal(1); - }); - - it('calls the updateState on update event', () => { - let visState = {}; - updateState = (state) => { - visState = state.vis; - }; - $scope.vis.emit('update'); - expect(visState).to.not.equal({}); - }); - - describe('request handler', () => { - - const requestHandler = sinon.stub().resolves(); - - /** - * Asserts that a specific parameter had a specific value in the last call to the requestHandler. - */ - function assertParam(obj) { - sinon.assert.calledWith(requestHandler, sinon.match.any, sinon.match(obj)); - } - - /** - * Wait for the next $scope.fetch call. - * Since we use an old lodash version we cannot use fake timers here. - */ - function waitForFetch() { - return new Promise(resolve => { setTimeout(resolve, 500); }); - } - - beforeEach(() => { - init(new CreateVis(null, requestHandler), fixtures.oneRangeBucket); - }); - - afterEach(() => { - requestHandler.resetHistory(); - }); - - describe('forceFetch param', () => { - it('should be true if triggered via vis.forceReload', async () => { - $scope.vis.forceReload(); - await waitForFetch(); - sinon.assert.calledOnce(requestHandler); - assertParam({ forceFetch: true }); - }); - - it('should be true if triggered via courier:searchRefresh event', async () => { - $scope.$emit('courier:searchRefresh'); - await waitForFetch(); - sinon.assert.calledOnce(requestHandler); - assertParam({ forceFetch: true }); - }); - - it('should be false if triggered via resize event', async () => { - $el.width(400); - $el.height(500); - await waitForFetch(); - sinon.assert.calledOnce(requestHandler); - assertParam({ forceFetch: false }); - }); - - it('should be false if triggered via uiState change', async () => { - uiState.set('foo', 'bar'); - await waitForFetch(); - sinon.assert.calledOnce(requestHandler); - assertParam({ forceFetch: false }); - }); - - it('should be true if at least one trigger required it to be true', async () => { - $el.width(400); - $scope.vis.forceReload(); // This requires forceFetch to be true - uiState.set('foo', 'bar'); - await waitForFetch(); - sinon.assert.calledOnce(requestHandler); - assertParam({ forceFetch: true }); - }); - }); - }); - -}); diff --git a/src/ui/public/visualize/components/index.js b/src/ui/public/visualize/components/index.js index 710bb4a853e7b..b11aba5ee8278 100644 --- a/src/ui/public/visualize/components/index.js +++ b/src/ui/public/visualize/components/index.js @@ -17,5 +17,6 @@ * under the License. */ +export * from './visualization'; export * from './visualization_chart'; export * from './visualization_noresults'; diff --git a/src/ui/public/visualize/visualization.js b/src/ui/public/visualize/components/visualization.js similarity index 96% rename from src/ui/public/visualize/visualization.js rename to src/ui/public/visualize/components/visualization.js index d21b5315b427b..053c90eb93212 100644 --- a/src/ui/public/visualize/visualization.js +++ b/src/ui/public/visualize/components/visualization.js @@ -17,10 +17,10 @@ * under the License. */ -import './visualize.less'; +import './visualization.less'; import _ from 'lodash'; import React, { Component } from 'react'; -import { VisualizationNoResults, VisualizationChart } from './components'; +import { VisualizationNoResults, VisualizationChart } from './'; const _showNoResultsMessage = (vis, visData) => { const requiresSearch = _.get(vis, 'type.requiresSearch'); diff --git a/src/ui/public/visualize/visualize.less b/src/ui/public/visualize/components/visualization.less similarity index 99% rename from src/ui/public/visualize/visualize.less rename to src/ui/public/visualize/components/visualization.less index 0daddc7495894..fbd1ba39cd00c 100644 --- a/src/ui/public/visualize/visualize.less +++ b/src/ui/public/visualize/components/visualization.less @@ -1,6 +1,6 @@ @import (reference) "~ui/styles/variables"; -visualize { +.visualize { display: flex; flex: 1 1 100%; overflow-x: hidden; diff --git a/src/ui/public/visualize/visualization.test.js b/src/ui/public/visualize/components/visualization.test.js similarity index 100% rename from src/ui/public/visualize/visualization.test.js rename to src/ui/public/visualize/components/visualization.test.js diff --git a/src/ui/public/visualize/index.js b/src/ui/public/visualize/index.js index b3dceae8dcd09..46a8968358294 100644 --- a/src/ui/public/visualize/index.js +++ b/src/ui/public/visualize/index.js @@ -17,4 +17,4 @@ * under the License. */ -import './visualize'; +export * from './loader'; diff --git a/src/ui/public/visualize/loader/__tests__/visualize_loader.js b/src/ui/public/visualize/loader/__tests__/visualize_loader.js index 2b5c4b7043e0a..1a9dc3ed8673f 100644 --- a/src/ui/public/visualize/loader/__tests__/visualize_loader.js +++ b/src/ui/public/visualize/loader/__tests__/visualize_loader.js @@ -31,6 +31,7 @@ import { VisProvider } from '../../../vis'; import { getVisualizeLoader } from '../visualize_loader'; import { EmbeddedVisualizeHandler } from '../embedded_visualize_handler'; import { Inspector } from '../../../inspector/inspector'; +import { dispatchRenderComplete } from '../../../render_complete'; describe('visualize loader', () => { @@ -59,9 +60,9 @@ describe('visualize loader', () => { function embedWithParams(params) { const container = newContainer(); - loader.embedVisualizationWithSavedObject(container, createSavedObject(), params); + loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), params); $rootScope.$digest(); - return container.find('visualize'); + return container.find('[data-test-subj="visualizationLoader"]'); } beforeEach(ngMock.module('kibana', 'kibana/directive')); @@ -138,19 +139,19 @@ describe('visualize loader', () => { it('should render the visualize element', () => { const container = newContainer(); - loader.embedVisualizationWithSavedObject(container, createSavedObject(), { }); - expect(container.find('visualize').length).to.be(1); + loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), { }); + expect(container.find('[data-test-subj="visualizationLoader"]').length).to.be(1); }); it('should replace content of container by default', () => { const container = angular.element('
'); - loader.embedVisualizationWithSavedObject(container, createSavedObject(), {}); + loader.embedVisualizationWithSavedObject(container[0], 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(), { + loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), { append: true }); expect(container.children().length).to.be(2); @@ -190,7 +191,8 @@ describe('visualize loader', () => { it('should reject if the id was not found', () => { const resolveSpy = sinon.spy(); const rejectSpy = sinon.spy(); - return loader.embedVisualizationWithId(newContainer(), 'not-existing', {}) + const container = newContainer(); + return loader.embedVisualizationWithId(container[0], 'not-existing', {}) .then(resolveSpy, rejectSpy) .then(() => { expect(resolveSpy.called).to.be(false); @@ -200,37 +202,31 @@ describe('visualize loader', () => { 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); + await loader.embedVisualizationWithId(container[0], 'exists', {}); + expect(container.find('[data-test-subj="visualizationLoader"]').length).to.be(1); }); }); describe('EmbeddedVisualizeHandler', () => { it('should be returned from embedVisualizationWithId via a promise', async () => { - const handler = await loader.embedVisualizationWithId(newContainer(), 'exists', {}); + const handler = await loader.embedVisualizationWithId(newContainer()[0], 'exists', {}); expect(handler instanceof EmbeddedVisualizeHandler).to.be(true); }); it('should be returned from embedVisualizationWithSavedObject', async () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer(), createSavedObject(), {}); + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); expect(handler instanceof EmbeddedVisualizeHandler).to.be(true); }); it('should give access to the visualize 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(); + const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); + expect(handler.getElement()).to.be(container.find('[data-test-subj="visualizationLoader"]')[0]); }); it('should allow opening the inspector of the visualization and return its session', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer(), createSavedObject(), {}); + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); sinon.spy(Inspector, 'open'); const inspectorSession = handler.openInspector(); expect(Inspector.open.calledOnce).to.be(true); @@ -240,72 +236,61 @@ describe('visualize loader', () => { it('should have whenFirstRenderComplete returns a promise resolving on first renderComplete event', async () => { const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), {}); + const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); const spy = sinon.spy(); handler.whenFirstRenderComplete().then(spy); expect(spy.notCalled).to.be(true); - container.find('visualize').trigger('renderComplete'); + dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); 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 handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); const spy = sinon.spy(); handler.addRenderCompleteListener(spy); expect(spy.notCalled).to.be(true); - container.find('visualize').trigger('renderComplete'); + dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); 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 handler = loader.embedVisualizationWithSavedObject(container[0], 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'); + dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); + dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); + dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); 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 handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); const spy = sinon.spy(); handler.addRenderCompleteListener(spy); expect(spy.notCalled).to.be(true); - container.find('visualize').trigger('renderComplete'); + dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); expect(spy.calledOnce).to.be(true); spy.resetHistory(); handler.removeRenderCompleteListener(spy); - container.find('visualize').trigger('renderComplete'); + dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); expect(spy.notCalled).to.be(true); }); - it('should call render complete listener also for native DOM events', async () => { - const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), {}); - const spy = sinon.spy(); - handler.addRenderCompleteListener(spy); - expect(spy.notCalled).to.be(true); - const event = new CustomEvent('renderComplete', { bubbles: true }); - container.find('visualize')[0].dispatchEvent(event); - await timeout(); - expect(spy.calledOnce).to.be(true); - }); it('should allow updating and deleting data attributes', () => { const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container, createSavedObject(), { + const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), { dataAttrs: { foo: 42 } }); - expect(container.find('visualize').attr('data-foo')).to.be('42'); + expect(container.find('[data-test-subj="visualizationLoader"]').attr('data-foo')).to.be('42'); handler.update({ dataAttrs: { foo: null, @@ -314,12 +299,12 @@ describe('visualize loader', () => { }); // Sync we are relying on $evalAsync we need to trigger a digest loop during tests $rootScope.$digest(); - expect(container.find('visualize')[0].hasAttribute('data-foo')).to.be(false); - expect(container.find('visualize').attr('data-added')).to.be('value'); + expect(container.find('[data-test-subj="visualizationLoader"]')[0].hasAttribute('data-foo')).to.be(false); + expect(container.find('[data-test-subj="visualizationLoader"]').attr('data-added')).to.be('value'); }); it('should allow updating the time range of the visualization', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer(), createSavedObject(), { + const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), { timeRange: { from: 'now-7d', to: 'now' } }); handler.update({ @@ -331,7 +316,7 @@ describe('visualize loader', () => { // Unfortunately we currently don't expose the timeRange in a better way. // Once we rewrite this to a react component we should spy on the timeRange // property in the component to match the passed in value. - expect(handler._scope.timeRange).to.eql({ from: 'now-10d/d', to: 'now' }); + expect(handler._params.timeRange).to.eql({ from: 'now-10d/d', to: 'now' }); }); }); diff --git a/src/ui/public/visualize/loader/embedded_visualize_handler.js b/src/ui/public/visualize/loader/embedded_visualize_handler.js index 7d41f8b20c511..62cee558e3d91 100644 --- a/src/ui/public/visualize/loader/embedded_visualize_handler.js +++ b/src/ui/public/visualize/loader/embedded_visualize_handler.js @@ -17,7 +17,11 @@ * under the License. */ +import { debounce } from 'lodash'; import { EventEmitter } from 'events'; +import { visualizationLoader } from './visualization_loader'; +import { VisualizeDataLoader } from './visualize_data_loader'; +import { RenderCompleteHelper } from '../../render_complete'; const RENDER_COMPLETE_EVENT = 'render_complete'; @@ -26,20 +30,92 @@ const RENDER_COMPLETE_EVENT = 'render_complete'; * with the visualization. */ export class EmbeddedVisualizeHandler { - constructor(element, scope, savedObject) { + constructor(element, savedObject, params) { + const { searchSource, vis } = savedObject; + + const { + appState, + uiState, + queryFilter, + timeRange, + filters, + query, + Private, + } = params; + + const aggs = vis.getAggConfig(); + this._element = element; - this._scope = scope; - this._savedObject = savedObject; + this._params = { uiState, queryFilter, searchSource, aggs, timeRange, filters, query }; + 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._elementListener = () => { this._listeners.emit(RENDER_COMPLETE_EVENT); - }); + }; + + this._element.addEventListener('renderComplete', this._elementListener); + + this._loaded = false; + this._destroyed = false; + + this._appState = appState; + this._vis = vis; + this._vis._setUiState(uiState); + this._uiState = this._vis.getUiState(); + + this._vis.on('update', this._handleVisUpdate); + this._vis.on('reload', this._reloadVis); + this._uiState.on('change', this._fetchAndRender); + + this._visualize = new VisualizeDataLoader(this._vis, Private); + this._renderCompleteHelper = new RenderCompleteHelper(this._element); + + this._render(); } + _handleVisUpdate = () => { + const visState = this._vis.getState(); + if (this._appState) { + this._appState.vis = visState; + this._appState.save(); + } + + this._fetchAndRender(); + }; + + _reloadVis = () => { + this._fetchAndRender(true); + }; + + _fetch = (forceFetch) => { + // we need to update this before fetch + this._params.aggs = this._vis.getAggConfig(); + + return this._visualize.fetch(this._params, forceFetch); + }; + + _render = (visData) => { + return visualizationLoader(this._element, this._vis, visData, this._uiState, { listenOnChange: false }).then(() => { + if (!this._loaded) { + this._loaded = true; + this._fetchAndRender(); + } + }); + }; + + _fetchAndRender = debounce((forceFetch = false) => { + if (this._destroyed) { + return; + } + + return this._fetch(forceFetch).then(this._render); + }, 100); + /** * Update properties of the embedded visualization. This method does not allow * updating all initial parameters, but only a subset of the ones allowed @@ -47,29 +123,42 @@ export class EmbeddedVisualizeHandler { * * @param {Object} [params={}] The parameters that should be updated. * @property {Object} [timeRange] A new time range for this visualization. + * @property {Object} [filters] New filters for this visualization. + * @property {Object} [query] A new query for this visualization. * @property {Object} [dataAttrs] An object of data attributes to modify. The * key will be the name of the data attribute and the value the value that * attribute will get. Use null to remove a specific data attribute from the visualization. */ update(params = {}) { - this._scope.$evalAsync(() => { - if (params.hasOwnProperty('timeRange')) { - this._scope.timeRange = params.timeRange; - } - if (params.hasOwnProperty('filters')) { - this._scope.filters = params.filters; - } - if (params.hasOwnProperty('query')) { - this._scope.query = params.query; - } + // Apply data- attributes to the element if specified + if (params.dataAttrs) { + Object.keys(params.dataAttrs).forEach(key => { + if (params.dataAttrs[key] === null) { + this._element.removeAttribute(`data-${key}`); + return; + } - // Apply data- attributes to the element if specified - if (params.dataAttrs) { - Object.keys(params.dataAttrs).forEach(key => { - this._element.attr(`data-${key}`, params.dataAttrs[key]); - }); - } - }); + this._element.setAttribute(`data-${key}`, params.dataAttrs[key]); + }); + } + + let fetchRequired = false; + if (params.hasOwnProperty('timeRange')) { + fetchRequired = true; + this._params.timeRange = params.timeRange; + } + if (params.hasOwnProperty('filters')) { + fetchRequired = true; + this._params.filters = params.filters; + } + if (params.hasOwnProperty('query')) { + fetchRequired = true; + this._params.query = params.query; + } + + if (fetchRequired) { + this._fetchAndRender(); + } } /** @@ -77,7 +166,14 @@ export class EmbeddedVisualizeHandler { * called whenever you remove the visualization. */ destroy() { - this._scope.$destroy(); + this._destroyed = true; + this._fetchAndRender.cancel(); + this._vis.removeListener('reload', this._reloadVis); + this._vis.removeListener('update', this._handleVisUpdate); + this._element.removeEventListener('renderComplete', this._elementListener); + this._uiState.off('change', this._fetchAndRender); + visualizationLoader.destroy(this._element); + this._renderCompleteHelper.destroy(); } /** @@ -95,7 +191,7 @@ export class EmbeddedVisualizeHandler { * @return {InspectorSession} An inspector session to interact with the opened inspector. */ openInspector() { - return this._savedObject.vis.openInspector(); + return this._vis.openInspector(); } /** diff --git a/src/ui/public/visualize/loader/loader_template.html b/src/ui/public/visualize/loader/loader_template.html deleted file mode 100644 index fc7af28579252..0000000000000 --- a/src/ui/public/visualize/loader/loader_template.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/src/ui/public/visualize/loader/visualization_loader.js b/src/ui/public/visualize/loader/visualization_loader.js index 47c5041338d58..faf99e9df4eeb 100644 --- a/src/ui/public/visualize/loader/visualization_loader.js +++ b/src/ui/public/visualize/loader/visualization_loader.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Visualization } from 'ui/visualize/visualization'; +import { Visualization } from '../components/visualization'; export const visualizationLoader = (element, vis, visData, uiState, params) => { diff --git a/src/ui/public/visualize/loader/visualize_data_loader.js b/src/ui/public/visualize/loader/visualize_data_loader.js new file mode 100644 index 0000000000000..c93351ff9bd82 --- /dev/null +++ b/src/ui/public/visualize/loader/visualize_data_loader.js @@ -0,0 +1,85 @@ +/* + * 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 { isEqual } from 'lodash'; +import { VisRequestHandlersRegistryProvider } from '../../registry/vis_request_handlers'; +import { VisResponseHandlersRegistryProvider } from '../../registry/vis_response_handlers'; + +import { + isTermSizeZeroError, +} from '../../elasticsearch_errors'; + +import { toastNotifications } from 'ui/notify'; + +function getHandler(from, name) { + if (typeof name === 'function') return name; + return from.find(handler => handler.name === name).handler; +} + +export class VisualizeDataLoader { + constructor(vis, Private) { + this._vis = vis; + + const { requestHandler, responseHandler } = this._vis.type; + + const requestHandlers = Private(VisRequestHandlersRegistryProvider); + const responseHandlers = Private(VisResponseHandlersRegistryProvider); + this._requestHandler = getHandler(requestHandlers, requestHandler); + this._responseHandler = getHandler(responseHandlers, responseHandler); + } + + fetch = async (props, forceFetch = false) => { + + this._vis.filters = { timeRange: props.timeRange }; + + const handlerParams = { ...props, forceFetch }; + + try { + // searchSource is only there for courier request handler + const requestHandlerResponse = await this._requestHandler(this._vis, handlerParams); + + //No need to call the response handler when there have been no data nor has been there changes + //in the vis-state (response handler does not depend on uiStat + const canSkipResponseHandler = ( + this._previousRequestHandlerResponse && this._previousRequestHandlerResponse === requestHandlerResponse && + this._previousVisState && isEqual(this._previousVisState, this._vis.getState()) + ); + + this._previousVisState = this._vis.getState(); + this._previousRequestHandlerResponse = requestHandlerResponse; + + if (!canSkipResponseHandler) { + this._visData = await Promise.resolve(this._responseHandler(this._vis, requestHandlerResponse)); + } + return this._visData; + } + catch (e) { + this.props.searchSource.cancelQueued(); + this._vis.requestError = e; + if (isTermSizeZeroError(e)) { + return toastNotifications.addDanger( + `Your visualization ('${props.vis.title}') has an error: it has a term ` + + `aggregation with a size of 0. Please set it to a number greater than 0 to resolve ` + + `the error.` + ); + } + toastNotifications.addDanger(e); + } + } + +} diff --git a/src/ui/public/visualize/loader/visualize_loader.js b/src/ui/public/visualize/loader/visualize_loader.js index 12ae31dd0eafd..8cd8180461012 100644 --- a/src/ui/public/visualize/loader/visualize_loader.js +++ b/src/ui/public/visualize/loader/visualize_loader.js @@ -22,11 +22,10 @@ * the docs (docs/development/visualize/development-create-visualization.asciidoc) * are up to date. */ -import angular from 'angular'; import chrome from '../../chrome'; import '..'; -import visTemplate from './loader_template.html'; import { EmbeddedVisualizeHandler } from './embedded_visualize_handler'; +import { FilterBarQueryFilterProvider } from '../../filter_bar/query_filter'; /** * The parameters accepted by the embedVisualize calls. @@ -52,46 +51,44 @@ import { EmbeddedVisualizeHandler } from './embedded_visualize_handler'; * @property {object} query The query that should apply to that visualization. */ -const VisualizeLoaderProvider = ($compile, $rootScope, savedVisualizations) => { - const renderVis = (el, savedObj, params) => { - const scope = $rootScope.$new(); - params = params || {}; - scope.savedObj = savedObj; - scope.uiState = params.uiState; - scope.timeRange = params.timeRange; - scope.filters = params.filters; - scope.query = params.query; - scope.updateState = (visState) => { - if (params.appState) { - params.appState.vis = visState; - params.appState.save(); - } - }; +const VisualizeLoaderProvider = ($compile, $rootScope, savedVisualizations, Private) => { + const renderVis = (container, savedObj, params) => { - const container = angular.element(el); + const { vis, description } = savedObj; - const visHtml = $compile(visTemplate)(scope); + vis.description = description; + vis.searchSource = savedObj.searchSource; + + // lets add query filter angular service to the params + params.queryFilter = Private(FilterBarQueryFilterProvider); + + // lets add Private to the params, we'll need to pass it to visualize later + params.Private = Private; + + if (!params.append) { + container.innerHTML = ''; + } + + const element = document.createElement('div'); + element.className = 'visualize'; + element.setAttribute('data-test-subj', 'visualizationLoader'); + container.appendChild(element); // If params specified cssClass, we will set this to the element. if (params.cssClass) { - visHtml.addClass(params.cssClass); + params.cssClass.split(' ').forEach(cssClass => { + element.classList.add(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]); + element.setAttribute(`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, savedObj); + return new EmbeddedVisualizeHandler(element, savedObj, params); }; return { diff --git a/src/ui/public/visualize/visualize.js b/src/ui/public/visualize/visualize.js deleted file mode 100644 index 0ccd0de135dff..0000000000000 --- a/src/ui/public/visualize/visualize.js +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { uiModules } from '../modules'; -import { VisRequestHandlersRegistryProvider } from '../registry/vis_request_handlers'; -import { VisResponseHandlersRegistryProvider } from '../registry/vis_response_handlers'; -import 'angular-sanitize'; -import './visualization'; -import { FilterBarQueryFilterProvider } from '../filter_bar/query_filter'; -import { visualizationLoader } from './loader/visualization_loader'; - -import { - isTermSizeZeroError, -} from '../elasticsearch_errors'; - -uiModules - .get('kibana/directive', ['ngSanitize']) - .directive('visualize', function ($timeout, Notifier, Private, Promise) { - const notify = new Notifier({ location: 'Visualize' }); - const requestHandlers = Private(VisRequestHandlersRegistryProvider); - const responseHandlers = Private(VisResponseHandlersRegistryProvider); - const queryFilter = Private(FilterBarQueryFilterProvider); - - function getHandler(from, name) { - if (typeof name === 'function') return name; - return from.find(handler => handler.name === name).handler; - } - - return { - restrict: 'E', - scope: { - savedObj: '=?', - uiState: '=?', - timeRange: '=?', - filters: '=?', - query: '=?', - updateState: '=?', - }, - link: async function ($scope, $el) { - let destroyed = false; - let loaded = false; - let forceFetch = false; - if (!$scope.savedObj) throw(`saved object was not provided to directive`); - - $scope.vis = $scope.savedObj.vis; - $scope.vis.searchSource = $scope.savedObj.searchSource; - - // Set the passed in uiState to the vis object. uiState reference should never be changed - if (!$scope.uiState) $scope.uiState = $scope.vis.getUiState(); - else $scope.vis._setUiState($scope.uiState); - - $scope.vis.description = $scope.savedObj.description; - - const requestHandler = getHandler(requestHandlers, $scope.vis.type.requestHandler); - const responseHandler = getHandler(responseHandlers, $scope.vis.type.responseHandler); - - $scope.fetch = _.debounce(function () { - // If destroyed == true the scope has already been destroyed, while this method - // was still waiting for its debounce, in this case we don't want to start - // fetching new data and rendering. - if (!loaded || !$scope.savedObj || destroyed) return; - - $scope.vis.filters = { timeRange: $scope.timeRange }; - - const handlerParams = { - uiState: $scope.uiState, - queryFilter: queryFilter, - searchSource: $scope.savedObj.searchSource, - aggs: $scope.vis.getAggConfig(), - timeRange: $scope.timeRange, - filters: $scope.filters, - query: $scope.query, - forceFetch, - }; - - // Reset forceFetch flag, since we are now executing our forceFetch in case it was true - forceFetch = false; - - // searchSource is only there for courier request handler - requestHandler($scope.vis, handlerParams) - .then(requestHandlerResponse => { - - //No need to call the response handler when there have been no data nor has been there changes - //in the vis-state (response handler does not depend on uiStat - const canSkipResponseHandler = ( - $scope.previousRequestHandlerResponse && $scope.previousRequestHandlerResponse === requestHandlerResponse && - $scope.previousVisState && _.isEqual($scope.previousVisState, $scope.vis.getState()) - ); - - $scope.previousVisState = $scope.vis.getState(); - $scope.previousRequestHandlerResponse = requestHandlerResponse; - return canSkipResponseHandler ? $scope.visData : Promise.resolve(responseHandler($scope.vis, requestHandlerResponse)); - }, e => { - $scope.savedObj.searchSource.cancelQueued(); - $scope.vis.requestError = e; - if (isTermSizeZeroError(e)) { - return notify.error( - `Your visualization ('${$scope.vis.title}') has an error: it has a term ` + - `aggregation with a size of 0. Please set it to a number greater than 0 to resolve ` + - `the error.` - ); - } - notify.error(e); - }) - .then(resp => { - $scope.visData = resp; - - visualizationLoader($el[0], $scope.vis, $scope.visData, $scope.uiState, { listenOnChange: false }); - - return resp; - }); - }, 100); - - const handleVisUpdate = () => { - if ($scope.updateState) { - const visState = $scope.vis.getState(); - $scope.updateState(visState); - } - $scope.fetch(); - }; - $scope.vis.on('update', handleVisUpdate); - - - const reload = () => { - forceFetch = true; - $scope.fetch(); - }; - $scope.vis.on('reload', reload); - // auto reload will trigger this event - $scope.$on('courier:searchRefresh', reload); - - $scope.$watch('filters', $scope.fetch, true); - $scope.$watch('query', $scope.fetch, true); - $scope.$watch('timeRange', $scope.fetch, true); - - // Listen on uiState changes to start fetching new data again. - // Some visualizations might need different data depending on their uiState, - // thus we need to retrigger. The request handler should take care about - // checking if anything changed, that actually require a new fetch or return - // cached data otherwise. - $scope.uiState.on('change', $scope.fetch); - - $scope.$on('$destroy', () => { - destroyed = true; - $scope.vis.removeListener('reload', reload); - $scope.vis.removeListener('update', handleVisUpdate); - $scope.uiState.off('change', $scope.fetch); - visualizationLoader.destroy($el[0]); - }); - - visualizationLoader( - $el[0], - $scope.vis, - $scope.visData, - $scope.uiState, - { listenOnChange: false } - ).then(() => { - loaded = true; - $scope.fetch(); - }); - - } - }; - }); diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index f107250c0c49f..f74fed8644edc 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -377,7 +377,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) { } async getGaugeValue() { - const elements = await find.allByCssSelector('visualize .chart svg'); + const elements = await find.allByCssSelector('[data-test-subj="visualizationLoader"] .chart svg'); return await Promise.all(elements.map(async element => await element.getVisibleText())); }