Add optional data point markers and mouse-over tooltips to display values on graphs. Fixes #3514

Ensure queries are no longer executed when dashboards are closed. Fixes #3576
This commit is contained in:
Aditya Toshniwal 2018-09-05 17:25:11 +01:00 committed by Dave Page
parent 0e59be237b
commit a74b9c96c1
10 changed files with 324 additions and 63 deletions

View File

@ -11,7 +11,10 @@ Features
********
| `Feature #2927 <https://redmine.postgresql.org/issues/2927>`_ - Move all CSS into SCSS files for consistency and ease of colour maintenance etc.
| `Feature #3514 <https://redmine.postgresql.org/issues/3514>`_ - Add optional data point markers and mouse-over tooltips to display values on graphs.
Bug fixes
*********
| `Bug #3576 <https://redmine.postgresql.org/issues/3576>`_ - Ensure queries are no longer executed when dashboards are closed.

View File

@ -4,6 +4,7 @@
"axios-mock-adapter": "^1.14.1",
"babel-core": "~6.24.0",
"babel-loader": "~7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-airbnb": "^2.4.0",
"babel-preset-es2015": "~6.24.0",
"babel-preset-react": "~6.23.0",
@ -66,7 +67,7 @@
"dropzone": "^5.1.1",
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
"exports-loader": "~0.6.4",
"flotr2": "^0.1.0",
"flotr2": "git+https://github.com/EnterpriseDB/Flotr2.git",
"font-awesome": "^4.7.0",
"hard-source-webpack-plugin": "^0.4.9",
"immutability-helper": "^2.2.0",

View File

@ -165,7 +165,8 @@ define(
if (eventName == 'panelClosed') {
pgBrowser.save_current_layout(pgBrowser);
pgAdmin.Dashboard.toggleVisibility(false);
/* Pass the closed flag also */
pgAdmin.Dashboard.toggleVisibility(false, true);
} else if (eventName == 'panelVisibilityChanged') {
if (pgBrowser.tree) {
pgBrowser.save_current_layout(pgBrowser);
@ -174,8 +175,10 @@ define(
pgAdmin.Dashboard.toggleVisibility(pgBrowser.panels.dashboard.panel.isVisible());
}
// Explicitly trigger tree selected event when we add the tab.
pgBrowser.Events.trigger('pgadmin-browser:tree:selected', selectedNode,
pgBrowser.tree.itemData(selectedNode), pgBrowser.Node);
if(selectedNode.length) {
pgBrowser.Events.trigger('pgadmin-browser:tree:selected', selectedNode,
pgBrowser.tree.itemData(selectedNode), pgBrowser.Node);
}
}
}
},

View File

@ -81,7 +81,7 @@ class DashboardModule(PgAdminModule):
help_str=gettext('The number of seconds between graph samples.')
)
self.session_stats_refresh = self.dashboard_preference.register(
self.tps_stats_refresh = self.dashboard_preference.register(
'dashboards', 'tps_stats_refresh',
gettext("Transaction throughput refresh rate"), 'integer',
1, min_val=1, max_val=999999,
@ -89,7 +89,7 @@ class DashboardModule(PgAdminModule):
help_str=gettext('The number of seconds between graph samples.')
)
self.session_stats_refresh = self.dashboard_preference.register(
self.ti_stats_refresh = self.dashboard_preference.register(
'dashboards', 'ti_stats_refresh',
gettext("Tuples in refresh rate"), 'integer',
1, min_val=1, max_val=999999,
@ -97,7 +97,7 @@ class DashboardModule(PgAdminModule):
help_str=gettext('The number of seconds between graph samples.')
)
self.session_stats_refresh = self.dashboard_preference.register(
self.to_stats_refresh = self.dashboard_preference.register(
'dashboards', 'to_stats_refresh',
gettext("Tuples out refresh rate"), 'integer',
1, min_val=1, max_val=999999,
@ -105,7 +105,7 @@ class DashboardModule(PgAdminModule):
help_str=gettext('The number of seconds between graph samples.')
)
self.session_stats_refresh = self.dashboard_preference.register(
self.bio_stats_refresh = self.dashboard_preference.register(
'dashboards', 'bio_stats_refresh',
gettext("Block I/O statistics refresh rate"), 'integer',
1, min_val=1, max_val=999999,
@ -129,6 +129,23 @@ class DashboardModule(PgAdminModule):
'will be displayed on dashboards.')
)
self.graph_data_points = self.dashboard_preference.register(
'display', 'graph_data_points',
gettext("Show graph data points?"), 'boolean', False,
category_label=gettext('Display'),
help_str=gettext('If set to True, data points will be '
'visible on graph lines.')
)
self.graph_mouse_track = self.dashboard_preference.register(
'display', 'graph_mouse_track',
gettext("Show mouse hover tooltip?"), 'boolean', True,
category_label=gettext('Display'),
help_str=gettext('If set to True, tooltip will appear on mouse '
'hover on the graph lines giving the data point '
'details')
)
def get_exposed_url_endpoints(self):
"""
Returns:

View File

@ -0,0 +1,99 @@
import Flotr from 'flotr2';
export class Chart {
constructor(container, options) {
this.chartApi = Flotr;
/* Html Node where the graph goes */
this._container = container;
/* Graph library options */
this._options = {};
this._defaultOptions = {
legend: {
position: 'nw',
backgroundColor: '#D2E8FF',
},
lines: {
show: true,
lineWidth: 2,
},
shadowSize: 0,
resolution : 3,
};
this._dataset = null;
this._tooltipFormatter = null;
/* Just to store other data related to charts. Used nowhere here in the module */
this._otherData = {};
this.setOptions(options);
}
getContainer() {
return this._container;
}
getContainerDimensions() {
return {
height: this._container.clientHeight,
width: this._container.clientWidth,
};
}
getOptions() {
return this._options;
}
/* This should be changed if library changed */
setOptions(options, mergeOptions=true) {
/* If mergeOptions then merge the options, else replace existing options */
if(mergeOptions) {
this._options = {...this._defaultOptions, ...this._options, ...options};
} else {
this._options = {...this._defaultOptions, ...options};
}
}
removeOptions(optionKey) {
if(this._options[optionKey]) {
delete this._options[optionKey];
}
}
getOtherData(key) {
return this._otherData[key];
}
setOtherData(key, value) {
this._otherData[key] = value;
}
isVisible() {
let dim = this.getContainerDimensions();
return (dim.height > 0 && dim.width > 0);
}
isInPage() {
return (this._container === document.body) ? false : document.body.contains(this._container);
}
setTooltipFormatter(tooltipFormatter) {
let opt = this.getOptions();
this._tooltipFormatter = tooltipFormatter;
if(this._tooltipFormatter) {
this.setOptions({
mouse: {
...opt.mouse,
trackFormatter: this._tooltipFormatter,
},
});
}
}
draw(dataset) {
this._dataset = dataset;
if(this._container) {
Flotr.draw(this._container, this._dataset, this._options);
}
}
}

View File

@ -1,11 +1,11 @@
define('pgadmin.dashboard', [
'sources/url_for', 'sources/gettext', 'require', 'jquery', 'underscore',
'sources/pgadmin', 'backbone', 'backgrid', 'flotr2',
'sources/pgadmin', 'backbone', 'backgrid', './charting',
'pgadmin.alertifyjs', 'pgadmin.backform',
'sources/nodes/dashboard', 'backgrid.filter',
'pgadmin.browser', 'bootstrap', 'wcdocker',
], function(
url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr,
url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, charting,
Alertify, Backform, NodesDashboard
) {
@ -210,10 +210,8 @@ define('pgadmin.dashboard', [
// Load the default welcome dashboard
var url = url_for('dashboard.index');
/* Store the interval ids of the graph interval functions so that we can clear
* them when graphs are disabled
*/
this.intervalIds = {};
/* Store the chart objects and there interval ids in this store */
this.chartStore = {};
var dashboardPanel = pgBrowser.panels['dashboard'].panel;
if (dashboardPanel) {
@ -266,7 +264,7 @@ define('pgadmin.dashboard', [
!_.isUndefined(itemData.connected) &&
itemData.connected !== true
) {
self.clearIntervalId();
self.clearChartFromStore();
}
} else if (itemData && itemData._type) {
var treeHierarchy = node.getTreeNodeHierarchy(item),
@ -331,8 +329,8 @@ define('pgadmin.dashboard', [
) {
$(div).empty();
/* Clear all the interval functions of previous dashboards */
self.clearIntervalId();
/* Clear all the charts previous dashboards */
self.clearChartFromStore();
$.ajax({
url: url,
@ -356,8 +354,8 @@ define('pgadmin.dashboard', [
!_.isUndefined(itemData.connected) &&
itemData.connected !== true
) {
/* Clear all the interval functions of previous dashboards */
self.clearIntervalId();
/* Clear all the charts previous dashboards */
self.clearChartFromStore();
}
$(div).html(
'<div class="alert alert-info pg-panel-message" role="alert">' + gettext('Please connect to the selected server to view the dashboard.') + '</div>'
@ -371,7 +369,7 @@ define('pgadmin.dashboard', [
}
},
renderChartLoop: function(container, sid, did, url, options, counter, refresh) {
renderChartLoop: function(chartObj, sid, did, url, counter, refresh) {
var data = [],
dataset = [];
@ -386,24 +384,22 @@ define('pgadmin.dashboard', [
dataType: 'html',
})
.done(function(resp) {
$(container).removeClass('graph-error');
$(chartObj.getContainer()).removeClass('graph-error');
data = JSON.parse(resp);
if (!dashboardVisible)
return;
var y = 0,
x;
if (dataset.length == 0) {
if (counter == true) {
// Have we stashed initial values?
if (_.isUndefined($(container).data('counter_previous_vals'))) {
$(container).data('counter_previous_vals', data[0]);
if (_.isUndefined(chartObj.getOtherData('counter_previous_vals'))) {
chartObj.setOtherData('counter_previous_vals', data[0]);
} else {
// Create the initial data structure
for (x in data[0]) {
dataset.push({
'data': [
[0, data[0][x] - $(container).data('counter_previous_vals')[x]],
[0, data[0][x] - chartObj.getOtherData('counter_previous_vals')[x]],
],
'label': x,
});
@ -429,10 +425,10 @@ define('pgadmin.dashboard', [
} else {
// Store the current value, minus the previous one we stashed.
// It's possible the tab has been reloaded, in which case out previous values are gone
if (_.isUndefined($(container).data('counter_previous_vals')))
if (_.isUndefined(chartObj.getOtherData('counter_previous_vals')))
return;
dataset[y]['data'].unshift([0, data[0][x] - $(container).data('counter_previous_vals')[x]]);
dataset[y]['data'].unshift([0, data[0][x] - chartObj.getOtherData('counter_previous_vals')[x]]);
}
// Reset the time index to get a proper scrolling display
@ -442,7 +438,7 @@ define('pgadmin.dashboard', [
y++;
}
$(container).data('counter_previous_vals', data[0]);
chartObj.setOtherData('counter_previous_vals', data[0]);
}
// Remove uneeded elements
@ -453,12 +449,9 @@ define('pgadmin.dashboard', [
}
}
// Draw Graph, if the container still exists and has a size
var dashboardPanel = pgBrowser.panels['dashboard'].panel;
var div = dashboardPanel.layout().scene().find('.pg-panel-content');
if ($(div).find(container).length) { // Exists?
if (container.clientHeight > 0 && container.clientWidth > 0) { // Not hidden?
Flotr.draw(container, dataset, options);
if (chartObj.isInPage()) {
if (chartObj.isVisible()) {
chartObj.draw(dataset);
}
} else {
return;
@ -487,8 +480,8 @@ define('pgadmin.dashboard', [
}
}
$(container).addClass('graph-error');
$(container).html(
$(chartObj.getContainer()).addClass('graph-error');
$(chartObj.getContainer()).html(
'<div class="alert alert-' + cls + ' pg-panel-message" role="alert">' + msg + '</div>'
);
});
@ -510,15 +503,41 @@ define('pgadmin.dashboard', [
// { data: [[0, y0], [1, y1]...], label: 'Label 3', [options] }
// ]
let self = this;
if(self.intervalIds[chartName]
let self = this,
tooltipFormatter = function(refresh, currVal) {
return(`Seconds ago: ${parseInt(currVal.x * refresh)}</br>
Value: ${currVal.y}`);
};
if(self.chartStore[chartName]
&& self.old_preferences[prefName] != self.preferences[prefName]) {
self.clearIntervalId(chartName);
self.clearChartFromStore(chartName);
}
if(!self.intervalIds[chartName]) {
self.intervalIds[chartName] = self.renderChartLoop(
container, self.sid, self.did, url,
options, counter, self.preferences[prefName]
if(self.chartStore[chartName]) {
let chartObj = self.chartStore[chartName].chartObj;
chartObj.setOptions(options, false);
chartObj.setTooltipFormatter(
tooltipFormatter.bind(null, self.preferences[prefName])
);
}
if(!self.chartStore[chartName]) {
let chartObj = new charting.Chart(container, options);
chartObj.setTooltipFormatter(
tooltipFormatter.bind(null, self.preferences[prefName])
);
self.chartStore[chartName] = {
'chartObj' : chartObj,
'intervalId' : undefined,
};
self.chartStore[chartName]['intervalId'] = self.renderChartLoop(
self.chartStore[chartName]['chartObj'], self.sid, self.did, url,
counter, self.preferences[prefName]
);
}
},
@ -666,17 +685,17 @@ define('pgadmin.dashboard', [
});
},
clearIntervalId: function(intervalId) {
clearChartFromStore: function(chartName) {
var self = this;
if(!intervalId){
_.each(self.intervalIds, function(id, key) {
clearInterval(id);
delete self.intervalIds[key];
if(!chartName){
_.each(self.chartStore, function(chart, key) {
clearInterval(chart.intervalId);
delete self.chartStore[key];
});
}
else {
clearInterval(self.intervalIds[intervalId]);
delete self.intervalIds[intervalId];
clearInterval(self.chartStore[chartName].intervalId);
delete self.chartStore[chartName];
}
},
@ -737,20 +756,41 @@ define('pgadmin.dashboard', [
yaxis: {
autoscale: 1,
},
legend: {
position: 'nw',
backgroundColor: '#D2E8FF',
},
shadowSize: 0,
resolution : 5,
};
if(self.preferences.graph_data_points) {
/* Merge data points related options */
options_line = {
...options_line,
...{
points: {
show:true,
radius: 1,
hitRadius: 3,
},
},
};
}
if(self.preferences.graph_mouse_track) {
/* Merge mouse track related options */
options_line = {
...options_line,
...{
mouse: {
track:true,
position: 'sw',
},
},
};
}
if(self.preferences.show_graphs && $('#dashboard-graphs').hasClass('dashboard-hidden')) {
$('#dashboard-graphs').removeClass('dashboard-hidden');
}
else if(!self.preferences.show_graphs) {
$('#dashboard-graphs').addClass('dashboard-hidden');
self.clearIntervalId();
self.clearChartFromStore();
}
if (self.preferences.show_activity && $('#dashboard-activity').hasClass('dashboard-hidden')) {
@ -1344,8 +1384,11 @@ define('pgadmin.dashboard', [
});
}
},
toggleVisibility: function(flag) {
dashboardVisible = flag;
toggleVisibility: function(visible, closed=false) {
dashboardVisible = visible;
if(closed) {
this.clearChartFromStore();
}
},
can_take_action: function(m) {
// We will validate if user is allowed to cancel the active query

View File

@ -0,0 +1,91 @@
import $ from 'jquery';
import {Chart} from 'top/dashboard/static/js/charting';
describe('In charting related testcases', ()=> {
let chartObj = undefined,
chartDiv = undefined,
options = {};
beforeEach(()=> {
$('body').append(
'<div id="charting-test-container"></div>'
);
chartDiv = $('#charting-test-container')[0];
chartObj = new Chart(chartDiv, options);
});
it('Chart api should be defined', ()=>{
expect(chartObj.chartApi).toBeDefined();
});
it('Return the correct container', ()=>{
expect(chartObj.getContainer()).toBe(chartDiv);
});
it('Returns the container dimensions', ()=>{
let dim = chartObj.getContainerDimensions();
expect(dim.height).toBeDefined();
expect(dim.width).toBeDefined();
});
it('Check if options are set', ()=>{
chartObj.setOptions({
mouse: {
track:true,
},
});
let opt = chartObj.getOptions();
expect(opt.mouse).toBeDefined();
});
it('Check if options are set with mergeOptions false', ()=>{
let overOpt = {
mouse: {
track:true,
},
};
chartObj.setOptions(overOpt, false);
let newOptShouldBe = {...chartObj._defaultOptions, ...overOpt};
let opt = chartObj.getOptions();
expect(JSON.stringify(opt)).toEqual(JSON.stringify(newOptShouldBe));
});
it('Check if other data is set', ()=>{
chartObj.setOtherData('some_val', 1);
expect(chartObj._otherData['some_val']).toEqual(1);
});
it('Check if other data is get', ()=>{
chartObj.setOtherData('some_val', 1);
expect(chartObj.getOtherData('some_val')).toEqual(1);
});
it('Check if isVisible returns correct', ()=>{
let dimSpy = spyOn(chartObj, 'getContainerDimensions');
dimSpy.and.returnValue({
height: 1, width: 1,
});
expect(chartObj.isVisible()).toBe(true);
dimSpy.and.stub();
dimSpy.and.returnValue({
height: 0, width: 0,
});
expect(chartObj.isVisible()).toBe(false);
});
it('Check if isInPage returns correct', ()=>{
expect(chartObj.isInPage()).toBe(true);
$('body').find('#charting-test-container').remove();
expect(chartObj.isInPage()).toBe(false);
});
afterEach(()=>{
$('body').find('#charting-test-container').remove();
});
});

View File

@ -168,6 +168,7 @@ module.exports = {
loader: 'babel-loader',
options: {
presets: ['es2015', 'react'],
plugins: ['transform-object-rest-spread'],
},
},
}, {

View File

@ -58,6 +58,7 @@ module.exports = {
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'top': path.join(__dirname, './pgadmin'),
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
'alertify': path.join(__dirname, './node_modules/alertifyjs/build/alertify'),
'jquery.event.drag': path.join(__dirname, './node_modules/slickgrid/lib/jquery.event.drag-2.3.0'),
@ -75,6 +76,8 @@ module.exports = {
'slickgrid': nodeModulesDir + '/slickgrid/',
'slickgrid.plugins': nodeModulesDir + '/slickgrid/plugins/',
'slickgrid.grid': nodeModulesDir + '/slickgrid/slick.grid',
'bean': path.join(__dirname, './node_modules/flotr2/lib/bean'),
'flotr2': path.join(__dirname, './node_modules/flotr2/flotr2.amd'),
'browser': path.resolve(__dirname, 'pgadmin/browser/static/js'),
'pgadmin': sourcesDir + '/js/pgadmin',
'pgadmin.sqlfoldcode': sourcesDir + '/js/codemirror/addon/fold/pgadmin-sqlfoldcode',

View File

@ -907,7 +907,7 @@ babel-plugin-transform-jscript@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
babel-plugin-transform-object-rest-spread@^6.23.0:
babel-plugin-transform-object-rest-spread@^6.23.0, babel-plugin-transform-object-rest-spread@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
dependencies:
@ -3766,9 +3766,9 @@ flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
flotr2@^0.1.0:
"flotr2@git+https://github.com/EnterpriseDB/Flotr2.git":
version "0.1.0"
resolved "https://registry.yarnpkg.com/flotr2/-/flotr2-0.1.0.tgz#8d31b0d1b3dc46f5e399edeb7a179e075b36e036"
resolved "git+https://github.com/EnterpriseDB/Flotr2.git#dc514e32d93b2c593ab379a52de1664ee9da63e1"
flush-write-stream@^1.0.2:
version "1.0.3"