Improve the explain plan details by showing popup instead of tooltip on clicking of the specified node. Fixes #5488

This commit is contained in:
Aditya Toshniwal
2021-01-12 16:43:30 +05:30
committed by Akshay Joshi
parent 3f089f31a3
commit 6589f82e77
9 changed files with 106 additions and 147 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -186,11 +186,11 @@ under the Explain panel.
* Graphical * Graphical
Please note that *EXPLAIN VERBOSE* cannot be displayed graphically. Hover over Please note that *EXPLAIN VERBOSE* cannot be displayed graphically. Click on
an icon on the *Graphical* tab to review information about that item; a popup a node icon on the *Graphical* tab to review information about that item; a popup
window will display information about the selected object. For information on window will display on the right side with the information about the selected
JIT statistics, triggers and a summary, hover over the icon on top-right object. For information on JIT statistics, triggers and a summary, click on the
corner; a similar popup window will be displayed when appropriate. button on top-right corner; a similar popup window will be displayed when appropriate.
Use the download button on top left corner of the *Explain* canvas to download Use the download button on top left corner of the *Explain* canvas to download
the plan as an SVG file. the plan as an SVG file.

View File

@@ -22,6 +22,7 @@ Bug fixes
| `Issue #4892 <https://redmine.postgresql.org/issues/4892>`_ - Fixed an issue where pressing the back button will show another instance of the main page inside of the Query Tool tab. | `Issue #4892 <https://redmine.postgresql.org/issues/4892>`_ - Fixed an issue where pressing the back button will show another instance of the main page inside of the Query Tool tab.
| `Issue #5282 <https://redmine.postgresql.org/issues/5282>`_ - Added 'Count Rows' option to the partition sub tables. | `Issue #5282 <https://redmine.postgresql.org/issues/5282>`_ - Added 'Count Rows' option to the partition sub tables.
| `Issue #5488 <https://redmine.postgresql.org/issues/5488>`_ - Improve the explain plan details by showing popup instead of tooltip on clicking of the specified node.
| `Issue #5571 <https://redmine.postgresql.org/issues/5571>`_ - Added support for expression in exclusion constraints. | `Issue #5571 <https://redmine.postgresql.org/issues/5571>`_ - Added support for expression in exclusion constraints.
| `Issue #5875 <https://redmine.postgresql.org/issues/5875>`_ - Ensure that the 'template1' database should not be visible after pg_upgrade. | `Issue #5875 <https://redmine.postgresql.org/issues/5875>`_ - Ensure that the 'template1' database should not be visible after pg_upgrade.
| `Issue #5965 <https://redmine.postgresql.org/issues/5965>`_ - Ensure that the macro query result should be download properly. | `Issue #5965 <https://redmine.postgresql.org/issues/5965>`_ - Ensure that the macro query result should be download properly.

View File

@@ -753,6 +753,12 @@ define('pgadmin.misc.explain', [
return data; return data;
}, },
getLabel: function() {
return this.get('Schema') == undefined ?
this.get('image_text') :
(this.get('Schema') + '.' + this.get('image_text'));
},
// Draw image, its name and its tooltip // Draw image, its name and its tooltip
draw: function( draw: function(
s, xpos, ypos, pXpos, pYpos, graphContainer, toolTipContainer, s, xpos, ypos, pXpos, pYpos, graphContainer, toolTipContainer,
@@ -802,9 +808,7 @@ define('pgadmin.misc.explain', [
// Draw text below the node // Draw text below the node
var node_label = this.get('Schema') == undefined ? var node_label = this.getLabel();
this.get('image_text') :
(this.get('Schema') + '.' + this.get('image_text'));
g.multitext( g.multitext(
currentXpos + (pWIDTH / 2) + TXT_ALIGN, currentXpos + (pWIDTH / 2) + TXT_ALIGN,
currentYpos + pHEIGHT - TXT_ALIGN, currentYpos + pHEIGHT - TXT_ALIGN,
@@ -943,34 +947,29 @@ define('pgadmin.misc.explain', [
IMAGE_WIDTH, IMAGE_WIDTH,
IMAGE_HEIGHT IMAGE_HEIGHT
); );
image.attr({style: 'cursor: pointer'});
// Draw tooltip // Draw tooltip
var image_data = this.toJSON(); var image_data = this.toJSON(),
var title = '<title>'; nodeLabel = this.getLabel();
_.each(image_data, function(value, key) { image.click(() => {
if (key !== 'image' && key !== 'Plans' &&
key !== 'level' && key !== 'image' &&
key !== 'image_text' && key !== 'xpos' &&
key !== 'ypos' && key !== 'width' &&
key !== 'height') {
title += `${key}: ${value}\n`;
}
});
title += '</title>';
image.append(Snap.parse(title));
image.mouseover(() => {
// Empty the tooltip content if it has any and add new data // Empty the tooltip content if it has any and add new data
toolTipContainer.empty(); let toolTipBody = toolTipContainer.find('.details-body');
let toolTipTitle = toolTipContainer.find('.details-title');
toolTipTitle.text(nodeLabel);
toolTipBody.empty();
// Remove the title content so that we can show our custom build tooltips. // Remove the title content so that we can show our custom build tooltips.
image.node.textContent = ''; image.node.textContent = '';
var tooltip = $('<table></table>', { var tooltipTable = $(`
class: 'pgadmin-tooltip-table', <table class='pgadmin-tooltip-table table table-bordered table-noouter-border table-bottom-border table-hover'>
}).appendTo(toolTipContainer); <tbody></tbody>
</table>`
).appendTo(toolTipBody);
var tooltip = tooltipTable.find('tbody');
_.each(image_data, function(value, key) { _.each(image_data, function(value, key) {
if (key !== 'image' && key !== 'Plans' && if (key !== 'image' && key !== 'Plans' &&
key !== 'level' && key !== 'image' && key !== 'level' && key !== 'image' &&
@@ -987,42 +986,8 @@ define('pgadmin.misc.explain', [
`); `);
} }
}); });
toolTipContainer.removeClass('d-none');
var zoomFactor = graphContainer.data('zoom-factor'); toolTipBody.scrollTop(0);
// Calculate co-ordinates for tooltip
var toolTipX = ((currentXpos + pWIDTH) * zoomFactor - graphContainer.scrollLeft());
var toolTipY = ((currentYpos) * zoomFactor - graphContainer.scrollTop());
toolTipX = toolTipX < 0 ? 0 : (toolTipX);
toolTipY = toolTipY < 0 ? 0 : (toolTipY);
toolTipX = toolTipX > graphContainer.width() - toolTipContainer[0].clientWidth ? toolTipX - (toolTipContainer[0].clientWidth+(pWIDTH* zoomFactor)) : toolTipX;
toolTipY = toolTipY > graphContainer.height() - toolTipContainer[0].clientHeight ? graphContainer.height() - toolTipContainer[0].clientHeight : toolTipY;
// Show toolTip at respective x,y coordinates
toolTipContainer.css({
'opacity': '0.8',
});
toolTipContainer.css('left', toolTipX);
toolTipContainer.css('top', toolTipY);
$('.pgadmin-explain-tooltip').css('padding', '5px');
$('.pgadmin-explain-tooltip').css('border', '1px solid white');
});
// Remove tooltip when mouse is out from node's area
image.mouseout(() => {
/* Append the title again which we have removed on mouse over event, so
* that our custom tooltip should be visible.
*/
image.append(Snap.parse(title));
toolTipContainer.empty();
toolTipContainer.css({
'opacity': '0',
});
toolTipContainer.css('left', 0);
toolTipContainer.css('top', 0);
}); });
}, },
}); });
@@ -1363,15 +1328,22 @@ define('pgadmin.misc.explain', [
// Main div to be drawn all images on // Main div to be drawn all images on
var planDiv = $('<div></div>', { var planDiv = $('<div></div>', {
class: 'pgadmin-explain-container w-100 h-100 overflow-auto', class: 'pgadmin-explain-container w-100 h-100 overflow-auto',
}).appendTo(graphicalContainer),
// Div to draw tool-tip on
toolTip = $('<div></div>', {
id: 'toolTip',
class: 'pgadmin-explain-tooltip',
}).appendTo(graphicalContainer); }).appendTo(graphicalContainer);
toolTip.empty();
planDiv.data('zoom-factor', curr_zoom_factor); planDiv.data('zoom-factor', curr_zoom_factor);
var explainDetails = $(
`<div class="pgadmin-explain-details card d-none" data-bs-backdrop="false" tabindex="-1" aria-hidden="true">
<div class="card-header details-header d-flex">
<div class="details-title my-auto"></div>
<div class="ml-auto"><button class="btn btn-sm fa fa-times ml-auto details-close"/></div>
</div>
<div class="card-body details-body"></div>
</div>`
).appendTo(graphicalContainer);
explainDetails.find('.details-close').on('click', ()=>{
explainDetails.addClass('d-none');
});
var w = 0, var w = 0,
h = yMargin; h = yMargin;
@@ -1420,7 +1392,7 @@ define('pgadmin.misc.explain', [
var s = Snap(w, h), var s = Snap(w, h),
$svg = $(s.node).detach(); $svg = $(s.node).detach();
planDiv.append($svg); planDiv.append($svg);
main_plan.draw(s, w - xMargin, yMargin, planDiv, toolTip, ctx); main_plan.draw(s, w - xMargin, yMargin, planDiv, explainDetails, ctx);
var initPanelWidth = planDiv.width(); var initPanelWidth = planDiv.width();

View File

@@ -30,12 +30,15 @@ let StatisticsModel = Backbone.Model.extend({
$('.pg-explain-stats-area').removeClass('d-none'); $('.pg-explain-stats-area').removeClass('d-none');
} }
var tooltip = $('<table></table>', { var tooltipTable = $(`
class: 'pgadmin-tooltip-table', <table class='pgadmin-tooltip-table table table-bordered table-noouter-border table-bottom-border table-hover'>
}); <tbody></tbody>
</table>`
);
var tooltip = tooltipTable.find('tbody');
if (Object.keys(jit_stats).length > 0){ if (Object.keys(jit_stats).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">' + gettext('JIT:') + '</td></tr>'); tooltip.append('<tr><td colspan="2" class="label explain-tooltip">' + gettext('JIT:') + '</td></tr>');
_.each(jit_stats, function(value, key) { _.each(jit_stats, function(value, key) {
key = _.escape(key); key = _.escape(key);
value = _.escape(value); value = _.escape(value);
@@ -49,7 +52,7 @@ let StatisticsModel = Backbone.Model.extend({
} }
if (Object.keys(triggers_stats).length > 0){ if (Object.keys(triggers_stats).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">' + gettext('Triggers:') + '</td></tr>'); tooltip.append('<tr><td colspan="2" class="label explain-tooltip">' + gettext('Triggers:') + '</td></tr>');
_.each(triggers_stats, function(triggers, key_id) { _.each(triggers_stats, function(triggers, key_id) {
if (triggers instanceof Object) { if (triggers instanceof Object) {
_.each(triggers, function(value, key) { _.each(triggers, function(value, key) {
@@ -88,7 +91,7 @@ let StatisticsModel = Backbone.Model.extend({
} }
if (Object.keys(summary).length > 0){ if (Object.keys(summary).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">' + gettext('Summary:') + '</td></tr>'); tooltip.append('<tr><td colspan="2" class="label explain-tooltip">' + gettext('Summary:') + '</td></tr>');
_.each(summary, function(value, key) { _.each(summary, function(value, key) {
key = _.escape(key); key = _.escape(key);
value = _.escape(value); value = _.escape(value);
@@ -101,7 +104,7 @@ let StatisticsModel = Backbone.Model.extend({
}); });
} }
$('.pg-explain-stats-area').off('mouseover').on('mouseover', () => { $('.pg-explain-stats-area').off('click').on('click', () => {
// Empty the tooltip content if it has any and add new data // Empty the tooltip content if it has any and add new data
if (Object.keys(jit_stats).length == 0 && if (Object.keys(jit_stats).length == 0 &&
@@ -110,30 +113,15 @@ let StatisticsModel = Backbone.Model.extend({
return; return;
} }
toolTipContainer.empty(); let toolTipBody = toolTipContainer.find('.details-body');
toolTipContainer.append(tooltip); let toolTipTitle = toolTipContainer.find('.details-title');
toolTipTitle.text('Statistics');
// Show toolTip at respective x,y coordinates toolTipBody.empty();
toolTipContainer.css({ toolTipBody.append(tooltipTable);
'opacity': '0.8',
'left': '',
'right': '65px',
'top': '15px',
});
$('.pgadmin-explain-tooltip').css('padding', '5px'); toolTipContainer.removeClass('d-none');
$('.pgadmin-explain-tooltip').css('border', '1px solid white'); toolTipBody.scrollTop(0);
});
// Remove tooltip when mouse is out from node's area
$('.pg-explain-stats-area').off('mouseout').on('mouseout', () => {
toolTipContainer.empty();
toolTipContainer.css({
'opacity': '0',
'left': 0,
'top': 0,
'right': '',
});
}); });
}, },
}); });

View File

@@ -1,12 +1,29 @@
.pgadmin-explain-tooltip { .pgadmin-explain-details {
min-width: 200px;
max-width: 300px;
position: absolute; position: absolute;
opacity: 0; top: 0.25rem;
color: $popover-body-color; bottom: 0.25rem;
background-color: $popover-bg; right: 0.25rem;
border-color: $popover-border-color; border-color: $popover-border-color;
box-shadow: $popover-box-shadow; box-shadow: $popover-box-shadow;
word-break: break-all;
display: flex;
flex-direction: column;
z-index: 99;
.details-header {
padding: 0.25rem;
font-weight: unset;
} }
.details-body {
overflow: auto;
flex-grow: 1;
}
}
.sql-editor-explain { .sql-editor-explain {
.backform-tab { .backform-tab {
.tab-content { .tab-content {

View File

@@ -245,6 +245,7 @@ legend {
.table { .table {
margin-bottom: 0rem; margin-bottom: 0rem;
user-select: text;
th { th {
padding: $table-header-cell-padding $table-cell-padding; padding: $table-header-cell-padding $table-cell-padding;
} }

View File

@@ -18,10 +18,14 @@ describe('ExplainStatistics', () => {
beforeEach(function() { beforeEach(function() {
statsModel = new StatisticsModel(); statsModel = new StatisticsModel();
statsDiv = '<div class="pg-explain-stats-area btn-group d-none"></div>'; statsDiv = '<div class="pg-explain-stats-area btn-group d-none"></div>';
tooltipContainer = $('<div></div>', { tooltipContainer = $(
id: 'toolTip', `<div class="pgadmin-explain-details d-none">
class: 'pgadmin-explain-tooltip', <div class="details-header">
}); <div class="details-title"></div>
</div>
<div class="details-body"></div>
</div>`
);
}); });
describe('No Statistics', () => { describe('No Statistics', () => {
@@ -49,20 +53,12 @@ describe('ExplainStatistics', () => {
expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(false); expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(false);
}); });
it('Mouse over event should be trigger', () => { it('Mouse click event should be trigger', () => {
// Trigger mouse over event // Trigger mouse over event
var hoverEvent = new $.Event('mouseover'); var clickEvent = new $.Event('click');
$('.pg-explain-stats-area').trigger(hoverEvent); $('.pg-explain-stats-area').trigger(clickEvent);
expect(tooltipContainer.css('opacity')).toEqual('0.8'); expect(tooltipContainer.hasClass('d-none')).toBe(false);
});
it('Mouse out event should be trigger', () => {
// Trigger mouse out event
var hoverEvent = new $.Event('mouseout');
$('.pg-explain-stats-area').trigger(hoverEvent);
expect(tooltipContainer.css('opacity')).toEqual('0');
}); });
}); });
@@ -78,20 +74,12 @@ describe('ExplainStatistics', () => {
expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(false); expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(false);
}); });
it('Mouse over event should be trigger', () => { it('Mouse click event should be trigger', () => {
// Trigger mouse over event // Trigger mouse over event
var hoverEvent = new $.Event('mouseover'); var clickEvent = new $.Event('click');
$('.pg-explain-stats-area').trigger(hoverEvent); $('.pg-explain-stats-area').trigger(clickEvent);
expect(tooltipContainer.css('opacity')).toEqual('0.8'); expect(tooltipContainer.hasClass('d-none')).toBe(false);
});
it('Mouse out event should be trigger', () => {
// Trigger mouse out event
var hoverEvent = new $.Event('mouseout');
$('.pg-explain-stats-area').trigger(hoverEvent);
expect(tooltipContainer.css('opacity')).toEqual('0');
}); });
}); });
@@ -111,20 +99,12 @@ describe('ExplainStatistics', () => {
expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(false); expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(false);
}); });
it('Mouse over event should be trigger', () => { it('Mouse click event should be trigger', () => {
// Trigger mouse over event // Trigger mouse over event
var hoverEvent = new $.Event('mouseover'); var clickEvent = new $.Event('click');
$('.pg-explain-stats-area').trigger(hoverEvent); $('.pg-explain-stats-area').trigger(clickEvent);
expect(tooltipContainer.css('opacity')).toEqual('0.8'); expect(tooltipContainer.hasClass('d-none')).toBe(false);
});
it('Mouse out event should be trigger', () => {
// Trigger mouse out event
var hoverEvent = new $.Event('mouseout');
$('.pg-explain-stats-area').trigger(hoverEvent);
expect(tooltipContainer.css('opacity')).toEqual('0');
}); });
}); });
}); });