pgadmin4/web/pgadmin/browser/static/js/node.js

1609 lines
55 KiB
JavaScript
Raw Normal View History

define(
'pgadmin.browser.node', [
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'pgadmin.browser.utils', 'pgadmin.backform'
], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl) {
var wcDocker = window.wcDocker,
keyCode = {
ENTER: 13,
ESCAPE: 27,
F1: 112
};
// It has already been defined.
// Avoid running this script again.
if (pgBrowser.Node)
return pgBrowser.Node;
pgBrowser.Nodes = pgBrowser.Nodes || {};
// A helper (base) class for all the nodes, this has basic
// operations/callbacks defined for basic operation.
pgBrowser.Node = function() {};
// Helper function to correctly set up the property chain, for subclasses.
// Uses a hash of class properties to be extended.
//
// It is unlikely - we will instantiate an object for this class.
// (Inspired by Backbone.extend function)
pgBrowser.Node.extend = function(props) {
var parent = this;
var child;
// The constructor function for the new subclass is defined to simply call
// the parent's constructor.
child = function(){ return parent.apply(this, arguments); };
// Add static properties to the constructor function, if supplied.
_.extend(child, parent, _.omit(props, 'callbacks'));
// Make sure - a child have all the callbacks of the parent.
child.callbacks = _.extend({}, parent.callbacks, props.callbacks);
var bindToChild = function(cb) {
if (typeof(child.callbacks[cb]) == 'function') {
child.callbacks[cb] = child.callbacks[cb].bind(child);
}
},
callbacks = _.keys(child.callbacks);
for(var idx = 0; idx < callbacks.length; idx++) bindToChild(callbacks[idx]);
// Registering the node by calling child.Init(...) function
child.Init.apply(child);
// Initialize the parent
this.Init.apply(child);
return child;
};
_.extend(pgAdmin.Browser.Node, Backbone.Events, {
// Node type
type: undefined,
// Label
label: '',
2016-05-16 10:25:51 -05:00
// Help pages
2016-04-12 07:35:47 -05:00
sqlAlterHelp: '',
sqlCreateHelp: '',
2016-05-16 10:25:51 -05:00
dialogHelp: '',
2016-04-12 07:35:47 -05:00
title: function(o, d) {
return o.label + (d ? (' - ' + d.label) : '');
},
hasId: true,
///////
// Initialization function
// Generally - used to register the menus for this type of node.
//
// Also, look at pgAdmin.Browser.add_menus(...) function.
//
// NOTE: Override this for each node for initialization purpose
Init: function() {
var self = this;
if (self.node_initialized)
return;
self.node_initialized = true;
pgAdmin.Browser.add_menus([{
name: 'refresh', node: self.type, module: self,
applies: ['object', 'context'], callback: 'refresh',
priority: 1, label: gettext('Refresh...'),
icon: 'fa fa-refresh'
}]);
2016-08-22 06:30:16 -05:00
if (self.canEdit) {
pgAdmin.Browser.add_menus([{
name: 'show_obj_properties', node: self.type, module: self,
applies: ['object', 'context'], callback: 'show_obj_properties',
priority: 999, label: gettext('Properties...'),
2016-08-22 06:30:16 -05:00
data: {'action': 'edit'}, icon: 'fa fa-pencil-square-o'
}]);
}
if (self.canDrop) {
pgAdmin.Browser.add_menus([{
name: 'delete_object', node: self.type, module: self,
applies: ['object', 'context'], callback: 'delete_obj',
priority: 2, label: gettext('Delete/Drop'),
data: {'url': 'drop'}, icon: 'fa fa-trash',
enable: _.isFunction(self.canDrop) ?
function() {
return !!(self.canDrop.apply(self, arguments));
} : (!!self.canDrop)
}]);
if (self.canDropCascade) {
pgAdmin.Browser.add_menus([{
name: 'delete_object_cascade', node: self.type, module: self,
applies: ['object', 'context'], callback: 'delete_obj',
priority: 3, label: gettext('Drop Cascade'),
data: {'url': 'delete'}, icon: 'fa fa-trash',
enable: _.isFunction(self.canDropCascade) ?
function() { return self.canDropCascade.apply(self, arguments); } : (!!self.canDropCascade)
}]);
}
}
// show query tool only in context menu of supported nodes.
if (true) {
if (_.indexOf(pgAdmin.unsupported_nodes, self.type) == -1) {
pgAdmin.Browser.add_menus([{
name: 'show_query_tool', node: self.type, module: self,
applies: ['context'], callback: 'show_query_tool',
priority: 998, label: gettext('Query Tool...'),
icon: 'fa fa-bolt',
enable: function(itemData, item, data) {
if (itemData._type == 'database' && itemData.allowConn)
return true;
else if(itemData._type != 'database')
return true;
else
return false;
}
}]);
}
}
// This will add options of scripts eg:'CREATE Script'
if (self.hasScriptTypes && _.isArray(self.hasScriptTypes)
&& self.hasScriptTypes.length > 0) {
// For each script type create menu
_.each(self.hasScriptTypes, function(stype) {
var type_label = S(
gettext("%s Script")
).sprintf(stype.toUpperCase()).value(),
stype = stype.toLowerCase();
// Adding menu for each script type
pgAdmin.Browser.add_menus([{
name: 'show_script_' + stype, node: self.type, module: self,
applies: ['object', 'context'], callback: 'show_script',
priority: 4, label: type_label, category: 'Scripts',
data: {'script': stype}, icon: 'fa fa-pencil',
enable: self.check_user_permission
}]);
});
}
},
///////
// Checks if Script Type is allowed to user
// First check if role node & create role allowed
// Otherwise test rest of database objects
// if no permission matched then do not allow create script
///////
check_user_permission: function(itemData, item, data) {
// Do not display CREATE script on server group and server node
if (itemData._type == 'server_group' || itemData._type == 'server') {
return false;
}
// Do not display the menu if the database connection is not allowed
if (itemData._type == 'database' && !itemData.allowConn)
return false;
var node = pgBrowser.Nodes[itemData._type],
parentData = node.getTreeNodeHierarchy(item);
if ( _.indexOf(['create','insert','update', 'delete'], data.script) != -1) {
if (itemData.type == 'role' &&
parentData.server.user.can_create_role) {
return true;
} else if (
(
parentData.server && (
parentData.server.user.is_superuser ||
parentData.server.user.can_create_db)
) ||
(
parentData.schema && parentData.schema.can_create
)
) {
return true;
} else {
return false;
}
} else {
return true;
}
},
///////
// Generate a Backform view using the node's model type
//
// Used to generate view for the particular node properties, edit,
// creation.
getView: function(item, type, el, node, formType, callback, ctx, cancelFunc) {
var that = this;
if (!this.type || this.type == '')
// We have no information, how to generate view for this type.
return null;
if (this.model) {
// This will be the URL, used for object manipulation.
// i.e. Create, Update in these cases
var urlBase = this.generate_url(item, type, node, false);
if (!urlBase)
// Ashamed of myself, I don't know how to manipulate this
// node.
return null;
var attrs = {};
// In order to get the object data from the server, we must set
// object-id in the model (except in the create mode).
if (type !== 'create') {
attrs[this.model.idAttribute || this.model.prototype.idAttribute ||
'id'] = node._id;
}
// We know - which data model to be used for this object.
var info = this.getTreeNodeHierarchy.apply(this, [item]),
newModel = new (this.model.extend({urlRoot: urlBase})) (
attrs, {node_info: info}
),
fields = Backform.generateViewSchema(
info, newModel, type, this, node
);
if (type == 'create' || type == 'edit') {
if (callback && ctx) {
callback = callback.bind(ctx);
} else {
callback = function() {
console.log("Broke something!!! Why we don't have the callback or the context???");
};
}
var onSessionInvalid = function(msg) {
var alertMessage = '\
<div class="media error-in-footer bg-red-1 border-red-2 font-red-3 text-14">\
<div class="media-body media-middle">\
<div class="alert-icon error-icon">\
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>\
</div>\
<div class="alert-text">' + msg + '</div>\
<div class="close-error-bar">\
<a class="close-error">x</a>\
</div>\
</div>\
</div>';
if(!_.isUndefined(that.statusBar)) {
that.statusBar.html(alertMessage).css("visibility", "visible");
that.statusBar.find("a.close-error").bind("click", function(e) {
this.empty().css("visibility", "hidden");
}.bind(that.statusBar));
}
callback(true);
return true;
};
var onSessionValidated = function(sessHasChanged) {
if(!_.isUndefined(that.statusBar)) {
2016-01-21 03:10:00 -06:00
that.statusBar.empty().css("visibility", "hidden");
}
callback(false, sessHasChanged);
};
callback(false, false);
newModel.on('pgadmin-session:valid', onSessionValidated);
newModel.on('pgadmin-session:invalid', onSessionInvalid);
}
// 'schema' has the information about how to generate the form.
if (_.size(fields)) {
// This will contain the actual view
var view;
if (formType == 'fieldset') {
// It is used to show, edit, create the object in the
// properties tab.
view = new Backform.Fieldset({
el: el, model: newModel, schema: fields
});
} else {
// This generates a view to be used by the node dialog
// (for create/edit operation).
view = new Backform.Dialog({
el: el, model: newModel, schema: fields
});
}
var setFocusOnEl = function() {
setTimeout(function() {
$(el).find('.tab-pane.active:first').find('input:first').focus();
}, 500);
};
if (!newModel.isNew()) {
// This is definetely not in create mode
var msgDiv = '<div class="alert alert-info pg-panel-message pg-panel-properties-message">'+
gettext("Retrieving data from the server...") + '</div>',
$msgDiv = $(msgDiv);
var timer = setTimeout(function(ctx) {
// notify user if request is taking longer than 1 second
if (!_.isUndefined(ctx)) {
$msgDiv.appendTo(ctx);
}
}, 1000, ctx);
newModel.fetch({
success: function(res, msg, xhr) {
// clear timeout and remove message
clearTimeout(timer);
$msgDiv.addClass('hidden');
// We got the latest attributes of the
// object. Render the view now.
view.render();
setFocusOnEl();
newModel.startNewSession();
},
error: function(xhr, error, message) {
var _label = that && item ?
that.getTreeNodeHierarchy(
item
)[that.type].label : '';
pgBrowser.Events.trigger(
'pgadmin:node:retrieval:error', 'properties',
xhr, error, message, item
);
if (
!Alertify.pgHandleItemError(
xhr, error, message, {item: item, info: info}
)
) {
Alertify.pgNotifier(
error, xhr,
S(
gettext("Error retrieving properties - %s")
).sprintf(message || _label).value(),
function() {
console.log(arguments);
}
);
}
// Close the panel (if could not fetch properties)
if (cancelFunc) {
cancelFunc();
}
}
});
} else {
// Yay - render the view now!
// $(el).focus();
view.render();
setFocusOnEl();
newModel.startNewSession();
}
}
return view;
}
return null;
},
register_node_panel: function() {
var w = pgBrowser.docker,
p = w.findPanels('node_props');
if (p && p.length == 1)
return;
var events = {};
events[wcDocker.EVENT.RESIZE_ENDED] = function() {
var $container = this.$container.find('.obj_properties').first(),
v = $container.data('obj-view');
if (v && v.model && v.model) {
v.model.trigger(
'pg-browser-resized', {
'view': v, 'panel': this, 'container': $container
});
}
};
p = new pgBrowser.Panel({
name: 'node_props',
showTitle: true,
isCloseable: true,
isPrivate: true,
2016-05-25 05:17:56 -05:00
elContainer: true,
content: '<div class="obj_properties"><div class="alert alert-info pg-panel-message">' + gettext('Please wait while we fetch information about the node from the server...') + '</div></div>',
2016-05-25 05:17:56 -05:00
onCreate: function(myPanel, $container) {
$container.addClass('pg-no-overflow');
},
events: events
});
p.load(pgBrowser.docker);
},
2016-08-22 06:30:16 -05:00
/*
* Default script type menu for node.
*
* Override this, to show more script type menus (e.g hasScriptTypes: ['create', 'select', 'insert', 'update', 'delete'])
*
* Or set it to empty array to disable script type menu on node (e.g hasScriptTypes: [])
*/
hasScriptTypes: ['create'],
/******************************************************************
* This function determines the given item is editable or not.
*
* Override this, when a node is not editable.
*/
canEdit: true,
/******************************************************************
* This function determines the given item is deletable or not.
*
* Override this, when a node is not deletable.
*/
canDrop: false,
/************************************************************************
* This function determines the given item and children are deletable or
* not.
*
* Override this, when a node is not deletable.
*/
canDropCascade: false,
// List of common callbacks - that can be used for different
// operations!
callbacks: {
/******************************************************************
* This function allows to create/edit/show properties of any
* object depending on the arguments provided.
*
* args must be a object containing:
* action - create/edit/properties
* item - The properties of the item (tree node item)
*
* NOTE:
* if item is not provided, the action will be done on the
* currently selected tree item node.
*
**/
show_obj_properties: function(args, item) {
var t = pgBrowser.tree,
i = args.item || item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined,
o = this,
l = o.title.apply(this, [d]);
// Make sure - the properties dialog type registered
pgBrowser.Node.register_node_panel();
// No node selected.
if (!d)
return;
var self = this,
isParent = (_.isArray(this.parent_type) ?
function(d) {
return (_.indexOf(self.parent_type, d._type) != -1);
} : function(d) {
return (self.parent_type == d._type);
}),
addPanel = function() {
var d = window.document,
b = d.body,
el = d.createElement('div');
d.body.insertBefore(el, d.body.firstChild);
2016-05-25 05:17:56 -05:00
var pW = screen.width < 800 ? '95%' : '500px',
pH = screen.height < 600 ? '95%' : '550px',
w = pgAdmin.toPx(el, self.width || pW, 'width', true),
h = pgAdmin.toPx(el, self.height|| pH, 'height', true),
x = (b.offsetWidth - w) / 2,
y = (b.offsetHeight - h) / 2;
var p = pgBrowser.docker.addPanel(
'node_props', wcDocker.DOCK.FLOAT, undefined,
{w: w + 'px', h: h + 'px', x: x + 'px', y: y + 'px'}
);
b.removeChild(el);
// delete(el);
return p;
};
if (args.action == 'create') {
// If we've parent, we will get the information of it for
// proper object manipulation.
//
// You know - we're working with RDBMS, relation is everything
// for us.
if (self.parent_type && !isParent(d)) {
// In browser tree, I can be under any node, But - that
// does not mean, it is my parent.
//
// We have some group nodes too.
//
// i.e.
// Tables, Views, etc. nodes under Schema node
//
// And, actual parent of a table is schema, not Tables.
while (i && t.hasParent(i)) {
i = t.parent(i);
var pd = t.itemData(i);
if (isParent(pd)) {
// Assign the data, this is my actual parent.
d = pd;
break;
}
}
}
// Seriously - I really don't have parent data present?
//
// The only node - which I know - who does not have parent
// node, is the Server Group (and, comes directly under root
// node - which has no parent.)
if (!d || (this.parent_type != null && !isParent(d))) {
// It should never come here.
// If it is here, that means - we do have some bug in code.
return;
}
if (!d)
return;
l = S( gettext('Create - %s')).sprintf(
[this.label]).value();
p = addPanel();
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
} else {
if (pgBrowser.Node.panels && pgBrowser.Node.panels[d.id] &&
pgBrowser.Node.panels[d.id].$container) {
var p = pgBrowser.Node.panels[d.id];
/** TODO ::
* Run in edit mode (if asked) only when it is
* not already been running edit mode
**/
var mode = p.$container.attr('action-mode');
if (mode) {
var msg = gettext('Are you sure want to stop editing the properties of %s "%s"?');
if (args.action == 'edit') {
msg = gettext('Are you sure want to reset the current changes and re-open the panel for %s "%s"?');
}
Alertify.confirm(
gettext('Edit in progress?'),
S(msg).sprintf(o.label.toLowerCase(), d.label).value(),
function() {
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
},
null).show();
} else {
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
}
} else {
pgBrowser.Node.panels = pgBrowser.Node.panels || {};
p = pgBrowser.Node.panels[d.id] = addPanel();
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
}
}
p.title(l);
p.icon('icon-' + this.type);
// Make sure the properties dialog is visible
p.focus();
},
// Delete the selected object
delete_obj: function(args, item) {
var input = args || {'url':'drop'},
obj = this,
t = pgBrowser.tree,
i = input.item || item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
if (!d)
return;
/*
* Make sure - we're using the correct version of node
*/
obj = pgBrowser.Nodes[d._type];
var objName = d.label;
var msg, title;
if (input.url == 'delete') {
msg = S( gettext('Are you sure you want to drop %s "%s" and all the objects that depend on it?'))
.sprintf(obj.label.toLowerCase(), d.label).value();
title = S( gettext('DROP CASCADE %s?')).sprintf(obj.label).value();
if (!(_.isFunction(obj.canDropCascade) ?
obj.canDropCascade.apply(obj, [d, i]) : obj.canDropCascade)) {
Alertify.error(
S( gettext('The %s "%s" cannot be dropped.'))
.sprintf(obj.label, d.label).value(),
10
);
return;
}
} else {
msg = S( gettext('Are you sure you want to drop %s "%s"?'))
.sprintf(obj.label.toLowerCase(), d.label).value();
title = S( gettext('DROP %s?')).sprintf(obj.label).value();
if (!(_.isFunction(obj.canDrop) ?
obj.canDrop.apply(obj, [d, i]) : obj.canDrop)) {
Alertify.error(
S( gettext('The %s "%s" cannot be dropped.'))
.sprintf(obj.label, d.label).value(),
10
);
return;
}
}
Alertify.confirm(title, msg,
function() {
$.ajax({
url: obj.generate_url(i, input.url, d, true),
type:'DELETE',
success: function(res) {
if (res.success == 0) {
pgBrowser.report_error(res.errormsg, res.info);
} else {
pgBrowser.removeTreeNode(i, true);
}
return true;
},
error: function(jqx) {
var msg = jqx.responseText;
/* Error from the server */
if (jqx.status == 417 || jqx.status == 410 || jqx.status == 500) {
try {
var data = $.parseJSON(jqx.responseText);
msg = data.errormsg;
} catch (e) {}
}
pgBrowser.report_error(
S( gettext('Error dropping %s: "%s"'))
.sprintf(obj.label, objName)
.value(), msg);
}
});
},
null).show()
},
// Callback for creating script(s) & opening them in Query editor
show_script: function(args, item) {
var scriptType = args.script,
obj = this,
t = pgBrowser.tree,
i = item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
if (!d)
return;
/*
* Make sure - we're using the correct version of node
*/
obj = pgBrowser.Nodes[d._type];
var objName = d.label,
sql_url;
// URL for script type
if(scriptType == 'insert') {
sql_url = 'insert_sql';
} else if(scriptType == 'update') {
sql_url = 'update_sql';
} else if(scriptType == 'delete') {
sql_url = 'delete_sql';
} else if(scriptType == 'select') {
sql_url = 'select_sql';
} else if(scriptType == 'exec') {
sql_url = 'exec_sql';
} else {
// By Default get CREATE SQL
sql_url = 'sql';
}
// Open data grid & pass the URL for fetching
pgAdmin.DataGrid.show_query_tool(
obj.generate_url(i, sql_url, d, true),
i, scriptType
);
},
// Callback to render query editor
show_query_tool: function(args, item) {
var obj = this,
t = pgBrowser.tree,
i = item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
if (!d)
return;
// Here call data grid method to render query tool
pgAdmin.DataGrid.show_query_tool('', i);
},
// Logic to change the server background colour
// There is no way of applying CSS to parent element so we have to
// do it via JS code only
change_server_background: function(item, data) {
if (!item || !data)
return;
// Go further only if node type is a Server
if (data && data._type && data._type == 'server') {
var element = $(item).find('span.aciTreeItem').first() || null,
// First element will be icon and second will be colour code
bgcolor = data.icon.split(" ")[1] || null,
fgcolor = data.icon.split(" ")[2] || '';
if(bgcolor) {
// li tag for the current branch
var first_level_element = element.parents()[3] || null,
dynamic_class = 'pga_server_' + data._id + '_bgcolor',
style_tag;
// Prepare dynamic style tag
style_tag = "<style id=" + dynamic_class + " type='text/css'> \n";
style_tag += "." + dynamic_class + ' .aciTreeItem {';
style_tag += " border-radius: 3px; margin-bottom: 2px;";
style_tag += " background: " + bgcolor + "} \n";
if(fgcolor) {
style_tag += "." + dynamic_class + ' .aciTreeText {';
style_tag += " color: " + fgcolor + ";} \n"
}
style_tag += "</style>";
// Prepare dynamic style tag using template
$('#' + dynamic_class).remove();
$(style_tag).appendTo("head");
if(first_level_element)
$(first_level_element).addClass(dynamic_class);
}
}
},
added: function(item, data, browser) {
var b = browser || pgBrowser,
t = b.tree,
pItem = t.parent(item),
pData = pItem && t.itemData(pItem),
pNode = pData && pgBrowser.Nodes[pData._type];
// Check node is a collection or not.
if (pNode && pNode.is_collection) {
/* If 'collection_count' is not present in data
* it means tree node expanded first time, so we will
* kept collection count and label in data itself.
*/
if (!('collection_count' in pData)) {
pData.collection_count = 0;
}
pData.collection_count++;
t.setLabel(
pItem, {
label: (
_.escape(pData._label) + ' <span>(' + pData.collection_count + ')</span>'
)
}
);
}
pgBrowser.Node.callbacks.change_server_background(item, data);
},
// Callback called - when a node is selected in browser tree.
selected: function(item, data, browser) {
// Show the information about the selected node in the below panels,
// which are visible at this time:
// + Properties
// + Query (if applicable, otherwise empty)
// + Dependents
// + Dependencies
// + Statistics
var b = browser || pgBrowser,
t = b.tree,
d = data || t.itemData(item);
// Update the menu items
pgAdmin.Browser.enable_disable_menus.apply(b, [item]);
if (d && b) {
if ('properties' in b.panels &&
b.panels['properties'] &&
b.panels['properties'].panel &&
b.panels['properties'].panel.isVisible()) {
// Show object properties (only when the 'properties' tab
// is active).
this.showProperties(item, d, b.panels['properties'].panel);
}
if ('sql' in b.panels &&
b.panels['sql'] &&
b.panels['sql'].panel &&
b.panels['sql'].panel.isVisible()) {
// TODO:: Show reverse engineered query for this object (when 'sql'
// tab is active.)
}
if ('statistics' in b.panels &&
b.panels['statistics'] &&
b.panels['statistics'].panel &&
b.panels['statistics'].panel.isVisible()) {
// TODO:: Show statistics for this object (when the 'statistics'
// tab is active.)
}
if ('dependencies' in b.panels &&
b.panels['dependencies'] &&
b.panels['dependencies'].panel &&
b.panels['dependencies'].panel.isVisible()) {
// TODO:: Show dependencies for this object (when the
// 'dependencies' tab is active.)
}
if ('dependents' in b.panels &&
b.panels['dependents'] &&
b.panels['dependents'].panel &&
b.panels['dependents'].panel.isVisible()) {
// TODO:: Show dependents for this object (when the 'dependents'
// tab is active.)
}
}
Introduced a event manager for the browser, which will generate certain known events, when any activity happens on the browser layout. The following types of events will be triggered through the browser event manager. - pgadmin-browser:frame:* [1] - pgadmin-browser:frame-<name>:* [1] - pgadmin-browser:panel:* [1] - pgadmin-browser:panel-<name>:* [1] - pgadmin-browser:panel - pgadmin-browser:frame - pgadmin-browser:tree - pgadmin-browser:tree:* [2] [1] The '*' denotes some of the events generated by the wcDocker, which can be useful to do some operations. These events are: + wcDocker.EVENT.UPDATED + wcDocker.EVENT.VISIBILITY_CHANGED + wcDocker.EVENT.BEGIN_DOCK + wcDocker.EVENT.END_DOCK + wcDocker.EVENT.GAIN_FOCUS, + wcDocker.EVENT.LOST_FOCUS + wcDocker.EVENT.CLOSED + wcDocker.EVENT.BUTTON + wcDocker.EVENT.ATTACHED + wcDocker.EVENT.DETACHED + wcDocker.EVENT.MOVE_STARTED + wcDocker.EVENT.MOVE_ENDED + wcDocker.EVENT.MOVED + wcDocker.EVENT.RESIZE_STARTED + wcDocker.EVENT.RESIZE_ENDED + wcDocker.EVENT.RESIZED + wcDocker.EVENT.SCROLLED [2] The '*' denotes all the events generated by the Browser Tree (aciTree). The extension modules can utilize these events to do some operations on nodes, and panels. This patch includes showing 'Reversed Engineered Query' for the selected node (if allowed) using the above approch. The ShowNodeSQL module looks for two events. 1. SQL Panel Visibility change. - Based on the visibility of that panel, we start/stop listening the node selection event. 2. Node Selection in Browser tree - Based on the selected node type, it will look for 'hasSQL' parameter of the node, and fetch the Query from the server, and show it in the SQL editor.
2016-01-19 06:31:14 -06:00
return true;
},
removed: function(item) {
var self = this,
t = pgBrowser.tree,
pItem = t.parent(item),
pData = pItem && t.itemData(pItem),
pNode = pData && pgBrowser.Nodes[pData._type];
// Check node is a collection or not.
if (
pNode && pNode.is_collection && 'collection_count' in pData
) {
pData.collection_count--;
t.setLabel(
pItem, {
label: (
_.escape(pData._label) + ' <span>(' + pData.collection_count + ')</span>'
)
}
);
}
setTimeout(function() { self.clear_cache.apply(self, item); }, 0);
},
unloaded: function(item) {
var self = this,
t = pgBrowser.tree,
data = item && t.itemData(item);
// In case of unload remove the collection counter
if (self.is_collection && 'collection_count' in data)
{
delete data.collection_count;
t.setLabel(item, {label: _.escape(data._label)});
}
},
refresh: function(cmd, i) {
var self = this,
t = pgBrowser.tree,
item = i || t.selected(),
d = t.itemData(item);
pgBrowser.Events.trigger('pgadmin:browser:tree:refresh', item);
}
},
/**********************************************************************
* A hook (not a callback) to show object properties in given HTML
* element.
*
* This has been used for the showing, editing properties of the node.
* This has also been used for creating a node.
**/
showProperties: function(item, data, panel, action) {
var that = this,
tree = pgAdmin.Browser.tree,
j = panel.$container.find('.obj_properties').first(),
view = j.data('obj-view'),
content = $('<div tabindex="1"></div>')
.addClass('pg-prop-content col-xs-12');
// Handle key press events for Cancel, save and help button
var handleKeyDown = function(event, context) {
// If called on panel other than node_props, return
if (panel && panel['_type'] !== 'node_props') return;
switch (event.which) {
case keyCode.ESCAPE:
closePanel();
break;
case keyCode.ENTER:
// Return if event is fired from child element
if (event.target !== context) return;
if (view && view.model && view.model.sessChanged()) {
onSave.call(this, view);
}
break;
case keyCode.F1:
onDialogHelp();
break;
default:
break;
}
}.bind(panel);
setTimeout(function() {
// Register key press events with panel element
panel.$container.find('.backform-tab').on("keydown", function(event) {
handleKeyDown(event, this);
});
}, 200); // wait for panel tab to render
// Template function to create the status bar
var createStatusBar = function(location){
var statusBar = $('<div></div>').addClass(
'pg-prop-status-bar'
).appendTo(j);
2016-01-21 03:10:00 -06:00
statusBar.css("visibility", "hidden");
if (location == "header") {
statusBar.appendTo(that.header);
} else {
statusBar.prependTo(that.footer);
}
that.statusBar = statusBar;
return statusBar;
}.bind(panel),
// Template function to create the button-group
createButtons = function(buttons, location, extraClasses) {
var panel = this;
// arguments must be non-zero length array of type
// object, which contains following attributes:
// label, type, extraClasses, register
if (buttons && _.isArray(buttons) && buttons.length > 0) {
// All buttons will be created within a single
// div area.
var btnGroup =
$('<div></div>').addClass(
'pg-prop-btn-group'
),
// Template used for creating a button
tmpl = _.template([
'<button type="<%= type %>" ',
'class="btn <%=extraClasses.join(\' \')%>"',
'<% if (disabled) { %> disabled="disabled"<% } %> title="<%-tooltip%>">',
'<span class="<%= icon %>"></span><% if (label != "") { %>&nbsp;<%-label%><% } %></button>'
].join(' '));
if (location == "header"){
btnGroup.appendTo(that.header);
}else{
btnGroup.appendTo(that.footer);
}
if (extraClasses) {
btnGroup.addClass(extraClasses);
}
_.each(buttons, function(btn) {
// Create the actual button, and append to
// the group div
// icon may not present for this button
if (!btn.icon) {
btn.icon = "";
}
var b = $(tmpl(btn));
btnGroup.append(b);
// Register is a callback to set callback
// for certain operation for this button.
btn.register(b);
});
return btnGroup;
}
return null;
}.bind(panel),
// Callback to show object properties
properties = function() {
// Avoid unnecessary reloads
var panel = this,
i = tree.selected(),
d = i && tree.itemData(i),
n_type = d._type,
n_value = -1,
n = i && d && pgBrowser.Nodes[d._type],
treeHierarchy = n.getTreeNodeHierarchy(i);
if (_.isEqual($(panel).data('node-prop'), treeHierarchy)) {
return;
}
// Cache the current IDs for next time
$(panel).data('node-prop', treeHierarchy);
if (!content.hasClass('has-pg-prop-btn-group'))
content.addClass('has-pg-prop-btn-group');
// We need to release any existing view, before
// creating new view.
if (view) {
// Release the view
view.remove({data: true, internal: true, silent: true});
// Deallocate the view
// delete view;
view = null;
// Reset the data object
j.data('obj-view', null);
}
// Make sure the HTML element is empty.
j.empty();
that.header = $('<div></div>').addClass(
'pg-prop-header'
).appendTo(j);
that.footer = $('<div></div>').addClass(
'pg-prop-footer'
).appendTo(j);
// Create a view to show the properties in fieldsets
view = that.getView(item, 'properties', content, data, 'fieldset', undefined, j);
if (view) {
// Save it for release it later
j.data('obj-view', view);
// Create proper buttons
var buttons = [];
2016-08-22 06:30:16 -05:00
buttons.push({
label: '', type: 'edit',
tooltip: gettext('Edit'),
extraClasses: ['btn-default'],
icon: 'fa fa-lg fa-pencil-square-o',
2016-08-22 06:30:16 -05:00
disabled: !that.canEdit,
register: function(btn) {
btn.click(function() {
onEdit();
});
}
});
2016-08-22 06:30:16 -05:00
2016-04-12 07:35:47 -05:00
buttons.push({
label: '', type: 'help',
tooltip: gettext('SQL help for this object type.'),
2016-05-16 10:25:51 -05:00
extraClasses: ['btn-default', 'pull-right'],
2016-04-15 07:29:57 -05:00
icon: 'fa fa-lg fa-info',
2016-04-12 07:35:47 -05:00
disabled: (that.sqlAlterHelp == '' && that.sqlCreateHelp == '') ? true : false,
register: function(btn) {
btn.click(function() {
onSqlHelp();
});
}
});
2017-06-07 10:31:47 -05:00
createButtons(buttons, 'header', 'pg-prop-btn-group-above bg-gray-2 border-gray-3');
}
j.append(content);
}.bind(panel),
2016-04-12 07:35:47 -05:00
onSqlHelp = function() {
var panel = this;
2016-04-12 07:35:47 -05:00
// See if we can find an existing panel, if not, create one
var pnlSqlHelp = pgBrowser.docker.findPanels('pnl_sql_help')[0];
2016-04-12 07:35:47 -05:00
if (pnlSqlHelp == null) {
var pnlProperties = pgBrowser.docker.findPanels('properties')[0];
2016-04-12 07:35:47 -05:00
pgBrowser.docker.addPanel('pnl_sql_help', wcDocker.DOCK.STACKED, pnlProperties);
pnlSqlHelp = pgBrowser.docker.findPanels('pnl_sql_help')[0];
}
// Construct the URL
var server = that.getTreeNodeHierarchy(item).server;
2016-04-12 07:35:47 -05:00
var url = pgBrowser.utils.pg_help_path;
2016-04-12 07:35:47 -05:00
if (server.server_type == 'ppas') {
url = pgBrowser.utils.edbas_help_path;
2016-04-12 07:35:47 -05:00
}
var major = Math.floor(server.version / 10000),
minor = Math.floor(server.version / 100) - (major * 100);
2016-04-12 07:35:47 -05:00
url = url.replace('$VERSION$', major + '.' + minor);
if (!S(url).endsWith('/')) {
2016-04-12 07:35:47 -05:00
url = url + '/'
}
if (that.sqlCreateHelp == '' && that.sqlAlterHelp != '') {
url = url + that.sqlAlterHelp
} else if (that.sqlCreateHelp != '' && that.sqlAlterHelp == '') {
url = url + that.sqlCreateHelp
} else {
if (view.model.isNew()) {
url = url + that.sqlCreateHelp
} else {
url = url + that.sqlAlterHelp
}
}
// Update the panel
var iframe = $(pnlSqlHelp).data('embeddedFrame');
2016-04-12 07:35:47 -05:00
pnlSqlHelp.title('SQL: ' + that.label);
2016-04-15 07:29:57 -05:00
2016-04-12 07:35:47 -05:00
pnlSqlHelp.focus();
iframe.openURL(url);
}.bind(panel),
2016-05-16 10:25:51 -05:00
onDialogHelp = function() {
var panel = this;
// See if we can find an existing panel, if not, create one
var pnlDialogHelp = pgBrowser.docker.findPanels('pnl_online_help')[0];
2016-05-16 10:25:51 -05:00
if (pnlDialogHelp == null) {
var pnlProperties = pgBrowser.docker.findPanels('properties')[0];
2016-05-16 10:25:51 -05:00
pgBrowser.docker.addPanel('pnl_online_help', wcDocker.DOCK.STACKED, pnlProperties);
pnlDialogHelp = pgBrowser.docker.findPanels('pnl_online_help')[0];
}
// Update the panel
var iframe = $(pnlDialogHelp).data('embeddedFrame');
2016-05-16 10:25:51 -05:00
pnlDialogHelp.focus();
iframe.openURL(that.dialogHelp);
}.bind(panel),
onSave = function(view) {
var m = view.model,
d = m.toJSON(true),
// Generate a timer for the request
timer = setTimeout(function(){
$('.obj_properties').addClass('show_progress');
}, 1000);
if (d && !_.isEmpty(d)) {
m.save({}, {
attrs: d,
validate: false,
cache: false,
success: function() {
onSaveFunc.call();
// Hide progress cursor
$('.obj_properties').removeClass('show_progress');
clearTimeout(timer);
// Removing the node-prop property of panel
// so that we show updated data on panel
var pnlProperties = pgBrowser.docker.findPanels('properties')[0],
pnlSql = pgBrowser.docker.findPanels('sql')[0],
pnlStats = pgBrowser.docker.findPanels('statistics')[0],
pnlDependencies = pgBrowser.docker.findPanels('dependencies')[0],
pnlDependents = pgBrowser.docker.findPanels('dependents')[0];
if(pnlProperties)
$(pnlProperties).removeData('node-prop');
if(pnlSql)
$(pnlSql).removeData('node-prop');
if(pnlStats)
$(pnlStats).removeData('node-prop');
if(pnlDependencies)
$(pnlDependencies).removeData('node-prop');
if(pnlDependents)
$(pnlDependents).removeData('node-prop');
},
error: function(m, jqxhr) {
Alertify.pgNotifier(
"error", jqxhr,
gettext("Error saving properties")
);
// Hide progress cursor
$('.obj_properties').removeClass('show_progress');
clearTimeout(timer);
}
});
}
}.bind(panel),
editFunc = function() {
var panel = this;
if (action && action == 'properties') {
action = 'edit';
}
panel.$container.attr('action-mode', action);
// We need to release any existing view, before
// creating the new view.
if (view) {
// Release the view
view.remove({data: true, internal: true, silent: true});
// Deallocate the view
view = null;
// Reset the data object
j.data('obj-view', null);
}
// Make sure the HTML element is empty.
j.empty();
that.header = $('<div></div>').addClass(
'pg-prop-header'
).appendTo(j)
that.footer = $('<div></div>').addClass(
'pg-prop-footer'
).appendTo(j);
var updateButtons = function(hasError, modified) {
var btnGroup = this.find('.pg-prop-btn-group'),
btnSave = btnGroup.find('button.btn-primary'),
btnReset = btnGroup.find('button.btn-warning');
if (hasError || !modified) {
btnSave.prop('disabled', true);
btnSave.attr('disabled', 'disabled');
} else {
btnSave.prop('disabled', false);
btnSave.removeAttr('disabled');
}
if (!modified) {
btnReset.prop('disabled', true);
btnReset.attr('disabled', 'disabled');
} else {
btnReset.prop('disabled', false);
btnReset.removeAttr('disabled');
}
};
// Create a view to edit/create the properties in fieldsets
view = that.getView(item, action, content, data, 'dialog', updateButtons, j, onCancelFunc);
if (view) {
// Save it to release it later
j.data('obj-view', view);
panel.icon(
_.isFunction(that['node_image']) ?
(that['node_image']).apply(that, [data, view.model]) :
(that['node_image'] || ('icon-' + that.type))
);
// Create proper buttons
createButtons([{
2016-04-12 07:35:47 -05:00
label: '', type: 'help',
tooltip: gettext('SQL help for this object type.'),
2016-05-16 10:25:51 -05:00
extraClasses: ['btn-default', 'pull-left'],
2016-04-15 07:29:57 -05:00
icon: 'fa fa-lg fa-info',
2016-05-16 10:25:51 -05:00
disabled: (that.sqlAlterHelp == '' && that.sqlCreateHelp == '') ? true : false,
2016-04-12 07:35:47 -05:00
register: function(btn) {
btn.click(function() {
onSqlHelp();
});
}
2016-05-16 10:25:51 -05:00
},{
label: '', type: 'help',
tooltip: gettext('Help for this dialog.'),
2016-05-16 10:25:51 -05:00
extraClasses: ['btn-default', 'pull-left'],
icon: 'fa fa-lg fa-question',
disabled: (that.dialogHelp == '') ? true : false,
register: function(btn) {
btn.click(function() {
onDialogHelp();
});
}
2016-04-12 07:35:47 -05:00
},{
label: gettext('Save'), type: 'save',
tooltip: gettext('Save this object.'),
extraClasses: ['btn-primary'],
icon: 'fa fa-lg fa-save',
disabled: true,
register: function(btn) {
// Save the changes
btn.click(function() {
onSave.call(this, view);
});
}
},{
label: gettext('Cancel'), type: 'cancel',
tooltip: gettext('Cancel changes to this object.'),
extraClasses: ['btn-danger'],
icon: 'fa fa-lg fa-close',
disabled: false,
register: function(btn) {
btn.click(function() {
// Removing the action-mode
panel.$container.removeAttr('action-mode');
onCancelFunc.call(arguments);
});
}
},{
label: gettext('Reset'), type: 'reset',
tooltip: gettext('Reset the fields on this dialog.'),
extraClasses: ['btn-warning'],
icon: 'fa fa-lg fa-recycle',
disabled: true,
register: function(btn) {
btn.click(function() {
setTimeout(function() { editFunc.call(); }, 0);
});
}
2017-06-07 10:31:47 -05:00
}],'footer' ,'pg-prop-btn-group-below bg-gray-2 border-gray-3');
};
// Create status bar.
createStatusBar('footer');
// Add some space, so that - button group does not override the
// space
content.addClass('pg-prop-has-btn-group-below');
// Show contents before buttons
j.prepend(content);
}.bind(panel),
closePanel = function() {
// Closing this panel
2016-04-15 08:02:21 -05:00
this.close();
}.bind(panel),
updateTreeItem = function(that) {
var _old = data,
_new = _.clone(view.model.tnode),
info = _.clone(view.model.node_info);
// Clear the cache for this node now.
setTimeout(function() { that.clear_cache.apply(that, item); }, 0);
pgBrowser.Events.trigger(
'pgadmin:browser:tree:update',
_old, _new, info, {
success: function(_item, _newNodeData, _oldNodeData) {
pgBrowser.Events.trigger(
'pgadmin:browser:node:updated', _item, _newNodeData,
_oldNodeData
);
pgBrowser.Events.trigger(
'pgadmin:browser:node:' + _newNodeData._type + ':updated',
_item, _newNodeData, _oldNodeData
);
}
}
);
closePanel();
},
saveNewNode = function(that) {
var panel = this,
j = panel.$container.find('.obj_properties').first(),
view = j.data('obj-view');
// Clear the cache for this node now.
setTimeout(function() { that.clear_cache.apply(that, item); }, 0);
try {
pgBrowser.Events.trigger(
'pgadmin:browser:tree:add', _.clone(view.model.tnode),
_.clone(view.model.node_info)
);
} catch (e) {
console.log(e);
}
closePanel();
}.bind(panel, that),
editInNewPanel = function() {
// Open edit in separate panel
setTimeout(function() {
that.callbacks.show_obj_properties.apply(that, [{
'action': 'edit',
'item': item
}]);
}, 0);
},
2016-04-15 08:02:21 -05:00
onCancelFunc = closePanel,
onSaveFunc = updateTreeItem.bind(panel, that),
onEdit = editFunc.bind(panel);
if (action) {
if (action == 'create'){
2016-04-15 08:02:21 -05:00
onCancelFunc = closePanel;
onSaveFunc = saveNewNode;
}
if (action != 'properties') {
// We need to keep track edit/create mode for this panel.
editFunc();
} else {
properties();
}
} else {
/* Show properties */
properties();
onEdit = editInNewPanel.bind(panel);
}
if (panel.closeable()) {
var onCloseFunc = function() {
var j = this.$container.find('.obj_properties').first(),
view = j && j.data('obj-view');
if (view) {
view.remove({data: true, internal: true, silent: true});
}
}.bind(panel);
panel.on(wcDocker.EVENT.CLOSED, onCloseFunc);
}
},
_find_parent_node: function(t, i, d) {
if (this.parent_type) {
d = d || t.itemData(i);
if (_.isString(this.parent_type)) {
if (this.parent_type == d._type) {
return i;
}
while(t.hasParent(i)) {
i = t.parent(i);
d = t.itemData(i);
if (this.parent_type == d._type)
return i;
}
} else {
if (_.indexOf(this.parent_type, d._type) >= 0) {
return i;
}
while(t.hasParent(i)) {
i = t.parent(i);
d = t.itemData(i);
if (_.indexOf(this.parent_type, d._type) >= 0)
return i;
}
}
}
return null;
},
/**********************************************************************
* Generate the URL for different operations
*
* arguments:
* type: Create/drop/edit/properties/sql/depends/statistics
* d: Provide the ItemData for the current item node
* with_id: Required id information at the end?
*
* Supports url generation for create, drop, edit, properties, sql,
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
var opURL = {
'create': 'obj', 'drop': 'obj', 'edit': 'obj',
'properties': 'obj', 'statistics': 'stats'
}, self = this,
priority = -Infinity;
var treeInfo = (_.isUndefined(item) || _.isNull(item)) ?
info || {} : this.getTreeNodeHierarchy(item);
var actionType = type in opURL ? opURL[type] : type;
var itemID = with_id && d._type == self.type ? encodeURIComponent(d._id) : '';
if (self.parent_type) {
if (_.isString(self.parent_type)) {
var p = treeInfo[self.parent_type];
if (p) {
priority = p.priority;
}
} else {
_.each(self.parent_type, function(o) {
var p = treeInfo[o];
if (p) {
if (priority < p.priority) {
priority = p.priority;
}
}
});
}
}
var nodePickFunction = function (treeInfoValue) {
return (treeInfoValue.priority <= priority);
};
return generateUrl.generate_url(pgBrowser.URL, treeInfo, actionType, self.type, nodePickFunction, itemID);
},
// Base class for Node Data Collection
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
getTreeNodeHierarchy: function(i) {
var idx = 0,
res = {},
t = pgBrowser.tree,
d;
do {
d = t.itemData(i);
if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
res[d._type] = _.extend({}, d, {
'priority': idx
});
idx -= 1;
}
i = t.hasParent(i) ? t.parent(i) : null;
} while (i);
return res;
},
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
min_priority = (
node_info && node_info[level] && node_info[level].priority
) || 0;
if (node_info) {
_.each(
_.sortBy(
_.values(
_.pick(
node_info,
function(v, k, o) {
return (v.priority <= min_priority);
})),
function(o) { return o.priority; }),
function(o) {
hash = S('%s/%s').sprintf(hash, encodeURI(o._id)).value();
});
}
if (_.isUndefined(data)) {
var res = cached[hash];
if (!_.isUndefined(res) &&
(res.at - Date.now() > 300000)) {
res = undefined;
}
return res;
}
res = cached[hash] = {data: data, at: Date.now(), level: level};
return res;
},
clear_cache: function(item) {
/*
* Reset the cache, when new node is created.
*
* FIXME:
* At the moment, we will clear all the cache for this node. But - we
* would like to clear the cache only this nodes parent, so that - it
* fetches the new data.
*/
this.cached = {};
// Trigger Notify event about node's cache
var self = this;
pgBrowser.Events.trigger(
'pgadmin:browser:node:' + self.type + ':cache_cleared',
item, self
);
},
cache_level: function(node_info, with_id) {
if (node_info) {
if (with_id && this.type in node_info) {
return this.type;
}
if (_.isArray(this.parent_type)) {
for (var parent in this.parent_type) {
if (parent in node_info) {
return parent;
}
}
return this.type;
}
return this.parent_type;
}
}
});
return pgAdmin.Browser.Node;
});