You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@atlas.apache.org by sa...@apache.org on 2019/03/20 19:21:25 UTC

[atlas] branch master updated: ATLAS-3031: UI : Allow user to export the lineage in PNG formatt

This is an automated email from the ASF dual-hosted git repository.

sarath pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/atlas.git


The following commit(s) were added to refs/heads/master by this push:
     new 27916dd  ATLAS-3031: UI : Allow user to export the lineage in PNG formatt
27916dd is described below

commit 27916dd49f854678b84bde8023aee6e47cbeb7ce
Author: gutkaBinit <bi...@gmail.com>
AuthorDate: Wed Mar 20 12:20:44 2019 -0700

    ATLAS-3031: UI : Allow user to export the lineage in PNG formatt
    
    Signed-off-by: Sarath Subramanian <ss...@hortonworks.com>
---
 dashboardv2/public/css/scss/graph.scss             |   4 +
 .../js/templates/graph/LineageLayoutView_tmpl.html |  10 +-
 .../public/js/views/graph/LineageLayoutView.js     | 156 +++++++++++++++++++--
 3 files changed, 157 insertions(+), 13 deletions(-)

diff --git a/dashboardv2/public/css/scss/graph.scss b/dashboardv2/public/css/scss/graph.scss
index b72236a..3073aee 100644
--- a/dashboardv2/public/css/scss/graph.scss
+++ b/dashboardv2/public/css/scss/graph.scss
@@ -421,4 +421,8 @@ span#zoom_in {
 
         word-break: break-all;
     }
+}
+
+.hidden-svg {
+    visibility: hidden;
 }
\ No newline at end of file
diff --git a/dashboardv2/public/js/templates/graph/LineageLayoutView_tmpl.html b/dashboardv2/public/js/templates/graph/LineageLayoutView_tmpl.html
index 8e10e7f..71edcba 100644
--- a/dashboardv2/public/js/templates/graph/LineageLayoutView_tmpl.html
+++ b/dashboardv2/public/js/templates/graph/LineageLayoutView_tmpl.html
@@ -90,6 +90,11 @@
     </div>
     <div class="graph-button-group pull-right">
         <div>
+            <button data-id="saveSvg" class="disabled btn btn-action btn-gray btn-sm" title="Export to PNG">
+                <i class="fa fa-camera"></i>
+            </button>
+        </div>
+        <div>
             <button type="button" data-id="setting-toggler" class="btn btn-action btn-gray btn-sm"><i class="fa fa-gear"></i></button>
         </div>
         <div>
@@ -127,12 +132,11 @@
     <div class="fontLoader">
         <i class="fa fa-refresh fa-spin-custom"></i>
     </div>
-    <canvas width="960" height="500" style="display:none; position: absolute;"></canvas>
     <div class="legends pull-left" style="height: 25px; padding: 2px;">
         <span style="margin-right: 8px; color:#fb4200;"><i class="fa fa-circle-o fa-fw" aria-hidden="true"></i>Current Entity</span>
         <span style="margin-right: 8px; color:#df9b00;"><i class="fa fa-long-arrow-right fa-fw" aria-hidden="true"></i>Lineage</span>
         <span style="margin-right: 8px; color:#fb4200;"><i class="fa fa-long-arrow-right fa-fw" aria-hidden="true"></i>Impact</span>
     </div>
-    <!-- <svg width="100%" height="calc(100% - 28px)" preserveAspectRatio="xMidYMid meet" viewBox="0 0 854 330" enable-background="new 0 0 854 330" xml:space="preserve"></svg> -->
     <svg width="{{width}}" height="{{height}}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"></svg>
-</div>
\ No newline at end of file
+</div>
+<div class="hidden-svg"></div>
\ No newline at end of file
diff --git a/dashboardv2/public/js/views/graph/LineageLayoutView.js b/dashboardv2/public/js/views/graph/LineageLayoutView.js
index 790e8cf..de510cb 100644
--- a/dashboardv2/public/js/views/graph/LineageLayoutView.js
+++ b/dashboardv2/public/js/views/graph/LineageLayoutView.js
@@ -63,7 +63,8 @@ define(['require',
                 searchNode: '[data-id="searchNode"]',
                 nodeDetailTable: '[data-id="nodeDetailTable"]',
                 showOnlyHoverPath: '[data-id="showOnlyHoverPath"]',
-                showTooltip: '[data-id="showTooltip"]'
+                showTooltip: '[data-id="showTooltip"]',
+                saveSvg: '[data-id="saveSvg"]',
             },
             templateHelpers: function() {
                 return {
@@ -82,6 +83,7 @@ define(['require',
                 events["click " + this.ui.settingToggler] = 'onClickSettingToggler';
                 events["click " + this.ui.lineageFullscreenToggler] = 'onClickLineageFullscreenToggler';
                 events["click " + this.ui.searchToggler] = 'onClickSearchToggler';
+                events["click " + this.ui.saveSvg] = 'onClickSaveSvg';
                 return events;
             },
 
@@ -488,7 +490,8 @@ define(['require',
             createGraph: function() {
                 var that = this,
                     width = this.$('svg').width(),
-                    height = this.$('svg').height();
+                    height = this.$('svg').height(),
+                    imageObject = {};
                 this.g.nodes().forEach(function(v) {
                     var node = that.g.node(v);
                     // Round the corners of the nodes
@@ -500,6 +503,8 @@ define(['require',
                 var render = new dagreD3.render();
                 // Add our custom arrow (a hollow-point)
                 render.arrows().arrowPoint = function normal(parent, id, edge, type) {
+                    var parentNode = parent && parent[0] && parent[0][0] && parent[0][0].parentNode ? parent[0][0].parentNode : parent;
+                    d3.select(parentNode).select('path.path').attr('marker-end', "url(#" + id + ")");
                     var marker = parent.append("marker")
                         .attr("id", id)
                         .attr("viewBox", "0 0 10 10")
@@ -536,17 +541,49 @@ define(['require',
                         .attr("height", "100%")
                         .append('image')
                         .attr("xlink:href", function(d) {
+                            var that = this;
                             if (node) {
-                                return Utils.getEntityIconPath({ entityData: node });
+                                var imageIconPath = Utils.getEntityIconPath({ entityData: node }),
+                                    imagePath = ((window.location.origin + Utils.getBaseUrl(window.location.pathname)) + imageIconPath);
+
+                                var xhr = new XMLHttpRequest();
+                                xhr.responseType = 'blob';
+
+                                xhr.onload = function() {
+                                    var reader = new FileReader();
+                                    reader.onloadend = function() {
+                                        _.each(imageObject[imageIconPath], function(obj) {
+                                            obj.attr("xlink:href", reader.result);
+                                        });
+                                        imageObject[imageIconPath] = reader.result;
+                                    }
+
+                                    if (xhr.status != 404) {
+                                        reader.readAsDataURL(xhr.response);
+                                    } else {
+                                        xhr.open('GET',
+                                            Utils.getEntityIconPath({ entityData: node, errorUrl: this.responseURL }),
+                                            true);
+                                        xhr.send();
+                                    }
+                                }
+                                if (_.isUndefined(imageObject[imageIconPath])) {
+                                    // before img success
+                                    imageObject[imageIconPath] = [d3.select(that)];
+                                    xhr.open('GET', imagePath, true);
+                                    xhr.send();
+                                } else if (_.isArray(imageObject[imageIconPath])) {
+                                    // before img success
+                                    imageObject[imageIconPath].push(d3.select(that));
+                                } else {
+                                    d3.select(that).attr("xlink:href", imageObject[imageIconPath]);
+                                    return imageObject[imageIconPath];
+                                }
                             }
                         })
                         .attr("x", "4")
-                        .attr("y", currentNode ? "3" : "4")
-                        .attr("width", "40")
-                        .attr("height", "40")
-                        .on("error", function() {
-                            this.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', Utils.getEntityIconPath({ entityData: node, errorUrl: this.href.baseVal }));
-                        });
+                        .attr("y", currentNode ? "3" : "4").attr("width", "40")
+                        .attr("height", "40");
 
                     node.intersect = function(point) {
                         return dagreD3.intersect.circle(node, currentNode ? 24 : 21, point);
@@ -770,6 +807,7 @@ define(['require',
                     g: this.g,
                     guid: this.guid
                 }).init();
+                this.$el.find('[data-id="saveSvg"]').removeClass('disabled')
             },
             renderLineageTypeSearch: function() {
                 var that = this;
@@ -870,7 +908,105 @@ define(['require',
                     "attributeDefs": attributeDefs,
                     "sortBy": false
                 }));
-            }
+            },
+            onClickSaveSvg: function(e, a) {
+                var that = this;
+                var loaderTargetDiv = $(e.currentTarget).find('>i');
+                if ($(e.currentTarget).hasClass('disabled')) {
+                    Utils.notifyWarn({
+                        content: "Lineage can be downloaded once it is rendered."
+                    });
+                    return false; // return if the lineage is not loaded.
+                }
+
+                if (loaderTargetDiv.hasClass('fa-refresh')) {
+                    Utils.notifyWarn({
+                        content: "Please wait while the lineage gets downloaded"
+                    });
+                    return false; // return if the lineage is not loaded.
+                }
+
+
+                that.toggleLoader(loaderTargetDiv);
+                Utils.notifyInfo({
+                    content: "Lineage will be downloaded in a moment."
+                });
+                setTimeout(function() {
+                    var svg = that.$('svg')[0],
+                        svgClone = svg.cloneNode(true),
+                        scaleFactor = 1;
+
+                    $('.hidden-svg').html(svgClone);
+                    $(svgClone).find('>g').attr("transform", "scale(" + scaleFactor + ")");
+                    var canvasOffset = { x: 150, y: 150 },
+                        setWidth = (svgClone.getBBox().width + (canvasOffset.x)),
+                        setHeight = (svgClone.getBBox().height + (canvasOffset.y));
+                    svgClone.attributes.viewBox.value = "-10,-10," + setWidth + "," + setHeight;
+
+                    var createCanvas = document.createElement('canvas');
+                    createCanvas.id = "canvas";
+                    createCanvas.style.display = 'none';
+
+                    var body = $('body').append(createCanvas),
+                        canvas = $('canvas')[0];
+                    canvas.width = (svgClone.getBBox().width * scaleFactor) + canvasOffset.x;
+                    canvas.height = (svgClone.getBBox().height * scaleFactor) + canvasOffset.y;
+
+                    var ctx = canvas.getContext('2d'),
+                        data = (new XMLSerializer()).serializeToString(svgClone),
+                        DOMURL = window.URL || window.webkitURL || window;
+
+                    ctx.fillStyle = "#FFFFFF";
+                    ctx.fillRect(0, 0, canvas.width, canvas.height);
+                    ctx.strokeRect(0, 0, canvas.width, canvas.height);
+                    ctx.restore();
+
+                    var img = new Image(canvas.width, canvas.height);
+                    var svgBlob = new Blob([data], { type: 'image/svg+xml;base64' });
+                    var url = DOMURL.createObjectURL(svgBlob);
+
+                    img.onload = function() {
+                        try {
+                            var a = document.createElement("a");
+                            a.download = "download.png";
+                            ctx.drawImage(img, 50, 50, canvas.width, canvas.height);
+                            canvas.toBlob(function(blob) {
+                                if (!blob) {
+                                    Utils.notifyError({
+                                        content: "There was an error in downloading Lineage!"
+                                    });
+                                    return;
+                                }
+                                a.href = DOMURL.createObjectURL(blob);
+                                if (blob.size > 10000000) {
+                                    Utils.notifyWarn({
+                                        content: "The Image size is huge, please open the image in a browser!"
+                                    });
+                                }
+                                a.click();
+                                that.toggleLoader(loaderTargetDiv);
+                            }, 'image/png');
+                            $('.hidden-svg').html('');
+                            createCanvas.remove();
+
+                        } catch (err) {
+                            Utils.notifyError({
+                                content: "There was an error in downloading Lineage!"
+                            });
+                            that.toggleLoader(loaderTargetDiv);
+                        }
+
+                    };
+                    img.src = url;
+                }, 0)
+            },
+            toggleLoader: function(element) {
+                if ((element).hasClass('fa-camera')) {
+                    (element).removeClass('fa-camera').addClass("fa-spin-custom fa-refresh");
+                } else {
+                    (element).removeClass("fa-spin-custom fa-refresh").addClass('fa-camera');
+                }
+            },
         });
     return LineageLayoutView;
 });
\ No newline at end of file