Integrate the graphical explain module in the Query Editor.
Added few TODO list for the graphical explain module by Ashesh.pull/3/head
							
								
								
									
										8
									
								
								TODO.txt
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -37,3 +37,11 @@ Restore Object
 | 
			
		|||
 | 
			
		||||
List down the objects within the backup file, and allow the user to
 | 
			
		||||
select/deselect the only objects, which user may want to restore.
 | 
			
		||||
 | 
			
		||||
Graphical Explain
 | 
			
		||||
-----------------
 | 
			
		||||
* A better zooming/scaling functionality. A minimap for the explain will be
 | 
			
		||||
  very nice.
 | 
			
		||||
* Explanation on the statistic for the graphical explain plan.
 | 
			
		||||
* Arrow colouring based on the percentage of the cost, and time taken on each
 | 
			
		||||
  explain node.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,24 +9,68 @@
 | 
			
		|||
 | 
			
		||||
"""A blueprint module providing utility functions for the application."""
 | 
			
		||||
 | 
			
		||||
from flask import url_for, render_template
 | 
			
		||||
 | 
			
		||||
import config
 | 
			
		||||
from pgadmin.utils import PgAdminModule
 | 
			
		||||
import pgadmin.utils.driver as driver
 | 
			
		||||
 | 
			
		||||
MODULE_NAME = 'misc'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MiscModule(PgAdminModule):
 | 
			
		||||
 | 
			
		||||
    def get_own_javascripts(self):
 | 
			
		||||
        return [{
 | 
			
		||||
            'name': 'pgadmin.misc.explain',
 | 
			
		||||
            'path': url_for('misc.index') + 'explain/explain',
 | 
			
		||||
            'preloaded': False
 | 
			
		||||
        }, {
 | 
			
		||||
            'name': 'snap.svg',
 | 
			
		||||
            'path': url_for(
 | 
			
		||||
                'misc.static', filename='explain/js/' + (
 | 
			
		||||
                    'snap.svg' if config.DEBUG else 'snap.svg-min'
 | 
			
		||||
                )),
 | 
			
		||||
            'preloaded': False
 | 
			
		||||
        }]
 | 
			
		||||
 | 
			
		||||
    def get_own_stylesheets(self):
 | 
			
		||||
        stylesheets = []
 | 
			
		||||
        stylesheets.append(
 | 
			
		||||
            url_for('misc.static', filename='explain/css/explain.css')
 | 
			
		||||
        )
 | 
			
		||||
        return stylesheets
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Initialise the module
 | 
			
		||||
blueprint = PgAdminModule(
 | 
			
		||||
    MODULE_NAME, __name__, url_prefix=''
 | 
			
		||||
)
 | 
			
		||||
blueprint = MiscModule(MODULE_NAME, __name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
##########################################################################
 | 
			
		||||
# A special URL used to "ping" the server
 | 
			
		||||
##########################################################################
 | 
			
		||||
@blueprint.route("/")
 | 
			
		||||
def index():
 | 
			
		||||
    return ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
##########################################################################
 | 
			
		||||
# A special URL used to "ping" the server
 | 
			
		||||
##########################################################################
 | 
			
		||||
@blueprint.route("/ping", methods=('get', 'post'))
 | 
			
		||||
def ping():
 | 
			
		||||
    """Generate a "PING" response to indicate that the server is alive."""
 | 
			
		||||
    driver.ping()
 | 
			
		||||
 | 
			
		||||
    return "PING"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.route("/explain/explain.js")
 | 
			
		||||
def explain_js():
 | 
			
		||||
    """
 | 
			
		||||
    explain_js()
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        javascript for the explain module
 | 
			
		||||
    """
 | 
			
		||||
    return render_template("explain/js/explain.js")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
.pg-explain-zoom-area {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 5px;
 | 
			
		||||
    left: 5px;
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pg-explain-zoom-btn {
 | 
			
		||||
    top: 5px;
 | 
			
		||||
    min-width: 25px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    border: 1px solid transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pg-explain-zoom-area:hover {
 | 
			
		||||
   opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.explain-tooltip {
 | 
			
		||||
  display: table-cell;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  line-height: 10px !important;
 | 
			
		||||
  padding: 2px !important;
 | 
			
		||||
  font-size: small;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
td.explain-tooltip-val {
 | 
			
		||||
  display: table-cell;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
  font-size: smaller;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pgadmin-explain-tooltip {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  padding:5px;
 | 
			
		||||
  border: 1px solid white;
 | 
			
		||||
  opacity:0;
 | 
			
		||||
  color: cornsilk;
 | 
			
		||||
  background-color: #010125;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pgadmin-tooltip-table {
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
  border-spacing: 1px;
 | 
			
		||||
  top: auto;
 | 
			
		||||
  left: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pgadmin-explain-container {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
| 
		 After Width: | Height: | Size: 574 B  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1006 B  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 685 B  | 
| 
		 After Width: | Height: | Size: 334 B  | 
| 
		 After Width: | Height: | Size: 1.9 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.6 KiB  | 
| 
		 After Width: | Height: | Size: 218 B  | 
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
| 
		 After Width: | Height: | Size: 1.4 KiB  | 
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
		 After Width: | Height: | Size: 1.4 KiB  | 
| 
		 After Width: | Height: | Size: 1.4 KiB  | 
| 
		 After Width: | Height: | Size: 1.4 KiB  | 
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
| 
		 After Width: | Height: | Size: 498 B  | 
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
		 After Width: | Height: | Size: 1.0 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.6 KiB  | 
| 
		 After Width: | Height: | Size: 980 B  | 
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.7 KiB  | 
| 
		 After Width: | Height: | Size: 1.6 KiB  | 
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
| 
		 After Width: | Height: | Size: 218 B  | 
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
		 After Width: | Height: | Size: 1.0 KiB  | 
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 After Width: | Height: | Size: 913 B  | 
| 
		 After Width: | Height: | Size: 2.0 KiB  | 
| 
		 After Width: | Height: | Size: 1.9 KiB  | 
| 
		 After Width: | Height: | Size: 725 B  | 
| 
		 After Width: | Height: | Size: 423 B  | 
| 
		 After Width: | Height: | Size: 996 B  | 
| 
		 After Width: | Height: | Size: 1017 B  | 
| 
		 After Width: | Height: | Size: 612 B  | 
| 
		 After Width: | Height: | Size: 793 B  | 
| 
		 After Width: | Height: | Size: 662 B  | 
| 
						 | 
				
			
			@ -0,0 +1,691 @@
 | 
			
		|||
define (
 | 
			
		||||
  'pgadmin.misc.explain',
 | 
			
		||||
  ['jquery', 'underscore', 'underscore.string', 'pgadmin', 'backbone', 'snap.svg'],
 | 
			
		||||
  function($, _, S, pgAdmin, Backbone, Snap) {
 | 
			
		||||
 | 
			
		||||
pgAdmin = pgAdmin || window.pgAdmin || {};
 | 
			
		||||
var pgExplain = pgAdmin.Explain;
 | 
			
		||||
 | 
			
		||||
// Snap.svg plug-in to write multitext as image name
 | 
			
		||||
Snap.plugin(function (Snap, Element, Paper, glob) {
 | 
			
		||||
  Paper.prototype.multitext = function (x, y, txt, max_width, attributes) {
 | 
			
		||||
    var svg = Snap(),
 | 
			
		||||
        abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
 | 
			
		||||
        isWordBroken = false,
 | 
			
		||||
        temp = svg.text(0, 0, abc);
 | 
			
		||||
 | 
			
		||||
    temp.attr(attributes);
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
     * Find letter width in pixels and
 | 
			
		||||
     * index from where the text should be broken
 | 
			
		||||
     */
 | 
			
		||||
    var letter_width = temp.getBBox().width / abc.length,
 | 
			
		||||
        word_break_index = Math.round((max_width / letter_width)) - 1;
 | 
			
		||||
 | 
			
		||||
    svg.remove();
 | 
			
		||||
 | 
			
		||||
    var words = txt.split(" "),
 | 
			
		||||
        width_so_far = 0,
 | 
			
		||||
        lines=[], curr_line = '',
 | 
			
		||||
        /*
 | 
			
		||||
         * Function to divide string into multiple lines
 | 
			
		||||
         * and store them in an array if it size crosses
 | 
			
		||||
         * the max-width boundary.
 | 
			
		||||
         */
 | 
			
		||||
        splitTextInMultiLine = function(leading, so_far, line) {
 | 
			
		||||
          var l = line.length,
 | 
			
		||||
              res = [];
 | 
			
		||||
 | 
			
		||||
          if (l == 0)
 | 
			
		||||
            return res;
 | 
			
		||||
 | 
			
		||||
          if (so_far && (so_far + (l * letter_width) > max_width)) {
 | 
			
		||||
            res.push(leading);
 | 
			
		||||
            res = res.concat(splitTextInMultiLine('', 0, line));
 | 
			
		||||
          } else if (so_far) {
 | 
			
		||||
            res.push(leading + ' ' + line);
 | 
			
		||||
          } else {
 | 
			
		||||
            if (leading)
 | 
			
		||||
                res.push(leading);
 | 
			
		||||
            if (line.length > word_break_index + 1)
 | 
			
		||||
                res.push(line.slice(0, word_break_index) + '-');
 | 
			
		||||
            else
 | 
			
		||||
                res.push(line);
 | 
			
		||||
            res = res.concat(splitTextInMultiLine('', 0, line.slice(word_break_index)));
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return res;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    for (var i = 0; i < words.length; i++) {
 | 
			
		||||
      var tmpArr = splitTextInMultiLine(
 | 
			
		||||
            curr_line, width_so_far, words[i]
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
      if (curr_line) {
 | 
			
		||||
        lines = lines.slice(0, lines.length - 2);
 | 
			
		||||
      }
 | 
			
		||||
      lines = lines.concat(tmpArr);
 | 
			
		||||
      curr_line = lines[lines.length - 1];
 | 
			
		||||
      width_so_far = (curr_line.length * letter_width);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create multiple tspan for each string in array
 | 
			
		||||
    var t = this.text(x,y,lines).attr(attributes);
 | 
			
		||||
    t.selectAll("tspan:nth-child(n+2)").attr({
 | 
			
		||||
      dy: "1.2em",
 | 
			
		||||
      x: x
 | 
			
		||||
    });
 | 
			
		||||
    return t;
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (pgAdmin.Explain)
 | 
			
		||||
    return pgAdmin.Explain;
 | 
			
		||||
 | 
			
		||||
var pgExplain = pgAdmin.Explain = {
 | 
			
		||||
   // Prefix path where images are stored
 | 
			
		||||
   prefix: '{{ url_for('misc.static', filename='explain/img') }}/'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * A map which is used to fetch the image to be drawn and
 | 
			
		||||
 * text which will appear below it
 | 
			
		||||
 */
 | 
			
		||||
var imageMapper = {
 | 
			
		||||
    "Aggregate" : {
 | 
			
		||||
        "image":"ex_aggregate.png", "image_text":"Aggregate"
 | 
			
		||||
    },
 | 
			
		||||
    'Append' : {
 | 
			
		||||
        "image":"ex_append.png","image_text":"Append"
 | 
			
		||||
    },
 | 
			
		||||
    "Bitmap Index Scan" : function(data) {
 | 
			
		||||
        return {
 | 
			
		||||
            "image":"ex_bmp_index.png", "image_text":data['Index Name']
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    "Bitmap Heap Scan" : function(data) {
 | 
			
		||||
  return {"image":"ex_bmp_heap.png","image_text":data['Relation Name']};
 | 
			
		||||
},
 | 
			
		||||
"BitmapAnd" : {"image":"ex_bmp_and.png","image_text":"Bitmap AND"},
 | 
			
		||||
"BitmapOr" : {"image":"ex_bmp_or.png","image_text":"Bitmap OR"},
 | 
			
		||||
"CTE Scan" : {"image":"ex_cte_scan.png","image_text":"CTE Scan"},
 | 
			
		||||
"Function Scan" : {"image":"ex_result.png","image_text":"Function Scan"},
 | 
			
		||||
"Foreign Scan" : {"image":"ex_foreign_scan.png","image_text":"Foreign Scan"},
 | 
			
		||||
"Gather" : {"image":"ex_gather_motion.png","image_text":"Gather"},
 | 
			
		||||
"Group" : {"image":"ex_group.png","image_text":"Group"},
 | 
			
		||||
"GroupAggregate": {"image":"ex_aggregate.png","image_text":"Group Aggregate"},
 | 
			
		||||
"Hash" : {"image":"ex_hash.png","image_text":"Hash"},
 | 
			
		||||
"Hash Join": function(data) {
 | 
			
		||||
  if (!data['Join Type']) return {"image":"ex_join.png","image_text":"Join"};
 | 
			
		||||
  switch(data['Join Type']) {
 | 
			
		||||
    case 'Anti': return {"image":"ex_hash_anti_join.png","image_text":"Hash Anti Join"};
 | 
			
		||||
    case 'Semi': return {"image":"ex_hash_semi_join.png","image_text":"Hash Semi Join"};
 | 
			
		||||
    default: return {"image":"ex_hash.png","image_text":String("Hash " + data['Join Type'] + " Join" )};
 | 
			
		||||
  }
 | 
			
		||||
},
 | 
			
		||||
"HashAggregate" : {"image":"ex_aggregate.png","image_text":"Hash Aggregate"},
 | 
			
		||||
"Index Only Scan" : function(data) {
 | 
			
		||||
  return {"image":"ex_index_only_scan.png","image_text":data['Index Name']};
 | 
			
		||||
},
 | 
			
		||||
"Index Scan" : function(data) {
 | 
			
		||||
  return {"image":"ex_index_scan.png","image_text":data['Index Name']};
 | 
			
		||||
},
 | 
			
		||||
"Index Scan Backword" : {"image":"ex_index_scan.png","image_text":"Index Backward Scan"},
 | 
			
		||||
"Limit" : {"image":"ex_limit.png","image_text":"Limit"},
 | 
			
		||||
"LockRows" : {"image":"ex_lock_rows.png","image_text":"Lock Rows"},
 | 
			
		||||
"Materialize" : {"image":"ex_materialize.png","image_text":"Materialize"},
 | 
			
		||||
"Merge Append": {"image":"ex_merge_append.png","image_text":"Merge Append"},
 | 
			
		||||
"Merge Join": function(data) {
 | 
			
		||||
  switch(data['Join Type']) {
 | 
			
		||||
    case 'Anti': return {"image":"ex_merge_anti_join.png","image_text":"Merge Anti Join"};
 | 
			
		||||
    case 'Semi': return {"image":"ex_merge_semi_join.png","image_text":"Merge Semi Join"};
 | 
			
		||||
    default: return {"image":"ex_merge.png","image_text":String("Merge " + data['Join Type'] + " Join" )};
 | 
			
		||||
  }
 | 
			
		||||
},
 | 
			
		||||
"ModifyTable" : function(data) {
 | 
			
		||||
  switch (data['Operaton']) {
 | 
			
		||||
    case "insert": return { "image":"ex_insert.png",
 | 
			
		||||
                            "image_text":"Insert"
 | 
			
		||||
                           };
 | 
			
		||||
    case "update": return {"image":"ex_update.png","image_text":"Update"};
 | 
			
		||||
    case "Delete": return {"image":"ex_delete.png","image_text":"Delete"};
 | 
			
		||||
  }
 | 
			
		||||
},
 | 
			
		||||
'Nested Loop' : function(data) {
 | 
			
		||||
  switch(data['Join Type']) {
 | 
			
		||||
    case 'Anti': return {"image":"ex_nested_loop_anti_join.png","image_text":"Nested Loop Anti Join"};
 | 
			
		||||
    case 'Semi': return {"image":"ex_nested_loop_semi_join.png","image_text":"Nested Loop Semi Join"};
 | 
			
		||||
    default: return {"image":"ex_nested.png","image_text":"Nested Loop " + data['Join Type'] + " Join"};
 | 
			
		||||
  }
 | 
			
		||||
},
 | 
			
		||||
"Recursive Union" : {"image":"ex_recursive_union.png","image_text":"Recursive Union"},
 | 
			
		||||
"Result" : {"image":"ex_result.png","image_text":"Result"},
 | 
			
		||||
"Sample Scan" : {"image":"ex_scan.png","image_text":"Sample Scan"},
 | 
			
		||||
"Scan" : {"image":"ex_scan.png","image_text":"Scan"},
 | 
			
		||||
"Seek" : {"image":"ex_seek.png","image_text":"Seek"},
 | 
			
		||||
"SetOp" : function(data) {
 | 
			
		||||
  var strategy = data['Strategy'],
 | 
			
		||||
      command = data['Command'];
 | 
			
		||||
 | 
			
		||||
  if(strategy == "Hashed") {
 | 
			
		||||
    if(command.startsWith("Intersect")) {
 | 
			
		||||
      if(command == "Intersect All")
 | 
			
		||||
        return {"image":"ex_hash_setop_intersect_all.png","image_text":"Hashed Intersect All"};
 | 
			
		||||
      return {"image":"ex_hash_setop_intersect.png","image_text":"Hashed Intersect"};
 | 
			
		||||
    }
 | 
			
		||||
    else if (command.startsWith("Except")) {
 | 
			
		||||
      if(command == "Except All")
 | 
			
		||||
        return {"image":"ex_hash_setop_except_all.png","image_text":"Hashed Except All"};
 | 
			
		||||
      return {"image":"ex_hash_setop_except.png","image_text":"Hash Except"};
 | 
			
		||||
    }
 | 
			
		||||
    return {"image":"ex_hash_setop_unknown.png","image_text":"Hashed SetOp Unknown"};
 | 
			
		||||
  }
 | 
			
		||||
  return {"image":"ex_setop.png","image_text":"SetOp"};
 | 
			
		||||
},
 | 
			
		||||
"Seq Scan": function(data) {
 | 
			
		||||
  return {"image":"ex_scan.png","image_text":data['Relation Name']};
 | 
			
		||||
},
 | 
			
		||||
"Subquery Scan" : {"image":"ex_subplan.png","image_text":"SubQuery Scan"},
 | 
			
		||||
"Sort" : {"image":"ex_sort.png","image_text":"Sort"},
 | 
			
		||||
"Tid Scan" : {"image":"ex_tid_scan.png","image_text":"Tid Scan"},
 | 
			
		||||
"Unique" : {"image":"ex_unique.png","image_text":"Unique"},
 | 
			
		||||
"Values Scan" : {"image":"ex_values_scan.png","image_text":"Values Scan"},
 | 
			
		||||
"WindowAgg" : {"image":"ex_window_aggregate.png","image_text":"Window Aggregate"},
 | 
			
		||||
"WorkTable Scan" : {"image":"ex_worktable_scan.png","image_text":"WorkTable Scan"},
 | 
			
		||||
"Undefined" : {"image":"ex_unknown.png","image_text":"Undefined"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Some predefined constants used to calculate image location and its border
 | 
			
		||||
var pWIDTH = pHEIGHT = 100.
 | 
			
		||||
    IMAGE_WIDTH = IMAGE_HEIGHT = 50;
 | 
			
		||||
var offsetX = 200,
 | 
			
		||||
    offsetY = 60;
 | 
			
		||||
var ARROW_WIDTH = 10,
 | 
			
		||||
    ARROW_HEIGHT = 10,
 | 
			
		||||
    DEFAULT_ARROW_SIZE = 2;
 | 
			
		||||
var TXT_ALLIGN = 5,
 | 
			
		||||
    TXT_SIZE = "15px";
 | 
			
		||||
var TOTAL_WIDTH = undefined,
 | 
			
		||||
    TOTAL_HEIGHT = undefined;
 | 
			
		||||
var xMargin = 25,
 | 
			
		||||
    yMargin = 25;
 | 
			
		||||
var MIN_ZOOM_FACTOR = 0.01,
 | 
			
		||||
    MAX_ZOOM_FACTOR = 2,
 | 
			
		||||
    INIT_ZOOM_FACTOR = 1;
 | 
			
		||||
    ZOOM_RATIO = 0.05;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Backbone model for each plan property of input JSON object
 | 
			
		||||
var PlanModel = Backbone.Model.extend({
 | 
			
		||||
    defaults: {
 | 
			
		||||
        "Plans": [],
 | 
			
		||||
        level: [],
 | 
			
		||||
        "image": undefined,
 | 
			
		||||
        "image_text": undefined,
 | 
			
		||||
        xpos: undefined,
 | 
			
		||||
        ypos: undefined,
 | 
			
		||||
        width: pWIDTH,
 | 
			
		||||
        height: pHEIGHT
 | 
			
		||||
    },
 | 
			
		||||
    parse: function(data) {
 | 
			
		||||
        var idx = 1,
 | 
			
		||||
            lvl = data.level = data.level || [idx],
 | 
			
		||||
            plans = [],
 | 
			
		||||
            node_type = data['Node Type'],
 | 
			
		||||
            // Calculating relative xpos of current node from top node
 | 
			
		||||
            xpos = data.xpos = data.xpos - pWIDTH,
 | 
			
		||||
            // Calculating relative ypos of current node from top node
 | 
			
		||||
            ypos = data.ypos,
 | 
			
		||||
            maxChildWidth = 0;
 | 
			
		||||
 | 
			
		||||
        data['width'] = pWIDTH;
 | 
			
		||||
        data['height'] = pHEIGHT;
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
         * calculating xpos, ypos, width and height if current node is a subplan
 | 
			
		||||
         */
 | 
			
		||||
        if (data['Parent Relationship'] === "SubPlan") {
 | 
			
		||||
            data['width'] += (xMargin * 2) + (xMargin / 2);
 | 
			
		||||
            data['height'] += (yMargin * 2);
 | 
			
		||||
            data['ypos'] += yMargin;
 | 
			
		||||
            xpos -= xMargin;
 | 
			
		||||
            ypos += yMargin;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(node_type.startsWith("(slice"))
 | 
			
		||||
            node_type = node_type.substring(0,7);
 | 
			
		||||
 | 
			
		||||
        // Get the image information for current node
 | 
			
		||||
        var mapperObj = (_.isFunction(imageMapper[node_type]) &&
 | 
			
		||||
                imageMapper[node_type].apply(undefined, [data])) ||
 | 
			
		||||
                imageMapper[node_type] || 'Undefined';
 | 
			
		||||
 | 
			
		||||
        data["image"] = mapperObj["image"];
 | 
			
		||||
        data["image_text"] = mapperObj["image_text"];
 | 
			
		||||
 | 
			
		||||
        // Start calculating xpos, ypos, width and height for child plans if any
 | 
			
		||||
        if ('Plans' in data) {
 | 
			
		||||
 | 
			
		||||
            data['width'] += offsetX;
 | 
			
		||||
 | 
			
		||||
            _.each(data['Plans'], function(p) {
 | 
			
		||||
                var level = _.clone(lvl),
 | 
			
		||||
                    plan = new PlanModel();
 | 
			
		||||
 | 
			
		||||
                level.push(idx);
 | 
			
		||||
                plan.set(plan.parse(_.extend(
 | 
			
		||||
                    p, {
 | 
			
		||||
                        "level": level,
 | 
			
		||||
                        xpos: xpos - offsetX,
 | 
			
		||||
                        ypos: ypos
 | 
			
		||||
                    })));
 | 
			
		||||
 | 
			
		||||
                if (maxChildWidth < plan.get('width')) {
 | 
			
		||||
                    maxChildWidth = plan.get('width');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var childHeight = plan.get('height');
 | 
			
		||||
 | 
			
		||||
                if (idx !== 1) {
 | 
			
		||||
                    data['height'] = data['height'] + childHeight + offsetY;
 | 
			
		||||
                } else if (childHeight > data['height']) {
 | 
			
		||||
                    data['height'] = childHeight;
 | 
			
		||||
                }
 | 
			
		||||
                ypos += childHeight + offsetY;
 | 
			
		||||
 | 
			
		||||
                plans.push(plan);
 | 
			
		||||
                idx++;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Final Width and Height of current node
 | 
			
		||||
        data['width'] += maxChildWidth;
 | 
			
		||||
        data['Plans'] = plans;
 | 
			
		||||
 | 
			
		||||
        return data;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
     * Required to parse and include non-default params of
 | 
			
		||||
     * plan into backbone model
 | 
			
		||||
     */
 | 
			
		||||
    toJSON: function(non_recursive) {
 | 
			
		||||
      var res = Backbone.Model.prototype.toJSON.apply(this, arguments);
 | 
			
		||||
 | 
			
		||||
      if (non_recursive) {
 | 
			
		||||
            delete res['Plans'];
 | 
			
		||||
      } else {
 | 
			
		||||
            var plans = [];
 | 
			
		||||
            _.each(res['Plans'], function(p) {
 | 
			
		||||
              plans.push(p.toJSON());
 | 
			
		||||
            });
 | 
			
		||||
            res['Plans'] = plans;
 | 
			
		||||
      }
 | 
			
		||||
      return res;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Draw an arrow to parent node
 | 
			
		||||
    drawPolyLine: function(g, startX, startY, endX, endY, opts, arrowOpts) {
 | 
			
		||||
      // Calculate end point of first starting straight line (startx1, starty1)
 | 
			
		||||
      // Calculate start point of 2nd straight line (endx1, endy1)
 | 
			
		||||
      var midX1 = startX + ((endX - startX) / 3),
 | 
			
		||||
          midX2 = startX + (2 * ((endX - startX) / 3));
 | 
			
		||||
 | 
			
		||||
      //create arrow head
 | 
			
		||||
      var arrow = g.polygon(
 | 
			
		||||
                    [0, ARROW_HEIGHT,
 | 
			
		||||
                    (ARROW_WIDTH / 2),ARROW_HEIGHT,
 | 
			
		||||
                    (ARROW_HEIGHT / 4), 0,
 | 
			
		||||
                    0, ARROW_WIDTH]
 | 
			
		||||
                    ).transform("r90");
 | 
			
		||||
      var marker = arrow.marker(
 | 
			
		||||
                         0, 0, ARROW_WIDTH, ARROW_HEIGHT, 0, (ARROW_WIDTH / 2)
 | 
			
		||||
                         ).attr(arrowOpts);
 | 
			
		||||
 | 
			
		||||
      // First straight line
 | 
			
		||||
      g.line(
 | 
			
		||||
        startX, startY, midX1, startY
 | 
			
		||||
        ).attr(opts);
 | 
			
		||||
 | 
			
		||||
      // Diagonal line
 | 
			
		||||
      g.line(
 | 
			
		||||
        midX1-1, startY, midX2, endY
 | 
			
		||||
        ).attr(opts);
 | 
			
		||||
 | 
			
		||||
      // Last straight line
 | 
			
		||||
      var line = g.line(
 | 
			
		||||
                   midX2, endY, endX, endY
 | 
			
		||||
                   ).attr(opts);
 | 
			
		||||
      line.attr({markerEnd: marker})
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Draw image, its name and its tooltip
 | 
			
		||||
    draw: function(s, xpos, ypos, pXpos, pYpos, graphContainer, toolTipContainer) {
 | 
			
		||||
        var g = s.g();
 | 
			
		||||
        var currentXpos = xpos + this.get('xpos') ,
 | 
			
		||||
            currentYpos = ypos + this.get('ypos'),
 | 
			
		||||
            isSubPlan = (this.get('Parent Relationship') === "SubPlan");
 | 
			
		||||
 | 
			
		||||
        // Draw the subplan rectangle
 | 
			
		||||
        if (isSubPlan) {
 | 
			
		||||
          g.rect(
 | 
			
		||||
            currentXpos - this.get('width') + pWIDTH + xMargin,
 | 
			
		||||
            currentYpos - yMargin,
 | 
			
		||||
            this.get('width') - xMargin,
 | 
			
		||||
            this.get('height'), 5
 | 
			
		||||
          ).attr({
 | 
			
		||||
              stroke: '#444444',
 | 
			
		||||
              'strokeWidth': 1.2,
 | 
			
		||||
              fill: 'gray',
 | 
			
		||||
              fillOpacity: 0.2
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          //provide subplan name
 | 
			
		||||
          var text = g.text(
 | 
			
		||||
            currentXpos  + pWIDTH - ( this.get('width') / 2) - xMargin,
 | 
			
		||||
            currentYpos + pHEIGHT  - (this.get('height') / 2) - yMargin,
 | 
			
		||||
            this.get('Subplan Name')
 | 
			
		||||
          ).attr({
 | 
			
		||||
            fontSize: TXT_SIZE, "text-anchor":"start",
 | 
			
		||||
            fill: 'red'
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Draw tooltip
 | 
			
		||||
        var image_data = this.toJSON();
 | 
			
		||||
        image.mouseover(function(evt){
 | 
			
		||||
 | 
			
		||||
          // Empty the tooltip content if it has any and add new data
 | 
			
		||||
          toolTipContainer.empty();
 | 
			
		||||
          var tooltip = $('<table></table>',{
 | 
			
		||||
                           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( '<tr><td class="label explain-tooltip">' + key + '</td><td class="label explain-tooltip-val">' + value + '</td></tr>' );
 | 
			
		||||
            };
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          var zoomFactor = graphContainer.data('zoom-factor');
 | 
			
		||||
 | 
			
		||||
          // Calculate co-ordinates for tooltip
 | 
			
		||||
          var toolTipX = ((currentXpos + pWIDTH) * zoomFactor - graphContainer.scrollLeft());
 | 
			
		||||
          var toolTipY = ((currentYpos + pHEIGHT) * zoomFactor - graphContainer.scrollTop());
 | 
			
		||||
 | 
			
		||||
          // 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));
 | 
			
		||||
 | 
			
		||||
          toolTipX = toolTipX < 0 ? 0 : (toolTipX);
 | 
			
		||||
          toolTipY = toolTipY < 0 ? 0 : (toolTipY);
 | 
			
		||||
 | 
			
		||||
          // 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(function() {
 | 
			
		||||
          toolTipContainer.empty();
 | 
			
		||||
          toolTipContainer.css({'opacity': '0'});
 | 
			
		||||
          toolTipContainer.css('left', 0);
 | 
			
		||||
          toolTipContainer.css( 'top', 0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Draw text below the node
 | 
			
		||||
        var node_label = (this.get('Schema') == undefined ?
 | 
			
		||||
                            this.get('image_text') :
 | 
			
		||||
                            this.get('Schema')+"."+this.get('image_text'));
 | 
			
		||||
        var label = g.g();
 | 
			
		||||
        g.multitext(
 | 
			
		||||
          currentXpos + (pWIDTH / 2),
 | 
			
		||||
          currentYpos + pHEIGHT - TXT_ALLIGN,
 | 
			
		||||
          node_label,
 | 
			
		||||
          150,
 | 
			
		||||
          {"font-size": TXT_SIZE ,"text-anchor":"middle"}
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Draw Arrow to parent only its not the first node
 | 
			
		||||
        if (!_.isUndefined(pYpos)) {
 | 
			
		||||
            var startx = currentXpos + pWIDTH;
 | 
			
		||||
            var starty = currentYpos + (pHEIGHT / 2);
 | 
			
		||||
            var endx = pXpos - ARROW_WIDTH;
 | 
			
		||||
            var endy = pYpos + (pHEIGHT / 2);
 | 
			
		||||
            var start_cost = this.get("Startup Cost"),
 | 
			
		||||
                total_cost = this.get("Total Cost");
 | 
			
		||||
            var arrow_size = DEFAULT_ARROW_SIZE;
 | 
			
		||||
            // Calculate arrow width according to cost of a particular plan
 | 
			
		||||
            if(start_cost != undefined && total_cost != undefined) {
 | 
			
		||||
              var arrow_size = Math.round(Math.log((start_cost+total_cost)/2 + start_cost));
 | 
			
		||||
              arrow_size = arrow_size < 1 ? 1 : arrow_size > 10 ? 10 : arrow_size;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var arrow_view_box = [0, 0, 2*ARROW_WIDTH, 2*ARROW_HEIGHT];
 | 
			
		||||
            var opts = {stroke: "#000000", strokeWidth: arrow_size + 1},
 | 
			
		||||
                subplanOpts = {stroke: "#866486", strokeWidth: arrow_size + 1},
 | 
			
		||||
                arrowOpts = {viewBox: arrow_view_box.join(" ")};
 | 
			
		||||
 | 
			
		||||
            // Draw an arrow from current node to its parent
 | 
			
		||||
            this.drawPolyLine(
 | 
			
		||||
              g, startx, starty, endx, endy,
 | 
			
		||||
              isSubPlan ? subplanOpts : opts, arrowOpts
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var plans = this.get('Plans');
 | 
			
		||||
 | 
			
		||||
        // Draw nodes for current plan's children
 | 
			
		||||
        _.each(plans, function(p) {
 | 
			
		||||
            p.draw(s, xpos, ypos, currentXpos, currentYpos, graphContainer, toolTipContainer);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Main backbone model to store JSON object
 | 
			
		||||
var MainPlanModel = Backbone.Model.extend({
 | 
			
		||||
    defaults: {
 | 
			
		||||
        "Plan": undefined,
 | 
			
		||||
        xpos: 0,
 | 
			
		||||
        ypos: 0,
 | 
			
		||||
    },
 | 
			
		||||
    initialize: function() {
 | 
			
		||||
        this.set("Plan", new PlanModel());
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Parse the JSON data and fetch its children plans
 | 
			
		||||
    parse: function(data) {
 | 
			
		||||
        if (data && 'Plan' in data) {
 | 
			
		||||
           var plan = this.get("Plan");
 | 
			
		||||
           plan.set(
 | 
			
		||||
             plan.parse(
 | 
			
		||||
               _.extend(
 | 
			
		||||
                 data['Plan'], {
 | 
			
		||||
                   xpos: 0,
 | 
			
		||||
                   ypos: 0
 | 
			
		||||
                 })));
 | 
			
		||||
 | 
			
		||||
           data['xpos'] = 0;
 | 
			
		||||
           data['ypos'] = 0;
 | 
			
		||||
           data['width'] = plan.get('width') + (xMargin * 2);
 | 
			
		||||
           data['height'] = plan.get('height') + (yMargin * 2);
 | 
			
		||||
 | 
			
		||||
           delete data['Plan'];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      return data;
 | 
			
		||||
    },
 | 
			
		||||
    toJSON: function() {
 | 
			
		||||
      var res = Backbone.Model.prototype.toJSON.apply(this, arguments);
 | 
			
		||||
 | 
			
		||||
      if (res.Plan) {
 | 
			
		||||
        res.Plan = res.Plan.toJSON();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return res;
 | 
			
		||||
    },
 | 
			
		||||
    draw: function(s, xpos, ypos, graphContainer, toolTipContainer) {
 | 
			
		||||
        var g = s.g();
 | 
			
		||||
 | 
			
		||||
        //draw the border
 | 
			
		||||
        g.rect(
 | 
			
		||||
	        0, 0, this.get('width') - 10, this.get('height') - 10, 5
 | 
			
		||||
	    ).attr({
 | 
			
		||||
            stroke: '#FFEBCD', 'strokeWidth': 1.2,
 | 
			
		||||
            fill: '#FFF8DC', fillOpacity: 0.5
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        //Fetch total width, height
 | 
			
		||||
        TOTAL_WIDTH = this.get('width');
 | 
			
		||||
        TOTAL_HEIGHT = this.get('height');
 | 
			
		||||
        var plan = this.get('Plan');
 | 
			
		||||
 | 
			
		||||
        //Draw explain graph
 | 
			
		||||
        plan.draw(g, xpos, ypos, undefined, undefined, graphContainer, toolTipContainer);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Parse and draw full graphical explain
 | 
			
		||||
_.extend(
 | 
			
		||||
    pgExplain, {
 | 
			
		||||
        // Assumption container is a jQuery object
 | 
			
		||||
        DrawJSONPlan: function(container, plan) {
 | 
			
		||||
          var my_plans = [];
 | 
			
		||||
          container.empty();
 | 
			
		||||
          var curr_zoom_factor = 1.0;
 | 
			
		||||
 | 
			
		||||
          var zoomArea =$('<div></div>', {
 | 
			
		||||
                class: 'pg-explain-zoom-area btn-group',
 | 
			
		||||
                role: 'group'
 | 
			
		||||
                }).appendTo(container),
 | 
			
		||||
              zoomInBtn = $('<button></button>', {
 | 
			
		||||
                class: 'btn pg-explain-zoom-btn badge',
 | 
			
		||||
                title: 'Zoom in'
 | 
			
		||||
                }).appendTo(zoomArea).append(
 | 
			
		||||
                  $('<i></i>',{
 | 
			
		||||
                    class: 'fa fa-search-plus'
 | 
			
		||||
                  })),
 | 
			
		||||
              zoomToNormal = $('<button></button>', {
 | 
			
		||||
                class: 'btn pg-explain-zoom-btn badge',
 | 
			
		||||
                title: 'Zoom to original'
 | 
			
		||||
                }).appendTo(zoomArea).append(
 | 
			
		||||
                  $('<i></i>',{
 | 
			
		||||
                    class: 'fa fa-arrows-alt'
 | 
			
		||||
                  }))
 | 
			
		||||
              zoomOutBtn = $('<button></button>', {
 | 
			
		||||
                class: 'btn pg-explain-zoom-btn badge',
 | 
			
		||||
                title: 'Zoom out'
 | 
			
		||||
                }).appendTo(zoomArea).append(
 | 
			
		||||
                  $('<i></i>', {
 | 
			
		||||
                    class: 'fa fa-search-minus'
 | 
			
		||||
                  }));
 | 
			
		||||
 | 
			
		||||
          // Main div to be drawn all images on
 | 
			
		||||
          var planDiv = $('<div></div>',
 | 
			
		||||
                           {class: "pgadmin-explain-container"}
 | 
			
		||||
                         ).appendTo(container),
 | 
			
		||||
              // Div to draw tool-tip on
 | 
			
		||||
              toolTip = $('<div></div>',
 | 
			
		||||
                           {id: "toolTip",
 | 
			
		||||
                           class: "pgadmin-explain-tooltip"
 | 
			
		||||
                           }
 | 
			
		||||
                         ).appendTo(container);
 | 
			
		||||
          toolTip.empty();
 | 
			
		||||
          planDiv.data('zoom-factor', curr_zoom_factor);
 | 
			
		||||
 | 
			
		||||
          var w = 0, h = 0,
 | 
			
		||||
              x = xMargin, h = yMargin;
 | 
			
		||||
 | 
			
		||||
          _.each(plan, function(p) {
 | 
			
		||||
            var main_plan = new MainPlanModel();
 | 
			
		||||
 | 
			
		||||
            // Parse JSON data to backbone model
 | 
			
		||||
            main_plan.set(main_plan.parse(p));
 | 
			
		||||
            w = main_plan.get('width');
 | 
			
		||||
            h = main_plan.get('height');
 | 
			
		||||
 | 
			
		||||
            var s = Snap(w, h),
 | 
			
		||||
                $svg = $(s.node).detach();
 | 
			
		||||
            planDiv.append($svg);
 | 
			
		||||
            main_plan.draw(s, w - xMargin, yMargin, planDiv, toolTip);
 | 
			
		||||
 | 
			
		||||
            var initPanelWidth = planDiv.width(),
 | 
			
		||||
                initPanelHeight = planDiv.height();
 | 
			
		||||
 | 
			
		||||
             /*
 | 
			
		||||
              * Scale graph in case its width is bigger than panel width
 | 
			
		||||
              * in which the graph is displayed
 | 
			
		||||
              */
 | 
			
		||||
            if(initPanelWidth < w) {
 | 
			
		||||
              var width_ratio = initPanelWidth / w;
 | 
			
		||||
 | 
			
		||||
              curr_zoom_factor = width_ratio;
 | 
			
		||||
              curr_zoom_factor = curr_zoom_factor < MIN_ZOOM_FACTOR ? MIN_ZOOM_FACTOR : curr_zoom_factor;
 | 
			
		||||
              curr_zoom_factor = curr_zoom_factor > INIT_ZOOM_FACTOR ? INIT_ZOOM_FACTOR : curr_zoom_factor;
 | 
			
		||||
 | 
			
		||||
              var zoomInMatrix = new Snap.matrix();
 | 
			
		||||
              zoomInMatrix.scale(curr_zoom_factor, curr_zoom_factor);
 | 
			
		||||
 | 
			
		||||
              $svg.find('g').first().attr({transform: zoomInMatrix});
 | 
			
		||||
              $svg.attr({'width': w * curr_zoom_factor, 'height': h * curr_zoom_factor});
 | 
			
		||||
              planDiv.data('zoom-factor', curr_zoom_factor);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            zoomInBtn.on('click', function(e){
 | 
			
		||||
              curr_zoom_factor = ((curr_zoom_factor + ZOOM_RATIO) > MAX_ZOOM_FACTOR) ? MAX_ZOOM_FACTOR : (curr_zoom_factor + ZOOM_RATIO);
 | 
			
		||||
              var zoomInMatrix = new Snap.matrix();
 | 
			
		||||
              zoomInMatrix.scale(curr_zoom_factor, curr_zoom_factor);
 | 
			
		||||
 | 
			
		||||
              $svg.find('g').first().attr({transform: zoomInMatrix});
 | 
			
		||||
              $svg.attr({'width': w * curr_zoom_factor, 'height': h * curr_zoom_factor});
 | 
			
		||||
              planDiv.data('zoom-factor', curr_zoom_factor);
 | 
			
		||||
              zoomInBtn.blur();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            zoomOutBtn.on('click', function(e) {
 | 
			
		||||
              curr_zoom_factor = ((curr_zoom_factor - ZOOM_RATIO) < MIN_ZOOM_FACTOR) ? MIN_ZOOM_FACTOR : (curr_zoom_factor - ZOOM_RATIO);
 | 
			
		||||
              var zoomInMatrix = new Snap.matrix();
 | 
			
		||||
              zoomInMatrix.scale(curr_zoom_factor, curr_zoom_factor);
 | 
			
		||||
 | 
			
		||||
              $svg.find('g').first().attr({transform: zoomInMatrix});
 | 
			
		||||
              $svg.attr({'width': w * curr_zoom_factor, 'height': h * curr_zoom_factor});
 | 
			
		||||
              planDiv.data('zoom-factor', curr_zoom_factor);
 | 
			
		||||
              zoomOutBtn.blur();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            zoomToNormal.on('click', function(e) {
 | 
			
		||||
              curr_zoom_factor = INIT_ZOOM_FACTOR;
 | 
			
		||||
              var zoomInMatrix = new Snap.matrix();
 | 
			
		||||
              zoomInMatrix.scale(curr_zoom_factor, curr_zoom_factor);
 | 
			
		||||
 | 
			
		||||
              $svg.find('g').first().attr({transform: zoomInMatrix});
 | 
			
		||||
              $svg.attr({'width': w * curr_zoom_factor, 'height': h * curr_zoom_factor});
 | 
			
		||||
              planDiv.data('zoom-factor', curr_zoom_factor);
 | 
			
		||||
              zoomToNormal.blur();
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return pgExplain;
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -68,11 +68,54 @@ body {
 | 
			
		|||
              <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
 | 
			
		||||
            </button>
 | 
			
		||||
            <ul class="dropdown-menu dropdown-menu">
 | 
			
		||||
              <li>
 | 
			
		||||
                <a id="btn-explain" href="#">
 | 
			
		||||
                  <span>{{ _('Explain') }}</span>
 | 
			
		||||
                </a>
 | 
			
		||||
              </li>
 | 
			
		||||
              <li>
 | 
			
		||||
                <a id="btn-explain-analyze" href="#">
 | 
			
		||||
                    <span>{{ _('Explain analyze') }}</span>
 | 
			
		||||
                </a>
 | 
			
		||||
              </li>
 | 
			
		||||
              <li class="divider"></li>
 | 
			
		||||
              <li class="dropdown-submenu dropdown-submenu">
 | 
			
		||||
                <a href="#">{{ _('Explain Options') }}</a>
 | 
			
		||||
                <ul class="dropdown-menu">
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a id="btn-explain-verbose" href="#" class="noclose">
 | 
			
		||||
                      <i class="explain-verbose fa fa-check visibility-hidden" aria-hidden="true"></i>
 | 
			
		||||
                      <span> {{ _('Verbose') }} </span>
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a id="btn-explain-costs" href="#" class="noclose">
 | 
			
		||||
                      <i class="explain-costs fa fa-check visibility-hidden" aria-hidden="true"></i>
 | 
			
		||||
                      <span> {{ _('Costs') }} </span>
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a id="btn-explain-buffers" href="#" class="noclose">
 | 
			
		||||
                      <i class="explain-buffers fa fa-check visibility-hidden" aria-hidden="true"></i>
 | 
			
		||||
                      <span> {{ _('Buffers') }} </span>
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a id="btn-explain-timing" href="#" class="noclose">
 | 
			
		||||
                      <i class="explain-timing fa fa-check visibility-hidden" aria-hidden="true"></i>
 | 
			
		||||
                      <span> {{ _('Timing') }} </span>
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
              </li>
 | 
			
		||||
              <li class="divider"></li>
 | 
			
		||||
              <li>
 | 
			
		||||
                <a id="btn-auto-commit" href="#">
 | 
			
		||||
                    <i class="auto-commit fa fa-check" aria-hidden="true"></i>
 | 
			
		||||
                    <span> {{ _('Auto-Commit') }} </span>
 | 
			
		||||
                </a>
 | 
			
		||||
              </li>
 | 
			
		||||
              <li>
 | 
			
		||||
                <a id="btn-auto-rollback" href="#">
 | 
			
		||||
                    <i class="auto-rollback fa fa-check visibility-hidden" aria-hidden="true"></i>
 | 
			
		||||
                    <span> {{ _('Auto-Rollback') }} </span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,8 @@ from flask import Response, url_for, render_template, session, request
 | 
			
		|||
from flask.ext.babel import gettext
 | 
			
		||||
from flask.ext.security import login_required
 | 
			
		||||
from pgadmin.utils import PgAdminModule
 | 
			
		||||
from pgadmin.utils.ajax import make_json_response, bad_request, success_return, internal_server_error
 | 
			
		||||
from pgadmin.utils.ajax import make_json_response, bad_request, \
 | 
			
		||||
     success_return, internal_server_error
 | 
			
		||||
from pgadmin.utils.driver import get_driver
 | 
			
		||||
from config import PG_DEFAULT_DRIVER
 | 
			
		||||
from pgadmin.tools.sqleditor.command import QueryToolCommand
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +74,42 @@ class SqlEditorModule(PgAdminModule):
 | 
			
		|||
            category_label=gettext('Display')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.explain_verbose = self.preference.register(
 | 
			
		||||
            'Explain Options', 'explain_verbose',
 | 
			
		||||
            gettext("Verbose"), 'boolean', False,
 | 
			
		||||
            category_label=gettext('Explain Options')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.explain_costs = self.preference.register(
 | 
			
		||||
            'Explain Options', 'explain_costs',
 | 
			
		||||
            gettext("Costs"), 'boolean', False,
 | 
			
		||||
            category_label=gettext('Explain Options')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.explain_buffers = self.preference.register(
 | 
			
		||||
            'Explain Options', 'explain_buffers',
 | 
			
		||||
            gettext("Buffers"), 'boolean', False,
 | 
			
		||||
            category_label=gettext('Explain Options')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.explain_timing = self.preference.register(
 | 
			
		||||
            'Explain Options', 'explain_timing',
 | 
			
		||||
            gettext("Timing"), 'boolean', False,
 | 
			
		||||
            category_label=gettext('Explain Options')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.auto_commit = self.preference.register(
 | 
			
		||||
            'Options', 'auto_commit',
 | 
			
		||||
            gettext("Auto-Commit"), 'boolean', True,
 | 
			
		||||
            category_label=gettext('Options')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.auto_rollback = self.preference.register(
 | 
			
		||||
            'Options', 'auto_rollback',
 | 
			
		||||
            gettext("Auto-Rollback"), 'boolean', False,
 | 
			
		||||
            category_label=gettext('Options')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
blueprint = SqlEditorModule(MODULE_NAME, __name__, static_url_path='/static')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -284,6 +321,43 @@ def start_query_tool(trans_id):
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.route('/query_tool/preferences', methods=["GET", "PUT"])
 | 
			
		||||
@login_required
 | 
			
		||||
def get_preferences():
 | 
			
		||||
    """
 | 
			
		||||
        This method is used to get/put explain options from/to preferences
 | 
			
		||||
    """
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        return make_json_response(
 | 
			
		||||
            data={
 | 
			
		||||
                'explain_verbose': blueprint.explain_verbose.get(),
 | 
			
		||||
                'explain_costs': blueprint.explain_costs.get(),
 | 
			
		||||
                'explain_buffers': blueprint.explain_buffers.get(),
 | 
			
		||||
                'explain_timing': blueprint.explain_timing.get(),
 | 
			
		||||
                'auto_commit': blueprint.auto_commit.get(),
 | 
			
		||||
                'auto_rollback': blueprint.auto_rollback.get()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        data = None
 | 
			
		||||
        if request.data:
 | 
			
		||||
            data = json.loads(request.data.decode())
 | 
			
		||||
        else:
 | 
			
		||||
            data = request.args or request.form
 | 
			
		||||
        for k,v in data.items():
 | 
			
		||||
            v = bool(v)
 | 
			
		||||
            if k == 'explain_verbose':
 | 
			
		||||
                blueprint.explain_verbose.set(v)
 | 
			
		||||
            elif k == 'explain_costs':
 | 
			
		||||
                blueprint.explain_costs.set(v)
 | 
			
		||||
            elif k == 'explain_buffers':
 | 
			
		||||
                blueprint.explain_buffers.set(v)
 | 
			
		||||
            elif k == 'explain_timing':
 | 
			
		||||
                blueprint.explain_timing.set(v)
 | 
			
		||||
 | 
			
		||||
        return success_return()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.route('/poll/<int:trans_id>', methods=["GET"])
 | 
			
		||||
@login_required
 | 
			
		||||
def poll(trans_id):
 | 
			
		||||
| 
						 | 
				
			
			@ -746,6 +820,9 @@ def set_auto_commit(trans_id):
 | 
			
		|||
        # Call the set_auto_commit method of transaction object
 | 
			
		||||
        trans_obj.set_auto_commit(auto_commit)
 | 
			
		||||
 | 
			
		||||
        # Set Auto commit in preferences
 | 
			
		||||
        blueprint.auto_commit.set(bool(auto_commit))
 | 
			
		||||
 | 
			
		||||
        # As we changed the transaction object we need to
 | 
			
		||||
        # restore it and update the session variable.
 | 
			
		||||
        session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
 | 
			
		||||
| 
						 | 
				
			
			@ -781,6 +858,9 @@ def set_auto_rollback(trans_id):
 | 
			
		|||
        # Call the set_auto_rollback method of transaction object
 | 
			
		||||
        trans_obj.set_auto_rollback(auto_rollback)
 | 
			
		||||
 | 
			
		||||
        # Set Auto Rollback in preferences
 | 
			
		||||
        blueprint.auto_rollback.set(bool(auto_rollback))
 | 
			
		||||
 | 
			
		||||
        # As we changed the transaction object we need to
 | 
			
		||||
        # restore it and update the session variable.
 | 
			
		||||
        session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -236,3 +236,10 @@
 | 
			
		|||
.CodeMirror-foldgutter-folded:after {
 | 
			
		||||
  content: "\25B6";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.sql-editor-explain {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,17 @@
 | 
			
		|||
define(
 | 
			
		||||
  ['jquery', 'underscore', 'alertify', 'pgadmin', 'backbone', 'backgrid', 'codemirror',
 | 
			
		||||
   'codemirror/mode/sql/sql', 'codemirror/addon/selection/mark-selection', 'codemirror/addon/selection/active-line',
 | 
			
		||||
   'codemirror/addon/fold/foldgutter', 'codemirror/addon/fold/foldcode', 'codemirror/addon/fold/pgadmin-sqlfoldcode',
 | 
			
		||||
   'backgrid.select.all', 'backbone.paginator', 'backgrid.paginator', 'backgrid.filter',
 | 
			
		||||
   'bootstrap', 'pgadmin.browser', 'wcdocker', 'pgadmin.file_manager'],
 | 
			
		||||
  function($, _, alertify, pgAdmin, Backbone, Backgrid, CodeMirror) {
 | 
			
		||||
 | 
			
		||||
  [
 | 
			
		||||
    'jquery', 'underscore', 'underscore.string', 'alertify', 'pgadmin',
 | 
			
		||||
    'backbone', 'backgrid', 'codemirror', 'pgadmin.misc.explain',
 | 
			
		||||
    'backgrid.select.all', 'backgrid.filter', 'bootstrap', 'pgadmin.browser',
 | 
			
		||||
    'codemirror/mode/sql/sql', 'codemirror/addon/selection/mark-selection',
 | 
			
		||||
    'codemirror/addon/selection/active-line', 'backbone.paginator',
 | 
			
		||||
    'codemirror/addon/fold/foldgutter', 'codemirror/addon/fold/foldcode',
 | 
			
		||||
    'codemirror/addon/fold/pgadmin-sqlfoldcode', 'backgrid.paginator',
 | 
			
		||||
    'wcdocker', 'pgadmin.file_manager'
 | 
			
		||||
  ],
 | 
			
		||||
  function(
 | 
			
		||||
    $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain
 | 
			
		||||
  ) {
 | 
			
		||||
    // Some scripts do export their object in the window only.
 | 
			
		||||
    // Generally the one, which do no have AMD support.
 | 
			
		||||
    var wcDocker = window.wcDocker,
 | 
			
		||||
| 
						 | 
				
			
			@ -162,6 +168,12 @@ define(
 | 
			
		|||
        "click #btn-auto-rollback": "on_auto_rollback",
 | 
			
		||||
        "click #btn-clear-history": "on_clear_history",
 | 
			
		||||
        "click .noclose": 'do_not_close_menu',
 | 
			
		||||
        "click #btn-explain": "on_explain",
 | 
			
		||||
        "click #btn-explain-analyze": "on_explain_analyze",
 | 
			
		||||
        "click #btn-explain-verbose": "on_explain_verbose",
 | 
			
		||||
        "click #btn-explain-costs": "on_explain_costs",
 | 
			
		||||
        "click #btn-explain-buffers": "on_explain_buffers",
 | 
			
		||||
        "click #btn-explain-timing": "on_explain_timing",
 | 
			
		||||
        "change .limit": "on_limit_change"
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -206,7 +218,6 @@ define(
 | 
			
		|||
          isPrivate: true
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        //sql_panel.load(main_docker);
 | 
			
		||||
        sql_panel.load(main_docker);
 | 
			
		||||
        var sql_panel_obj = main_docker.addPanel('sql_panel', wcDocker.DOCK.TOP);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -248,7 +259,7 @@ define(
 | 
			
		|||
          height:'100%',
 | 
			
		||||
          isCloseable: false,
 | 
			
		||||
          isPrivate: true,
 | 
			
		||||
          content: '<div class="sql-editor-explian"></div>'
 | 
			
		||||
          content: '<div class="sql-editor-explain"></div>'
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        var messages = new pgAdmin.Browser.Panel({
 | 
			
		||||
| 
						 | 
				
			
			@ -284,6 +295,87 @@ define(
 | 
			
		|||
        self.history_panel = main_docker.addPanel('history', wcDocker.DOCK.STACKED, self.data_output_panel);
 | 
			
		||||
 | 
			
		||||
        self.render_history_grid();
 | 
			
		||||
 | 
			
		||||
        // Get auto-rollback/auto-commit and explain options from preferences
 | 
			
		||||
        self.get_preferences();
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      /*
 | 
			
		||||
       * This function get explain options and auto rollback/auto commit
 | 
			
		||||
       * values from preferences
 | 
			
		||||
       */
 | 
			
		||||
      get_preferences: function() {
 | 
			
		||||
        $.ajax({
 | 
			
		||||
          url: "{{ url_for('sqleditor.index') }}" + "query_tool/preferences" ,
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          async: false,
 | 
			
		||||
          success: function(res) {
 | 
			
		||||
            if (res.data) {
 | 
			
		||||
              self.explain_verbose = res.data.explain_verbose;
 | 
			
		||||
              self.explain_costs = res.data.explain_costs;
 | 
			
		||||
              self.explain_buffers = res.data.explain_buffers;
 | 
			
		||||
              self.explain_timing = res.data.explain_timing;
 | 
			
		||||
              self.auto_commit = res.data.auto_commit;
 | 
			
		||||
              self.auto_rollback = res.data.auto_rollback;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
              self.explain_verbose = false;
 | 
			
		||||
              self.explain_costs = false;
 | 
			
		||||
              self.explain_buffers = false;
 | 
			
		||||
              self.explain_timing = false;
 | 
			
		||||
              self.auto_commit = true;
 | 
			
		||||
              self.auto_rollback = false;
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          error: function(e) {
 | 
			
		||||
            self.explain_verbose = false;
 | 
			
		||||
            self.explain_costs = false;
 | 
			
		||||
            self.explain_buffers = false;
 | 
			
		||||
            self.explain_timing = false;
 | 
			
		||||
            self.auto_commit = true;
 | 
			
		||||
            self.auto_rollback = false;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Set Auto-commit and auto-rollback on query editor
 | 
			
		||||
        if (self.auto_commit &&
 | 
			
		||||
            $('.auto-commit').hasClass('visibility-hidden') === true)
 | 
			
		||||
          $('.auto-commit').removeClass('visibility-hidden');
 | 
			
		||||
        else {
 | 
			
		||||
          $('.auto-commit').addClass('visibility-hidden');
 | 
			
		||||
        }
 | 
			
		||||
        if (self.auto_rollback &&
 | 
			
		||||
            $('.auto-rollback').hasClass('visibility-hidden') === true)
 | 
			
		||||
          $('.auto-rollback').removeClass('visibility-hidden');
 | 
			
		||||
        else {
 | 
			
		||||
          $('.auto-rollback').addClass('visibility-hidden');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set explain options on query editor
 | 
			
		||||
        if (self.explain_verbose &&
 | 
			
		||||
            $('.explain-verbose').hasClass('visibility-hidden') === true)
 | 
			
		||||
          $('.explain-verbose').removeClass('visibility-hidden');
 | 
			
		||||
        else {
 | 
			
		||||
          $('.explain-verbose').addClass('visibility-hidden');
 | 
			
		||||
        }
 | 
			
		||||
        if (self.explain_costs &&
 | 
			
		||||
            $('.explain-costs').hasClass('visibility-hidden') === true)
 | 
			
		||||
          $('.explain-costs').removeClass('visibility-hidden');
 | 
			
		||||
        else {
 | 
			
		||||
          $('.explain-costs').addClass('visibility-hidden');
 | 
			
		||||
        }
 | 
			
		||||
        if (self.explain_buffers &&
 | 
			
		||||
            $('.explain-buffers').hasClass('visibility-hidden') === true)
 | 
			
		||||
            $('.explain-buffers').removeClass('visibility-hidden');
 | 
			
		||||
        else {
 | 
			
		||||
            $('.explain-buffers').addClass('visibility-hidden');
 | 
			
		||||
        }
 | 
			
		||||
        if (self.explain_timing &&
 | 
			
		||||
            $('.explain-timing').hasClass('visibility-hidden') === true)
 | 
			
		||||
            $('.explain-timing').removeClass('visibility-hidden');
 | 
			
		||||
        else {
 | 
			
		||||
            $('.explain-timing').addClass('visibility-hidden');
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      /* This function is responsible to create and render the
 | 
			
		||||
| 
						 | 
				
			
			@ -640,6 +732,79 @@ define(
 | 
			
		|||
            self.handler
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // Callback function for explain button click.
 | 
			
		||||
      on_explain: function() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
 | 
			
		||||
        // Trigger the explain signal to the SqlEditorController class
 | 
			
		||||
        self.handler.trigger(
 | 
			
		||||
            'pgadmin-sqleditor:button:explain',
 | 
			
		||||
            self,
 | 
			
		||||
            self.handler
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // Callback function for explain analyze button click.
 | 
			
		||||
      on_explain_analyze: function() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
 | 
			
		||||
        // Trigger the explain analyze signal to the SqlEditorController class
 | 
			
		||||
        self.handler.trigger(
 | 
			
		||||
            'pgadmin-sqleditor:button:explain-analyze',
 | 
			
		||||
            self,
 | 
			
		||||
            self.handler
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // Callback function for explain option "verbose" button click
 | 
			
		||||
      on_explain_verbose: function() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
 | 
			
		||||
        // Trigger the explain "verbose" signal to the SqlEditorController class
 | 
			
		||||
        self.handler.trigger(
 | 
			
		||||
            'pgadmin-sqleditor:button:explain-verbose',
 | 
			
		||||
            self,
 | 
			
		||||
            self.handler
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // Callback function for explain option "costs" button click
 | 
			
		||||
      on_explain_costs: function() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
 | 
			
		||||
        // Trigger the explain "costs" signal to the SqlEditorController class
 | 
			
		||||
        self.handler.trigger(
 | 
			
		||||
            'pgadmin-sqleditor:button:explain-costs',
 | 
			
		||||
            self,
 | 
			
		||||
            self.handler
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // Callback function for explain option "buffers" button click
 | 
			
		||||
      on_explain_buffers: function() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
 | 
			
		||||
        // Trigger the explain "buffers" signal to the SqlEditorController class
 | 
			
		||||
        self.handler.trigger(
 | 
			
		||||
            'pgadmin-sqleditor:button:explain-buffers',
 | 
			
		||||
            self,
 | 
			
		||||
            self.handler
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // Callback function for explain option "timing" button click
 | 
			
		||||
      on_explain_timing: function() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
 | 
			
		||||
        // Trigger the explain "timing" signal to the SqlEditorController class
 | 
			
		||||
        self.handler.trigger(
 | 
			
		||||
            'pgadmin-sqleditor:button:explain-timing',
 | 
			
		||||
            self,
 | 
			
		||||
            self.handler
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      do_not_close_menu: function(ev) {
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			@ -685,6 +850,10 @@ define(
 | 
			
		|||
          self.items_per_page = 25;
 | 
			
		||||
          self.rows_affected = 0;
 | 
			
		||||
          self.marked_line_no = 0;
 | 
			
		||||
          self.explain_verbose = false;
 | 
			
		||||
          self.explain_costs = false;
 | 
			
		||||
          self.explain_buffers = false;
 | 
			
		||||
          self.explain_timing = false;
 | 
			
		||||
 | 
			
		||||
          // We do not allow to call the start multiple times.
 | 
			
		||||
          if (self.gridView)
 | 
			
		||||
| 
						 | 
				
			
			@ -728,6 +897,12 @@ define(
 | 
			
		|||
          self.on('pgadmin-sqleditor:button:download', self._download, self);
 | 
			
		||||
          self.on('pgadmin-sqleditor:button:auto_rollback', self._auto_rollback, self);
 | 
			
		||||
          self.on('pgadmin-sqleditor:button:auto_commit', self._auto_commit, self);
 | 
			
		||||
          self.on('pgadmin-sqleditor:button:explain', self._explain, self);
 | 
			
		||||
          self.on('pgadmin-sqleditor:button:explain-analyze', self._explain_analyze, self);
 | 
			
		||||
          self.on('pgadmin-sqleditor:button:explain-verbose', self._explain_verbose, self);
 | 
			
		||||
          self.on('pgadmin-sqleditor:button:explain-costs', self._explain_costs, self);
 | 
			
		||||
          self.on('pgadmin-sqleditor:button:explain-buffers', self._explain_buffers, self);
 | 
			
		||||
          self.on('pgadmin-sqleditor:button:explain-timing', self._explain_timing, self);
 | 
			
		||||
 | 
			
		||||
          if (self.is_query_tool) {
 | 
			
		||||
            self.gridView.query_tool_obj.refresh();
 | 
			
		||||
| 
						 | 
				
			
			@ -771,6 +946,7 @@ define(
 | 
			
		|||
                self.gridView.query_tool_obj.setValue(res.data.sql);
 | 
			
		||||
                self.query = res.data.sql;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                /* If filter is applied then remove class 'btn-default'
 | 
			
		||||
                 * and add 'btn-warning' to change the colour of the button.
 | 
			
		||||
                 */
 | 
			
		||||
| 
						 | 
				
			
			@ -908,6 +1084,7 @@ define(
 | 
			
		|||
          self.cell_selected = false;
 | 
			
		||||
          self.selected_model = null;
 | 
			
		||||
          self.changedModels = [];
 | 
			
		||||
          $('.sql-editor-explain').empty();
 | 
			
		||||
 | 
			
		||||
          // Stop listening to all the events
 | 
			
		||||
          if (self.collection) {
 | 
			
		||||
| 
						 | 
				
			
			@ -976,10 +1153,26 @@ define(
 | 
			
		|||
          var message = 'Total query runtime: ' + self.total_time + '\n' + self.rows_affected + ' rows retrieved.';
 | 
			
		||||
          $('.sql-editor-message').text(message);
 | 
			
		||||
 | 
			
		||||
          // Add the data to the collection and render the grid.
 | 
			
		||||
          self.collection.add(data.result, {parse: true});
 | 
			
		||||
          self.gridView.render_grid(self.collection, self.columns);
 | 
			
		||||
          self.gridView.data_output_panel.focus();
 | 
			
		||||
          /* Add the data to the collection and render the grid.
 | 
			
		||||
           * In case of Explain draw the graph on explain panel
 | 
			
		||||
           * and add json formatted data to collection and render.
 | 
			
		||||
           */
 | 
			
		||||
          var explain_data_array = [];
 | 
			
		||||
          if(data.result &&
 | 
			
		||||
              'QUERY PLAN' in data.result[0] &&
 | 
			
		||||
              _.isObject(data.result[0]['QUERY PLAN'])) {
 | 
			
		||||
              var explain_data = {'QUERY PLAN' : JSON.stringify(data.result[0]['QUERY PLAN'], null, 2)};
 | 
			
		||||
              explain_data_array.push(explain_data);
 | 
			
		||||
              self.gridView.explain_panel.focus();
 | 
			
		||||
              pgExplain.DrawJSONPlan($('.sql-editor-explain'), data.result[0]['QUERY PLAN']);
 | 
			
		||||
              self.collection.add(explain_data_array, {parse: true});
 | 
			
		||||
              self.gridView.render_grid(self.collection, self.columns);
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            self.collection.add(data.result, {parse: true});
 | 
			
		||||
            self.gridView.render_grid(self.collection, self.columns);
 | 
			
		||||
            self.gridView.data_output_panel.focus();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Hide the loading icon
 | 
			
		||||
          self.trigger('pgadmin-sqleditor:loading-icon:hide');
 | 
			
		||||
| 
						 | 
				
			
			@ -1832,16 +2025,11 @@ define(
 | 
			
		|||
 | 
			
		||||
        // This function will fetch the sql query from the text box
 | 
			
		||||
        // and execute the query.
 | 
			
		||||
        _execute: function () {
 | 
			
		||||
        _execute: function (explain_prefix) {
 | 
			
		||||
          var self = this,
 | 
			
		||||
              sql = '',
 | 
			
		||||
              history_msg = '';
 | 
			
		||||
 | 
			
		||||
          self.trigger(
 | 
			
		||||
            'pgadmin-sqleditor:loading-icon:show',
 | 
			
		||||
            '{{ _('Initializing query execution.') }}'
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          /* If code is selected in the code mirror then execute
 | 
			
		||||
           * the selected part else execute the complete code.
 | 
			
		||||
           */
 | 
			
		||||
| 
						 | 
				
			
			@ -1851,6 +2039,17 @@ define(
 | 
			
		|||
          else
 | 
			
		||||
            sql = self.gridView.query_tool_obj.getValue();
 | 
			
		||||
 | 
			
		||||
          // If it is an empty query, do nothing.
 | 
			
		||||
          if (sql.length <= 0) return;
 | 
			
		||||
 | 
			
		||||
          self.trigger(
 | 
			
		||||
            'pgadmin-sqleditor:loading-icon:show',
 | 
			
		||||
            '{{ _('Initializing the query execution!') }}'
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (explain_prefix != undefined)
 | 
			
		||||
            sql = explain_prefix + ' ' + sql;
 | 
			
		||||
 | 
			
		||||
          self.query_start_time = new Date();
 | 
			
		||||
          self.query = sql;
 | 
			
		||||
          self.rows_affected = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -2169,6 +2368,172 @@ define(
 | 
			
		|||
              alertify.alert('Auto Commit Error', msg);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // This function will
 | 
			
		||||
        _explain: function() {
 | 
			
		||||
          var self = this;
 | 
			
		||||
          var verbose = $('.explain-verbose').hasClass('visibility-hidden') ? 'OFF' : 'ON';
 | 
			
		||||
          var costs = $('.explain-costs').hasClass('visibility-hidden') ? 'OFF' : 'ON';
 | 
			
		||||
 | 
			
		||||
          // No need to check for buffers and timing option value in explain
 | 
			
		||||
          var explain_query = 'EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE %s, COSTS %s, BUFFERS OFF, TIMING OFF) ';
 | 
			
		||||
          explain_query = S(explain_query).sprintf(verbose, costs).value();
 | 
			
		||||
          self._execute(explain_query);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // This function will
 | 
			
		||||
        _explain_analyze: function() {
 | 
			
		||||
          var self = this;var verbose = $('.explain-verbose').hasClass('visibility-hidden') ? 'OFF' : 'ON';
 | 
			
		||||
          var costs = $('.explain-costs').hasClass('visibility-hidden') ? 'OFF' : 'ON';
 | 
			
		||||
          var buffers = $('.explain-buffers').hasClass('visibility-hidden') ? 'OFF' : 'ON';
 | 
			
		||||
          var timing = $('.explain-timing').hasClass('visibility-hidden') ? 'OFF' : 'ON';
 | 
			
		||||
 | 
			
		||||
          var explain_query = 'Explain (FORMAT JSON, ANALYZE ON, VERBOSE %s, COSTS %s, BUFFERS %s, TIMING %s) ';
 | 
			
		||||
          explain_query = S(explain_query).sprintf(verbose, costs, buffers, timing).value();
 | 
			
		||||
          self._execute(explain_query);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // This function will toggle "verbose" option in explain
 | 
			
		||||
        _explain_verbose: function() {
 | 
			
		||||
          if ($('.explain-verbose').hasClass('visibility-hidden') === true) {
 | 
			
		||||
            $('.explain-verbose').removeClass('visibility-hidden');
 | 
			
		||||
            self.explain_verbose = true;
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            $('.explain-verbose').addClass('visibility-hidden');
 | 
			
		||||
            self.explain_verbose = false;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Set this option in preferences
 | 
			
		||||
          var data = {
 | 
			
		||||
            'explain_verbose': self.explain_verbose
 | 
			
		||||
          };
 | 
			
		||||
          $.ajax({
 | 
			
		||||
          url: "{{ url_for('sqleditor.index') }}" + "query_tool/preferences" ,
 | 
			
		||||
          method: 'PUT',
 | 
			
		||||
          contentType: "application/json",
 | 
			
		||||
          data: JSON.stringify(data),
 | 
			
		||||
          success: function(res) {
 | 
			
		||||
            if(res.success == undefined || !res.success) {
 | 
			
		||||
              alertify.alert('Explain options error',
 | 
			
		||||
                '{{ _('Error occurred while setting verbose option in explain') }}'
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          error: function(e) {
 | 
			
		||||
            alertify.alert('Explain options error',
 | 
			
		||||
                '{{ _('Error occurred while setting verbose option in explain') }}'
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // This function will toggle "costs" option in explain
 | 
			
		||||
        _explain_costs: function() {
 | 
			
		||||
          if ($('.explain-costs').hasClass('visibility-hidden') === true) {
 | 
			
		||||
            $('.explain-costs').removeClass('visibility-hidden');
 | 
			
		||||
            self.explain_costs = true;
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            $('.explain-costs').addClass('visibility-hidden');
 | 
			
		||||
            self.explain_costs = false;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Set this option in preferences
 | 
			
		||||
          var data = {
 | 
			
		||||
            'explain_costs': self.explain_costs
 | 
			
		||||
          };
 | 
			
		||||
          $.ajax({
 | 
			
		||||
          url: "{{ url_for('sqleditor.index') }}" + "query_tool/preferences" ,
 | 
			
		||||
          method: 'PUT',
 | 
			
		||||
          contentType: "application/json",
 | 
			
		||||
          data: JSON.stringify(data),
 | 
			
		||||
          success: function(res) {
 | 
			
		||||
            if(res.success == undefined || !res.success) {
 | 
			
		||||
              alertify.alert('Explain options error',
 | 
			
		||||
                '{{ _('Error occurred while setting costs option in explain') }}'
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          error: function(e) {
 | 
			
		||||
            alertify.alert('Explain options error',
 | 
			
		||||
                '{{ _('Error occurred while setting costs option in explain') }}'
 | 
			
		||||
              );
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // This function will toggle "buffers" option in explain
 | 
			
		||||
        _explain_buffers: function() {
 | 
			
		||||
          if ($('.explain-buffers').hasClass('visibility-hidden') === true) {
 | 
			
		||||
            $('.explain-buffers').removeClass('visibility-hidden');
 | 
			
		||||
            self.explain_buffers = true;
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            $('.explain-buffers').addClass('visibility-hidden');
 | 
			
		||||
            self.explain_buffers = false;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Set this option in preferences
 | 
			
		||||
          var data = {
 | 
			
		||||
            'explain_buffers': self.explain_buffers
 | 
			
		||||
          };
 | 
			
		||||
          $.ajax({
 | 
			
		||||
          url: "{{ url_for('sqleditor.index') }}" + "query_tool/preferences" ,
 | 
			
		||||
          method: 'PUT',
 | 
			
		||||
          contentType: "application/json",
 | 
			
		||||
          data: JSON.stringify(data),
 | 
			
		||||
          success: function(res) {
 | 
			
		||||
            if(res.success == undefined || !res.success) {
 | 
			
		||||
              alertify.alert('Explain options error',
 | 
			
		||||
                '{{ _('Error occurred while setting buffers option in explain') }}'
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          error: function(e) {
 | 
			
		||||
            alertify.alert('Explain options error',
 | 
			
		||||
              '{{ _('Error occurred while setting buffers option in explain') }}'
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // This function will toggle "timing" option in explain
 | 
			
		||||
        _explain_timing: function() {
 | 
			
		||||
          if ($('.explain-timing').hasClass('visibility-hidden') === true) {
 | 
			
		||||
            $('.explain-timing').removeClass('visibility-hidden');
 | 
			
		||||
            self.explain_timing = true;
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            $('.explain-timing').addClass('visibility-hidden');
 | 
			
		||||
            self.explain_timing = true;
 | 
			
		||||
          }
 | 
			
		||||
          // Set this option in preferences
 | 
			
		||||
          var data = {
 | 
			
		||||
            'explain_timing': self.explain_timing
 | 
			
		||||
          };
 | 
			
		||||
          $.ajax({
 | 
			
		||||
          url: "{{ url_for('sqleditor.index') }}" + "query_tool/preferences" ,
 | 
			
		||||
          method: 'PUT',
 | 
			
		||||
          contentType: "application/json",
 | 
			
		||||
          data: JSON.stringify(data),
 | 
			
		||||
          success: function(res) {
 | 
			
		||||
            if(res.success == undefined || !res.success) {
 | 
			
		||||
              alertify.alert('Explain options error',
 | 
			
		||||
                '{{ _('Error occurred while setting timing option in explain') }}'
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          error: function(e) {
 | 
			
		||||
            alertify.alert('Explain options error',
 | 
			
		||||
                '{{ _('Error occurred while setting timing option in explain') }}'
 | 
			
		||||
              );
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||