mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Move visualize editor out of visualize directive (#20263)
This commit is contained in:
parent
59426cbfa4
commit
2d4bb6a19f
27 changed files with 186 additions and 307 deletions
|
@ -67,12 +67,18 @@
|
|||
</div>
|
||||
|
||||
<visualize
|
||||
ng-if="!chrome.getVisible()"
|
||||
saved-obj="savedVis"
|
||||
ui-state="uiState"
|
||||
time-range="timeRange"
|
||||
editor-mode="chrome.getVisible()"
|
||||
>
|
||||
/>
|
||||
|
||||
</visualize>
|
||||
<visualization-editor
|
||||
ng-if="chrome.getVisible()"
|
||||
saved-obj="savedVis"
|
||||
ui-state="uiState"
|
||||
time-range="timeRange"
|
||||
class="vis-editor-content"
|
||||
/>
|
||||
|
||||
</visualize-app>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import '../saved_visualizations/saved_visualizations';
|
||||
import './visualization_editor';
|
||||
import 'ui/vis/editors/default/sidebar';
|
||||
import 'ui/visualize';
|
||||
import 'ui/collapsible_sidebar';
|
||||
|
|
|
@ -17,36 +17,34 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './visualize.less';
|
||||
import './visualize_legend';
|
||||
import { uiModules } from '../modules';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import 'angular-sanitize';
|
||||
import { VisEditorTypesRegistryProvider } from '../registry/vis_editor_types';
|
||||
import { getUpdateStatus } from '../vis/update_status';
|
||||
import { VisEditorTypesRegistryProvider } from 'ui/registry/vis_editor_types';
|
||||
|
||||
uiModules
|
||||
.get('kibana/directive', ['ngSanitize'])
|
||||
.directive('visualizationEditor', function (Private, $timeout) {
|
||||
.directive('visualizationEditor', function (Private, $timeout, getAppState) {
|
||||
const editorTypes = Private(VisEditorTypesRegistryProvider);
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
vis: '=',
|
||||
visData: '=',
|
||||
savedObj: '=',
|
||||
uiState: '=?',
|
||||
searchSource: '='
|
||||
timeRange: '='
|
||||
},
|
||||
link: function ($scope, element) {
|
||||
// Clone the _vis instance.
|
||||
const vis = $scope.vis;
|
||||
const Editor = typeof vis.type.editor === 'function' ? vis.type.editor :
|
||||
editorTypes.find(editor => editor.key === vis.type.editor);
|
||||
const editor = new Editor(element[0], vis);
|
||||
const editorType = $scope.savedObj.vis.type.editor;
|
||||
const Editor = typeof editorType === 'function' ? editorType :
|
||||
editorTypes.find(editor => editor.key === editorType);
|
||||
const editor = new Editor(element[0], $scope.savedObj);
|
||||
|
||||
$scope.renderFunction = () => {
|
||||
if (!$scope.vis) return;
|
||||
editor.render($scope.visData, $scope.searchSource, getUpdateStatus(Editor.requiresUpdateStatus, $scope, $scope), $scope.uiState);
|
||||
editor.render({
|
||||
uiState: $scope.uiState,
|
||||
timeRange: $scope.timeRange,
|
||||
appState: getAppState(),
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('render', (event) => {
|
||||
|
@ -58,9 +56,8 @@ uiModules
|
|||
editor.destroy();
|
||||
});
|
||||
|
||||
if (!vis.initialized) {
|
||||
$timeout(() => { $scope.renderFunction(); });
|
||||
}
|
||||
$scope.$watch('timeRange', $scope.renderFunction);
|
||||
|
||||
}
|
||||
};
|
||||
});
|
|
@ -81,7 +81,7 @@ class VisEditor extends Component {
|
|||
this.setState({ dirty: false });
|
||||
};
|
||||
|
||||
if (!this.props.vis.isEditorMode()) {
|
||||
if (!this.props.isEditorMode) {
|
||||
if (!this.props.vis.params || !this.props.visData) return null;
|
||||
const reversed = this.state.reversed;
|
||||
return (
|
||||
|
@ -110,7 +110,9 @@ class VisEditor extends Component {
|
|||
dirty={this.state.dirty}
|
||||
autoApply={this.state.autoApply}
|
||||
model={model}
|
||||
visData={this.props.visData}
|
||||
appState={this.props.appState}
|
||||
savedObj={this.props.savedObj}
|
||||
timeRange={this.props.timeRange}
|
||||
onUiState={this.handleUiState}
|
||||
uiState={this.props.vis.getUiState()}
|
||||
onBrush={this.onBrush}
|
||||
|
@ -155,7 +157,10 @@ VisEditor.propTypes = {
|
|||
visData: PropTypes.object,
|
||||
appState: PropTypes.object,
|
||||
renderComplete: PropTypes.func,
|
||||
config: PropTypes.object
|
||||
config: PropTypes.object,
|
||||
isEditorMode: PropTypes.bool,
|
||||
savedObj: PropTypes.object,
|
||||
timeRange: PropTypes.object,
|
||||
};
|
||||
|
||||
export default VisEditor;
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { keyCodes } from '@elastic/eui';
|
||||
import Visualization from './visualization';
|
||||
import Toggle from 'react-toggle';
|
||||
import 'react-toggle/style.css';
|
||||
import { getVisualizeLoader } from 'ui/visualize/loader/visualize_loader';
|
||||
|
||||
const MIN_CHART_HEIGHT = 250;
|
||||
|
||||
|
@ -37,6 +37,8 @@ class VisEditorVisualization extends Component {
|
|||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
this.onSizeHandleKeyDown = this.onSizeHandleKeyDown.bind(this);
|
||||
|
||||
this._visEl = React.createRef();
|
||||
}
|
||||
|
||||
handleMouseDown() {
|
||||
|
@ -62,8 +64,37 @@ class VisEditorVisualization extends Component {
|
|||
componentWillUnmount() {
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
if (this._handler) {
|
||||
this._handler.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
_loadVisualization() {
|
||||
getVisualizeLoader().then(loader => {
|
||||
if (!this._visEl.current) {
|
||||
// In case the visualize loader isn't done before the component is unmounted.
|
||||
return;
|
||||
}
|
||||
|
||||
this._loader = loader;
|
||||
this._handler = this._loader.embedVisualizationWithSavedObject(this._visEl.current, this.props.savedObj, {
|
||||
uiState: this.props.uiState,
|
||||
listenOnChange: false,
|
||||
timeRange: this.props.timeRange,
|
||||
appState: this.props.appState,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._handler.update({
|
||||
timeRange: this.props.timeRange
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._loadVisualization();
|
||||
}
|
||||
/**
|
||||
* Resize the chart height when pressing up/down while the drag handle
|
||||
* for resizing has the focus.
|
||||
|
@ -126,7 +157,6 @@ class VisEditorVisualization extends Component {
|
|||
</div>
|
||||
);
|
||||
|
||||
const visBackgroundColor = '#FFF';
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
|
@ -137,19 +167,8 @@ class VisEditorVisualization extends Component {
|
|||
data-title={this.props.title}
|
||||
data-description={this.props.description}
|
||||
data-render-complete="disabled"
|
||||
>
|
||||
<Visualization
|
||||
backgroundColor={visBackgroundColor}
|
||||
className="dashboard__visualization"
|
||||
dateFormat={this.props.dateFormat}
|
||||
model={this.props.model}
|
||||
onBrush={this.props.onBrush}
|
||||
onChange={this.handleChange}
|
||||
onUiState={this.props.onUiState}
|
||||
uiState={this.props.uiState}
|
||||
visData={this.props.visData}
|
||||
/>
|
||||
</div>
|
||||
ref={this._visEl}
|
||||
/>
|
||||
<div className="vis-editor-hide-for-reporting">
|
||||
{applyButton}
|
||||
<button
|
||||
|
@ -175,10 +194,12 @@ VisEditorVisualization.propTypes = {
|
|||
onUiState: PropTypes.func,
|
||||
uiState: PropTypes.object,
|
||||
onToggleAutoApply: PropTypes.func,
|
||||
visData: PropTypes.object,
|
||||
savedObj: PropTypes.object,
|
||||
timeRange: PropTypes.object,
|
||||
dirty: PropTypes.bool,
|
||||
autoApply: PropTypes.bool,
|
||||
dateFormat: PropTypes.string
|
||||
dateFormat: PropTypes.string,
|
||||
appState: PropTypes.object,
|
||||
};
|
||||
|
||||
export default VisEditorVisualization;
|
||||
|
|
|
@ -22,25 +22,20 @@ import { render, unmountComponentAtNode } from 'react-dom';
|
|||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { FetchFieldsProvider } from '../lib/fetch_fields';
|
||||
import { extractIndexPatterns } from '../lib/extract_index_patterns';
|
||||
const AUTO_APPLY_KEY = 'metrics_autoApply';
|
||||
|
||||
function ReactEditorControllerProvider(Private, localStorage, config) {
|
||||
function ReactEditorControllerProvider(Private, config) {
|
||||
const fetchFields = Private(FetchFieldsProvider);
|
||||
const savedObjectsClient = Private(SavedObjectsClientProvider);
|
||||
|
||||
class ReactEditorController {
|
||||
constructor(el, vis) {
|
||||
constructor(el, savedObj) {
|
||||
this.el = el;
|
||||
this.vis = vis;
|
||||
this.savedObj = savedObj;
|
||||
this.vis = savedObj.vis;
|
||||
this.vis.fields = {};
|
||||
|
||||
const autoApply = localStorage.get(AUTO_APPLY_KEY);
|
||||
vis.autoApply = autoApply != null ? autoApply : true;
|
||||
vis.initialized = true;
|
||||
}
|
||||
|
||||
render(visData) {
|
||||
this.visData = visData;
|
||||
render(params) {
|
||||
return new Promise((resolve) => {
|
||||
Promise.resolve().then(() => {
|
||||
if (this.vis.params.index_pattern === '') {
|
||||
|
@ -53,7 +48,15 @@ function ReactEditorControllerProvider(Private, localStorage, config) {
|
|||
fetchFields(indexPatterns).then(fields => {
|
||||
this.vis.fields = { ...fields, ...this.vis.fields };
|
||||
const Component = this.vis.type.editorConfig.component;
|
||||
render(<Component config={config} vis={this.vis} visData={visData} renderComplete={resolve}/>, this.el);
|
||||
render(<Component
|
||||
config={config}
|
||||
vis={this.vis}
|
||||
savedObj={this.savedObj}
|
||||
timeRange={params.timeRange}
|
||||
renderComplete={resolve}
|
||||
isEditorMode={true}
|
||||
appState={params.appState}
|
||||
/>, this.el);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -300,7 +300,8 @@ class TagCloud extends EventEmitter {
|
|||
rotate: tag.rotate,
|
||||
size: tag.size,
|
||||
rawText: tag.rawText || tag.text,
|
||||
displayText: tag.displayText
|
||||
displayText: tag.displayText,
|
||||
meta: tag.meta,
|
||||
};
|
||||
})
|
||||
};
|
||||
|
|
|
@ -21,7 +21,6 @@ import $ from 'jquery';
|
|||
import moment from 'moment';
|
||||
import dateMath from '@kbn/datemath';
|
||||
import * as vega from 'vega-lib';
|
||||
import * as vegaLite from 'vega-lite';
|
||||
import { Utils } from '../data_model/utils';
|
||||
import { VISUALIZATION_COLORS } from '@elastic/eui';
|
||||
import { TooltipHandler } from './vega_tooltip';
|
||||
|
@ -63,7 +62,6 @@ export class VegaBaseView {
|
|||
// $rootScope is a temp workaround, see usage below
|
||||
this._$rootScope = opts.$rootScope;
|
||||
this._vegaConfig = opts.vegaConfig;
|
||||
this._editorMode = opts.editorMode;
|
||||
this._$parentEl = $(opts.parentEl);
|
||||
this._parser = opts.vegaParser;
|
||||
this._serviceSettings = opts.serviceSettings;
|
||||
|
@ -331,38 +329,6 @@ export class VegaBaseView {
|
|||
return { from, to, mode };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set global debug variable to simplify vega debugging in console. Show info message first time
|
||||
*/
|
||||
setDebugValues(view, spec, vlspec) {
|
||||
if (!this._editorMode) {
|
||||
// VEGA_DEBUG should only be enabled in the editor mode
|
||||
return;
|
||||
}
|
||||
|
||||
if (window) {
|
||||
if (window.VEGA_DEBUG === undefined && console) {
|
||||
console.log('%cWelcome to Kibana Vega Plugin!', 'font-size: 16px; font-weight: bold;');
|
||||
console.log('You can access the Vega view with VEGA_DEBUG. ' +
|
||||
'Learn more at https://vega.github.io/vega/docs/api/debugging/.');
|
||||
}
|
||||
const debugObj = {};
|
||||
window.VEGA_DEBUG = debugObj;
|
||||
window.VEGA_DEBUG.VEGA_VERSION = vega.version;
|
||||
window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLite.version;
|
||||
window.VEGA_DEBUG.view = view;
|
||||
window.VEGA_DEBUG.vega_spec = spec;
|
||||
window.VEGA_DEBUG.vegalite_spec = vlspec;
|
||||
|
||||
// On dispose, clean up, but don't use undefined to prevent repeated debug statements
|
||||
this._addDestroyHandler(() => {
|
||||
if (debugObj === window.VEGA_DEBUG) {
|
||||
window.VEGA_DEBUG = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// properly handle multiple destroy() calls by converting this._destroyHandlers
|
||||
// into the _ongoingDestroy promise, while handlers are being disposed
|
||||
|
|
|
@ -107,7 +107,6 @@ export class VegaMapView extends VegaBaseView {
|
|||
});
|
||||
|
||||
const vegaView = vegaMapLayer.getVegaView();
|
||||
this.setDebugValues(vegaView, vegaMapLayer.getVegaSpec());
|
||||
await this.setView(vegaView);
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ export class VegaView extends VegaBaseView {
|
|||
if (!this._$container) return;
|
||||
|
||||
const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig);
|
||||
this.setDebugValues(view, this._parser.spec, this._parser.vlspec);
|
||||
|
||||
view.warn = this.onWarn.bind(this);
|
||||
view.error = this.onError.bind(this);
|
||||
|
|
|
@ -87,7 +87,6 @@ export function VegaVisualizationProvider(Private, vegaConfig, serviceSettings,
|
|||
|
||||
const vegaViewParams = {
|
||||
vegaConfig,
|
||||
editorMode: this._vis.editorMode,
|
||||
parentEl: this._el,
|
||||
vegaParser,
|
||||
serviceSettings,
|
||||
|
|
|
@ -30,15 +30,16 @@ import { keyCodes } from '@elastic/eui';
|
|||
import { DefaultEditorSize } from '../../editor_size';
|
||||
|
||||
import { VisEditorTypesRegistryProvider } from '../../../registry/vis_editor_types';
|
||||
import { visualizationLoader } from '../../../visualize/loader/visualization_loader';
|
||||
import { getVisualizeLoader } from '../../../visualize/loader/visualize_loader';
|
||||
|
||||
const defaultEditor = function ($rootScope, $compile) {
|
||||
return class DefaultEditor {
|
||||
static key = 'default';
|
||||
|
||||
constructor(el, vis) {
|
||||
constructor(el, savedObj) {
|
||||
this.el = $(el);
|
||||
this.vis = vis;
|
||||
this.savedObj = savedObj;
|
||||
this.vis = savedObj.vis;
|
||||
|
||||
if (!this.vis.type.editorConfig.optionTabs && this.vis.type.editorConfig.optionsTemplate) {
|
||||
this.vis.type.editorConfig.optionTabs = [
|
||||
|
@ -47,15 +48,13 @@ const defaultEditor = function ($rootScope, $compile) {
|
|||
}
|
||||
}
|
||||
|
||||
render(visData, searchSource, updateStatus, uiState) {
|
||||
render({ uiState, timeRange, appState }) {
|
||||
let $scope;
|
||||
|
||||
const updateScope = () => {
|
||||
$scope.vis = this.vis;
|
||||
$scope.visData = visData;
|
||||
$scope.uiState = uiState;
|
||||
$scope.searchSource = searchSource;
|
||||
$scope.$apply();
|
||||
//$scope.$apply();
|
||||
};
|
||||
|
||||
return new Promise(resolve => {
|
||||
|
@ -138,8 +137,26 @@ const defaultEditor = function ($rootScope, $compile) {
|
|||
updateScope();
|
||||
}
|
||||
|
||||
const visualizationEl = this.el.find('.vis-editor-canvas')[0];
|
||||
visualizationLoader(visualizationEl, this.vis, visData, uiState, { listenOnChange: false });
|
||||
if (!this._handler) {
|
||||
const visualizationEl = this.el.find('.vis-editor-canvas')[0];
|
||||
getVisualizeLoader().then(loader => {
|
||||
if (!visualizationEl) {
|
||||
return;
|
||||
}
|
||||
this._loader = loader;
|
||||
this._handler = this._loader.embedVisualizationWithSavedObject(visualizationEl, this.savedObj, {
|
||||
uiState: uiState,
|
||||
listenOnChange: false,
|
||||
timeRange: timeRange,
|
||||
appState: appState,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this._handler.update({
|
||||
timeRange: timeRange
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -152,6 +169,9 @@ const defaultEditor = function ($rootScope, $compile) {
|
|||
this.$scope.$destroy();
|
||||
this.$scope = null;
|
||||
}
|
||||
if (this._handler) {
|
||||
this._handler.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -161,10 +161,6 @@ export function VisProvider(Private, indexPatterns, getAppState) {
|
|||
return adapters;
|
||||
}
|
||||
|
||||
isEditorMode() {
|
||||
return this.editorMode || false;
|
||||
}
|
||||
|
||||
setCurrentState(state) {
|
||||
this.title = state.title || '';
|
||||
const type = state.type || this.type;
|
||||
|
|
|
@ -46,8 +46,8 @@ export function AngularVisTypeProvider(Private, $compile, $rootScope) {
|
|||
|
||||
if (!this.$scope) {
|
||||
this.$scope = $rootScope.$new();
|
||||
updateScope();
|
||||
this.$scope.uiState = this.vis.getUiState();
|
||||
updateScope();
|
||||
this.el.html($compile(this.vis.type.visConfig.template)(this.$scope));
|
||||
} else {
|
||||
updateScope();
|
||||
|
|
|
@ -1,117 +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 { VisProvider } from '../../vis';
|
||||
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import FixturesStubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
|
||||
import MockState from 'fixtures/mock_state';
|
||||
import { AggResponseIndexProvider } from '../../agg_response';
|
||||
|
||||
describe('visualization_editor directive', function () {
|
||||
let $rootScope;
|
||||
let $compile;
|
||||
let $scope;
|
||||
let $el;
|
||||
let Vis;
|
||||
let indexPattern;
|
||||
let fixtures;
|
||||
let searchSource;
|
||||
let appState;
|
||||
let $timeout;
|
||||
let vis;
|
||||
let aggResponse;
|
||||
|
||||
beforeEach(ngMock.module('kibana', 'kibana/table_vis'));
|
||||
beforeEach(ngMock.inject(function (Private, $injector) {
|
||||
$rootScope = $injector.get('$rootScope');
|
||||
$compile = $injector.get('$compile');
|
||||
$timeout = $injector.get('$timeout');
|
||||
fixtures = require('fixtures/fake_hierarchical_data');
|
||||
Vis = Private(VisProvider);
|
||||
appState = new MockState({ filters: [] });
|
||||
appState.toJSON = () => { return {}; };
|
||||
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
|
||||
searchSource = Private(FixturesStubbedSearchSourceProvider);
|
||||
aggResponse = Private(AggResponseIndexProvider);
|
||||
|
||||
const requiresSearch = false;
|
||||
vis = new CreateVis(null, requiresSearch);
|
||||
init(vis, fixtures.oneRangeBucket);
|
||||
$timeout.flush();
|
||||
$timeout.verifyNoPendingTasks();
|
||||
}));
|
||||
|
||||
// basically a parameterized beforeEach
|
||||
function init(vis, esResponse) {
|
||||
vis.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); });
|
||||
|
||||
$rootScope.vis = vis;
|
||||
$rootScope.visData = aggResponse.tabify(vis.getAggConfig().getResponseAggs(), esResponse, {
|
||||
isHierarchical: vis.isHierarchical()
|
||||
});
|
||||
$rootScope.searchSource = searchSource;
|
||||
$el = $('<visualization-editor vis="vis" vis-data="visData" search-source="searchSource">');
|
||||
$compile($el)($rootScope);
|
||||
$rootScope.$apply();
|
||||
|
||||
$scope = $el.isolateScope();
|
||||
}
|
||||
|
||||
function CreateVis(params, requiresSearch) {
|
||||
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 = requiresSearch ? 'default' : 'none';
|
||||
vis.type.responseHandler = 'none';
|
||||
vis.type.requiresSearch = requiresSearch;
|
||||
return vis;
|
||||
}
|
||||
|
||||
it('calls render complete when editor is rendered', function () {
|
||||
let renderComplete = 0;
|
||||
$scope.renderFunction = () => {
|
||||
renderComplete++;
|
||||
};
|
||||
|
||||
$scope.$emit('render');
|
||||
$scope.$apply();
|
||||
$timeout.flush();
|
||||
$timeout.verifyNoPendingTasks();
|
||||
expect(renderComplete).to.equal(1);
|
||||
});
|
||||
});
|
|
@ -24,7 +24,6 @@ import sinon from 'sinon';
|
|||
import { VisProvider } from '../../vis';
|
||||
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import FixturesStubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
|
||||
import MockState from 'fixtures/mock_state';
|
||||
import { PersistedState } from '../../persisted_state';
|
||||
|
||||
describe('visualize directive', function () {
|
||||
|
@ -36,7 +35,7 @@ describe('visualize directive', function () {
|
|||
let indexPattern;
|
||||
let fixtures;
|
||||
let searchSource;
|
||||
let appState;
|
||||
let updateState;
|
||||
let uiState;
|
||||
|
||||
beforeEach(ngMock.module('kibana', 'kibana/table_vis'));
|
||||
|
@ -45,8 +44,6 @@ describe('visualize directive', function () {
|
|||
$compile = $injector.get('$compile');
|
||||
fixtures = require('fixtures/fake_hierarchical_data');
|
||||
Vis = Private(VisProvider);
|
||||
appState = new MockState({ filters: [] });
|
||||
appState.toJSON = () => { return {}; };
|
||||
uiState = new PersistedState({});
|
||||
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
|
||||
searchSource = Private(FixturesStubbedSearchSourceProvider);
|
||||
|
@ -65,14 +62,14 @@ describe('visualize directive', function () {
|
|||
$rootScope.vis = vis;
|
||||
$rootScope.esResponse = esResponse;
|
||||
$rootScope.uiState = uiState;
|
||||
$rootScope.appState = appState;
|
||||
$rootScope.appState.vis = vis.getState();
|
||||
$rootScope.searchSource = searchSource;
|
||||
$rootScope.savedObject = {
|
||||
vis: vis,
|
||||
searchSource: searchSource
|
||||
};
|
||||
$el = $('<visualize saved-obj="savedObject" ui-state="uiState" app-state="appState">');
|
||||
$rootScope.updateState = updateState;
|
||||
|
||||
$el = $('<visualize saved-obj="savedObject" ui-state="uiState" update-state="updateState">');
|
||||
$compile($el)($rootScope);
|
||||
$rootScope.$apply();
|
||||
|
||||
|
@ -118,11 +115,13 @@ describe('visualize directive', function () {
|
|||
expect(counter).to.equal(1);
|
||||
});
|
||||
|
||||
it('updates the appState in editor mode on update event', () => {
|
||||
$scope.editorMode = true;
|
||||
$scope.appState.vis = {};
|
||||
it('calls the updateState on update event', () => {
|
||||
let visState = {};
|
||||
updateState = (state) => {
|
||||
visState = state.vis;
|
||||
};
|
||||
$scope.vis.emit('update');
|
||||
expect($scope.appState.vis).to.not.equal({});
|
||||
expect(visState).to.not.equal({});
|
||||
});
|
||||
|
||||
describe('request handler', () => {
|
||||
|
|
|
@ -38,7 +38,7 @@ export class VisualizationChart extends Component {
|
|||
tap(() => {
|
||||
dispatchRenderStart(this.chartDiv.current);
|
||||
}),
|
||||
filter(({ vis, visData, container }) => vis && vis.initialized && container && (!vis.type.requiresSearch || visData)),
|
||||
filter(({ vis, visData, container }) => vis && container && (!vis.type.requiresSearch || visData)),
|
||||
debounceTime(100),
|
||||
switchMap(async ({ vis, visData, container }) => {
|
||||
vis.size = [container.clientWidth, container.clientHeight];
|
||||
|
@ -85,23 +85,18 @@ export class VisualizationChart extends Component {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { vis, listenOnChange } = this.props;
|
||||
const { vis, onInit } = this.props;
|
||||
const Visualization = vis.type.visualization;
|
||||
|
||||
this.visualization = new Visualization(this.chartDiv.current, vis);
|
||||
|
||||
if (this.visualization.isLoaded) {
|
||||
this.visualization.isLoaded().then(() => {
|
||||
vis.initialized = true;
|
||||
});
|
||||
} else {
|
||||
vis.initialized = true;
|
||||
if (onInit) {
|
||||
const visLoaded = this.visualization.isLoaded ? this.visualization.isLoaded() : true;
|
||||
Promise.resolve(visLoaded).then(onInit);
|
||||
}
|
||||
|
||||
if (listenOnChange) {
|
||||
this.resizeChecker = new ResizeChecker(this.containerDiv.current);
|
||||
this.resizeChecker.on('resize', this._startRenderVisualization);
|
||||
}
|
||||
this.resizeChecker = new ResizeChecker(this.containerDiv.current);
|
||||
this.resizeChecker.on('resize', this._startRenderVisualization);
|
||||
|
||||
this._startRenderVisualization();
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ class VisualizationStub {
|
|||
|
||||
describe('VisualizationChart', () => {
|
||||
const vis = {
|
||||
initialized: true,
|
||||
type: {
|
||||
title: 'Test Visualization',
|
||||
visualization: VisualizationStub
|
||||
|
@ -65,7 +64,7 @@ describe('VisualizationChart', () => {
|
|||
domNode.addEventListener('renderStart', renderStart);
|
||||
domNode.addEventListener('renderComplete', renderComplete);
|
||||
|
||||
mount(<VisualizationChart vis={vis} listenOnChange={true} />, {
|
||||
mount(<VisualizationChart vis={vis} />, {
|
||||
attachTo: domNode
|
||||
});
|
||||
|
||||
|
@ -77,7 +76,7 @@ describe('VisualizationChart', () => {
|
|||
});
|
||||
|
||||
it('should render visualization', async () => {
|
||||
const wrapper = mount(<VisualizationChart vis={vis} listenOnChange={true} />);
|
||||
const wrapper = mount(<VisualizationChart vis={vis} />);
|
||||
jest.runAllTimers();
|
||||
await renderPromise;
|
||||
expect(wrapper.find('.visualize-chart').text()).toMatch(/markdown/);
|
||||
|
@ -85,7 +84,7 @@ describe('VisualizationChart', () => {
|
|||
|
||||
it('should re-render on param change', async () => {
|
||||
const renderComplete = jest.fn();
|
||||
const wrapper = mount(<VisualizationChart vis={vis} listenOnChange={true} />);
|
||||
const wrapper = mount(<VisualizationChart vis={vis} />);
|
||||
const domNode = wrapper.getDOMNode();
|
||||
domNode.addEventListener('renderComplete', renderComplete);
|
||||
jest.runAllTimers();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<visualize
|
||||
saved-obj="savedObj"
|
||||
app-state="appState"
|
||||
update-state="updateState"
|
||||
ui-state="uiState"
|
||||
time-range="timeRange"
|
||||
filters="filters"
|
||||
|
|
|
@ -24,8 +24,16 @@ import { Visualization } from 'ui/visualize/visualization';
|
|||
|
||||
|
||||
export const visualizationLoader = (element, vis, visData, uiState, params) => {
|
||||
const listenOnChange = _.get(params, 'listenOnChange', false);
|
||||
render(<Visualization vis={vis} visData={visData} uiState={uiState} listenOnChange={listenOnChange} />, element);
|
||||
return new Promise(resolve => {
|
||||
const listenOnChange = _.get(params, 'listenOnChange', false);
|
||||
render(<Visualization
|
||||
vis={vis}
|
||||
visData={visData}
|
||||
uiState={uiState}
|
||||
listenOnChange={listenOnChange}
|
||||
onInit={resolve}
|
||||
/>, element);
|
||||
});
|
||||
};
|
||||
|
||||
visualizationLoader.destroy = (element) => {
|
||||
|
|
|
@ -57,11 +57,16 @@ const VisualizeLoaderProvider = ($compile, $rootScope, savedVisualizations) => {
|
|||
const scope = $rootScope.$new();
|
||||
params = params || {};
|
||||
scope.savedObj = savedObj;
|
||||
scope.appState = params.appState;
|
||||
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 container = angular.element(el);
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ export class Visualization extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { vis, visData, listenOnChange } = this.props;
|
||||
const { vis, visData, onInit } = this.props;
|
||||
|
||||
return (
|
||||
<div className="visualization">
|
||||
|
@ -70,7 +70,7 @@ export class Visualization extends Component {
|
|||
(<VisualizationChart
|
||||
vis={vis}
|
||||
visData={visData}
|
||||
listenOnChange={listenOnChange}
|
||||
onInit={onInit}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
<visualization-editor
|
||||
ng-if="editorMode===true"
|
||||
vis="vis"
|
||||
vis-data="visData"
|
||||
ui-state="uiState"
|
||||
class="vis-editor-content"
|
||||
search-source="savedObj.searchSource"
|
||||
/>
|
|
@ -19,14 +19,11 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import { uiModules } from '../modules';
|
||||
import visualizeTemplate from './visualize.html';
|
||||
import { VisRequestHandlersRegistryProvider } from '../registry/vis_request_handlers';
|
||||
import { VisResponseHandlersRegistryProvider } from '../registry/vis_response_handlers';
|
||||
import 'angular-sanitize';
|
||||
import './visualization';
|
||||
import './visualization_editor';
|
||||
import { FilterBarQueryFilterProvider } from '../filter_bar/query_filter';
|
||||
import { ResizeChecker } from '../resize_checker';
|
||||
import { visualizationLoader } from './loader/visualization_loader';
|
||||
|
||||
import {
|
||||
|
@ -35,7 +32,7 @@ import {
|
|||
|
||||
uiModules
|
||||
.get('kibana/directive', ['ngSanitize'])
|
||||
.directive('visualize', function ($timeout, Notifier, Private, getAppState, Promise) {
|
||||
.directive('visualize', function ($timeout, Notifier, Private, Promise) {
|
||||
const notify = new Notifier({ location: 'Visualize' });
|
||||
const requestHandlers = Private(VisRequestHandlersRegistryProvider);
|
||||
const responseHandlers = Private(VisResponseHandlersRegistryProvider);
|
||||
|
@ -49,28 +46,18 @@ uiModules
|
|||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
editorMode: '=?',
|
||||
savedObj: '=?',
|
||||
appState: '=?',
|
||||
uiState: '=?',
|
||||
timeRange: '=?',
|
||||
filters: '=?',
|
||||
query: '=?',
|
||||
updateState: '=?',
|
||||
},
|
||||
template: visualizeTemplate,
|
||||
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`);
|
||||
if (!$scope.appState) $scope.appState = getAppState();
|
||||
|
||||
const resizeChecker = new ResizeChecker($el, { disabled: true });
|
||||
$timeout(() => {
|
||||
// We give the visualize one digest cycle time to actually render before
|
||||
// we start tracking its size. If we don't do that, we cause a double
|
||||
// initial rendering in editor mode.
|
||||
resizeChecker.enable();
|
||||
});
|
||||
|
||||
$scope.vis = $scope.savedObj.vis;
|
||||
$scope.vis.searchSource = $scope.savedObj.searchSource;
|
||||
|
@ -81,9 +68,6 @@ uiModules
|
|||
|
||||
$scope.vis.description = $scope.savedObj.description;
|
||||
|
||||
$scope.editorMode = $scope.editorMode || false;
|
||||
$scope.vis.editorMode = $scope.editorMode;
|
||||
|
||||
const requestHandler = getHandler(requestHandlers, $scope.vis.type.requestHandler);
|
||||
const responseHandler = getHandler(responseHandlers, $scope.vis.type.responseHandler);
|
||||
|
||||
|
@ -91,12 +75,11 @@ uiModules
|
|||
// 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 (!$scope.vis.initialized || !$scope.savedObj || destroyed) return;
|
||||
if (!loaded || !$scope.savedObj || destroyed) return;
|
||||
|
||||
$scope.vis.filters = { timeRange: $scope.timeRange };
|
||||
|
||||
const handlerParams = {
|
||||
appState: $scope.appState,
|
||||
uiState: $scope.uiState,
|
||||
queryFilter: queryFilter,
|
||||
searchSource: $scope.savedObj.searchSource,
|
||||
|
@ -138,21 +121,17 @@ uiModules
|
|||
})
|
||||
.then(resp => {
|
||||
$scope.visData = resp;
|
||||
$scope.$apply();
|
||||
$scope.$broadcast('render');
|
||||
|
||||
if (!$scope.editorMode) {
|
||||
visualizationLoader($el[0], $scope.vis, $scope.visData, $scope.uiState, { listenOnChange: false });
|
||||
}
|
||||
visualizationLoader($el[0], $scope.vis, $scope.visData, $scope.uiState, { listenOnChange: false });
|
||||
|
||||
return resp;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
const handleVisUpdate = () => {
|
||||
if ($scope.appState.vis) {
|
||||
$scope.appState.vis = $scope.vis.getState();
|
||||
$scope.appState.save();
|
||||
if ($scope.updateState) {
|
||||
const visState = $scope.vis.getState();
|
||||
$scope.updateState(visState);
|
||||
}
|
||||
$scope.fetch();
|
||||
};
|
||||
|
@ -178,29 +157,25 @@ uiModules
|
|||
// cached data otherwise.
|
||||
$scope.uiState.on('change', $scope.fetch);
|
||||
|
||||
resizeChecker.on('resize', $scope.fetch);
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
destroyed = true;
|
||||
$scope.vis.removeListener('reload', reload);
|
||||
$scope.vis.removeListener('update', handleVisUpdate);
|
||||
$scope.uiState.off('change', $scope.fetch);
|
||||
resizeChecker.destroy();
|
||||
visualizationLoader.destroy($el[0]);
|
||||
});
|
||||
|
||||
$scope.$watch('vis.initialized', () => {
|
||||
visualizationLoader(
|
||||
$el[0],
|
||||
$scope.vis,
|
||||
$scope.visData,
|
||||
$scope.uiState,
|
||||
{ listenOnChange: false }
|
||||
).then(() => {
|
||||
loaded = true;
|
||||
$scope.fetch();
|
||||
});
|
||||
|
||||
if (!$scope.editorMode) {
|
||||
visualizationLoader(
|
||||
$el[0],
|
||||
$scope.vis,
|
||||
$scope.visData,
|
||||
$scope.uiState,
|
||||
{ listenOnChange: false }
|
||||
);
|
||||
} }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
@import (reference) "~ui/styles/variables";
|
||||
|
||||
visualize {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.visualization {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -8,6 +14,7 @@
|
|||
overflow: auto;
|
||||
position: relative;
|
||||
padding: 8px 8px 8px 8px;
|
||||
flex: 1 1 100%;
|
||||
|
||||
.k4tip {
|
||||
white-space: pre-line;
|
||||
|
|
|
@ -167,7 +167,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
it('should apply filter with unformatted value', async function () {
|
||||
await PageObjects.visualize.selectTagCloudTag('30GB');
|
||||
await PageObjects.common.sleep(500);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const data = await PageObjects.visualize.getTextTag();
|
||||
expect(data).to.eql([ '30GB' ]);
|
||||
});
|
||||
|
|
|
@ -38,12 +38,14 @@ export default function ({ getService, getPageObjects }) {
|
|||
it('should show the correct count in the legend with 2h offset', async function () {
|
||||
await PageObjects.visualBuilder.clickSeriesOption();
|
||||
await PageObjects.visualBuilder.enterOffsetSeries('2h');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const actualCount = await PageObjects.visualBuilder.getRhythmChartLegendValue();
|
||||
expect(actualCount).to.be('293');
|
||||
});
|
||||
|
||||
it('should show the correct count in the legend with -2h offset', async function () {
|
||||
await PageObjects.visualBuilder.enterOffsetSeries('-2h');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const actualCount = await PageObjects.visualBuilder.getRhythmChartLegendValue();
|
||||
expect(actualCount).to.be('53');
|
||||
});
|
||||
|
@ -163,6 +165,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
it('allow positive time offsets', async () => {
|
||||
await PageObjects.visualBuilder.enterOffsetSeries('2h');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const text = await PageObjects.visualBuilder.getMarkdownText();
|
||||
const [timestamp, value] = text.split('#');
|
||||
expect(timestamp).to.be('1442901600000');
|
||||
|
@ -171,6 +174,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
it('allow negative time offsets', async () => {
|
||||
await PageObjects.visualBuilder.enterOffsetSeries('-2h');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const text = await PageObjects.visualBuilder.getMarkdownText();
|
||||
const [timestamp, value] = text.split('#');
|
||||
expect(timestamp).to.be('1442901600000');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue