Remove Angular from visualize (#20295) (#20593)

This commit is contained in:
Peter Pisljar 2018-07-11 14:22:50 +02:00 committed by GitHub
parent f88d0b92a2
commit 6c4b4d2437
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 277 additions and 498 deletions

View file

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

View file

@ -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 = $('<visualize saved-obj="savedObject" ui-state="uiState" update-state="updateState">');
$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 });
});
});
});
});

View file

@ -17,5 +17,6 @@
* under the License.
*/
export * from './visualization';
export * from './visualization_chart';
export * from './visualization_noresults';

View file

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

View file

@ -1,6 +1,6 @@
@import (reference) "~ui/styles/variables";
visualize {
.visualize {
display: flex;
flex: 1 1 100%;
overflow-x: hidden;

View file

@ -17,4 +17,4 @@
* under the License.
*/
import './visualize';
export * from './loader';

View file

@ -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('<div><div id="prevContent"></div></div>');
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('<div><div id="prevContent"></div></div>');
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' });
});
});

View file

@ -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();
}
/**

View file

@ -1,9 +0,0 @@
<visualize
saved-obj="savedObj"
update-state="updateState"
ui-state="uiState"
time-range="timeRange"
filters="filters"
query="query"
render-complete
></visualize>

View file

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

View file

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

View file

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

View file

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

View file

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