Adding support for collection browser node.

pull/3/head
Khushboo Vashi 2015-11-17 11:51:09 +05:30 committed by Ashesh Vashi
parent 52b86b6fb4
commit ed8600ef89
9 changed files with 394 additions and 139 deletions

View File

@ -119,7 +119,8 @@ class BrowserModule(PgAdminModule):
for name, script in [
['pgadmin.browser', 'js/browser'],
['pgadmin.browser.error', 'js/error'],
['pgadmin.browser.node', 'js/node']]:
['pgadmin.browser.node', 'js/node'],
['pgadmin.browser.collection', 'js/collection']]:
scripts.append({
'name': name,
'path': url_for('browser.index') + script,
@ -297,6 +298,13 @@ def node_js():
render_template('browser/js/node.js', _=gettext),
200, {'Content-Type': 'application/x-javascript'})
@blueprint.route("/js/collection.js")
@login_required
def collection_js():
return make_response(
render_template('browser/js/collection.js', _=gettext),
200, {'Content-Type': 'application/x-javascript'})
@blueprint.route("/browser.css")
@login_required

View File

@ -0,0 +1,208 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import six
from abc import ABCMeta, abstractmethod
from flask import url_for, render_template
from flask.ext.babel import gettext
from pgadmin.browser.utils import PgAdminModule, PGChildModule
from pgadmin.browser import BrowserPluginModule
from pgadmin.browser.utils import NodeView, PGChildNodeView
@six.add_metaclass(ABCMeta)
class CollectionNodeModule(PgAdminModule, PGChildModule):
"""
Base class for collection node submodules.
"""
browser_url_prefix = BrowserPluginModule.browser_url_prefix
def __init__(self, import_name, **kwargs):
kwargs.setdefault("url_prefix", self.node_path)
kwargs.setdefault("static_url_path", '/static')
super(CollectionNodeModule, self).__init__(
"NODE-%s" % self.node_type,
import_name,
**kwargs
)
@property
def jssnippets(self):
"""
Returns a snippet of javascript to include in the page
"""
return []
def get_own_javascripts(self):
scripts = []
scripts.extend([{
'name': 'pgadmin.node.%s' % self.node_type,
'path': url_for('browser.index') + '%s/module' % self.node_type,
'when': self.script_load
}])
for module in self.submodules:
scripts.extend(module.get_own_javascripts())
return scripts
def generate_browser_node(
self, node_id, parents, label, icon, **kwargs
):
obj = {
"id": "%s/%s" % (self.node_type, node_id),
"label": label,
"icon": self.node_icon if not icon else icon,
"inode": self.node_inode,
"_type": self.node_type,
"_id": node_id,
"refid": parents,
"module": 'pgadmin.node.%s' % self.node_type
}
for key in kwargs:
obj.setdefault(key, kwargs[key])
return obj
def generate_browser_collection_node(self, sid, parents, **kwargs):
obj = {
"id": "coll-%s/%d" % (self.node_type, sid),
"label": self.collection_label,
"icon": self.collection_icon,
"inode": True,
"_type": 'coll-%s' % (self.node_type),
"_id": sid,
"refid": parents,
"module": 'pgadmin.node.%s' % self.node_type
}
for key in kwargs:
obj.setdefault(key, kwargs[key])
return obj
@property
def node_type(self):
return '%s-%s' % (self.SERVER_TYPE, self.NODE_TYPE)
@property
def csssnippets(self):
"""
Returns a snippet of css to include in the page
"""
snippets = [
render_template(
"browser/css/collection.css",
node_type=self.node_type,
_=gettext
),
render_template(
"browser/css/node.css",
node_type=self.node_type,
_=gettext
)
]
for submodule in self.submodules:
snippets.extend(submodule.csssnippets)
print "snipp==",snippets
return snippets
@property
def collection_label(self):
"""
Label to be shown for the collection node, do not forget to set the
class level variable COLLECTION_LABEL.
"""
return self.COLLECTION_LABEL
@property
def collection_icon(self):
"""
icon to be displayed for the browser collection node
"""
return 'icon-coll-%s' % (self.node_type)
@property
def node_icon(self):
"""
icon to be displayed for the browser nodes
"""
return 'icon-%s' % (self.node_type)
@property
def node_inode(self):
"""
Override this property to make the node as leaf node.
"""
return True
@abstractmethod
def get_nodes(self, sid, *args, **kwargs):
"""
Generate the collection node
You need to override this method because every node will have different
url pattern for the parent.
"""
pass
@property
def script_load(self):
"""
This property defines, when to load this script.
In order to allow creation of an object, we need to load script for any
node at the parent level.
i.e.
- In order to allow creating a server object, it should be loaded at
server-group node.
"""
pass
@property
def node_path(self):
return self.browser_url_prefix + self.node_type
@property
def javascripts(self):
return []
class CollectionNodeView(NodeView, PGChildNodeView):
"""
A PostgreSQL Collection node has specific functions needs to address
i.e.
- List the nodes
- Get the list of nodes objects (model)
This class can be inherited to achieve the diffrent routes for each of the
object types/collections.
OPERATION | URL | Method
---------------+------------------------+--------
List | /coll/[Parent URL]/ | GET
Children Nodes | /node/[Parent URL]/ | GET
NOTE:
Parent URL can be seen as the path to identify the particular node.
"""
operations = dict({
})
operations = dict({
'obj': [
{'get': 'properties', 'delete': 'delete', 'put': 'update'},
{'get': 'list', 'post': 'create'}
],
'nodes': [{'get': 'nodes'}],
'sql': [{'get': 'sql', 'post': 'modified_sql'}],
'stats': [{'get': 'statistics'}],
'deps': [{'get': 'dependencies', 'post': 'dependents'}],
'module.js': [{}, {}, {'get': 'module_js'}],
'coll': [{}, {'get': 'collections'}]
})

View File

@ -584,7 +584,7 @@ class ServerNode(NodeView):
if e.message:
return internal_server_error(errormsg=e.message)
else:
return internal_server_error(errormsg=str(e))
return internal_server_error(errormsg=str(e))
if not status:
current_app.logger.error(

View File

@ -0,0 +1,7 @@
.icon-coll-{{node_type}} {
background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/coll-%s.png' % node_type )}}') !important;
background-repeat: no-repeat;
align-content: center;
vertical-align: middle;
height: 1.3em;
}

View File

@ -356,7 +356,9 @@ OWNER TO helpdesk;\n';
ajaxHook: function(item, settings) {
if (item != null) {
var d = this.itemData(item);
settings.url = '{{ url_for('browser.index') }}' + d._type + '/nodes/' + (d.refid ? d.refid + '/' : '') + d._id
n = obj.Nodes[d._type];
if (n)
settings.url = n.generate_url('nodes', d, true);
}
}
});
@ -549,7 +551,6 @@ OWNER TO helpdesk;\n';
add_menus: function(menus) {
var pgMenu = this.menus;
var MenuItem = pgAdmin.Browser.MenuItem;
_.each(menus, function(m) {
_.each(m.applies, function(a) {
/* We do support menu type only from this list */

View File

@ -0,0 +1,119 @@
define(
['jquery', 'underscore', 'underscore.string', 'pgadmin',
'backbone', 'alertify', 'backform', 'pgadmin.backform',
'pgadmin.backgrid', 'pgadmin.browser.node'
],
function($, _, S, pgAdmin, Backbone, Alertify, Backform) {
var pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
// It has already been defined.
// Avoid running this script again.
if (pgBrowser.Collection)
return pgBrowser.Collection;
pgBrowser.Collection = _.extend(_.clone(pgBrowser.Node), {
///////
// Initialization function
// Generally - used to register the menus for this type of node.
//
// Also, look at pgAdmin.Browser.add_menus(...) function.
//
// Collection will not have 'Properties' menu.
//
// NOTE: Override this for each node for initialization purpose
Init: function() {
if (this.node_initialized)
return;
this.node_initialized = true;
pgAdmin.Browser.add_menus([{
name: 'refresh', node: this.type, module: this,
applies: ['object', 'context'], callback: 'refresh_node',
priority: 1, label: '{{ _("Refresh...") }}',
icon: 'fa fa-refresh'
}]);
},
showProperties: function(item, data, panel) {
var that = this,
j = panel.$container.find('.obj_properties').first(),
view = j.data('obj-view'),
content = $('<div></div>')
.addClass('pg-prop-content col-xs-12'),
node = pgBrowser.Nodes[that.node],
// This will be the URL, used for object manipulation.
urlBase = this.generate_url('properties', data),
collections = new (node.Collection.extend({
url: urlBase,
model: node.model
}))(),
gridSchema = Backform.generateGridColumnsFromModel(
node.model, 'prorperties', that.columns
),
// Initialize a new Grid instance
grid = new Backgrid.Grid({
columns: gridSchema.columns,
collection: collections,
className: "backgrid table-bordered"
}),
gridView = {
'remove': function() {
if (this.grid) {
delete (this.grid);
this.grid = null;
}
}
};
gridView.grid = grid;
if (view) {
// Release the view
view.remove();
// 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();
j.data('obj-view', gridView);
// Render subNode grid
content.append(grid.render().$el);
j.append(content);
// Fetch Data
collections.fetch({reset: true});
},
generate_url: function(type, d) {
var url = pgAdmin.Browser.URL + '{TYPE}/{REDIRECT}{REF}',
ref = S('/%s/').sprintf(d._id).value(),
/*
* Using list, and collections functions of a node to get the nodes
* under the collection, and properties of the collection respectively.
*/
opURL = {
'nodes': 'obj', 'properties': 'coll'
};
if (d._type == this.type) {
if (d.refid)
ref = S('/%s/').sprintf(d.refid).value();
}
var TYPE = d.module.split(".");
var args = {'TYPE': TYPE[TYPE.length-1], 'REDIRECT': '', 'REF': ref};
if (type in opURL) {
args.REDIRECT = opURL[type];
} else {
args.REDIRECT = type;
}
return url.replace(/{(\w+)}/g, function(match, arg) {
return args[arg];
});
}
});
return pgAdmin.Browser.Collection;
});

View File

@ -803,9 +803,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
opURL = {
'create': 'obj', 'drop': 'obj', 'edit': 'obj',
'properties': 'obj', 'depends': 'deps',
'statistics': 'stats', 'collections': 'nodes'
'statistics': 'stats', 'nodes': 'nodes'
};
if (d._type == this.type) {
ref = '';
if (d.refid)
@ -964,6 +963,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
self.on('add', self.onModelAdd);
self.on('remove', self.onModelRemove);
self.on('change', self.onModelChange);
return self;
},
startNewSession: function() {
var self = this;
@ -1171,8 +1172,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
self.onChangeData = options.onChangeData;
self.onChangeCallback = options.onChangeCallback;
if (this.schema && _.isArray(this.schema)) {
_.each(this.schema, function(s) {
if (self.schema && _.isArray(self.schema)) {
_.each(self.schema, function(s) {
var obj = null;
switch(s.type) {
case 'collection':
@ -1208,6 +1209,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
self.set(s.id, obj, {silent: true});
});
}
return self;
},
onChange: function() {
var self = this;
@ -1337,105 +1340,5 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
})
});
pgBrowser.Collection = _.extend(_.clone(pgAdmin.Browser.Node), {
///////
// Initialization function
// Generally - used to register the menus for this type of node.
//
// Also, look at pgAdmin.Browser.add_menus(...) function.
//
// Collection will not have 'Properties' menu.
//
// NOTE: Override this for each node for initialization purpose
Init: function() {
if (this.node_initialized)
return;
this.node_initialized = true;
pgAdmin.Browser.add_menus([{
name: 'refresh', node: this.type, module: this,
applies: ['object', 'context'], callback: 'refresh_collection',
priority: 2, label: '{{ _("Refresh...") }}',
icon: 'fa fa-refresh'
}]);
},
callbacks: {
refresh_collection: function() {
// TODO:: Refresh the collection node
console.log(arguments);
},
selected: function(o) {
// Show (Node information on these panels, which one is visible.)
// + Properties (list down the children nodes in pages)
// + Query (Remove existing SQL)
// + Dependents (Remove dependents)
// + Dependencies (Remove dependencies)
// + Statistics (TODO:: Check the current implementation in pgAdmin 3)
// Update the menu items
pgAdmin.Browser.enable_disable_menus.apply(o.browser, [o.item]);
if (o && o.data && o.browser) {
var br = o.browser;
if ('properties' in br.panels &&
br.panels['properties'] &&
br.panels['properties'].panel &&
br.panels['properties'].panel.isVisible()) {
// Show object properties (only when the 'properties' tab
// is active).
this.showProperties(o.item, o.data,
pgBrowser.panels['properties'].panel);
}
if ('sql' in br.panels &&
br.panels['sql'] &&
br.panels['sql'].panel &&
br.panels['sql'].panel.isVisible()) {
// TODO::
// Remove the information from the sql pane
}
if ('statistics' in br.panels &&
br.panels['statistics'] &&
br.panels['statistics'].panel &&
br.panels['statistics'].panel.isVisible()) {
// TODO::
// Remove information from the statistics pane
}
if ('dependencies' in br.panels &&
br.panels['dependencies'] &&
br.panels['dependencies'].panel &&
br.panels['dependencies'].panel.isVisible()) {
// TODO::
// Remove information from the dependencies pane
}
if ('dependents' in br.panels &&
br.panels['dependents'] &&
br.panels['dependents'].panel &&
br.panels['dependents'].panel.isVisible()) {
// TODO::
// Remove information from the dependents pane
}
}
}
},
/**********************************************************************
* 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) {
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-xs-12');
// TODO:: Show list of children in paging mode
return;
}
});
return pgAdmin.Browser.Node;
});

View File

@ -63,7 +63,6 @@ class PGChildModule(object):
def BackendSupported(self, mangaer):
sversion = getattr(mangaer, 'sversion', None)
if (sversion is None or not isinstance(sversion, int)):
return False
@ -180,7 +179,6 @@ class NodeView(with_metaclass(MethodViewType, View)):
'with_id': (idx != 2), 'methods': meths
})
idx += 1
return cmds
# Inherited class needs to modify these parameters
@ -321,9 +319,9 @@ class NodeView(with_metaclass(MethodViewType, View)):
return make_json_response(data=nodes)
class PGChildNodeView(NodeView):
class PGChildNodeView(object):
def nodes(self, sid=None, **kwargs):
def nodes(self, sid, **kwargs):
"""Build a list of treeview nodes from the child nodes."""
from pgadmin.utils.driver import get_driver
@ -334,8 +332,8 @@ class PGChildNodeView(NodeView):
if isinstance(module, ServerChildModule):
if sid is not None and manager is not None and \
module.BackendSupported(manager):
nodes.extend(module.get_nodes(*args, **kwargs))
nodes.extend(module.get_nodes(sid=sid, **kwargs))
else:
nodes.extend(module.get_nodes(*args, **kwargs))
nodes.extend(module.get_nodes(sid=sid, **kwargs))
return make_json_response(data=nodes)

View File

@ -296,6 +296,33 @@
events: {}
});
var generateGridColumnsFromModel = Backform.generateGridColumnsFromModel = function(m, type, cols) {
var groups = Backform.generateViewSchema(m, type),
schema = [],
columns = [],
addAll = _.isUndefined(cols) || _.isNull(cols);
// Prepare columns for backgrid
_.each(groups, function(fields, key) {
_.each(fields, function(f) {
if (!f.control && !f.cell) {
return;
}
f.cell_priority = _.indexOf(cols, f.name);
if (addAll || f.cell_priority != -1) {
columns.push(f);
}
});
schema.push({label: key, fields: fields});
});
return {
'columns': _.sortBy(columns, function(c) {
return c.cell_priority;
}),
'schema': schema
};
}
var SubNodeCollectionControl = Backform.SubNodeCollectionControl = Backform.Control.extend({
render: function() {
var field = _.defaults(this.field.toJSON(), this.defaults),
@ -339,24 +366,9 @@
gridBody = $("<div class='pgadmin-control-group backgrid form-group col-xs-12 object subnode'></div>").append(gridHeader);
var subnode = data.subnode.schema ? data.subnode : data.subnode.prototype,
columns = [],
gridColumns = [],
groups = Backform.generateViewSchema(subnode, this.field.get('mode')),
schema = [];
// Prepare columns for backgrid
_.each(groups, function(fields, key) {
_.each(fields, function(f) {
if (!f.control && !f.cell) {
return;
}
f.cel_priority = _.indexOf(data.columns, f.name);
if (f.cel_priority != -1) {
columns.push(f);
}
});
schema.push({label: key, fields: fields});
});
gridSchema = Backform.generateGridColumnsFromModel(
subnode, this.field.get('mode'), data.columns
);
// Set visibility of Add button
if (data.disabled || data.canAdd == false) {
@ -365,30 +377,29 @@
// Insert Delete Cell into Grid
if (data.disabled == false && data.canDelete) {
columns.unshift({
gridSchema.columns.unshift({
name: "pg-backform-delete", label: "",
cell: Backgrid.Extension.DeleteCell,
editable: false, priority: -1
editable: false, cell_priority: -1
});
}
// Insert Edit Cell into Grid
if (data.disabled == false && data.canEdit) {
var editCell = Backgrid.Extension.ObjectCell.extend({
schema: schema
schema: gridSchema.schema
});
columns.unshift({
gridSchema.columns.unshift({
name: "pg-backform-edit", label: "", cell : editCell,
priority: -2
cell_priority: -2
});
}
var collections = this.model.get(data.name);
// Initialize a new Grid instance
var grid = new Backgrid.Grid({
columns: _.sortBy(columns, function(c) { return c.cell_priority; }),
columns: gridSchema.columns,
collection: collections,
className: "backgrid table-bordered"
});