diff --git a/web/pgadmin/misc/static/explain/js/explain.js b/web/pgadmin/misc/static/explain/js/explain.js index 9ee2d6e..8e0a04b 100644 --- a/web/pgadmin/misc/static/explain/js/explain.js +++ b/web/pgadmin/misc/static/explain/js/explain.js @@ -1,9 +1,12 @@ define('pgadmin.misc.explain', [ 'sources/url_for', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin', 'backbone', 'snapsvg', 'explain_statistics', -], function(url_for, $, _, S, pgAdmin, Backbone, Snap, StatisticsModel) { + 'svg_downloader', +], function(url_for, $, _, S, pgAdmin, Backbone, Snap, StatisticsModel, + svgDownloader) { pgAdmin = pgAdmin || window.pgAdmin || {}; + svgDownloader = svgDownloader.default; // Snap.svg plug-in to write multitext as image name Snap.plugin(function(Snap, Element, Paper) { @@ -565,70 +568,88 @@ define('pgadmin.misc.explain', [ }); } - // Draw the actual image for current node - var image = g.image( - pgExplain.prefix + this.get('image'), - currentXpos + (pWIDTH - IMAGE_WIDTH) / 2, - currentYpos + (pHEIGHT - IMAGE_HEIGHT) / 2, - IMAGE_WIDTH, - IMAGE_HEIGHT - ); + var that = this; + /* This function is a callback function called when we load any svg file + * using Snap. In this function we append the SVG binary data to the new + * temporary Snap object and then embedded it to the original Snap() object. + */ + function onSVGLoaded(data){ + var svg_image = Snap(); + svg_image.append(data); + + // Draw the actual image for current node + var image = g.image( + svg_image.toDataURL(), + currentXpos + (pWIDTH - IMAGE_WIDTH) / 2, + currentYpos + (pHEIGHT - IMAGE_HEIGHT) / 2, + IMAGE_WIDTH, + IMAGE_HEIGHT + ); - // Draw tooltip - var image_data = this.toJSON(); - image.mouseover(() => { - - // Empty the tooltip content if it has any and add new data - toolTipContainer.empty(); - var tooltip = $('
', { - class: 'pgadmin-tooltip-table', - }).appendTo(toolTipContainer); - _.each(image_data, function(value, key) { - if (key !== 'image' && key !== 'Plans' && - key !== 'level' && key !== 'image' && - key !== 'image_text' && key !== 'xpos' && - key !== 'ypos' && key !== 'width' && - key !== 'height') { - tooltip.append('' + key + '' + value + ''); - } - }); - var zoomFactor = graphContainer.data('zoom-factor'); + // This attribute is required to download the file as SVG image. + s.parent().attr({'xmlns:xlink':'http://www.w3.org/1999/xlink'}); + + // Draw tooltip + var image_data = that.toJSON(); + image.mouseover(() => { + + // Empty the tooltip content if it has any and add new data + toolTipContainer.empty(); + var tooltip = $('
', { + class: 'pgadmin-tooltip-table', + }).appendTo(toolTipContainer); + _.each(image_data, function(value, key) { + if (key !== 'image' && key !== 'Plans' && + key !== 'level' && key !== 'image' && + key !== 'image_text' && key !== 'xpos' && + key !== 'ypos' && key !== 'width' && + key !== 'height') { + tooltip.append('' + key + '' + value + ''); + } + }); - // Calculate co-ordinates for tooltip - var toolTipX = ((currentXpos + pWIDTH) * zoomFactor - graphContainer.scrollLeft()); - var toolTipY = ((currentYpos + pHEIGHT) * zoomFactor - graphContainer.scrollTop()); + var zoomFactor = graphContainer.data('zoom-factor'); - // Recalculate x.y if tooltip is going out of screen - if (graphContainer.width() < (toolTipX + toolTipContainer[0].clientWidth)) - toolTipX -= (toolTipContainer[0].clientWidth + (pWIDTH * zoomFactor)); - //if(document.children[0].clientHeight < (toolTipY + toolTipContainer[0].clientHeight)) - if (graphContainer.height() < (toolTipY + toolTipContainer[0].clientHeight)) - toolTipY -= (toolTipContainer[0].clientHeight + ((pHEIGHT / 2) * zoomFactor)); + // Calculate co-ordinates for tooltip + var toolTipX = ((currentXpos + pWIDTH) * zoomFactor - graphContainer.scrollLeft()); + var toolTipY = ((currentYpos + pHEIGHT) * zoomFactor - graphContainer.scrollTop()); - toolTipX = toolTipX < 0 ? 0 : (toolTipX); - toolTipY = toolTipY < 0 ? 0 : (toolTipY); + // Recalculate x.y if tooltip is going out of screen + if (graphContainer.width() < (toolTipX + toolTipContainer[0].clientWidth)) + toolTipX -= (toolTipContainer[0].clientWidth + (pWIDTH * zoomFactor)); + //if(document.children[0].clientHeight < (toolTipY + toolTipContainer[0].clientHeight)) + if (graphContainer.height() < (toolTipY + toolTipContainer[0].clientHeight)) + toolTipY -= (toolTipContainer[0].clientHeight + ((pHEIGHT / 2) * zoomFactor)); - // Show toolTip at respective x,y coordinates - toolTipContainer.css({ - 'opacity': '0.8', - }); - toolTipContainer.css('left', toolTipX); - toolTipContainer.css('top', toolTipY); + toolTipX = toolTipX < 0 ? 0 : (toolTipX); + toolTipY = toolTipY < 0 ? 0 : (toolTipY); - $('.pgadmin-explain-tooltip').css('padding', '5px'); - $('.pgadmin-explain-tooltip').css('border', '1px solid white'); - }); + // Show toolTip at respective x,y coordinates + toolTipContainer.css({ + 'opacity': '0.8', + }); + toolTipContainer.css('left', toolTipX); + toolTipContainer.css('top', toolTipY); - // Remove tooltip when mouse is out from node's area - image.mouseout(() => { - toolTipContainer.empty(); - toolTipContainer.css({ - 'opacity': '0', + $('.pgadmin-explain-tooltip').css('padding', '5px'); + $('.pgadmin-explain-tooltip').css('border', '1px solid white'); }); - toolTipContainer.css('left', 0); - toolTipContainer.css('top', 0); - }); + + // Remove tooltip when mouse is out from node's area + image.mouseout(() => { + toolTipContainer.empty(); + toolTipContainer.css({ + 'opacity': '0', + }); + toolTipContainer.css('left', 0); + toolTipContainer.css('top', 0); + }); + } + + var svg_file = pgExplain.prefix + this.get('image'); + // Load the SVG file for explain plan + Snap.load(svg_file, onSVGLoaded); // Draw text below the node var node_label = this.get('Schema') == undefined ? @@ -801,6 +822,14 @@ define('pgadmin.misc.explain', [ }).appendTo(zoomArea).append( $('', { class: 'fa fa-search-minus', + })), + downloadBtn = $('', { + class: 'btn btn-secondary pg-explain-zoom-btn badge', + title: 'Download', + tabindex: 0, + }).appendTo(zoomArea).append( + $('', { + class: 'fa fa-download', })); var statsArea = $('
', { @@ -919,6 +948,14 @@ define('pgadmin.misc.explain', [ planDiv.data('zoom-factor', curr_zoom_factor); zoomToNormal.trigger('blur'); }); + + downloadBtn.on('click', function() { + var s = Snap('.pgadmin-explain-container svg'); + var today = new Date(); + var filename = 'explain_plan_' + today.getTime() + '.svg'; + svgDownloader.downloadSVG(s.toString(), filename); + downloadBtn.trigger('blur'); + }); }); }, diff --git a/web/pgadmin/misc/static/explain/js/svg_downloader.js b/web/pgadmin/misc/static/explain/js/svg_downloader.js new file mode 100644 index 0000000..5e2a5d8 --- /dev/null +++ b/web/pgadmin/misc/static/explain/js/svg_downloader.js @@ -0,0 +1,21 @@ +let svgDownloader = { + blobURL: function(content, contentType) { + var blob = new Blob([content], {type: contentType}); + return (window.URL || window.webkitURL).createObjectURL(blob); + }, + + downloadSVG: function(content, fileName) { + // Safari xlink NS issue fix + content = content.replace(/NS\d+:href/gi, 'xlink:href'); + + var svgURL = this.blobURL(content, 'image/svg+xml'); + var newElement = document.createElement('a'); + newElement.href = svgURL; + newElement.setAttribute('download', fileName); + document.body.appendChild(newElement); + newElement.click(); + document.body.removeChild(newElement); + }, +}; + +export default svgDownloader;