Refactor spy panels (#15682) (#15734)

* Fix design of debug spy panel

* Simplify the debug spy panel

* Fix height of spy content

* Use scope bindings for spy

* Allow modes to be enabled/disabled based on vis

* Make showSpyPanel docs for loader more detailed

* Only show default panels when using courier

* Fix styling of req/resp spies

* Fix title styling in spy panel

* Get rid of visData in spy panels

* Use tabs instead of select box

* Ensure uiState in spy

* Restyle code in default editor

* Remove unnecessary if protection

* Use local scope variable for rowsPerPage

Due to issues with primitive datatype bindings in Angular this anyway
didn't save it correctly back into the variable.

* Pull all logic into spy directive

* Fix bug when closing panel

* Check for minimum chart size

* Skip spy tests for now

* Fix functional tests

* Improve uiState mock

* Create unit tests

* Remove dead scope binding
This commit is contained in:
Tim Roes 2017-12-21 12:55:13 +01:00 committed by GitHub
parent 60f809826d
commit 008034c1aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 525 additions and 263 deletions

View file

@ -1,35 +1,23 @@
<div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>
Vis State
</label>
<pre>{{visStateJson}}</pre>
<div class="euiFlexGroup euiFlexGroup--responsive euiFlexGroup--gutterMedium">
<div class="euiFlexItem">
<h3 class="euiTitle euiTitle--small">
Vis State
</h3>
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--fontSmall euiCodeBlock--paddingSmall">
<code class="euiCodeBlock__code">
<pre class="euiCodeBlock__pre">{{vis.getEnabledState() | json}}</pre>
</code>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Details</label>
<table class="table">
<tr>
<th scope="row">
Type Name
</th>
<td>
{{vis.type.name}}
</td>
</tr>
<tr>
<th scope="row">
Hierarchical Data
</th>
<td>
{{vis.isHierarchical()}}
</td>
</tr>
</table>
</div>
<div class="euiFlexItem">
<h3 class="euiTitle euiTitle--small">Details</h3>
<dl class="euiDescriptionList euiDescriptionList--column">
<dt class="euiDescriptionList__title">Type Name</dt>
<dd class="euiDescriptionList__description">{{vis.type.name}}</dd>
<dt class="euiDescriptionList__title">Hierarchical Data</dt>
<dd class="euiDescriptionList__description">{{vis.isHierarchical()}}</dd>
</dl>
</div>
</div>
</div>

View file

@ -6,12 +6,7 @@ function VisDetailsSpyProvider() {
name: 'debug',
display: 'Debug',
template: visDebugSpyPanelTemplate,
order: 5,
link: function ($scope) {
$scope.$watch('vis.getEnabledState() | json', function (json) {
$scope.visStateJson = json;
});
}
order: 5
};
}

View file

@ -10,21 +10,32 @@
<i class="fa fa-danger"></i> Request Failed
</div>
<div ng-if="spy.mode.name === 'request'">
<label>
Elasticsearch request body &nbsp;
</label>
<pre data-test-subj="visualizationEsRequestBody">{{req.fetchParams.body | json}}</pre>
<div ng-if="mode === 'request'">
<h3 class="euiTitle euiTitle--small">
Elasticsearch request body
</h3>
<div
data-test-subj="visualizationEsRequestBody"
class="euiCodeBlock euiCodeBlock--light euiCodeBlock--fontSmall euiCodeBlock--paddingSmall"
>
<code class="euiCodeBlock__code">
<pre class="euiCodeBlock__pre">{{req.fetchParams.body | json}}</pre>
</code>
</div>
</div>
<div ng-if="spy.mode.name === 'response'">
<label>
Elasticsearch response body &nbsp;
</label>
<pre>{{req.resp | json}}</pre>
<div ng-if="mode === 'response'">
<h3 class="euiTitle euiTitle--small">
Elasticsearch response body
</h3>
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--fontSmall euiCodeBlock--paddingSmall">
<code class="euiCodeBlock__code">
<pre class="euiCodeBlock__pre">{{req.resp | json}}</pre>
</code>
</div>
</div>
<div ng-if="spy.mode.name === 'stats'">
<div ng-if="mode === 'stats'">
<table class="table">
<tr ng-repeat="pair in stats">
<td>{{pair[0]}}</td>

View file

@ -1,7 +1,8 @@
import reqRespStatsHTML from 'plugins/spy_modes/req_resp_stats_spy_mode.html';
import { SpyModesRegistryProvider } from 'ui/registry/spy_modes';
const linkReqRespStats = function ($scope) {
const linkReqRespStats = function (mode, $scope) {
$scope.mode = mode;
$scope.$bind('req', 'searchSource.history[searchSource.history.length - 1]');
$scope.$watchMulti([
'req',
@ -27,6 +28,10 @@ const linkReqRespStats = function ($scope) {
});
};
function shouldShowSpyMode(vis) {
return vis.type.requestHandler === 'courier' && vis.type.requiresSearch;
}
SpyModesRegistryProvider
.register(function () {
return {
@ -34,7 +39,8 @@ SpyModesRegistryProvider
display: 'Request',
order: 2,
template: reqRespStatsHTML,
link: linkReqRespStats
showMode: shouldShowSpyMode,
link: linkReqRespStats.bind(null, 'request')
};
})
.register(function () {
@ -43,7 +49,8 @@ SpyModesRegistryProvider
display: 'Response',
order: 3,
template: reqRespStatsHTML,
link: linkReqRespStats
showMode: shouldShowSpyMode,
link: linkReqRespStats.bind(null, 'response')
};
})
.register(function () {
@ -52,6 +59,7 @@ SpyModesRegistryProvider
display: 'Statistics',
order: 4,
template: reqRespStatsHTML,
link: linkReqRespStats
showMode: shouldShowSpyMode,
link: linkReqRespStats.bind(null, 'stats')
};
});

View file

@ -1,5 +1,5 @@
<kbn-agg-table
table="table"
export-title="vis.title"
per-page="spy.params.spyPerPage">
per-page="rowsPerPage">
</kbn-agg-table>

View file

@ -12,17 +12,16 @@ function VisSpyTableProvider(Notifier, $filter, $rootScope, config, Private) {
display: 'Table',
order: 1,
template: tableSpyModeTemplate,
showMode: vis => vis.type.requestHandler === 'courier' && vis.type.requiresSearch,
link: function tableLinkFn($scope) {
$rootScope.$watchMulti.call($scope, [
'vis',
'visData'
'searchSource.rawResponse'
], function () {
if (!$scope.vis || !$scope.visData) {
if (!$scope.vis || !$scope.searchSource.rawResponse) {
$scope.table = null;
} else {
if (!$scope.spy.params.spyPerPage) {
$scope.spy.params.spyPerPage = PER_PAGE_DEFAULT;
}
$scope.rowsPerPage = PER_PAGE_DEFAULT;
$scope.table = tabifyAggResponse($scope.vis, $scope.searchSource.rawResponse, {
canSplit: false,

View file

@ -1,18 +1,21 @@
import _ from 'lodash';
const keys = {};
let values = {};
export default {
get: function (path, def) {
return keys[path] == null ? def : keys[path];
return _.get(values, path, def);
},
set: function (path, val) {
keys[path] = val;
_.set(values, path, val);
return val;
},
setSilent: function (path, val) {
keys[path] = val;
_.set(values, path, val);
return val;
},
emit: _.noop,
on: _.noop,
off: _.noop
off: _.noop,
_reset: function () {
values = {};
}
};

View file

@ -16,5 +16,11 @@
data-title="{{vis.title}}"
data-description="{{vis.description}}"
>
<visualization vis="vis" vis-data="visData" ui-state="uiState" search-source="searchSource" show-spy-panel="showSpyPanel" />
<visualization
vis="vis"
vis-data="visData"
ui-state="uiState"
search-source="searchSource"
show-spy-panel="showSpyPanel"
/>
</div>

View file

@ -1,49 +1,62 @@
import $ from 'jquery';
import _ from 'lodash';
import expect from 'expect.js';
import sinon from 'sinon';
import { expect } from 'chai';
import ngMock from 'ng_mock';
import angular from 'angular';
import { VisProvider } from 'ui/vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import FixturesStubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
import MockState from 'fixtures/mock_state';
import { uiRegistry } from 'ui/registry/_registry';
import { SpyModesRegistryProvider } from 'ui/registry/spy_modes';
import mockUiState from 'fixtures/mock_ui_state';
describe('visualize spy directive', function () {
let $rootScope;
describe('visualize spy panel', function () {
let $scope;
let $compile;
let $timeout;
let $el;
let visElement;
let Vis;
let indexPattern;
let fixtures;
let searchSource;
let appState;
let vis;
let spyModeStubRegistry;
beforeEach(ngMock.module('kibana', 'kibana/table_vis', (PrivateProvider) => {
spyModeStubRegistry = uiRegistry({
name: 'spyModes',
index: ['name'],
order: ['order']
});
PrivateProvider.swap(SpyModesRegistryProvider, spyModeStubRegistry);
}));
beforeEach(ngMock.module('kibana', 'kibana/table_vis'));
beforeEach(ngMock.inject(function (Private, $injector) {
$rootScope = $injector.get('$rootScope');
$scope = $injector.get('$rootScope').$new();
$timeout = $injector.get('$timeout');
$compile = $injector.get('$compile');
visElement = angular.element('<div>');
visElement.width(500);
visElement.height(500);
fixtures = require('fixtures/fake_hierarchical_data');
Vis = Private(VisProvider);
appState = new MockState({ filters: [] });
appState.toJSON = () => { return {}; };
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
searchSource = Private(FixturesStubbedSearchSourceProvider);
const requiresSearch = false;
vis = new CreateVis(null, requiresSearch);
vis = new CreateVis(null, false);
init(vis, fixtures.oneRangeBucket);
}));
// basically a parameterized beforeEach
function init(vis, esResponse) {
vis.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); });
$rootScope.spy = {};
$rootScope.vis = vis;
$rootScope.esResponse = esResponse;
$rootScope.uiState = require('fixtures/mock_ui_state');
$rootScope.searchSource = searchSource;
$el = $('<visualize-spy>');
$compile($el)($rootScope);
$rootScope.$apply();
mockUiState._reset();
$scope.vis = vis;
$scope.esResponse = esResponse;
$scope.uiState = mockUiState;
$scope.searchSource = searchSource;
$scope.visElement = visElement;
}
function CreateVis(params, requiresSearch) {
@ -72,36 +85,221 @@ describe('visualize spy directive', function () {
return vis;
}
it('toggleDisplay toggles spy display', () => {
$rootScope.toggleDisplay();
$rootScope.$apply();
let mode = _.get($rootScope.spy, 'mode.name');
expect(mode).to.equal('table');
$rootScope.toggleDisplay();
$rootScope.$apply();
mode = _.get($rootScope.spy, 'mode.name');
expect(mode).to.be.undefined;
function compile() {
const spyElem = $('<visualize-spy vis="vis" vis-element="visElement" search-source="searchSource" ui-state="uiState">');
const $el = $compile(spyElem)($scope);
$scope.$apply();
$el.toggleButton = $el.find('[data-test-subj="spyToggleButton"]');
$el.maximizedButton = $el.find('[data-test-subj="toggleSpyFullscreen"]');
$el.panel = $el.find('[data-test-subj="spyContainer"]');
$el.tabs = $el.find('[data-test-subj="spyModTabs"]');
return $el;
}
function fillRegistryAndCompile() {
spyModeStubRegistry.register(() => ({
name: 'spymode1',
display: 'SpyMode1',
order: 1,
template: '<div></div>',
}));
spyModeStubRegistry.register(() => ({
name: 'spymode2',
display: 'SpyMode2',
order: 2,
template: '<div></div>',
}));
$el = compile();
}
function openSpy(el = $el) {
el.toggleButton.click();
}
// Returns an array of the title of all shown mode tabs.
function getModeTabTitles($el) {
const tabElems = $el.tabs.find('button').get();
return tabElems.map(btn => btn.textContent.trim());
}
describe('toggle button', () => {
it('should not be shown if no spy mode is registered', () => {
const $el = compile();
expect($el.toggleButton.length).to.equal(0);
});
});
it('toggleFullPage toggles full page display', () => {
$rootScope.spy = { mode: { name: 'table' } };
$rootScope.toggleFullPage();
$rootScope.$apply();
let mode = _.get($rootScope.spy, 'mode.fill');
expect(mode).to.equal(true);
describe('open and closing the spy panel', () => {
$rootScope.toggleFullPage();
$rootScope.$apply();
mode = _.get($rootScope.spy, 'mode.fill');
expect(mode).to.equal(false);
beforeEach(fillRegistryAndCompile);
it('should show spy-panel on toggle click', () => {
expect($el.panel.hasClass('ng-hide')).to.equal(true);
$el.toggleButton.click();
expect($el.panel.hasClass('ng-hide')).to.equal(false);
});
it('should hide spy-panel on toggle button, when opened', () => {
$el.toggleButton.click();
expect($el.panel.hasClass('ng-hide')).to.equal(false);
$el.toggleButton.click();
expect($el.panel.hasClass('ng-hide')).to.equal(true);
});
});
it('onSpyModeChange updates the spy display mode', () => {
$rootScope.selectedModeName = 'table';
$rootScope.onSpyModeChange();
$rootScope.$apply();
const mode = _.get($rootScope.spy, 'mode.name');
expect(mode).to.equal('table');
describe('maximized mode', () => {
beforeEach(fillRegistryAndCompile);
it('should toggle to maximized mode when maximized button is clicked', () => {
openSpy();
$el.maximizedButton.click();
expect($el.panel.hasClass('only')).to.equal(true);
expect(visElement.hasClass('spy-only')).to.equal(true);
});
it('should exit maximized mode on a second click', () => {
openSpy();
$el.maximizedButton.click();
expect($el.panel.hasClass('only')).to.equal(true);
expect(visElement.hasClass('spy-only')).to.equal(true);
$el.maximizedButton.click();
expect($el.panel.hasClass('only')).to.equal(false);
expect(visElement.hasClass('spy-only')).to.equal(false);
});
it('will be forced when vis would be too small otherwise', () => {
visElement.height(50);
openSpy();
$timeout.flush();
expect($el.panel.hasClass('only')).to.equal(true);
expect(visElement.hasClass('spy-only')).to.equal(true);
});
});
describe('spy modes', () => {
function registerRegularPanels() {
spyModeStubRegistry.register(() => ({
name: 'spymode2',
display: 'SpyMode2',
order: 2,
template: '<div class="spymode2"></div>',
}));
spyModeStubRegistry.register(() => ({
name: 'spymode1',
display: 'SpyMode1',
order: 1,
template: '<div class="spymode1"></div>',
}));
}
it('should show registered spy modes as tabs', () => {
registerRegularPanels();
const $el = compile();
openSpy($el);
expect($el.tabs.find('button').length).to.equal(2);
expect(getModeTabTitles($el)).to.eql(['SpyMode1', 'SpyMode2']);
});
it('should by default be on the first spy mode when opening', async () => {
registerRegularPanels();
const $el = compile();
openSpy($el);
expect($el.panel.find('.spymode1').length).to.equal(1);
});
describe('conditional spy modes', () => {
let filterOutSpy;
beforeEach(() => {
filterOutSpy = sinon.spy(() => false);
spyModeStubRegistry.register(() => ({
name: 'test',
display: 'ShouldBeFiltered',
showMode: filterOutSpy,
order: 1,
template: '<div></div>'
}));
spyModeStubRegistry.register(() => ({
name: 'test2',
display: 'ShouldNotBeFiltered',
order: 2,
template: '<div></div>'
}));
spyModeStubRegistry.register(() => ({
name: 'test3',
display: 'Test3',
order: 3,
showMode: () => true,
template: '<div></div>'
}));
$el = compile();
openSpy();
});
it('should filter out panels, that return false in showMode', () => {
expect(getModeTabTitles($el)).not.to.include('ShouldBeFiltered');
});
it('should show modes without a showMode function', () => {
expect(getModeTabTitles($el)).to.include('ShouldNotBeFiltered');
});
it('should show mods whose showMode returns true', () => {
expect(getModeTabTitles($el)).to.include('Test3');
});
it('should pass the visualization to the showMode method', () => {
expect(filterOutSpy.called).to.equal(true);
expect(filterOutSpy.getCall(0).args[0]).to.equal(vis);
});
});
describe('uiState', () => {
beforeEach(fillRegistryAndCompile);
it('should sync the active tab to the uiState', () => {
expect($scope.uiState.get('spy.mode.name', null)).to.be.null;
openSpy();
expect($scope.uiState.get('spy.mode.name', null)).to.equal('spymode1');
});
it('should sync uiState when closing the panel', () => {
openSpy();
expect($scope.uiState.get('spy.mode.name', null)).to.equal('spymode1');
$el.toggleButton.click();
expect($scope.uiState.get('spy.mode.name', null)).to.equal(null);
});
it('should sync uiState when maximizing', () => {
openSpy();
expect($scope.uiState.get('spy.mode.fill', null)).to.equal(null);
$el.maximizedButton.click(); // Maximize it initially
expect($scope.uiState.get('spy.mode.fill', false)).to.equal(true);
$el.maximizedButton.click(); // Reset maximized state again
expect($scope.uiState.get('spy.mode.fill', false)).to.equal(false);
});
it('should also reset fullscreen when closing panel', () => {
openSpy();
$el.maximizedButton.click();
expect($scope.uiState.get('spy.mode.fill', false)).to.equal(true);
$el.toggleButton.click(); // Close spy panel
expect($scope.uiState.get('spy.mode.fill', null)).to.equal(null);
});
});
});
});

View file

@ -24,7 +24,9 @@ import { EmbeddedVisualizeHandler } from './embedded_visualize_handler';
* either a date in ISO format, or a valid datetime Elasticsearch expression,
* e.g.: { min: 'now-7d/d', max: 'now' }
* @property {boolean} showSpyPanel Whether or not the spy panel should be available
* on this chart. (default: false)
* on this chart. If set to true, spy panels will only be shown if there are
* spy panels available for this specific visualization, since not every visualization
* supports all spy panels. (default: false)
* @property {boolean} append If set to true, the visualization will be appended
* to the passed element instead of replacing all its content. (default: false)
* @property {string} cssClass If specified this CSS class (or classes with space separated)

View file

@ -1,4 +1,7 @@
<div class="visualize-show-spy">
<div
class="visualize-show-spy"
ng-if="modes.length > 0"
>
<button
data-test-subj="spyToggleButton"
ng-click="toggleDisplay()"
@ -7,45 +10,51 @@
>
<span
class="kuiIcon"
ng-class="spy.mode.name ? 'fa-chevron-circle-down' : 'fa-chevron-circle-up'"
ng-class="shouldShowSpyPanel() ? 'fa-chevron-circle-down' : 'fa-chevron-circle-up'"
></span>
</button>
</div>
<div
data-test-subj="spyContainer"
class="visualize-spy-container"
ng-show="spy.mode.name"
ng-class="{ 'only': maximizedSpy || forceMaximized }"
ng-show="shouldShowSpyPanel()"
>
<div class="kuiBar kuiVerticalRhythm">
<div class="kuiBarSection">
<select
class="kuiSelect visualize-fixed-position"
ng-model="selectedModeName"
ng-change="onSpyModeChange()"
data-test-subj="spyModeSelect"
<div class="visualize-spy__tab-container euiTabs euiTabs--small">
<div role="tablist" class="visualize-spy__tabs" data-test-subj="spyModTabs">
<button
role="tab"
class="euiTab"
ng-repeat="mode in modes"
aria-selected="{{mode.name === currentMode}}"
ng-class="{'euiTab-isSelected': mode.name === currentMode}"
data-test-subj="spyModeSelect-{{::mode.name}}"
ng-click="setSpyMode(mode.name)"
>
<option
ng-repeat="mode in modes.inOrder"
value="{{mode.name}}"
data-test-subj="spyModeSelect-{{ mode.name }}"
>
{{mode.display}}
</option>
</select>
</div>
<div class="kuiBarSection">
<button class="kuiButton kuiButton--hollow" ng-click="toggleFullPage()">
<span
class="kuiIcon"
ng-class="spy.mode.fill ? 'fa-compress' : 'fa-expand'"
></span>
<span class="euiTab__content">
{{::mode.display}}
</span>
</button>
</div>
<button
data-test-subj="toggleSpyFullscreen"
aria-label="Toggle spy panel fullscreen"
class="kuiButton kuiButton--hollow"
ng-hide="forceMaximized"
ng-click="toggleMaximize()"
>
<span
class="kuiIcon"
ng-class="currentMode.fill ? 'fa-compress' : 'fa-expand'"
></span>
</button>
</div>
<div
data-test-subj="spyContentContainer"
data-spy-content-container
class="kuiVerticalRhythm"
class="kuiVerticalRhythm visualize-spy__content-container"
></div>
</div>

View file

@ -1,106 +1,181 @@
import $ from 'jquery';
import _ from 'lodash';
import { SpyModesRegistryProvider } from 'ui/registry/spy_modes';
import { uiModules } from 'ui/modules';
import spyTemplate from 'ui/visualize/spy.html';
import { PersistedState } from 'ui/persisted_state';
uiModules
.get('app/visualize')
.directive('visualizeSpy', function (Private, $compile) {
.directive('visualizeSpy', function (Private, $compile, $timeout) {
const spyModes = Private(SpyModesRegistryProvider);
const defaultMode = spyModes.inOrder[0].name;
return {
restrict: 'E',
template: spyTemplate,
scope: {
vis: '<',
searchSource: '<',
uiState: '<',
visElement: '<',
},
link: function ($scope, $el) {
// If no uiState has been passed, create a local one for this spy.
if (!$scope.uiState) $scope.uiState = new PersistedState({});
let currentSpy;
let defaultModeName;
const $container = $el.find('[data-spy-content-container]');
let fullPageSpy = _.get($scope.spy, 'mode.fill', false);
$scope.modes = spyModes;
$scope.spy.params = $scope.spy.params || {};
function getSpyObject(name) {
name = _.isUndefined(name) ? $scope.spy.mode.name : name;
fullPageSpy = (_.isNull(name)) ? false : fullPageSpy;
$scope.modes = [];
return {
name: name,
fill: fullPageSpy,
};
$scope.currentMode = null;
$scope.maximizedSpy = false;
$scope.forceMaximized = false;
function checkForcedMaximized() {
$timeout(() => {
if ($scope.visElement && $scope.visElement.height() < 180) {
$scope.forceMaximized = true;
} else {
$scope.forceMaximized = false;
}
});
}
function setSpyMode(modeName) {
if (!_.isString(modeName)) modeName = null;
$scope.spy.mode = getSpyObject(modeName);
$scope.$emit('render');
checkForcedMaximized();
/**
* Filter for modes that should actually be active for this visualization.
* This will call the showMode method of the mode, pass it the vis object.
* Depending on whether or not that returns a truthy value, it will be shown
* or not. If the method is not present, the mode will always be shown.
*/
function filterModes() {
$scope.modes = spyModes.inOrder.filter(mode =>
mode.showMode ? mode.showMode($scope.vis) : true
);
defaultModeName = $scope.modes.length > 0 ? $scope.modes[0].name : null;
}
const renderSpy = function (spyName) {
const newMode = $scope.modes.byName[spyName];
filterModes();
$scope.$watch('vis', filterModes);
// clear the current value
if (currentSpy) {
currentSpy.$container && currentSpy.$container.remove();
currentSpy.$scope && currentSpy.$scope.$destroy();
$scope.spy.mode = {};
currentSpy = null;
function syncFromUiState() {
$scope.currentMode = $scope.uiState.get('spy.mode.name');
$scope.maximizedSpy = $scope.uiState.get('spy.mode.fill');
}
/**
* Write our current state into the uiState.
* This will write the name and fill (maximized) into the uiState
* if a panel is opened (currentMode is set) or it will otherwise
* remove the spy key from the uiState.
*/
function updateUiState() {
if ($scope.currentMode) {
$scope.uiState.set('spy.mode', {
name: $scope.currentMode,
fill: $scope.maximizedSpy,
});
} else {
$scope.uiState.set('spy', null);
}
}
// no further changes
if (!newMode) return;
// Initially sync the panel state from the uiState.
syncFromUiState();
// update the spy mode and append to the container
const selectedSpyMode = getSpyObject(newMode.name);
$scope.spy.mode = selectedSpyMode;
$scope.selectedModeName = selectedSpyMode.name;
// Whenever the uiState changes, update the settings from it.
$scope.uiState.on('change', syncFromUiState);
$scope.$on('$destroy', () => $scope.uiState.off('change', syncFromUiState));
currentSpy = _.assign({
$scope: $scope.$new(),
$container: $('<div class="visualize-spy-content">').appendTo($container)
}, $scope.spy.mode);
currentSpy.$container.append($compile(newMode.template)(currentSpy.$scope));
newMode.link && newMode.link(currentSpy.$scope, currentSpy.$container);
$scope.setSpyMode = function setSpyMode(modeName) {
$scope.currentMode = modeName;
updateUiState();
$scope.$emit('render');
};
$scope.toggleDisplay = function () {
const modeName = _.get($scope.spy, 'mode.name');
setSpyMode(modeName ? null : defaultMode);
// If the spy panel is already shown (a currentMode is set),
// close the panel by setting the name to null, otherwise open the
// panel (i.e. set it to the default mode name).
if ($scope.currentMode) {
$scope.setSpyMode(null);
$scope.forceMaximized = false;
} else {
$scope.setSpyMode(defaultModeName);
checkForcedMaximized();
}
};
$scope.toggleFullPage = function () {
fullPageSpy = !fullPageSpy;
$scope.spy.mode = getSpyObject();
/**
* Should we currently show the spy panel. True if a currentMode has been set.
*/
$scope.shouldShowSpyPanel = () => {
return !!$scope.currentMode;
};
$scope.onSpyModeChange = function onSpyModeChange() {
setSpyMode($scope.selectedModeName);
/**
* Toggle maximized state of spy panel and update the UI state.
*/
$scope.toggleMaximize = function () {
$scope.maximizedSpy = !$scope.maximizedSpy;
updateUiState();
};
if ($scope.uiState) {
// sync external uiState changes
const syncUIState = () => $scope.spy.mode = $scope.uiState.get('spy.mode');
$scope.uiState.on('change', syncUIState);
$scope.$on('$destroy', () => $scope.uiState.off('change', syncUIState));
}
/**
* Whenever the maximized state changes, we also need to toggle the class
* of the visualization.
*/
$scope.$watchMulti(['maximizedSpy', 'forceMaximized'], () => {
$scope.visElement.toggleClass('spy-only', $scope.maximizedSpy || $scope.forceMaximized);
});
// re-render the spy when the name of fill modes change
$scope.$watchMulti([
'spy.mode.name',
'spy.mode.fill'
], function (newVals, oldVals) {
// update the ui state, but only if it really changes
const changedVals = newVals.filter((val) => !_.isUndefined(val)).length > 0;
if (changedVals && !_.isEqual(newVals, oldVals)) {
if ($scope.uiState) $scope.uiState.set('spy.mode', $scope.spy.mode);
/**
* Watch for changes of the currentMode. Whenever it changes, we render
* the new mode into the template. Therefore we remove the previously rendered
* mode (if existing) and compile and bind the template of the new mode.
*/
$scope.$watch('currentMode', (mode, prevMode) => {
if (mode === prevMode && (currentSpy && currentSpy.mode === mode)) {
// When the mode hasn't changed and we have already rendered it, return.
return;
}
// ensure the fill mode is synced
fullPageSpy = _.get($scope.spy, 'mode.fill', fullPageSpy);
const newMode = spyModes.byName[mode];
renderSpy(_.get($scope.spy, 'mode.name', null));
if (currentSpy) {
// If we already have a spy loaded, remove that HTML element and
// destroy the previous Angular scope.
currentSpy.$container.remove();
currentSpy.$scope.$destroy();
currentSpy = null;
}
// If we want haven't specified a new mode we won't do anything further.
if (!newMode) {
// Reset the forced maximized flag if we are about to close the panel.
$scope.forceMaximized = false;
return;
}
const contentScope = $scope.$new();
const contentContainer = $('<div class="visualize-spy-content">');
contentContainer.append($compile(newMode.template)(contentScope));
$container.append(contentContainer);
currentSpy = {
$scope: contentScope,
$container: contentContainer,
mode: mode,
};
newMode.link && newMode.link(currentSpy.$scope, currentSpy.$element);
});
}
};

View file

@ -21,4 +21,10 @@
class="visualize-chart"></div>
<visualize-legend ng-if="addLegend"></visualize-legend>
</div>
<visualize-spy ng-if="showSpyPanel"></visualize-spy>
<visualize-spy
ng-if="showSpyPanel"
vis="vis"
vis-element="visElement"
search-source="searchSource"
ui-state="uiState"
></visualize-spy>

View file

@ -26,13 +26,11 @@ uiModules
},
template: visualizationTemplate,
link: function ($scope, $el) {
const minVisChartHeight = 180;
const resizeChecker = new ResizeChecker($el);
//todo: lets make this a simple function call.
const getVisEl = jQueryGetter('.visualize-chart');
const getVisContainer = jQueryGetter('.vis-container');
const getSpyContainer = jQueryGetter('.visualize-spy-container');
$scope.addLegend = false;
@ -66,26 +64,7 @@ uiModules
return legendPositionToVisContainerClassMap[$scope.vis.params.legendPosition];
};
$scope.spy = {};
$scope.spy.mode = ($scope.uiState) ? $scope.uiState.get('spy.mode', {}) : {};
const applyClassNames = function () {
const $visEl = getVisContainer();
const $spyEl = getSpyContainer();
if (!$spyEl) return;
const fullSpy = ($scope.spy.mode && ($scope.spy.mode.fill || $scope.fullScreenSpy));
$visEl.toggleClass('spy-only', Boolean(fullSpy));
$spyEl.toggleClass('only', Boolean(fullSpy));
$timeout(function () {
if (shouldHaveFullSpy()) {
$visEl.addClass('spy-only');
$spyEl.addClass('only');
}
}, 0);
};
$scope.visElement = getVisContainer();
const loadingDelay = config.get('visualization:loadingDelay');
$scope.loadingStyle = {
@ -93,23 +72,6 @@ uiModules
'transition-delay': loadingDelay
};
function shouldHaveFullSpy() {
const $visEl = getVisEl();
if (!$visEl) return;
return ($visEl.height() < minVisChartHeight)
&& _.get($scope.spy, 'mode.fill')
&& _.get($scope.spy, 'mode.name');
}
// spy watchers
$scope.$watch('fullScreenSpy', applyClassNames);
$scope.$watchCollection('spy.mode', function () {
$scope.fullScreenSpy = shouldHaveFullSpy();
applyClassNames();
});
const Visualization = $scope.vis.type.visualization;
const visualization = new Visualization(getVisEl()[0], $scope.vis);

View file

@ -6,7 +6,7 @@
ui-state="uiState"
class="vis-editor-content"
search-source="savedObj.searchSource"
show-spy-panel="shouldShowSpyPanel()"
show-spy-panel="showSpyPanel"
/>
<visualization
ng-if="editorMode!==true"
@ -15,5 +15,5 @@
vis-data="visData"
ui-state="uiState"
search-source="savedObj.searchSource"
show-spy-panel="shouldShowSpyPanel()"
show-spy-panel="showSpyPanel"
/>

View file

@ -77,12 +77,6 @@ uiModules
$scope.editorMode = $scope.editorMode || false;
$scope.vis.editorMode = $scope.editorMode;
// spy panel is supported only with courier request handler
$scope.shouldShowSpyPanel = () => {
if ($scope.vis.type.requestHandler !== 'courier') return false;
return $scope.vis.type.requiresSearch && $scope.showSpyPanel;
};
const requestHandler = getHandler(requestHandlers, $scope.vis.type.requestHandler);
const responseHandler = getHandler(responseHandlers, $scope.vis.type.responseHandler);

View file

@ -153,11 +153,6 @@ visualize-spy {
flex: 0 0 auto;
}
.visualize-fixed-position {
position: absolute;
z-index: 999;
}
.visualize-spy-content {
position: relative;
}
@ -187,9 +182,22 @@ visualize-spy {
flex: 0 0 auto;
}
.visualize-show-spy-tab {
position: absolute;
z-index: 1000;
left: 5px;
bottom: 0px;
}
.visualize-show-spy-tab {
position: absolute;
z-index: 1000;
left: 5px;
bottom: 0px;
}
.visualize-spy__content-container {
overflow: auto;
flex: 1 1 auto;
}
.visualize-spy__tab-container {
flex: 0 0 auto;
}
.visualize-spy__tabs {
flex: 1 1 auto;
}

View file

@ -242,22 +242,22 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
async openSpyPanel() {
log.debug('openSpyPanel');
const isOpen = await testSubjects.exists('spyModeSelect');
const isOpen = await testSubjects.exists('spyContentContainer');
if (!isOpen) {
await retry.try(async () => {
await this.toggleSpyPanel();
await testSubjects.find('spyModeSelect');
await testSubjects.find('spyContentContainer');
});
}
}
async closeSpyPanel() {
log.debug('closeSpyPanel');
let isOpen = await testSubjects.exists('spyModeSelect');
let isOpen = await testSubjects.exists('spyContentContainer');
if (isOpen) {
await retry.try(async () => {
await this.toggleSpyPanel();
isOpen = await testSubjects.exists('spyModeSelect');
isOpen = await testSubjects.exists('spyContentContainer');
if (isOpen) {
throw new Error('Failed to close spy panel');
}
@ -613,7 +613,6 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
}
async selectTableInSpyPaneSelect() {
await testSubjects.click('spyModeSelect');
await testSubjects.click('spyModeSelect-table');
}
@ -660,7 +659,6 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
async getVisualizationRequest() {
log.debug('getVisualizationRequest');
await this.openSpyPanel();
await testSubjects.click('spyModeSelect');
await testSubjects.click('spyModeSelect-request');
return await testSubjects.getVisibleText('visualizationEsRequestBody');
}