pgadmin4/web/pgadmin/browser/static/js/node.js
Akshay Joshi b9bc628189 1. Placeholder should be inside the controlProps. Change the placeholder from 'Select...' to 'Select an item...'
2. Updated grant wizard screenshot and documentation.
3. Remove '(Beta)' from ERD Tool and PSQL Tool.
2021-09-23 16:17:39 +05:30

2000 lines
68 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import {getNodeView, removeNodeView} from './node_view';
define('pgadmin.browser.node', [
'sources/tree/pgadmin_tree_node', 'sources/url_for',
'sources/gettext', 'jquery', 'underscore', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'pgadmin.help', 'sources/utils',
'pgadmin.browser.utils', 'pgadmin.backform',
], function(
pgadminTreeNode, url_for,
gettext, $, _, pgAdmin,
Menu, Backbone, Alertify, pgBrowser,
Backform, generateUrl, help,
commonUtils
) {
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, initialize) {
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);
// Let's not bind the callbacks, or initialize the child.
if (initialize === false)
return child;
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: '',
// Help pages
sqlAlterHelp: '',
sqlCreateHelp: '',
dialogHelp: '',
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-sync-alt',
}]);
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...'),
data: {
'action': 'edit',
},
icon: 'fa fa-edit',
enable: _.isFunction(self.canEdit) ?
function() {
return !!(self.canEdit.apply(self, arguments));
} : (!!self.canEdit),
}]);
}
if (self.canDrop) {
pgAdmin.Browser.add_menus([{
name: 'delete_object',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'delete_obj',
priority: self.dropPriority,
label: (self.dropAsRemove) ? gettext('Remove %s', self.label) : gettext('Delete/Drop'),
data: {
'url': 'drop',
data_disabled: gettext('The selected tree node does not support this option.'),
},
icon: 'fa fa-trash-alt',
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-alt',
enable: _.isFunction(self.canDropCascade) ?
function() {
return self.canDropCascade.apply(self, arguments);
} : (!!self.canDropCascade),
}]);
}
}
// Show query tool only in context menu of supported nodes.
if (_.indexOf(pgAdmin.unsupported_nodes, self.type) == -1) {
let enable = function(itemData) {
if (itemData._type == 'database' && itemData.allowConn)
return true;
else if (itemData._type != 'database')
return true;
else
return false;
};
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: 'pg-font-icon icon-query_tool',
enable: enable,
}]);
// show search objects same as query tool
pgAdmin.Browser.add_menus([{
name: 'search_objects', node: self.type, module: pgAdmin.SearchObjects,
applies: ['context'], callback: 'show_search_objects',
priority: 997, label: gettext('Search Objects...'),
icon: 'fa fa-search', enable: enable,
}]);
if(pgAdmin['enable_psql']) {
// show psql tool same as query tool.
pgAdmin.Browser.add_menus([{
name: 'show_psql_tool', node: this.type, module: this,
applies: ['context'], callback: 'show_psql_tool',
priority: 998, label: gettext('PSQL Tool'),
icon: 'fas fa-terminal',
}]);
}
}
// 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 = gettext('%s Script',stype.toUpperCase());
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: gettext('Scripts'),
data: {
'script': stype,
data_disabled: gettext('The selected tree node does not support this option.'),
},
icon: 'fa fa-pencil-alt',
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, null, that.url_jump_after_node);
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.warn(
'Broke something!!! Why we don\'t have the callback or the context???'
);
};
}
var onSessionInvalid = function(msg) {
var alertMessage = `
<div class="error-in-footer">
<div class="d-flex px-2 py-1">
<div class="pr-2">
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true" role="img"></i>
</div>
<div role="alert" class="alert-text">${msg}</div>
<div class="ml-auto close-error-bar">
<a class="close-error fa fa-times text-danger"></a>
</div>
</div>
</div>`;
if (!_.isUndefined(that.statusBar)) {
that.statusBar.html(alertMessage).css('visibility', 'visible');
that.statusBar.find('a.close-error').bind('click', function() {
this.empty().css('visibility', 'hidden');
}.bind(that.statusBar));
}
var sessHasChanged = false;
if(this.sessChanged && this.sessChanged()){
sessHasChanged = true;
}
callback(true, sessHasChanged);
return true;
};
var onSessionValidated = function(sessHasChanged) {
if (!_.isUndefined(that.statusBar)) {
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.Accordian({
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() {
var container = $(el).find('.tab-content:first > .tab-pane.active:first');
commonUtils.findAndSetFocus(container);
};
if (!newModel.isNew()) {
// This is definetely not in create mode
var msgDiv = '<div role="status" class="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);
var fetchAjaxHook = function() {
newModel.fetch({
success: function() {
// Clear timeout and remove message
clearTimeout(timer);
$msgDiv.addClass('d-none');
// We got the latest attributes of the object. Render the view
// now.
view.render();
setFocusOnEl();
newModel.startNewSession();
},
error: function(model, xhr, options) {
var _label = that && item ?
that.getTreeNodeHierarchy(
item
)[that.type].label : '';
pgBrowser.Events.trigger(
'pgadmin:node:retrieval:error', 'properties',
xhr, options.textStatus, options.errorThrown, item
);
if (!Alertify.pgHandleItemError(
xhr, options.textStatus, options.errorThrown, {
item: item,
info: info,
}
)) {
Alertify.pgNotifier(
options.textStatus, xhr,
gettext('Error retrieving properties - %s', options.errorThrown || _label),
function(msg) {
if(msg === 'CRYPTKEY_SET') {
fetchAjaxHook();
} else {
console.warn(arguments);
}
}
);
}
// Close the panel (if could not fetch properties)
if (cancelFunc) {
cancelFunc();
}
},
});
};
fetchAjaxHook();
} else {
// Yay - render the view now!
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,
isLayoutMember: false,
canMaximise: true,
elContainer: true,
content: '<div class="obj_properties container-fluid h-100"><div role="status" class="pg-panel-message">' + gettext('Please wait while we fetch information about the node from the server...') + '</div></div>',
onCreate: function(myPanel, $container) {
$container.addClass('pg-no-overflow');
},
events: events,
});
p.load(pgBrowser.docker);
},
/*
* 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,
/*********************************************************************************
dropAsRemove should be true in case, Drop object label needs to be replaced by Remove
*/
dropAsRemove: false,
/******************************************************************************
dropPriority is set to 2 by default, override it when change is required
*/
dropPriority: 2,
/******************************************************************************
select collection node on deletion.
*/
selectParentNodeOnDelete: 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 && 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 body = window.document.body,
el = document.createElement('div');
body.insertBefore(el, body.firstChild);
let w, h, x, y;
if(screen.width < 800) {
w = pgAdmin.toPx(el, '95%', 'width', true);
} else {
w = pgAdmin.toPx(
el, self.width || (pgBrowser.stdW.default + 'px'),
'width', true
);
/* Fit to standard sizes */
if(w <= pgBrowser.stdW.sm) {
w = pgBrowser.stdW.sm;
} else {
if(w <= pgBrowser.stdW.md) {
w = pgBrowser.stdW.md;
} else {
w = pgBrowser.stdW.lg;
}
}
}
if(screen.height < 600) {
h = pgAdmin.toPx(el, '95%', 'height', true);
} else {
h = pgAdmin.toPx(
el, self.height || (pgBrowser.stdH.default + 'px'),
'height', true
);
/* Fit to standard sizes */
if(h <= pgBrowser.stdH.sm) {
h = pgBrowser.stdH.sm;
} else {
if(h <= pgBrowser.stdH.md) {
h = pgBrowser.stdH.md;
} else {
h = pgBrowser.stdH.lg;
}
}
}
x = (body.offsetWidth - w) / 2;
y = (body.offsetHeight - h) / 4;
// If the screen resolution is higher, but - it is zoomed, dialog
// may be go out of window, and will not be accessible through the
// keyboard.
if (w > window.innerWidth) {
x = 0;
w = window.innerWidth;
}
if (h > window.innerHeight) {
y = 0;
h = window.innerHeight;
}
var new_panel = pgBrowser.docker.addPanel(
'node_props', wcDocker.DOCK.FLOAT, undefined, {
w: w + 'px',
h: h + 'px',
x: x + 'px',
y: y + 'px',
}
);
body.removeChild(el);
return new_panel;
};
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;
}
l = gettext('Create - %s', this.label);
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?'),
commonUtils.sprintf(msg, o.label.toLowerCase(), d.label),
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 = gettext('Are you sure you want to drop %s "%s" and all the objects that depend on it?',
obj.label.toLowerCase(), d.label);
title = gettext('DROP CASCADE %s?', obj.label);
if (!(_.isFunction(obj.canDropCascade) ?
obj.canDropCascade.apply(obj, [d, i]) : obj.canDropCascade)) {
Alertify.error(
gettext('The %s "%s" cannot be dropped.', obj.label, d.label),
10
);
return;
}
} else {
if (obj.dropAsRemove) {
msg = gettext('Are you sure you want to remove %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Remove %s?', obj.label);
} else {
msg = gettext('Are you sure you want to drop %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Drop %s?', obj.label);
}
if (!(_.isFunction(obj.canDrop) ?
obj.canDrop.apply(obj, [d, i]) : obj.canDrop)) {
Alertify.error(
gettext('The %s "%s" cannot be dropped/removed.', obj.label, d.label),
10
);
return;
}
}
Alertify.confirm(title, msg,
function() {
$.ajax({
url: obj.generate_url(i, input.url, d, true),
type: 'DELETE',
})
.done(function(res) {
if (res.success == 0) {
pgBrowser.report_error(res.errormsg, res.info);
} else {
// Remove the node from tree and set collection node as selected.
var selectNextNode = true;
if(obj.selectParentNodeOnDelete) {
var prv_i = t.parent(i);
setTimeout(function() {
t.select(prv_i);
}, 10);
selectNextNode = false;
}
pgBrowser.removeTreeNode(i, selectNextNode);
}
return true;
})
.fail(function(jqx) {
var errmsg = jqx.responseText;
/* Error from the server */
if (jqx.status == 417 || jqx.status == 410 || jqx.status == 500) {
try {
var data = JSON.parse(jqx.responseText);
errmsg = data.info || data.errormsg;
} catch (e) {
console.warn(e.stack || e);
}
}
pgBrowser.report_error(
gettext('Error dropping/removing %s: "%s"', obj.label, objName), errmsg);
});
},
null
).set('labels', {
ok: gettext('Yes'),
cancel: gettext('No'),
}).show();
},
// Callback for creating script(s) & opening them in Query editor
show_script: function(args, item) {
var scriptType = args.script,
obj,
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 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 preference = pgBrowser.get_preference('sqleditor', 'copy_sql_to_query_tool');
var 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
//Open query tool with create script if copy_sql_to_query_tool is true else open blank query tool
if(preference.value && !d._type.includes('coll-')){
var stype = d._type.toLowerCase();
var data = {
'script': stype,
data_disabled: gettext('The selected tree node does not support this option.'),
};
pgBrowser.Node.callbacks.show_script(data);
}else{
pgAdmin.DataGrid.show_query_tool('', i);
}
},
// Callback to render psql tool.
show_psql_tool: function(args) {
var input = args || {},
t = pgBrowser.tree,
i = input.item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
pgBrowser.psql.psql_tool(d, i, true);
},
// 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._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 && 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 + ' !important;} \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.Events.trigger('pgadmin:browser:tree:expand-from-previous-tree-state',
item);
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);
}
}
pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state',
item);
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 && data === Object(data) &&'collection_count' in data) {
delete data.collection_count;
t.setLabel(item, {
label: _.escape(data._label),
});
}
},
refresh: function(cmd, _item) {
var self = this,
t = pgBrowser.tree,
data = _item && t.itemData(_item);
$(pgBrowser.panels['properties'].panel).removeData('node-prop');
pgBrowser.Events.trigger(
'pgadmin:browser:tree:refresh', _item || pgBrowser.tree.selected(), {
success: function() {
self.callbacks.selected.apply(self, [_item, data, pgBrowser]);
},
});
},
opened: function(item) {
let tree = pgBrowser.tree,
auto_expand = pgBrowser.get_preference('browser', 'auto_expand_sole_children');
pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state',
item);
if (auto_expand && auto_expand.value == true && tree.children(item).length == 1) {
// Automatically expand the child node, if a treeview node has only a single child.
tree.open(tree.first(item));
}
},
closed: function(item) {
pgBrowser.Events.trigger('pgadmin:browser:tree:remove-from-tree-state',
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></div>')
.addClass('pg-prop-content col-12'),
confirm_close = true;
// 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(true);
break;
case keyCode.ENTER:
// Return if event is fired from child element
if (event.target !== context) return;
if (view && view.model && view.model.sessChanged()) {
var btn = $(event.target).closest('.obj_properties')
.find('.pg-prop-btn-group')
.find('button.btn-primary');
onSave.call(this, view, btn);
}
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 role="status"></div>').addClass(
'pg-prop-status-bar'
).appendTo(j);
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) {
// 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 class="pg-prop-btn-group"></div>'),
// Template used for creating a button
tmpl = _.template([
'<button tabindex="0" type="<%= type %>" ',
'class="btn <%=extraClasses.join(\' \')%>"',
'<% if (disabled) { %> disabled="disabled"<% } %> title="<%-tooltip%>"',
'<% if (label != "") {} else { %> aria-label="<%-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 i = tree.selected(),
d = i && tree.itemData(i),
n = i && d && pgBrowser.Nodes[d._type],
treeHierarchy = n.getTreeNodeHierarchy(i);
if (_.isEqual($(this).data('node-prop'), treeHierarchy)) {
return;
}
// Cache the current IDs for next time
$(this).data('node-prop', treeHierarchy);
/* Remove any dom rendered by getNodeView */
removeNodeView(j[0]);
/* getSchema is a schema for React. Get the react node view */
if(that.getSchema) {
let treeNodeInfo = that.getTreeNodeHierarchy.apply(this, [item]);
getNodeView(
that.type, treeNodeInfo, 'properties', data, 'tab', j[0], this, onEdit
);
return;
}
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 = [];
buttons.push({
label: gettext('Edit'),
type: 'edit',
tooltip: gettext('Edit'),
extraClasses: ['btn', 'btn-primary', 'pull-right', 'm-1'],
icon: 'fa fa-sm fa-pencil-alt',
disabled: _.isFunction(that.canEdit) ? !that.canEdit.apply(that, [d, i]) : !that.canEdit,
register: function(btn) {
btn.on('click',() => {
onEdit();
});
},
});
buttons.push({
label: '',
type: 'help',
tooltip: gettext('SQL help for this object type.'),
extraClasses: ['btn-primary-icon', 'btn-primary-icon', 'm-1'],
icon: 'fa fa-info',
disabled: (that.sqlAlterHelp == '' && that.sqlCreateHelp == '') ? true : false,
register: function(btn) {
btn.on('click',() => {
onSqlHelp();
});
},
});
createButtons(buttons, 'header', 'pg-prop-btn-group-above');
}
j.append(content);
}.bind(panel),
onSqlHelp = function() {
// Construct the URL
var server = that.getTreeNodeHierarchy(item).server;
var url = pgBrowser.utils.pg_help_path;
if (server.server_type == 'ppas') {
url = pgBrowser.utils.edbas_help_path;
}
var fullUrl = '';
if (that.sqlCreateHelp == '' && that.sqlAlterHelp != '') {
fullUrl = help.getHelpUrl(url, that.sqlAlterHelp, server.version, server.server_type);
} else if (that.sqlCreateHelp != '' && that.sqlAlterHelp == '') {
fullUrl = help.getHelpUrl(url, that.sqlCreateHelp, server.version, server.server_type);
} else {
if (view.model.isNew()) {
fullUrl = help.getHelpUrl(url, that.sqlCreateHelp, server.version, server.server_type);
} else {
fullUrl = help.getHelpUrl(url, that.sqlAlterHelp, server.version, server.server_type);
}
}
window.open(fullUrl, 'postgres_help');
}.bind(panel),
onDialogHelp = function() {
window.open(that.dialogHelp, 'pgadmin_help');
}.bind(panel),
warnBeforeChangesLost = function(warn_text, yes_callback) {
var $props = this.$container.find('.obj_properties').first(),
objview = $props && $props.data('obj-view'),
self = this;
let confirm_on_properties_close = pgBrowser.get_preferences_for_module('browser').confirm_on_properties_close;
if (confirm_on_properties_close && confirm_close && objview && objview.model) {
if(objview.model.sessChanged()){
Alertify.confirm(
gettext('Warning'),
warn_text,
function() {
setTimeout(function(){
yes_callback();
}.bind(self), 50);
return true;
},
function() {
return true;
}
).set('labels', {
ok: gettext('Yes'),
cancel: gettext('No'),
}).show();
} else {
return true;
}
} else {
yes_callback();
return true;
}
}.bind(panel),
warnBeforeAttributeChange = function(yes_callback) {
var $props = this.$container.find('.obj_properties').first(),
objview = $props && $props.data('obj-view'),
self = this;
if (objview && objview.model && !_.isUndefined(objview.model.warn_text) && !_.isNull(objview.model.warn_text)) {
let warn_text;
warn_text = gettext(objview.model.warn_text);
if(objview.model.sessChanged()){
Alertify.confirm(
gettext('Warning'),
warn_text,
function() {
setTimeout(function(){
yes_callback();
}.bind(self), 50);
return true;
},
function() {
return true;
}
).set('labels', {
ok: gettext('Yes'),
cancel: gettext('No'),
}).show();
} else {
return true;
}
} else {
yes_callback();
return true;
}
}.bind(panel),
informBeforeAttributeChange = function(ok_callback) {
var $props = this.$container.find('.obj_properties').first(),
objview = $props && $props.data('obj-view');
if (objview && objview.model && !_.isUndefined(objview.model.inform_text) && !_.isNull(objview.model.inform_text)) {
Alertify.alert(
gettext('Warning'),
gettext(objview.model.inform_text)
);
}
ok_callback();
return true;
}.bind(panel),
onSave = function(_view, saveBtn) {
var m = _view.model,
d = m.toJSON(true),
// Generate a timer for the request
timer = setTimeout(function() {
$('.obj_properties').addClass('show_progress');
}, 1000);
// Prevent subsequent save operation by disabling Save button
if (saveBtn)
$(saveBtn).prop('disabled', true);
if (d && !_.isEmpty(d)) {
m.save({}, {
attrs: d,
validate: false,
cache: false,
wait: true,
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);
if (saveBtn)
$(saveBtn).prop('disabled', false);
},
});
}
}.bind(panel),
editFunc = function() {
var self = this;
if (action && action == 'properties') {
action = 'edit';
}
self.$container.attr('action-mode', action);
self.icon(
_.isFunction(that['node_image']) ?
(that['node_image']).apply(that, [data]) :
(that['node_image'] || ('icon-' + that.type))
);
/* Remove any dom rendered by getNodeView */
removeNodeView(j[0]);
/* getSchema is a schema for React. Get the react node view */
if(that.getSchema) {
let treeNodeInfo = that.getTreeNodeHierarchy.apply(this, [item]);
getNodeView(
that.type, treeNodeInfo, action, data, 'dialog', j[0], this, onEdit,
(nodeData)=>{
if(nodeData.node) {
onSaveFunc(nodeData.node, treeNodeInfo);
// 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');
}
}
);
return;
}
// 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-secondary[type="reset"]');
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);
self.icon(
_.isFunction(that['node_image']) ?
(that['node_image']).apply(that, [data, view.model]) :
(that['node_image'] || ('icon-' + that.type))
);
// Create proper buttons
let btn_grp = createButtons([{
label: '',
type: 'help',
tooltip: gettext('SQL help for this object type.'),
extraClasses: ['btn-primary-icon', 'pull-left', 'mx-1'],
icon: 'fa fa-info',
disabled: (that.sqlAlterHelp == '' && that.sqlCreateHelp == '') ? true : false,
register: function(btn) {
btn.on('click',() => {
onSqlHelp();
});
},
}, {
label: '',
type: 'help',
tooltip: gettext('Help for this dialog.'),
extraClasses: ['btn-primary-icon', 'pull-left', 'mx-1'],
icon: 'fa fa-question',
disabled: (that.dialogHelp == '') ? true : false,
register: function(btn) {
btn.on('click',() => {
onDialogHelp();
});
},
}, {
label: gettext('Cancel'),
type: 'cancel',
tooltip: gettext('Cancel changes to this object.'),
extraClasses: ['btn-secondary', 'mx-1'],
icon: 'fa fa-times pg-alertify-button',
disabled: false,
register: function(btn) {
btn.on('click',() => {
// Removing the action-mode
self.$container.removeAttr('action-mode');
onCancelFunc.call(true);
});
},
}, {
label: gettext('Reset'),
type: 'reset',
tooltip: gettext('Reset the fields on this dialog.'),
extraClasses: ['btn-secondary', 'mx-1'],
icon: 'fa fa-recycle pg-alertify-button',
disabled: true,
register: function(btn) {
btn.on('click',() => {
warnBeforeChangesLost.call(
self,
gettext('Changes will be lost. Are you sure you want to reset?'),
function() {
setTimeout(function() {
editFunc.call();
}, 0);
}
);
});
},
}, {
label: gettext('Save'),
type: 'save',
tooltip: gettext('Save this object.'),
extraClasses: ['btn-primary', 'mx-1'],
icon: 'fa fa-save pg-alertify-button',
disabled: true,
register: function(btn) {
// Save the changes
btn.on('click',() => {
warnBeforeAttributeChange.call(
self,
function() {
informBeforeAttributeChange.call(self, function(){
setTimeout(function() {
onSave.call(this, view, btn);
}, 0);
});
}
);
});
},
}], 'footer', 'pg-prop-btn-group-below');
btn_grp.on('keydown', 'button', function(event) {
if (!event.shiftKey && event.keyCode == 9 && $(this).nextAll('button:not([disabled])').length == 0) {
// set focus back to first focusable element on dialog
view.$el.closest('.wcFloating').find('[tabindex]:not([tabindex="-1"]').first().focus();
return false;
}
let btnGroup = $(self.$container.find('.pg-prop-btn-group'));
let el = $(btnGroup).find('button:first');
if (self.$container.find('.number-cell.editable:last').is(':visible')){
if (event.keyCode === 9 && event.shiftKey) {
if ($(el).is($(event.target))){
$(self.$container.find('td.editable:last').trigger('click'));
event.preventDefault();
event.stopPropagation();
}
}
}
});
setTimeout(function() {
pgBrowser.keyboardNavigation.getDialogTabNavigator(self.pgElContainer);
}, 200);
}
// 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);
// add required attributes to select2 input to resolve accessibility issue.
$('.select2-search__field').attr('aria-label', 'select2');
view.$el.closest('.wcFloating').find('.wcFrameButtonBar > .wcFrameButton[style!="display: none;"]').on('keydown', function(e) {
if(e.shiftKey && e.keyCode === 9) {
e.stopPropagation();
setTimeout(() => {
view.$el.closest('.wcFloating').find('[tabindex]:not([tabindex="-1"]):not([disabled])').last().focus();
}, 10);
}
});
}.bind(panel),
closePanel = function(confirm_close_flag) {
if(!_.isUndefined(confirm_close_flag)) {
confirm_close = confirm_close_flag;
}
// Closing this panel
this.close();
}.bind(panel),
updateTreeItem = function(obj, tnode, node_info) {
var _old = data,
_new = tnode || _.clone(view.model.tnode),
info = node_info || _.clone(view.model.node_info);
// Clear the cache for this node now.
setTimeout(function() {
obj.clear_cache.apply(obj, 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(false);
},
saveNewNode = function(obj, tnode, node_info) {
var $props = this.$container.find('.obj_properties').first(),
objview = $props.data('obj-view');
// Clear the cache for this node now.
setTimeout(function() {
obj.clear_cache.apply(obj, item);
}, 0);
try {
pgBrowser.Events.trigger(
'pgadmin:browser:tree:add', _.clone(tnode || objview.model.tnode),
_.clone(node_info || objview.model.node_info)
);
} catch (e) {
console.warn(e.stack || e);
}
closePanel(false);
}.bind(panel, that),
editInNewPanel = function() {
// Open edit in separate panel
setTimeout(function() {
that.callbacks.show_obj_properties.apply(that, [{
'action': 'edit',
'item': item,
}]);
}, 0);
},
onCancelFunc = closePanel,
onSaveFunc = updateTreeItem.bind(panel, that),
onEdit = editFunc.bind(panel);
if (action) {
if (action == 'create') {
onSaveFunc = saveNewNode;
}
if (action != 'properties') {
// We need to keep track edit/create mode for this panel.
editFunc();
} else {
properties();
}
} else {
/* Show properties */
onEdit = editInNewPanel.bind(panel);
properties();
}
if (panel.closeable() && !that.getSchema) {
panel.on(wcDocker.EVENT.CLOSING, warnBeforeChangesLost.bind(
panel,
gettext('Changes will be lost. Are you sure you want to close the dialog?'),
function() {
panel.off(wcDocker.EVENT.CLOSING);
panel.close();
}
));
var onCloseFunc = function() {
var $props = this.$container.find('.obj_properties').first(),
objview = $props && $props.data('obj-view');
if (objview) {
objview.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?
* jump_after_node: This will skip all the value between jump_after_node
* to the last node, excluding jump_after_node and the last node. This is particularly
* helpful in partition table where we need to skip parent table OID of a partitioned
* table in URL formation. Partitioned table itself is a "table" and can be multilevel
* Supports url generation for create, drop, edit, properties, sql,
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info, jump_after_node) {
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)) {
let p = treeInfo[self.parent_type];
if (p) {
priority = p.priority;
}
} else {
_.each(self.parent_type, function(o) {
let p = treeInfo[o];
if (p) {
if (priority < p.priority) {
priority = p.priority;
}
}
});
}
}
let jump_after_priority = priority;
if(jump_after_node && treeInfo[jump_after_node]) {
jump_after_priority = treeInfo[jump_after_node].priority;
}
var nodePickFunction = function(treeInfoValue) {
return (treeInfoValue.priority <= jump_after_priority || 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: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
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) {
return (v.priority <= min_priority);
}
)), function(o) {
return o.priority;
}), function(o) {
hash = commonUtils.sprintf('%s/%s', hash, encodeURI(o._id));
});
}
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;
});