Integrate the graphical explain module in the Query Editor.
Added few TODO list for the graphical explain module by Ashesh.
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")
|
||||
|
56
web/pgadmin/misc/static/explain/css/explain.css
Normal file
@ -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;
|
||||
}
|
BIN
web/pgadmin/misc/static/explain/img/ex_aggregate.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
web/pgadmin/misc/static/explain/img/ex_append.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_bmp_and.png
Normal file
After Width: | Height: | Size: 1006 B |
BIN
web/pgadmin/misc/static/explain/img/ex_bmp_heap.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_bmp_index.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_bmp_or.png
Normal file
After Width: | Height: | Size: 685 B |
BIN
web/pgadmin/misc/static/explain/img/ex_broadcast_motion.png
Normal file
After Width: | Height: | Size: 334 B |
BIN
web/pgadmin/misc/static/explain/img/ex_cte_scan.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_delete.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_foreign_scan.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_gather_motion.png
Normal file
After Width: | Height: | Size: 218 B |
BIN
web/pgadmin/misc/static/explain/img/ex_group.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_hash.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_hash_anti_join.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_hash_semi_join.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_hash_setop_except.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_hash_setop_except_all.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_hash_setop_intersect.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_hash_setop_unknown.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_index_only_scan.png
Normal file
After Width: | Height: | Size: 498 B |
BIN
web/pgadmin/misc/static/explain/img/ex_index_scan.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_insert.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_join.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_limit.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_lock_rows.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_materialize.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_merge.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_merge_anti_join.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_merge_append.png
Normal file
After Width: | Height: | Size: 980 B |
BIN
web/pgadmin/misc/static/explain/img/ex_merge_semi_join.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_nested.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_nested_loop_anti_join.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_nested_loop_semi_join.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_recursive_union.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_redistribute_motion.png
Normal file
After Width: | Height: | Size: 218 B |
BIN
web/pgadmin/misc/static/explain/img/ex_result.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_scan.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_seek.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_setop.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_sort.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_subplan.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_tid_scan.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_unique.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_unknown.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_update.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_values_scan.png
Normal file
After Width: | Height: | Size: 913 B |
BIN
web/pgadmin/misc/static/explain/img/ex_window_aggregate.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
web/pgadmin/misc/static/explain/img/ex_worktable_scan.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
web/pgadmin/misc/static/explain/img/exclude.png
Normal file
After Width: | Height: | Size: 725 B |
BIN
web/pgadmin/misc/static/explain/img/extension-sm.png
Normal file
After Width: | Height: | Size: 423 B |
BIN
web/pgadmin/misc/static/explain/img/extension.png
Normal file
After Width: | Height: | Size: 996 B |
BIN
web/pgadmin/misc/static/explain/img/extensions.png
Normal file
After Width: | Height: | Size: 1017 B |
BIN
web/pgadmin/misc/static/explain/img/exttable-sm.png
Normal file
After Width: | Height: | Size: 612 B |
BIN
web/pgadmin/misc/static/explain/img/exttable.png
Normal file
After Width: | Height: | Size: 793 B |
BIN
web/pgadmin/misc/static/explain/img/exttables.png
Normal file
After Width: | Height: | Size: 662 B |
21
web/pgadmin/misc/static/explain/js/snap.svg-min.js
vendored
Normal file
8149
web/pgadmin/misc/static/explain/js/snap.svg.js
Normal file
691
web/pgadmin/misc/templates/explain/js/explain.js
Normal file
@ -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') }}'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
|