mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
parent
f88d0b92a2
commit
6c4b4d2437
15 changed files with 277 additions and 498 deletions
|
@ -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');
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -17,5 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './visualization';
|
||||
export * from './visualization_chart';
|
||||
export * from './visualization_noresults';
|
||||
|
|
|
@ -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');
|
|
@ -1,6 +1,6 @@
|
|||
@import (reference) "~ui/styles/variables";
|
||||
|
||||
visualize {
|
||||
.visualize {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
overflow-x: hidden;
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './visualize';
|
||||
export * from './loader';
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<visualize
|
||||
saved-obj="savedObj"
|
||||
update-state="updateState"
|
||||
ui-state="uiState"
|
||||
time-range="timeRange"
|
||||
filters="filters"
|
||||
query="query"
|
||||
render-complete
|
||||
></visualize>
|
|
@ -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) => {
|
||||
|
|
85
src/ui/public/visualize/loader/visualize_data_loader.js
Normal file
85
src/ui/public/visualize/loader/visualize_data_loader.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue