diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index d47852611..060420230 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -97,6 +97,11 @@ class ServerModule(sg.ServerGroupPluginModule):
'name': 'pgadmin.browser.server.privilege',
'path': url_for('browser.index') + 'server/static/js/privilege',
'when': self.node_type
+ },
+ {
+ 'name': 'pgadmin.browser.server.variable',
+ 'path': url_for('browser.index') + 'server/static/js/variable',
+ 'when': self.node_type
}])
for module in self.submodules:
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/variable.js b/web/pgadmin/browser/server_groups/servers/static/js/variable.js
new file mode 100644
index 000000000..ef5991093
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/static/js/variable.js
@@ -0,0 +1,562 @@
+(function(root, factory) {
+ // Set up Backform appropriately for the environment. Start with AMD.
+ if (typeof define === 'function' && define.amd) {
+ define([
+ 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
+ 'pgadmin', 'pgadmin.browser.node', 'pgadmin.browser.node.ui'
+ ],
+ function(_, $, Backbone, Backform, Backgrid, Alertify, pgAdmin, pgNode) {
+ // Export global even in AMD case in case this script is loaded with
+ // others that may still expect a global Backform.
+ return factory(root, _, $, Backbone, Backform, Alertify, pgAdmin, pgNode);
+ });
+
+ // Next for Node.js or CommonJS. jQuery may not be needed as a module.
+ } else if (typeof exports !== 'undefined') {
+ var _ = require('underscore') || root._,
+ $ = root.jQuery || root.$ || root.Zepto || root.ender,
+ Backbone = require('backbone') || root.Backbone,
+ Backform = require('backform') || root.Backform;
+ Alertify = require('alertify') || root.Alertify;
+ pgAdmin = require('pgadmin') || root.pgAdmin,
+ pgNode = require('pgadmin.browser.node') || root.pgAdmin.Browser.Node;
+ factory(root, _, $, Backbone, Backform, Alertify, pgAdmin, pgNode);
+
+ // Finally, as a browser global.
+ } else {
+ factory(
+ root, root._, (root.jQuery || root.Zepto || root.ender || root.$),
+ root.Backbone, root.Backform, root.pgAdmin.Browser.Node
+ );
+ }
+} (this, function(root, _, $, Backbone, Backform, Alertify, pgAdmin, pgNode) {
+
+ /**
+ * VariableModel used to represent configuration parameters (variables tab)
+ * for database objects.
+ **/
+ var VariableModel = pgNode.VariableModel = pgNode.Model.extend({
+ defaults: {
+ name: undefined,
+ value: undefined,
+ role: undefined,
+ database: undefined,
+ },
+ schema: [
+ {id: 'name', label:'Name', type:'text', editable: false, cellHeaderClasses: 'width_percent_30'},
+ {
+ id: 'value', label:'Value', type: 'text', cell: 'dynamic-variable',
+ editable: true, cellHeaderClasses: 'width_percent_50'
+ },
+ {id: 'database', label:'Database', type: 'text', editable: false},
+ {id: 'role', label:'Role', type: 'text', editable: false}
+ ],
+ toJSON: function() {
+ var d = Backbone.Model.prototype.toJSON.apply(this);
+
+ // Remove not defined values from model values.
+ // i.e.
+ // role, database
+ if (_.isUndefined(d.database)) {
+ delete d.database;
+ }
+
+ if (_.isUndefined(d.role) || _.isNull(d.role)) {
+ delete d.role;
+ }
+
+ return d;
+ }
+ });
+
+ /*
+ * Dynamic Variable cell. Used for variable data type column in Variables tab.
+ * Behaviour of cell depends on variable data type.
+ */
+ var DynamicVariableCell = Backgrid.Extension.DynamicVariableCell = Backgrid.Cell.extend({
+ /*
+ * Mapping of postgres data type to backgrid cell type.
+ */
+ variableCellMapper: {
+ "bool":Backgrid.Extension.SwitchCell,
+ "enum":Backgrid.Extension.Select2Cell,
+ "string":Backgrid.Cell,
+ "integer":Backgrid.IntegerCell,
+ "real":Backgrid.NumberCell
+ },
+ initialize: function (opts) {
+
+ var self = this,
+ name = opts.model.get("name");
+ self.availVariables = opts.column.get('availVariables');
+
+ var variable = (self.availVariables[name]),
+ cell = self.variableCellMapper[variable.vartype] || Backgrid.Cell;
+
+ /*
+ * Set properties for dynamic cell.
+ */
+ _.each(cell.prototype, function(v,k) {
+ self[k] = v;
+ });
+
+ DynamicVariableCell.__super__.initialize.apply(self, arguments);
+
+ switch(variable.vartype) {
+ case "bool":
+ // There are no specific properties for BooleanCell.
+ break;
+
+ case "enum":
+ var options = [],
+ name = self.model.get("name"),
+ enumVals = variable.enumvals;
+
+ _.each(enumVals, function(enumVal) {
+ options.push([enumVal, enumVal]);
+ });
+
+ self.optionValues = options;
+ self.multiple = cell.prototype.multiple;
+ self.delimiter = cell.prototype.delimiter;
+
+ self.listenTo(
+ self.model, "backgrid:edit",
+ function (model, column, cell, editor) {
+ if (column.get("name") == self.column.get("name")) {
+ editor.setOptionValues(self.optionValues);
+ editor.setMultiple(self.multiple);
+ }
+ });
+ break;
+
+ case "integer":
+
+ self.decimals = 0;
+ self.decimalSeparator = cell.prototype.decimalSeparator;
+ self.orderSeparator = cell.prototype.orderSeparator;
+ var formatter = self.formatter;
+
+ formatter.decimals = self.decimals;
+ formatter.decimalSeparator = self.decimalSeparator;
+ formatter.orderSeparator = self.orderSeparator;
+
+ break;
+
+ case "real":
+
+ self.decimals = cell.prototype.decimals;
+ self.decimalSeparator = cell.prototype.decimalSeparator;
+ self.orderSeparator = cell.prototype.orderSeparator;
+
+ var formatter = self.formatter;
+
+ formatter.decimals = self.decimals;
+ formatter.decimalSeparator = self.decimalSeparator;
+ formatter.orderSeparator = self.orderSeparator;
+
+ break;
+
+ case "string":
+ default:
+ // There are no specific properties for StringCell and Cell.
+ break;
+ }
+ }
+ });
+
+ /**
+ * Variable Tab Control to set/update configuration values for database object.
+ *
+ **/
+ var VariableCollectionControl = Backform.VariableCollectionControl =
+ Backform.UniqueColCollectionControl.extend({
+
+ hasDatabase: false,
+ hasRole: false,
+
+ defaults: _.extend({
+ uniqueCol: ['name', 'role', 'database']
+ },
+ Backform.UniqueColCollectionControl.prototype.defaults
+ ),
+
+ initialize: function(opts) {
+ var self = this;
+
+ // Overriding the uniqueCol in the field
+ if (opts && opts.field) {
+ if (opts.field instanceof Backform.Field) {
+ opts.field.set({
+ uniqueCol: ['name', 'role', 'database'],
+ model: pgNode.VariableModel
+ },
+ {
+ silent: true
+ });
+ } else {
+ opts.field.extend({
+ uniqueCol: ['name', 'role', 'database'],
+ model: pgNode.VariableModel
+ });
+ }
+ }
+
+ Backform.UniqueColCollectionControl.prototype.initialize.apply(
+ self, arguments
+ );
+
+ self.hasDatabase = self.field.get('hasDatabase');
+ self.hasRole = self.field.get('hasRole');
+ self.availVariables = {};
+
+ var node = self.field.get('node').type,
+ headerSchema = [{
+ id: 'name', label:'', type:'text',
+ url: self.field.get('variable_opts') || 'vopts',
+ control: Backform.NodeAjaxOptionsControl,
+ select2: {
+ allowClear: false, width: 'style'
+ },
+ availVariables: self.availVariables,
+ node: node, first_empty: false,
+ transform: function(vars) {
+ var self = this,
+ opts = self.field.get('availVariables');
+
+ res = [];
+
+ for (var prop in opts) {
+ if (opts.hasOwnProperty(prop)) {
+ delete opts[prop];
+ }
+ }
+
+ _.each(vars, function(v) {
+ opts[v.name] = _.extend({}, v);
+ res.push({
+ 'label': v.name,
+ 'value': v.name
+ });
+ });
+
+ return res;
+ }
+ }],
+ headerDefaults = {name: null},
+ gridCols = ['name', 'value'];
+
+ if (self.hasDatabase) {
+ headerSchema.push({
+ id: 'database', label:'', type: 'text',
+ control: Backform.NodeListByNameControl, node: 'database'
+ });
+ headerDefaults['database'] = null;
+ gridCols.push('database');
+ }
+
+ if (self.hasRole) {
+ headerSchema.push({
+ id: 'role', label:'', type: 'text',
+ control: Backform.NodeListByNameControl, node: 'role'
+ });
+ headerDefaults['role'] = null;
+ gridCols.push('role');
+ }
+
+ self.headerData = new (Backbone.Model.extend({
+ defaults: headerDefaults,
+ schema: headerSchema
+ }))({});
+
+ var headerGroups = Backform.generateViewSchema(
+ self.field.get('node_info'), self.headerData, 'create',
+ node, self.field.get('node_data')
+ );
+
+ var fields = [];
+
+ _.each(headerGroups, function(val, key) {
+ fields = fields.concat(headerGroups[key]);
+ });
+
+ self.headerFields = new Backform.Fields(fields);
+ self.gridSchema = Backform.generateGridColumnsFromModel(
+ null, VariableModel, 'edit', gridCols
+ );
+
+ // Make sure - we do have the data for variables
+ self.getVariables();
+
+ self.controls = [];
+ self.listenTo(self.headerData, "change", self.headerDataChanged);
+ self.listenTo(self.headerData, "select2", self.headerDataChanged);
+ self.listenTo(self.collection, "remove", self.onRemoveVariable);
+ },
+
+ /*
+ * Get the variable options for this control.
+ */
+ getVariables: function() {
+ var self = this,
+ url = this.field.get('url'),
+ m = self.model;
+
+ if (url && !m.isNew()) {
+ var node = self.field.get('node'),
+ node_data = self.field.get('node_data'),
+ node_info = self.field.get('node_info'),
+ full_url = node.generate_url.apply(
+ node, [
+ null, url, node_data, true, node_info
+ ]),
+ data;
+
+ m.trigger('pgadmin:view:fetching', m, self.field);
+ $.ajax({
+ async: false,
+ url: full_url,
+ success: function (res) {
+ data = res.data;
+ },
+ error: function() {
+ m.trigger('pgadmin:view:fetch:error', m, self.field);
+ }
+ });
+ m.trigger('pgadmin:view:fetched', m, self.field);
+
+ if (data && _.isArray(data)) {
+ self.collection.reset(data, {silent: true});
+ /*
+ * Make sure - new data will be taken care by the session management
+ */
+ self.collection.startNewSession();
+ }
+ }
+ },
+
+ generateHeader: function(data) {
+ var header = [
+ "
"].join("\n"));
+
+ var self = this,
+ $header = $(header.join("\n")),
+ controls = this.controls;
+
+ this.headerFields.each(function(field) {
+ var control = new (field.get("control"))({
+ field: field,
+ model: self.headerData
+ });
+
+ $header.find('div[header="' + field.get('name') + '"]').append(
+ control.render().$el
+ );
+
+ controls.push(control);
+ });
+
+ // Set visibility of Add button
+ if (data.disabled || data.canAdd == false) {
+ $header.find("button.add").remove();
+ }
+
+ self.$header = $header;
+
+ return $header;
+ },
+
+ events: _.extend(
+ {},
+ Backform.UniqueColCollectionControl.prototype.events,
+ {
+ 'click button.add': 'addVariable'
+ }
+ ),
+
+ showGridControl: function(data) {
+
+ var self = this,
+ titleTmpl = _.template([
+ ""].join("\n")),
+ $gridBody =
+ $("
").append(
+ titleTmpl({label: data.label})
+ );
+
+ $gridBody.append(self.generateHeader(data));
+
+ var gridSchema = _.clone(this.gridSchema);
+
+ _.each(gridSchema.columns, function(col) {
+ if (col.name == 'value') {
+ col.availVariables = self.availVariables;
+ }
+ });
+
+ // Insert Delete Cell into Grid
+ if (data.disabled == false && data.canDelete) {
+ gridSchema.columns.unshift({
+ name: "pg-backform-delete", label: "",
+ cell: Backgrid.Extension.DeleteCell,
+ editable: false, cell_priority: -1
+ });
+ }
+
+ // Initialize a new Grid instance
+ var grid = self.grid = new Backgrid.Grid({
+ columns: gridSchema.columns,
+ collection: self.collection,
+ className: "backgrid table-bordered"
+ });
+ self.$grid = grid.render().$el;
+
+ $gridBody.append(self.$grid);
+
+ self.headerData.set(
+ 'name',
+ self.$header.find(
+ 'div[header="name"] select option:first'
+ ).val()
+ );
+
+ // Render node grid
+ return $gridBody;
+ },
+
+ addVariable: function(ev) {
+ ev.preventDefault();
+
+ var self = this,
+ m = new (self.field.get('model'))(
+ self.headerData.toJSON(), {silent: true}
+ ),
+ coll = self.model.get(self.field.get('name'));
+
+ coll.add(m);
+
+ var idx = coll.indexOf(m);
+
+ // idx may not be always > -1 because our UniqueColCollection may
+ // remove 'm' if duplicate value found.
+ if (idx > -1) {
+ self.$grid.find('.new').removeClass('new');
+
+ var newRow = self.grid.body.rows[idx].$el;
+
+ newRow.addClass("new");
+ $(newRow).pgMakeVisible('backform-tab');
+ } else {
+ delete m;
+ }
+
+ this.headerDataChanged();
+
+ return false;
+ },
+
+ headerDataChanged: function() {
+ var self = this, val,
+ data = this.headerData.toJSON(),
+ inSelected = false,
+ checkVars = ['name'];
+
+ if (!self.$header) {
+ return;
+ }
+
+ if (self.hasDatabase) {
+ checkVars.push('database');
+ }
+
+ if (self.role) {
+ checkVars.push('role');
+ }
+
+ self.collection.each(function(m) {
+ if (!inSelected) {
+ var has = true;
+ _.each(checkVars, function(v) {
+ val = m.get(v);
+ has = has && ((
+ (_.isUndefined(val) || _.isNull(val)) &&
+ (_.isUndefined(data[v]) || _.isNull(data[v]))
+ ) ||
+ (val == data[v]));
+ });
+
+ inSelected = has;
+ }
+ });
+
+ self.$header.find('button.add').prop('disabled', inSelected);
+ },
+
+ onRemoveVariable: function() {
+ var self = this;
+
+ // Wait for collection to be updated before checking for the button to be
+ // enabled, or not.
+ setTimeout(function() {
+ self.headerDataChanged();
+ }, 10);
+ },
+
+ remove: function() {
+ /*
+ * Stop listening the events registered by this control.
+ */
+ this.stopListening(this.headerData, "change", this.headerDataChanged);
+ this.listenTo(this.headerData, "select2", this.headerDataChanged);
+ this.listenTo(this.collection, "remove", this.onRemoveVariable);
+
+ VariableCollectionControl.__super__.remove.apply(this, arguments);
+
+ // Remove the header model
+ delete (this.headerData);
+
+ // Clear the available Variables object
+ self.availVariables = {};
+ }
+ });
+
+ return VariableModel;
+}));
diff --git a/web/pgadmin/browser/server_groups/servers/templates/macros/security.macros b/web/pgadmin/browser/server_groups/servers/templates/macros/security.macros
new file mode 100644
index 000000000..83fb9d238
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/templates/macros/security.macros
@@ -0,0 +1,6 @@
+{% macro APPLY(conn, type, name, provider, label) -%}
+SECURITY LABEL FOR {{ conn|qtIdent(provider) }} ON {{ type }} {{ conn|qtIdent(name) }} IS {{ label|qtLiteral }};
+{%- endmacro %}
+{% macro DROP(conn, type, name, provider) -%}
+SECURITY LABEL FOR {{ conn|qtIdent(provider) }} ON {{ type }} {{ conn|qtIdent(name) }} IS NULL;
+{%- endmacro %}
diff --git a/web/pgadmin/browser/server_groups/servers/templates/macros/variable.macros b/web/pgadmin/browser/server_groups/servers/templates/macros/variable.macros
new file mode 100644
index 000000000..a40d33d4f
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/templates/macros/variable.macros
@@ -0,0 +1,6 @@
+{% macro APPLY(conn, database, role, param, value) -%}
+ALTER {% if role %}ROLE {{ self.conn|qtIdent(role) }}{% if database %} IN DATABASE {{ conn|qtIdent(database) }}{% endif %}{% else %}DATABASE {{ conn|qtIdent(database) }}{% endif %} SET {{ conn|qtIdent(param) }} TO {{ value|qtLiteral }};
+{%- endmacro %}
+{% macro DROP(conn, database, role, param) -%}
+ALTER {% if role %}ROLE {{ self.conn|qtIdent(role) }}{% if database %} IN DATABASE {{ conn|qtIdent(database) }}{% endif %}{% else %}DATABASE {{ conn|qtIdent(database) }}{% endif %} RESET {{ conn|qtIdent(param) }};
+{%- endmacro %}
diff --git a/web/pgadmin/browser/static/js/node.ui.js b/web/pgadmin/browser/static/js/node.ui.js
index 1412cc824..be2bf0d92 100644
--- a/web/pgadmin/browser/static/js/node.ui.js
+++ b/web/pgadmin/browser/static/js/node.ui.js
@@ -4,6 +4,21 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
var pgBrowser = pgAdmin.Browser;
+
+ // Store value in DOM as stringified JSON.
+ var StringOrJSONFormatter = function() {};
+ _.extend(StringOrJSONFormatter.prototype, {
+ fromRaw: function(rawData, model) {
+ return JSON.stringify(rawData);
+ },
+ toRaw: function(formattedData, model) {
+ if (typeof(formattedData) == 'string') {
+ return formattedData;
+ }
+ return JSON.parse(formattedData);
+ }
+ });
+
/*
* NodeAjaxOptionsControl
* This control will fetch the options required to render the select
@@ -21,8 +36,28 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
defaults: _.extend(Backform.SelectControl.prototype.defaults, {
url: undefined,
transform: undefined,
- url_with_id: false
+ url_with_id: false,
+ first_empty: false,
+ select2: {
+ allowClear: true,
+ placeholder: 'Select from the list',
+ width: 'style'
+ }
}),
+ template: _.template([
+ '<%=label%> ',
+ ''].join("\n")),
+ formatter: StringOrJSONFormatter,
initialize: function() {
/*
* Initialization from the original control.
@@ -47,14 +82,19 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
this.field.get('url_with_id') || false, node_info
]),
cache_level = this.field.get('cache_level'),
- /*
- * We needs to check, if we have already cached data for this url.
- * If yes - use that, and do not bother about fetching it again,
- * and use it.
- */
- data = node.cache(url, node_info, cache_level);
+ cache_node = this.field.get('cache_node');
+
+ cache_node = (cache_node && pgAdmin.Browser.Nodes['cache_node']) || node;
+
+ /*
+ * We needs to check, if we have already cached data for this url.
+ * If yes - use that, and do not bother about fetching it again,
+ * and use it.
+ */
+ var data = cache_node.cache(url, node_info, cache_level);
+
if (_.isUndefined(data) || _.isNull(data)) {
- m.trigger('pgadmin-view:fetching', m, self.field);
+ m.trigger('pgadmin:view:fetching', m, self.field);
$.ajax({
async: false,
url: full_url,
@@ -63,13 +103,13 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
* We will cache this data for short period of time for avoiding
* same calls.
*/
- data = node.cache(url, node_info, cache_level, res.data);
+ data = cache_node.cache(url, node_info, cache_level, res.data);
},
error: function() {
- m.trigger('pgadmin-view:fetch:error', m, self.field);
+ m.trigger('pgadmin:view:fetch:error', m, self.field);
}
});
- m.trigger('pgadmin-view:fetched', m, self.field);
+ m.trigger('pgadmin:view:fetched', m, self.field);
}
// To fetch only options from cache, we do not need time from 'at'
// attribute but only options.
@@ -90,28 +130,45 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
self.field.set('options', data);
}
}
+ },
+ render: function() {
+ /*
+ * Let SelectControl render it, we will do our magic on the
+ * select control in it.
+ */
+ Backform.SelectControl.prototype.render.apply(this, arguments);
+
+ var d = this.field.toJSON(),
+ select2_opts = _.defaults({}, d.select2, this.defaults.select2);
+
+ /*
+ * Add empty option as Select2 requires any empty '' for
+ * some of its functionality to work and initialize select2 control.
+ */
+ this.$el.find("select").select2(select2_opts);
+
+ return this;
}
});
+ var formatNode = function(opt) {
+ if (!opt.id) {
+ return opt.text;
+ }
+
+ var optimage = $(opt.element).data('image');
+
+ if(!optimage){
+ return opt.text;
+ } else {
+ return $(
+ ' ' + opt.text + ' '
+ );
+ }
+ };
+
var NodeListByIdControl = Backform.NodeListByIdControl = NodeAjaxOptionsControl.extend({
controlClassName: 'pgadmin-node-select form-control',
- template: _.template([
- '<%=label%> ',
- ''].join("\n")),
defaults: _.extend(NodeAjaxOptionsControl.prototype.defaults, {
first_empty: true,
empty_value: '-- None --',
@@ -131,16 +188,27 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
(node['node_label']).apply(node, [r, self.model, self]) :
r.label),
image= (_.isFunction(node['node_image']) ?
- (node['node_image']).apply(node, [r, self.model, self]) : node.type);
+ (node['node_image']).apply(
+ node, [r, self.model, self]
+ ) :
+ (node['node_image'] || ('icon-' + node.type)));
+
res.push({
'value': r._id,
- 'node': image,
+ 'image': image,
'label': l
});
}
});
return res;
+ },
+ select2: {
+ allowClear: true,
+ placeholder: 'Select from the list',
+ width: 'style',
+ templateResult: formatNode,
+ templateSelection: formatNode
}
})
});
@@ -161,11 +229,14 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
var l = (_.isFunction(node['node_label']) ?
(node['node_label']).apply(node, [r, self.model, self]) :
r.label),
- image= (_.isFunction(node['node_image']) ?
- (node['node_image']).apply(node, [r, self.model, self]) : node.type);
+ image = (_.isFunction(node['node_image']) ?
+ (node['node_image']).apply(
+ node, [r, self.model, self]
+ ) :
+ (node['node_image'] || ('icon-' + node.type)));
res.push({
'value': r.label,
- 'node': image,
+ 'image': image,
'label': l
});
}
@@ -176,5 +247,32 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
})
});
- return Backform.NodeListControl;
+ /*
+ * Global function to make visible particular dom element in it's parent
+ * with given class.
+ */
+ $.fn.pgMakeVisible = function( cls ) {
+ return this.each(function() {
+ if (!this || !$(this.length))
+ return;
+ var top, p = $(this), hasScrollbar = function(j) {
+ if (j && j.length > 0) {
+ return j.get(0).scrollHeight > j.height();
+ }
+ return false;
+ };
+
+ while(p) {
+ top = p.get(0).offsetTop + p.height();
+ p = p.parent();
+ if (hasScrollbar(p)) {
+ p.scrollTop(top);
+ }
+ if (p.hasClass(cls)) //'backform-tab'
+ return;
+ }
+ });
+ };
+
+ return Backform;
});
diff --git a/web/pgadmin/static/css/overrides.css b/web/pgadmin/static/css/overrides.css
index 6d0f662f7..351d64614 100755
--- a/web/pgadmin/static/css/overrides.css
+++ b/web/pgadmin/static/css/overrides.css
@@ -582,13 +582,27 @@ table.backgrid tr.new {
height: 0px; width: 0px;
}
-.width_percent_40 {
- width: 40%;
-}
-
-.width_percent_60 {
- width: 60%;
-}
+.width_percent_5 { width: 5%; }
+.width_percent_10 { width: 10%; }
+.width_percent_15 { width: 15%; }
+.width_percent_20 { width: 20%; }
+.width_percent_25 { width: 25%; }
+.width_percent_30 { width: 30%; }
+.width_percent_35 { width: 35%; }
+.width_percent_40 { width: 40%; }
+.width_percent_45 { width: 45%; }
+.width_percent_50 { width: 50%; }
+.width_percent_55 { width: 55%; }
+.width_percent_60 { width: 60%; }
+.width_percent_65 { width: 65%; }
+.width_percent_70 { width: 70%; }
+.width_percent_75 { width: 75%; }
+.width_percent_80 { width: 80%; }
+.width_percent_85 { width: 85%; }
+.width_percent_90 { width: 90%; }
+.width_percent_95 { width: 95%; }
+.width_percent_99 { width: 99%; }
+.width_percent_100 { width: 100%; }
.pg-prop-status-bar {
left: 0px;
@@ -611,3 +625,27 @@ table.backgrid tr.new {
right: 0px;
bottom :0;
}
+.subnode-header-form {
+ background-color:#2c76b4;
+ color:#FFFFFF;
+ padding:3px 0 10px 0;
+ border-top: solid 1.5px white;
+}
+
+.subnode-header-form button.add {
+ float:right;
+ margin-right:15px;
+}
+
+.select2-container--default .select2-search--inline .select2-search__field {
+ background: transparent none repeat scroll 0% 0%;
+ border: medium none;
+ outline: 0px none;
+ box-shadow: none;
+ width: 100% !important;
+}
+
+div.rolmembership {
+ border: 1.5px solid #faebd7;
+ margin-top: 15px;
+}
diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js
index 1a2d350a9..1591a4d34 100644
--- a/web/pgadmin/static/js/backform.pgadmin.js
+++ b/web/pgadmin/static/js/backform.pgadmin.js
@@ -74,7 +74,8 @@
'multiline': ['textarea', 'textarea', 'string'],
'collection': ['sub-node-collection', 'sub-node-collection', 'string'],
'uniqueColCollection': ['unique-col-collection', 'unique-col-collection', 'string'],
- 'switch' : 'switch'
+ 'switch' : 'switch',
+ 'select2': 'select2',
};
var getMappedControl = Backform.getMappedControl = function(type, mode) {
@@ -111,95 +112,131 @@
}
+ var BackformControlInit = Backform.Control.prototype.initialize,
+ BackformControlRemove = Backform.Control.prototype.remove;
+
// Override the Backform.Control to allow to track changes in dependencies,
// and rerender the View element
- var BackformControlInit = Backform.Control.prototype.initialize;
- Backform.Control.prototype.initialize = function() {
- BackformControlInit.apply(this, arguments);
+ _.extend(Backform.Control.prototype, {
- // Listen to the dependent fields in the model for any change
- var deps = this.field.get('deps');
- var that = this;
- if (deps && _.isArray(deps))
- _.each(deps, function(d) {
- attrArr = d.split('.');
- name = attrArr.shift();
- that.listenTo(that.model, "change:" + name, that.render);
- });
- };
- Backform.Control.prototype.template = _.template([
- '<%=label%> ',
- '',
- ' >',
- ' <%=value%>',
- ' ',
- '
'
- ].join("\n"));
- Backform.Control.prototype.clearInvalid = function() {
- this.$el.removeClass(Backform.errorClassName);
- this.$el.find(".pgadmin-control-error-message").remove();
- return this;
- };
- Backform.Control.prototype.updateInvalid = function() {
- var self = this;
- var errorModel = this.model.errorModel;
- if (!(errorModel instanceof Backbone.Model)) return this;
+ initialize: function() {
+ BackformControlInit.apply(this, arguments);
- this.clearInvalid();
+ // Listen to the dependent fields in the model for any change
+ var deps = this.field.get('deps');
+ var self = this;
- this.$el.find(':input').not('button').each(function(ix, el) {
- var attrArr = $(el).attr('name').split('.'),
- name = attrArr.shift(),
- path = attrArr.join('.'),
- error = self.keyPathAccessor(errorModel.toJSON(), $(el).attr('name'));
+ if (deps && _.isArray(deps)) {
+ _.each(deps, function(d) {
+ attrArr = d.split('.');
+ name = attrArr.shift();
+ self.listenTo(self.model, "change:" + name, self.render);
+ });
+ }
+ },
+
+ remove: function() {
+ // Listen to the dependent fields in the model for any change
+ var self = this,
+ deps = self.field.get('deps');
+
+ self.stopListening(self.model, "change:" + name, self.render);
+ self.stopListening(self.model.errorModel, "change:" + name, self.updateInvalid);
+
+ if (deps && _.isArray(deps)) {
+ _.each(deps, function(d) {
+
+ attrArr = d.split('.');
+ name = attrArr.shift();
+
+ self.stopListening(that.model, "change:" + name, self.render);
+ });
+ }
+
+ if (BackformControlRemove) {
+ BackformControlRemove.apply(self, arguments);
+ } else {
+ Backbone.View.prototype.remove.apply(self, arguments);
+ }
+ },
+
+ template: _.template([
+ '<%=label%> ',
+ '',
+ ' >',
+ ' <%=value%>',
+ ' ',
+ '
'
+ ].join("\n")),
+
+ clearInvalid: function() {
+ this.$el.removeClass(Backform.errorClassName);
+ this.$el.find(".pgadmin-control-error-message").remove();
+ return this;
+ },
+
+ updateInvalid: function() {
+ var self = this,
+ errorModel = this.model.errorModel;
+
+ if (!(errorModel instanceof Backbone.Model)) return this;
+
+ this.clearInvalid();
+
+ this.$el.find(':input').not('button').each(function(ix, el) {
+ var attrArr = $(el).attr('name').split('.'),
+ name = attrArr.shift(),
+ path = attrArr.join('.'),
+ error = self.keyPathAccessor(errorModel.toJSON(), $(el).attr('name'));
if (_.isEmpty(error)) return;
self.$el.addClass(Backform.errorClassName).append(
$("
").addClass('pgadmin-control-error-message col-xs-12 help-block').text(error)
- );
- });
- };
+ );
+ });
+ },
- /*
- * Overriding the render function of the control to allow us to eval the
- * values properly.
- */
- Backform.Control.prototype.render = function() {
- var field = _.defaults(this.field.toJSON(), this.defaults),
- attributes = this.model.toJSON(),
- attrArr = field.name.split('.'),
- name = attrArr.shift(),
- path = attrArr.join('.'),
- rawValue = this.keyPathAccessor(attributes[name], path),
- data = _.extend(field, {
- rawValue: rawValue,
- value: this.formatter.fromRaw(rawValue, this.model),
- attributes: attributes,
- formatter: this.formatter
- }),
- evalF = function(f, d, m) {
- return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f);
- };
+ /*
+ * Overriding the render function of the control to allow us to eval the
+ * values properly.
+ */
+ render: function() {
+ var field = _.defaults(this.field.toJSON(), this.defaults),
+ attributes = this.model.toJSON(),
+ attrArr = field.name.split('.'),
+ name = attrArr.shift(),
+ path = attrArr.join('.'),
+ rawValue = this.keyPathAccessor(attributes[name], path),
+ data = _.extend(field, {
+ rawValue: rawValue,
+ value: this.formatter.fromRaw(rawValue, this.model),
+ attributes: attributes,
+ formatter: this.formatter
+ }),
+ evalF = function(f, d, m) {
+ return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f);
+ };
- // Evaluate the disabled, visible, and required option
- _.extend(data, {
- disabled: evalF(data.disabled, data, this.model),
- visible: evalF(data.visible, data, this.model),
- required: evalF(data.required, data, this.model)
- });
+ // Evaluate the disabled, visible, and required option
+ _.extend(data, {
+ disabled: evalF(data.disabled, data, this.model),
+ visible: evalF(data.visible, data, this.model),
+ required: evalF(data.required, data, this.model)
+ });
- // Clean up first
- this.$el.removeClass(Backform.hiddenClassname);
+ // Clean up first
+ this.$el.removeClass(Backform.hiddenClassname);
- if (!data.visible)
- this.$el.addClass(Backform.hiddenClassname);
+ if (!data.visible)
+ this.$el.addClass(Backform.hiddenClassname);
- this.$el.html(this.template(data)).addClass(field.name);
- this.updateInvalid();
+ this.$el.html(this.template(data)).addClass(field.name);
+ this.updateInvalid();
- return this;
- };
+ return this;
+ }
+ });
/*
* Overriding the render function of the select control to allow us to use
@@ -235,7 +272,7 @@
data.options = data.options.apply(this)
} catch(e) {
// Do nothing
- data = []
+ data.options = []
this.model.trigger('pgadmin-view:transform:error', m, self.field, e);
}
}
@@ -582,70 +619,113 @@
var uniqueCol = this.field.get('uniqueCol') || [],
m = this.field.get('model'),
schema = m.prototype.schema || m.__super__.schema,
- columns = [];
+ columns = [],
+ self = this;
_.each(schema, function(s) {
columns.push(s.id);
});
// Check if unique columns provided are also in model attributes.
- if (uniqueCol.length > _.intersection(columns, uniqueCol).length){
+ if (uniqueCol.length > _.intersection(columns, uniqueCol).length) {
errorMsg = "Developer: Unique column/s [ "+_.difference(uniqueCol, columns)+" ] not found in collection model [ " + columns +" ]."
alert (errorMsg);
}
- var collection = this.model.get(this.field.get('name')),
- self = this;
+ var collection = self.collection = self.model.get(self.field.get('name'));
+
if (!collection) {
- collection = new (pgAdmin.Browser.Node.Collection)(null, {
- model: self.field.get('model'),
- silent: true,
- handler: self.model.handler || self.model
- });
+ collection = self.collection = new (pgAdmin.Browser.Node.Collection)(
+ null,
+ {
+ model: self.field.get('model'),
+ silent: true,
+ handler: self.model.handler || self.model
+ });
self.model.set(self.field.get('name'), collection, {silent: true});
}
+
self.listenTo(collection, "add", self.collectionChanged);
self.listenTo(collection, "change", self.collectionChanged);
},
- collectionChanged: function(newModel, coll, op) {
- var uniqueCol = this.field.get('uniqueCol') || [],
- uniqueChangedAttr = [],
- changedAttr = newModel.changedAttributes();
- // Check if changed model attributes are also in unique columns. And then only check for uniqueness.
- if (changedAttr) {
- _.each(uniqueCol, function(col) {
- if ( _.has(changedAttr,col))
- {
- uniqueChangedAttr.push(col);
- }
- });
- if(uniqueChangedAttr.length == 0) {
- return;
- }
- } else {
- return;
- }
+ remove: function() {
+ var self = this;
- var collection = this.model.get(this.field.get('name'));
- this.stopListening(collection, "change", this.collectionChanged);
- // Check if changed attribute's value of new/updated model also exist for another model in collection.
- // If duplicate value exists then set the attribute's value of new/updated model to it's previous values.
- collection.each(function(model) {
- if (newModel != model) {
- var duplicateAttrValues = []
- _.each(uniqueCol, function(attr) {
- attrValue = newModel.get(attr);
- if (!_.isUndefined(attrValue) && attrValue == model.get(attr)) {
- duplicateAttrValues.push(attrValue)
- }
- });
- if (duplicateAttrValues.length == uniqueCol.length){
- newModel.set(uniqueChangedAttr[0], newModel.previous(uniqueChangedAttr[0]), {silent: true});
- // TODO- Need to add notification in status bar for unique column.
- }
+ self.stopListening(collection, "add", self.collectionChanged);
+ self.stopListening(collection, "change", self.collectionChanged);
+
+ Backform.Control.prototype.remove.apply(this, arguments);
+ },
+ collectionChanged: function(newModel, coll, op) {
+ var uniqueCol = this.field.get('uniqueCol') || [],
+ uniqueChangedAttr = [],
+ self = this;
+ // Check if changed model attributes are also in unique columns. And then only check for uniqueness.
+ if (newModel.attributes) {
+ _.each(uniqueCol, function(col) {
+ if (_.has(newModel.attributes,col))
+ {
+ uniqueChangedAttr.push(col);
}
});
- this.listenTo(collection, "change", this.collectionChanged);
+ if(uniqueChangedAttr.length == 0) {
+ return;
+ }
+ } else {
+ return;
+ }
+
+ var collection = this.model.get(this.field.get('name'));
+ this.stopListening(collection, "change", this.collectionChanged);
+ // Check if changed attribute's value of new/updated model also exist for another model in collection.
+ // If duplicate value exists then set the attribute's value of new/updated model to its previous values.
+ var m = undefined,
+ oldModel = undefined;
+ collection.each(function(model) {
+ if (newModel != model) {
+ var duplicateAttrValues = []
+ _.each(uniqueCol, function(attr) {
+ attrValue = newModel.get(attr);
+ if (!_.isUndefined(attrValue) && attrValue == model.get(attr)) {
+ duplicateAttrValues.push(attrValue)
+ }
+ });
+ if (duplicateAttrValues.length == uniqueCol.length) {
+ m = newModel;
+ // Keep reference of model to make it visible in dialog.
+ oldModel = model;
+ }
+ }
+ });
+ if (m) {
+ if (op && op.add) {
+ // Remove duplicate model.
+ setTimeout(function() {
+ collection.remove(m);
+ }, 0);
+
+ } else {
+ /*
+ * Set model value to its previous value as its new value is
+ * conflicting with another model value.
+ */
+
+ m.set(uniqueChangedAttr[0], m.previous(uniqueChangedAttr[0]), {silent: true});
+ }
+ if (oldModel) {
+ var idx = collection.indexOf(oldModel);
+ if (idx > -1) {
+ var newRow = self.grid.body.rows[idx].$el;
+ newRow.addClass("new");
+ $(newRow).pgMakeVisible('backform-tab');
+ setTimeout(function() {
+ newRow.removeClass("new");
+ }, 3000);
+ }
+ }
+ }
+
+ this.listenTo(collection, "change", this.collectionChanged);
},
render: function() {
var field = _.defaults(this.field.toJSON(), this.defaults),
@@ -673,7 +753,7 @@
canDelete: evalF(data.canDelete, this.model)
});
// Show Backgrid Control
- grid = (data.subnode == undefined) ? "" : this.showGridControl(data);
+ grid = this.showGridControl(data);
this.$el.html(grid).addClass(field.name);
this.updateInvalid();
@@ -687,6 +767,10 @@
""].join("\n"),
gridBody = $("
").append(gridHeader);
+ if (!(data.subnode)) {
+ return '';
+ }
+
var subnode = data.subnode.schema ? data.subnode : data.subnode.prototype,
gridSchema = Backform.generateGridColumnsFromModel(
data.node_info, subnode, this.field.get('mode'), data.columns
@@ -709,10 +793,10 @@
var collection = this.model.get(data.name);
// Initialize a new Grid instance
- var grid = new Backgrid.Grid({
- columns: gridSchema.columns,
- collection: collection,
- className: "backgrid table-bordered"
+ var grid = self.grid = new Backgrid.Grid({
+ columns: gridSchema.columns,
+ collection: collection,
+ className: "backgrid table-bordered"
});
// Render subNode grid
@@ -728,35 +812,38 @@
// Add button callback
if (!(data.disabled || data.canAdd == false)) {
$dialog.find('button.add').first().click(function(e) {
- e.preventDefault();
- var allowMultipleEmptyRows = !!self.field.get('allowMultipleEmptyRows');
+ e.preventDefault();
+ var allowMultipleEmptyRows = !!self.field.get('allowMultipleEmptyRows');
- // If allowMultipleEmptyRows is not set or is false then don't allow second new empty row.
- // There should be only one empty row.
- if (!allowMultipleEmptyRows && collection){
- var isEmpty = false;
- collection.each(function(model){
- var modelValues = [];
- _.each(model.attributes, function(val, key){
- modelValues.push(val);
- })
- if(!_.some(modelValues, _.identity)){
- isEmpty = true;
- }
- });
- if(isEmpty){
- return false;
- }
+ // If allowMultipleEmptyRows is not set or is false then don't allow second new empty row.
+ // There should be only one empty row.
+ if (!allowMultipleEmptyRows && collection) {
+ var isEmpty = false;
+ collection.each(function(model) {
+ var modelValues = [];
+ _.each(model.attributes, function(val, key) {
+ modelValues.push(val);
+ })
+ if(!_.some(modelValues, _.identity)) {
+ isEmpty = true;
+ }
+ });
+ if(isEmpty) {
+ return false;
}
+ }
- $(grid.body.$el.find($("tr.new"))).removeClass("new")
- var m = new (data.model)(null, {silent: true});
- collection.add(m);
+ $(grid.body.$el.find($("tr.new"))).removeClass("new")
+ var m = new (data.model)(null, {silent: true});
+ collection.add(m);
- var idx = collection.indexOf(m);
- newRow = grid.body.rows[idx].$el;
- newRow.addClass("new");
- return false;
+ var idx = collection.indexOf(m),
+ newRow = grid.body.rows[idx].$el;
+
+ newRow.addClass("new");
+ $(newRow).pgMakeVisible('backform-tab');
+
+ return false;
});
}
@@ -869,7 +956,7 @@
}
// Initialize a new Grid instance
- var grid = new Backgrid.Grid({
+ var grid = self.grid = new Backgrid.Grid({
columns: gridSchema.columns,
collection: collection,
className: "backgrid table-bordered"
@@ -890,10 +977,11 @@
$dialog.find('button.add').click(function(e) {
e.preventDefault();
grid.insertRow({});
- newRow = $(grid.body.rows[collection.length - 1].$el);
+ var newRow = $(grid.body.rows[collection.length - 1].$el);
newRow.attr("class", "new").click(function(e) {
$(this).attr("class", "");
});
+ $(newRow).pgMakeVisible('backform-tab');
return false;
});
@@ -1152,11 +1240,13 @@
schema_node: schema_node,
visible: (mode == 'properties'?
(ver_in_limit ?
- (s.version || true) : false) : s.version || true)
+ (s.version || true) : false) : s.version || true),
+ node: node,
+ node_data: treeData
});
delete o.id;
- // Temporarily store in dictionaly format for
+ // Temporarily store in dictionary format for
// utilizing it later.
groups[group].push(o);
}
@@ -1182,7 +1272,7 @@
}
return groups;
- }
+ };
return Backform;
}));
diff --git a/web/pgadmin/static/js/backgrid/backgrid.pgadmin.js b/web/pgadmin/static/js/backgrid/backgrid.pgadmin.js
index 3343edca1..e55b6c1b0 100644
--- a/web/pgadmin/static/js/backgrid/backgrid.pgadmin.js
+++ b/web/pgadmin/static/js/backgrid/backgrid.pgadmin.js
@@ -352,7 +352,7 @@
_.defaults(
{'disabled': !editable},
col.select2,
- this.defatuls.select2
+ this.defaults.select2
));
this.delegateEvents();