mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
1902 lines
64 KiB
JavaScript
1902 lines
64 KiB
JavaScript
/////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
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',
|
|
},
|
|
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,
|
|
}]);
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
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,
|
|
elContainer: true,
|
|
content: '<div class="obj_properties container-fluid"><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,
|
|
// 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 {
|
|
pgBrowser.removeTreeNode(i, true);
|
|
}
|
|
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 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._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 + ';} \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 != "") { %> <%-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);
|
|
|
|
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);
|
|
// 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) {
|
|
var _old = data,
|
|
_new = _.clone(view.model.tnode),
|
|
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) {
|
|
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(objview.model.tnode),
|
|
_.clone(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 */
|
|
properties();
|
|
onEdit = editInNewPanel.bind(panel);
|
|
}
|
|
if (panel.closeable()) {
|
|
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;
|
|
});
|