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 = [ + "
", + "
", + "
", + "
", + " ", + "
", + "
", + "
", + "
", + "
"]; + + if(this.hasDatabase) { + header.push([ + "
", + "
", + " ", + "
", + "
", + "
"].join("\n") + ); + } + + if (this.hasRole) { + header.push([ + "
", + "
", + " ", + "
", + "
", + "
"].join("\n") + ); + } + + header.push([ + "
", + "
"].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([ + '', + '
', + ' ', + '
'].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 '